查看原文
其他

线程同步手记

张飞洪 SpringForAll社区 2021-05-26
点击上方☝SpringForAll社区 轻松关注!
及时获取有趣有料的技术文章

本文来源:

https://www.cnblogs.com/jackyfei/archive/2012/04/11/synchronization.html

一、前言

线程同步其实很简单,但是往往被老师教的很复杂。这是之前上课受的伤。脑袋瓜当人人家的跑马场,被蹂躏一番,最后老师留下的是先入为主的错误,以至于后面不停的干扰我的理解,纠起错来,真是不知道浪费了多少精力。

二、什么是线程同步

一直想要找一个良好的方式来表达什么是线程同步。

先看一个模拟线程同步的图:

   

假如这个盒子一次只能放一个东西,并且接力赛又要保持顺畅,该是怎样的情景?

首先对于Reader来说,取货的时候,箱子必须有货,如果没有货,要在旁边等候;

其次对于Writer来说,存货的时候,箱子必须为空,如果不为空,也要在旁边等候;

两个人要步调一致,并且配合默契,才能顺利的搬运东西。反过来,如果Reader执行了好几次,Writer才执行一次,或者Write执行了好几次,Reader才执行一次,最后都不能很好的保持步调的一致。

把两个人看成是两个线程,这时候线程要同步,也必须要满足上面的要求。两个线程要协同一致的工作,才能完成一项任务。

三、代码演示:

1    public class ThreadSyn
2     {

3        //缓存区,假设一次只能缓存一个字符
4        private static char buffer;
5        //线程1:写操作
6        public Thread thread1 = new Thread(()=>
7                                        {
8                                            string str = "横看成岭侧成峰,远近高低各不同。不识庐山真面目,只缘身在此山中。";
9                                           for (int i = 0; i < 32; i++)
10                                           {
11                                               buffer = str[i];
12                                               Thread.Sleep(26);
13                                           }
14
15                                       });
16
17        //线程2:读操作
18        public Thread thread2 = new Thread(() =>
19                                        { 
20                                            for (int i = 0; i < 32; i++)
21                                            {
22                                                char ch = buffer;
23                                                Console.WriteLine(ch);
24                                                Thread.Sleep(36);
25                                            }
26                                        });
27        }
28
29         public class Program
30       {

31          static void Main(string[] args)
32          
{
33              ThreadSyn threadSyn=new ThreadSyn();
34              threadSyn.thread1.Start();
35              threadSyn.thread2.Start();
36
37              Console.Read();
38          } 
39    }

运行效果图:

四、原因和方案:

此时线程还是没有协同工作。因为如果写一个,读一个,再写一个,再读一个,那么这首诗应该是一首完整的显示。但是效果图的诗句却是紊乱的。

如何才能解决真正的同步,.net为我们提供了一系列的同步类。包括:互锁(Interlocked),管程(Monitor)和互斥体(Mutex).


4.1下面用互锁来解决上面的问题。


1   public class ThreadSyn
2     {
3        //缓存区
4        private static char _buffer;
5        //标示盒子,即缓冲区使用的空间,盒子初始化为0
6        private static long _box = 0;
7        //线程1:写操作
8        public Thread thread1 = new Thread(()=>
9                                        {
10                                           string str = "横看成岭侧成峰,远近高低各不同。不识庐山真面目,只缘身在此山中。";
11                                           for (int i = 0; i < 32; i++)
12                                           {
13                                               //写入之前检查缓冲区
14                                               //如果缓冲区已满,就进行等待,直到缓冲区的数据被进程读取为止
15                                               while(Interlocked.Read(ref _box) == 1)
16                                               { 
17                                                   Thread.Sleep(10);
18                                               }
19
20                                               //向缓冲区写数据
21                                               _buffer = str[i];
22                                               //写完数据,标记缓冲区已满
23                                               Interlocked.Increment(ref _box);
24                                           }
25                                       });
26
27        //线程2:读操作
28        public Thread thread2 = new Thread(() =>
29                                        {
30                                            for (int j = 0; j < 32; j++)
31                                            {
32                                                //写入之前检查缓冲区
33                                                //如果缓冲区为空,就进行等待,直到缓冲区的数据被进程填充为止
34                                                while(Interlocked.Read(ref _box) == 0)
35                                                {
36                                                    Thread.Sleep(10);
37                                                }
38
39                                                //向缓冲区读数据
40                                                char ch = _buffer;
41                                                Console.Write(ch);
42                                                //读完数据,标记缓冲区已空
43                                                Interlocked.Decrement(ref _box);
44                                            }
45                                        });
46    }

