当前位置: 代码迷 >> 综合 >> java多线程之volatile、synchronize、reentrantlock浅显总结
  详细解决方案

java多线程之volatile、synchronize、reentrantlock浅显总结

热度:25   发布时间:2023-10-28 10:20:43.0

目录

1.volatile

     1.1 volatile 的特性

     1.2 volatile 的实现原理

2.synchronize

     2.1 synchronize特性

     2.2 synchronize实现原理

3.reentrantlock

4.synchronize和volatile的区别

5.synchronize和reentrantlock异同点


1.volatile

     1.1 volatile 的特性

  • 保证了不同线程对同一个变量进行操作时的可见性。
  • 禁止进行指令重排序。

     1.2 volatile 的实现原理

       volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现。内存屏障:又称内存栅栏,是一个 CPU 指令。作用:

  1. 保证特定操作的执行顺序。
  2. 影响某些数据(或则是某条指令的执行结果)的内存可见性。

       在程序运行时,为了提高执行性能,编译器和CPU会对指令进行重排序,JMM 为了保证在不同的编译器和 CPU 上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器CPU:不管什么指令都不能和这条 Memory Barrier 指令重排序。java中的volatile就是基于Memory Barrier实现的。为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存后再进行操作,但操作完不知道何时会写到内存。如果对声明了 volatile 的变量进行写操作,JVM 就会向处理器发送一条 lock 前缀的指令,将这个变量所在缓存行的数据写回到系统内存。为了保证各个处理器的缓存是一致的,实现了缓存一致性协议(MESI),每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。volatile 变量通过这样的机制就使得每个线程都能获得该变量的最新值。

2.synchronize

     2.1 synchronize特性

       synchronized:可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

  1.  普通同步方法,锁是当前实例对象
  2.  静态同步方法,锁是当前类的class对象
  3.  同步方法块,锁是括号里面的对象

     2.2 synchronize实现原理

     java对象布局:

  1. 对象头
  2. 实例变量
  3. 对齐填充

     java对象头

Mark WordKlass pointer两部分组成。以64位JVM为例:

Mark Word:存储了同步状态、标识、hashcode、GC状态等等。

       unused:未被使用的位

       identity_hashcode:对象标识Hash码,采用延迟加载技术。当对象使用HashCode()计算后,并会将结果写到该对象头中。当对象被锁定时,该值会移动到线程Monitor中。

       thread:持有偏向锁的线程ID和其他信息。

       age:Java GC标记位对象年龄。

       ptr_to_lock_record:指向栈中锁记录的指针。

       ptr_to_heavyweight_monitor:指向线程Monitor的指针。

       lock:  锁状态标记位,该标记的值不同,整个mark word表示的含义不同。

       biased_lock:偏向锁标记,为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。

       Klass pointer:这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。如果应用的对象过多,使用64位的指针将浪费大量内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位。 那怎么判断对象是否持有锁呢?反汇编之后会看到,出现了两个字符:monitorentermonitorexit

       Monitor

       什么是Monitor?我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

       monitorenter:

       进入一个对象的monitor。且当一个monitor被持有后,它就会处于锁定状态,线程执行到monitorenter指令后,将会尝试获取对象所对应的monitor的所有权。过程如下:

  1. 如果monitor的计数器为0,则线程进入成功,将计数器值设置为1,就意味着当前线程是monitor的所有者。
  2. 如果线程已经占有了当前monitor,则直接进入,将计数器加1.
  3. 如果monitor已经被其他线程占有,那么当前线程被阻塞,直到monitor的计数器变成0,重新尝试获取monitor的所有权。

       monitorexit:

       退出一个对象的monitor。执行monitorexit指令的线程必须是对象对应的monitor的所有者,执行时,线程把monitor的计数器值减1,如果减1后计数器值为0,那线程退出monitor,不再是这个monitor的拥有者,其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。

       总结:synchronize加锁原理是通过检测monitor计数器的值,来确定当前线程是否能获取该对象所有权,然后改变Mark Word中其中两位对当前对象锁的类型进行标记。

3.reentrantlock

       为了保证同步的安全性,java中除了synchronized关键字,java并发包中java.util.concurrent.locks中的ReentrantLock和ReentrantReadWriteLock也是常用的锁实现。ReentrantLock基于抽象类AbstractQueuedSynchronizer(AQS)实现。

构造方法:

//默认非公平锁
public ReentrantLock() {sync = new NonfairSync();}public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}

从构造器上可以看出,ReentrantLock有公平锁和非公平锁两种机制。

  1. 公平锁:多个线程之间讲究先来后到。类似于排队,后面来的线程依次排在队列最后。
  2. 非公平锁:进行锁的争抢。抢到就执行,没抢到就阻塞。等待获得锁的线程释放后,再参与竞争。

       写到这里发现reentrantlock具体原理好复杂,现在的知识量看不懂。。。以后补上。。。

4.synchronize和volatile的区别

       1)volatile是变量修饰符,而synchronized则作用于一段代码或方法。

       2)volatile只是在线程内存和主内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值, 显然synchronized要比volatile消耗更多资源。

       3)volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

       4)volatile保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以保证可见性。

       5)volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

5.synchronize和reentrantlock异同点

       相同点:

  1. 都是用来协调多线程对共享对象、变量的访问
  2. 都是可重入锁,同一线程可以多次获得同一个锁
  3. 都保证了可见性和互斥性

       不同点: 

  1. ReentrantLock 显示的获得、释放锁,synchronized 隐式获得释放锁
  2. ReentrantLock 可响应中断、可轮回,synchronized 是不可以响应中断的
  3. ReentrantLock 是 API 级别的,synchronized 是 JVM 级别的
  4. ReentrantLock 可以实现公平锁
  5. ReentrantLock 通过 Condition 可以绑定多个条件
  6. 底层实现不一样, synchronized 是同步阻塞,使用的是悲观并发策略,lock 是同步非阻塞,采用的是乐观并发策略
  7. Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现。
  8. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。
  9. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
  10. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
  11. Lock 可以提高多个线程进行读操作的效率,既就是实现读写锁等。

 

借鉴博文:https://www.jianshu.com/p/ccfe24b63d87

                  https://www.cnblogs.com/LemonFive/p/11246086.html

                  https://www.jianshu.com/p/3d38cba67f8b

                  https://blog.csdn.net/m0_37700275/article/details/83151850

  相关解决方案