如何在Java中使用同步

  介绍

这篇文章给大家介绍如何在Java中使用同步,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。

生活中随处可见并行的例子,并行顾名思义就是一起进行的意思,同样的程序在某些时候也需要并行来提高效率,在上一篇文章中我们了解了Java语言对缓存导致的可见性问题,编译优化导致的顺序性问题的解决方法、下面我们就来看看Java中解决因线程切换导致的原子性问题的解决方案——锁。

说到锁我们并不陌生,日常工作中也可能经常会用的到,但是我们不能只停留在用的层面上,为什么要加锁,不加锁行不,行不行的话会导致哪些问题,这些都是在使用加锁语句时我们需要考虑的。

来看一个使用32位的CPU写长型变量需不需要加锁的问题:

我们知道长期型变量长度为64位,在32位CPU上写长型变量至少需要拆分成2个步骤:一次写高32位,一次写低32位。

对于单核CPU来说,同一时刻只有一个线程在执行,禁止CPU中断就意味着禁止线程切换,获得CPU使用权的这个线程就会一直运行,所以2次写操作要么同时都被执行,要么都不被执行,单CPU核是保证原子性的。

对于多核CPU、同一时刻,一个线程在CPU 1上运行,另一个线程在CPU - 2上执行上那么运行,此时禁止CPU切换,只能保证CPU上有线程运行,并不能保证同一时刻只有一个线程运行,如果两个线程同时都在写高位,那么得出的结果可就不正确了。

所以,互斥修改共享变量这个条件非常重要,也就是说同一时刻只有一个线程在修改共享变量,只要保证这个条件,不论单核还是多核,操作就都是原子性的了。

一说到互斥,原子性,我们马上就想到了代码加锁,没错加锁是正确的选择,但是怎么加呢?要想知道怎么加锁,首先我们要知道加锁锁的是什么以及我们想要保护的资源是什么,看下图说说锁的是什么,要保护的是什么呢?

如何在Java中使用同步

图中锁的M资源,保护的也是M资源。

程序中的锁与现实中的锁也是类似的,每一把锁都有自己要保护的资源,这是至关重要的,如图保护资源M的锁为LM,就像我家大门的锁保护我的家,你家大门的锁保护你家一样,如果程序出现类似我家大门锁保护你家的情况,那么就会导致诡异的并发问题了。

了解了锁的是什么与保护的是什么之后,我们看看怎么加锁的问题,还是用计数+=1的例子,看代码:

class 测试{   ,long  value =, 0 l;   ,long  get (), {   return 才能;价值;   ,}   ,synchronized  void  addOne (), {   value 才能+=,1;   ,}   }

分析一下,这段代码中锁的是当前对象,要保护的资源是对象中的成员属性价值,这样的加锁方式开启10个线程分别调用10000次addOne()方法,我们预期的结果是价值最终会达到100000年,结果如何呢?

经过测试,addOne()不加同步结果会出现小于100000年的情况,加上同步结果符合我们的预期,针对测试结果,简要分析如下:

加锁之后,线程之间是互斥的,也就是说同一时刻只有一个线程执行,这样就原子性可以保证了。

那么可见性呢?一个线程操作结束后另一个线程能获取到上一个线程的操作结果吗?答案是肯定的,这就跟我们上一章说的发生在原则联系到一起了,“一个锁的解锁操作对另一个锁的加锁操作是可见的”,再结合传递性规则,一个锁在解锁前,对共享变量的修改,即解锁前对共享变量修改之前发生于这个锁的解锁,这个锁的解锁操作前发生于另一个锁的加锁。

所以,解锁前对共享变量修改之前发生于另一个锁的加锁,也就是说解锁前对共享变量修改对于另一个锁的加锁是可见的。

到这一切看似还挺完美,其实我们忽略了得到()方法,多线程操作得到()方法会是安全的吗?在没有任何前提操作的情况下,直接调用得到()方法当然没问题,就是取值又不涉及修改。但是如果在执行addOne()方法后调用呢?显然,这时候值值的修改对得到()方法是不可见的,发生在中只说了锁的规则,这里要想保证可见性,对获得()方法也需要加上一把锁。代码如下:

class 测试{   ,long  value =, 0 l;   ,synchronized  long  get (), {   return 才能;价值;   ,}   ,synchronized  void  addOne (), {   value 才能+=,1;   ,}   }

这里我们用同一把锁,保护了共享资源值。说到这,我们根据资源关系来将使用锁的情况分为两种:

如何在Java中使用同步