运行效果图:

InterLocked提供了单个指令的操作,因此他提供了性能非常高的同步。

  


4.2用Monitor来解决问题

  Monitor的原理是这样的:先执行的线程,独占锁,进入临界区,执行临界区资源代码。其他线程,只能在集中在临界资源上等待被叫唤。当独占锁推出资源区,也可以继续让自己等待,等待下一次被叫唤。

1     //缓存区
2        private static char _buffer;
3        //用于同步的对象
4        private static object _objForLock = new object();
5
6        //线程1:写操作
7        public Thread thread1 = new Thread(() =>
8        {
9            string str = "横看成岭侧成峰,远近高低各不同。不识庐山真面目,只缘身在此山中。";
10            for (int i = 0; i < 32; i++)
11            {
12                try
13                {
14                    //进入临界区,获取独占锁
15                    Monitor.Enter(_objForLock);
16
17                    //向缓冲区写数据
18                    _buffer = str[i];
19
20                    //写完后,唤醒在临界资源上睡眠的线程
21                    Monitor.Pulse(_objForLock);
22
23                    //让当前线程睡眠在临界资源上
24                    Monitor.Wait(_objForLock);
25
26                    //整个流程有点像轮班吃饭,
27                    //第一个人先去吃饭,第二个在值班等待,第一个吃完了,唤醒第二个吃饭,自己则在等待下一次吃饭。
28                }
29                catch (ThreadInterruptedException ex)
30                {   
31                   Console.WriteLine("线程被中断……");
32                }
33                finally
34                {
35                    //退出临界区
36                    Monitor.Exit(_objForLock);
37                }
38            }
39        });
40
41        //线程2:读操作
42        public Thread thread2 = new Thread(() =>
43        {
44            for (int j = 0; j < 32; j++)
45            {
46                try
47                {
48                    //进入临界区,获取独占锁
49                    Monitor.Enter(_objForLock); 
50
51                    //向缓冲区读数据
52                    char ch = _buffer;
53                    Console.Write(ch);
54
55                    //写完后,唤醒在临界资源上睡眠的线程
56                    Monitor.Pulse(_objForLock);
57
58                    //让当前线程睡眠在临界资源上
59                    Monitor.Wait(_objForLock);
60
61                }
62                catch (ThreadInterruptedException ex)
63                {   
64                   Console.WriteLine("线程被中断……");
65                }
66                finally
67                {
68                    //退出临界区
69                    Monitor.Exit(_objForLock);
70                }
71            }
72        });

  不同的是Monitor只能锁定引用类型的对象,值类型会被装箱,等于生成另外一个对象,不能达到同步。为了保证推出临界区资源得到释放,使用了finally。为了方便使用,C#专门使用了lock语句。

  所以我们可以完全更简洁的重写上面的try{}finally{}中的关键代码,如下所示:

1          lock (_objForLock)
2                {
3                    //进入临界区,获取独占锁
4                    Monitor.Enter(_objForLock);
5
6                    //向缓冲区写数据
7                    _buffer = str[i];
8
9                    //写完后,唤醒在临界资源上睡眠的线程
10                    Monitor.Pulse(_objForLock);
11
12                    //让当前线程睡眠在临界资源上
13                    Monitor.Wait(_objForLock);  
14                }

  

img


独占锁注意:

  因为独占锁,其他线程就不能再访问,只有Lock结束后,其他线程才可以访问,这保证了访问的正确性。但是,如果有多个线程对同一个资源进行写操作,在独占锁解开前,其他线程只能被临时暂停,这使得程序的效率大打折扣。所以应该慎用锁,只有必要时才使用。




● 漫谈何时从单体架构迁移到微服务?

● 微服务的时间和成本去哪儿了

● 微服务学习导航

● 为什么在做微服务设计的时候需要DDD?

● 假如你是架构师,你要做些什么

● 微服务划分的姿势

● Java IO模型之NIO模型

● MongoDB 集群构建:分片+副本+选举

● Fork-Join框架

● Spring Boot实现动态增删启停定时任务

● MongoDB - 用户与权限

● SpringForAll社区,2019年文章精选10篇

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存