我们测试一个同步方法:
然后反编译 class文件,可以看到:
其中的方法标识:
- 代表public修饰
- 表示是静态方法
- 指明该方法为同步方法。
这个时候我们可以理解《深入理解java虚拟机》里,对于同步方法底层实现的描述如下:
方法级的同步是隐式的。 无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否被声明为同步方法。(静态方法也是如此)
- 当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程(Monitor),然后才能执行方法,最后当方法完成 (无论是正常完成还是非正常完成)时释放管程。
- 在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。
- 如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法边界之外时自动释放。
测试一段同步代码:
然后反编译 class 文件:
可以看到,在指令方面多了关于 Monitor 操作的指令,或者和上一种修饰方法的区别来看,是显式的用指令去操作管程(Monitor)了。
同理,这个时候我们可以理解《深入理解java虚拟机》里的描述如下:
同步一段指令集序列的情况。Java虚拟机的指令集中有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义。(monitorenter 和 monitorexit 两条指令是 C 语言的实现)正确实现 synchronized 关键字需要 Javac 编译器与 Java 虚拟机两者共同协作支持。Monitor的实现基本都是 C++ 代码,通过JNI(java native interface)的操作,直接和cpu的交互编程。
- 悲观锁:独占锁,会导致其他所有需要所的线程都挂起,等待持有所的线程释放锁,就是说它的看法比较悲观,认为悲观锁认为对于同一个数据的并发操作,一定是会发生修改的。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。比如前面讲过的,最传统的 synchronized 修饰的底层实现,或者重量级锁。(但是现在synchronized升级之后,已经不是单纯的悲观锁了)
- 乐观锁:每次不是加锁,而是假设没有冲突而去试探性的完成操作,如果因为冲突失败了就重试,直到成功。比如 CAS 自旋锁的操作,实际上并没有加锁。
- 公平锁。公平锁是指多个线程按照申请锁的顺序来获取锁。java 里面可以通过 ReentrantLock 这个锁对象,然后指定是否公平
- 非公平锁。非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。使用 synchronized 是无法指定公平与否的,他是不公平的。
- 独占锁也叫排他锁,是指该锁一次只能被一个线程所持有。对 ReentrantLock 和 Sychronized 而言都是独占锁。
- 共享锁:是指该锁可被多个线程所持有。对 ReentrantReadWriteLock 而言,其读锁是共享锁,其写锁是独占锁。读锁的共享性可保证并发读是非常高效的,读写、写读、写写的过程都是互斥的。
独占锁/共享锁是一种广义的说法,互斥锁/读写锁是java里具体的实现。
上面我们讲到了,synchronized 关键字下层的锁,是在 jvm 层面实现的,而后来在 jdk 5 之后,在 juc 包里有了显式的锁,Lock 完全用 Java 写成,在java这个层面是无关JVM实现的。虽然 Lock 缺少了 (通过 synchronized 块或者方法所提供的) 隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种 synchronized 关键字所不具备的同步特性。
Lock 是一个接口,实现类常见的有:
- 重入锁()
- 读锁()
- 写锁()
实现基本都是通过聚合了一个同步器( 缩写为 )的子类来完成线程访问控制的。
我们可以看看:
这里面的各个锁实现了 Lock 接口,然后任意打开一个类,可以发现里面的实现,Lock 的操作借助于内部类 Sync,而 Sync 是继承了 AbstractQueuedSynchronizer类的,这个类就是很重要的一个 AQS 类。
整体来看,这些类的关系还是挺复杂:
不过一般的直接使用还是很简单,比如 new 一个锁,然后在需要的操作之前之后分别加锁和释放锁。
在 Lock 接口里定义的方法有 6 个,他们的含义如下:
接下来我们们就分步看看常用的各种类。
队列同步器 AbstractQueuedSynchronizer(以下简称同步器或者 AQS),是用来构建锁或者其他同步组件的基础框架,它使用了一个 int 成员变量表示同步状态,通过内置的 FIFO 队列来完成资源获取线程的排队工作。
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的 3 个方法来进行操作,因为它们能够保证状态的改变是安全的。
这三个方法分别是:
- ,// 获取当前同步状态
- ,// 设置当前同步状态
- ,// 使用 CAS 设置当前状态,该方法能够保证状态设置的原子性
子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件 (ReentrantLock、 ReentrantReadWriteLock 和 CountDownLatch 等)。
AQS 定义的三类模板方法;
- 独占式同步状态获取与释放
- 共享式同步状态获取与释放
- 同步状态和查询同步队列中的等待线程情况
同步器的内置 FIFO 队列,从源码里可以看到,Node 就是保存着线程引用和线程状态的容器。
- 每个线程对同步器的访问,都可以看做是队列中的一个节点(Node)。
- 节点是构成同步队列的基础,同步器拥有首节点 (head) 和尾节点 (tail);
- 没有成功获取同步状态的线程将会成为节点加入该队列的尾部。
- 首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。
因为源码很多,这里暂且不去分析具体的实现。
- 重入锁 ReentrantLock,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。
- 除此之外,该锁的还支持获取锁时的公平和非公平性选择。
ReentrantLock 支持公平与非公平选择,内部实现机制为:
- 内部基于 实现一个公平与非公平公共的父类 ,(在代码里,Sync 是一个内部类,继承 AQS)用于管理同步状态;
- 继承 用于处理公平问题;
- 继承 用于处理非公平问题。
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.bianchenghao6.com/h6javajc/5426.html