多线程:synchronize、volatile、Lock 的区别与用法

版权声明:本博客为记录本人学习过程而开,内容大多从网上学习与整理所得,若侵权请告知! https://blog.csdn.net/Fly_as_tadpole/article/details/86286193

Java多线程之内存可见性和原子性:Synchronized和Volatile的比较

      在说明Java多线程内存可见性之前,先来简单了解一下Java内存模型。

     (1)Java所有变量都存储在主内存中
     (2)每个线程都有自己独立的工作内存,里面保存该线程的使用到的变量副本(该副本就是主内存中该变量的一份拷贝)

   (1)线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接在主内存中读写
   (2)不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
线程1对共享变量的修改,要想被线程2及时看到,必须经过如下2个过程:


   (1)把工作内存1中更新过的共享变量刷新到主内存中
   (2)将主内存中最新的共享变量的值更新到工作内存2中


可见性与原子性


   可见性:一个线程对共享变量的修改,更够及时的被其他线程看到
   原子性:即不可再分了,不能分为多步操作。比如赋值或者return。比如"a = 1;"和 "returna;"这样的操作都具有原子性。类似"a +=b"这样的操作不具有原子性,在某些JVM中"a +=b"可能要经过这样三个步骤:
① 取出a和b
② 计算a+b
③ 将计算结果写入内存


(1)Synchronized:保证可见性和原子性
   Synchronized能够实现原子性和可见性;在Java内存模型中,synchronized规定,线程在加锁→先清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。


(2)Volatile:保证可见性,但不保证操作的原子性
   Volatile实现内存可见性是通过store和load指令完成的;也就是对volatile变量执行写操作时,会在写操作后加入一条store指令,即强迫线程将最新的值刷新到主内存中;而在读操作时,会加入一条load指令,即强迫从主内存中读入变量的值。但volatile不保证volatile变量的原子性,例如:

(3)Synchronized和Volatile的比较
    1)Synchronized保证内存可见性和操作的原子性

             加锁----清空内存----在主存中拷贝最新副本----执行+修改--------刷回主存-------释放锁


    2)Volatile只能保证内存可见性

          a.每次读取的时候都会CAS

         b.每次写完都会store回主存


    3)Volatile不需要加锁(忙等待,做自旋),比Synchronized更轻量级,并不会阻塞线程(volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。)
    4)volatile标记的变量不会被编译器优化(通过volatile保证了有序性),而synchronized标记的变量可以被编译器优化(如编译器重排序的优化). (synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性)所以根据before-happen原则,也可以保证有序性。

    5)volatile是变量修饰符,仅能用于变量,而synchronized是一个方法或块的修饰符。
     volatile本质是在告诉JVM当前变量在寄存器中的值是不确定的,使用前,需要先从主存中读取,因此可以实现可见性。而对n=n+1,n++等操作时,volatile关键字将失效,不能起到像synchronized一样的线程同步(原子性)的效果。
 


 Java  Synchronize 和 Lock 的区别与用法

    java为此也提供了2种锁机制,synchronized和lock。

一、synchronized和lock的用法区别
 
synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
 
lock:需要显示指定起始位置和终止位置。一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类做为对象才能保证锁的生效。且在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
 
用法区别比较简单,这里不赘述了,如果不懂的可以看看Java基本语法。

 

二、synchronized和lock性能区别
 
synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多

但是到了Java1.6,发生了变化。synchronized在语义上很清晰,可以进行很多优化,有适应自旋,锁消除(消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。),锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地。
 
说到这里,还是想提一下这2中机制的具体区别。

synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
 
而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compareand Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。
 
现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。

三、synchronized和lock用途区别
 
synchronized原语和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用ReentrantLock,特别是遇到下面几种需求的时候。

*(持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。注意:synchronized不提供中断)
 
1.某个线程在等待一个锁的控制权的这段时间需要中断
2.需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程(所谓notify也就是notify等待这个加锁对象的锁池(entryset)里面的线程
3.具有公平锁功能,每个到来的线程都将排队等候
 
先说第一种情况,ReentrantLock的lock机制有2种,忽略中断锁和响应中断锁,这给我们带来了很大的灵活性。

比如:如果A、B2个线程去竞争锁,A线程得到了锁,B线程等待,但是A线程这个时候实在有太多事情要处理,就是一直不返回,B线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候ReentrantLock就提供了2种机制,

第一,B线程中断自己(或者别的线程中断它),但是ReentrantLock不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此)(也就是说,B在我的锁池里,B如果想要中断自己不想等待这把锁,那么需要经过我的响应,比如做一些引用计数的操作,才能中断);第二,B线程中断自己(或者别的线程中断它),ReentrantLock处理了这个中断,并且不再等待这个锁的到来,完全放弃。
 
这里来做个试验,首先搞一个Buffer类,它有读操作和写操作,为了不读到脏数据,写和读都需要加锁,我们先用synchronized原语来加锁

我们期待“读”这个线程能退出等待锁,可是事与愿违,一旦读这个线程发现自己得不到锁,就一直开始等待了,就算它等死,也得不到锁,因为写线程要21亿秒才能完成 T_T ,即使我们中断它,它都不来响应下,看来真的要等死了。这个时候,ReentrantLock给了一种机制让我们来响应中断,让“读”能伸能屈,勇敢放弃对这个锁的等待。我们来改写Buffer这个类,就叫BufferInterruptibly吧,可中断缓存。

这次“读”线程接收到了lock.lockInterruptibly()中断,并且有效处理了这个“异常”。

 

至于第二种情况,ReentrantLock可以与Condition的配合使用,Condition为ReentrantLock锁的等待和释放提供控制逻辑。
 

Lock lock = new ReentrantLock();
Condition cond = lock.newCondition();


例如,使用ReentrantLock加锁之后,可以通过它自身的Condition.await()方法释放该锁,线程在此等待Condition.signal()方法,然后继续执行下去。await方法需要放在while循环中,因此,在不同线程之间实现并发控制,还需要一个volatile的变量,boolean是原子性的变量。

调用spillDone.await()时可以释放spillLock锁,线程进入阻塞状态,而等待其他线程的spillDone.signal()操作时,就会唤醒线程,重新持有spillLock锁。
 
这里可以看出,利用lock可以使我们多线程交互变得方便,而使用synchronized则无法做到这点。



 
最后呢,ReentrantLock这个类还提供了2种竞争锁的机制:公平锁(先来后到原则,估计就是一个队列性质)和非公平锁(不分先后,估计就是一个类似于set)。

这2种机制的意思从字面上也能了解个大概:即对于多线程来说,公平锁会依赖线程进来的顺序,后进来的线程后获得锁。而非公平锁的意思就是后进来的锁也可以和前边等待锁的线程同时竞争锁资源。对于效率来讲,当然是非公平锁效率更高,因为公平锁还要判断是不是线程队列的第一个才会让线程获得锁。

这两种排队策略供我们自己选择。
 


总结

  (1)synchronized与volatile的比较

         1)volatile比synchronized更轻量级。

         2)volatile没有synchronized使用的广泛。

         3)volatile不需要加锁,比synchronized更轻量级,不会阻塞线程。

         4)从内存可见性角度看,volatile读相当于加锁,volatile写相当于解锁。

         5)synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性。

         6)volatile本身不保证获取和设置操作的原子性,仅仅保持修改的可见性。但是java的内存模型保证声明为

volatile的long和double变量的get和set操作是原子的。

(2)补充1

         共享数据的访问权限都必须定义为private。一般是考虑安全性,对数据提供保护,可以通过set()方法赋值,再

通过get()方法取值,这就是java封装的思想。

        Java中对共享数据操作的并发控制是采用加锁技术

        Java中没有提供检测与避免死锁的专门机制,但应用程序员可以采用某些策略防止死锁的发生。

        final也可以保证内存可见性。

 (3)补充2

        x86系统对64位(long、double)变量的读写可能不是原子操作.

volatile本身不保证获取和设置操作的原子性,仅仅保持修改的可见性。但是java的内存模型保证声明为volatile的long和double变量的get和set操作是原子的.两次操作变一次操作!

        因此:Java内存模型允许JVM将没有被volatile修饰的64位数据类型的读写操作划分为两次32位的读写操作来运行。

        导致问题:有可能会出现读取到半个变量的情况。

        解决方法:加volatile关键字。

       不过现在也有很多系统实现将没有使用volatile的64位long\double的变量也设置为原子操作

 (4)一个问题

        即使没有保证可见性的措施,很多时候共享变量依然能够在主内存和工作内存间得到及时的更新?

          是的,不过要看当前线程并发量

        答:一般只有在短时间内高并发的情况下才会出现变量得不到及时更新的情况,因为CPU在执行时会很快地刷新

缓存,所以一般情况下很难看到这种问题。

        慢了不就不会刷新了。。。CPU运算快的话,在分配的时间片内就能完成所有工作:工作内从1->主内存->工作

内存2,然后这个线程就释放CPU时间片,这样一来就保证了数据的可见性。如果是慢了话CPU强行剥夺该线的资

源,分配给其它线程,该线程就需要等待CPU下次给该线程分配时间片,如果在这段时间内有别的线程访问共享变

量,可见性就没法保证了。

选择比较

除非需要使用 ReentrantLock 的高级功能(中断、公平锁)否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。

猜你喜欢

转载自blog.csdn.net/Fly_as_tadpole/article/details/86286193