今日分享开始啦,请大家多多指教~
unlock方法
解锁调用的就是unlock方法
可以看到其调用的还是内部类sync的方法,而且可以看到这是一个无返回值的方法。
并且传入了一个为1的参数
release方法
可以看到,其调用的是AQS里面的release方法
步骤如下:
tryRelease方法
可以看到这个方法是AQS里面的一个未实现的方法,实现这个方法有ReentrantLock与ReentrantReadWriteLock。
所以,具体的实现肯定是ReentrantLock的。
实现的源码如下所示
步骤如下:
如果不拥有锁就会 错,因为锁并不是自己的,没有资格释放。
如果是0,让free变量为true,并且将锁记录占用自己的线程为null。
接下来我们返回到release方法
下面的判断是这样的
从上面可以看到,如果锁没被占用了,那么tryRelease方法就会返回True,那么就会进行下面的判断:
1.先记录一下当前线程队列的头结点;
2.判断头结点是否不为空,而且waitStatus状态是否不为0(0代表线程正常运行,-1代表被挂起)。
3.如果头结点为空,或者头结点状态为0。
unparkSuccessor方法
这个方法是唤醒被挂起的头结点,并且还要去整理线程队列。
这个方法也是AQS里面的
源码如下
步骤如下
1.判断上一个执行完成的结点的waitStatus状态。
2.接下来,唤醒后面的线程,其实是去获取最先的一个未被取消的线程。
此时就要进行遍历,从尾进行遍历整个队列,去找到最先的一个被挂起的线程(不包括新插入进来正在尝试获取锁的线程,也就是状态为0)。
3.接下来,让最先的一个未被取消的线程重新启动。
这里这样做的原因是,前面提到过,在线程去抢锁的过程中,CAS第一次时,会认为前面的一个线程被挂起了,将前面线程的waitStatus改为-1,CAS第二次,如果前面的线程仍然为-1,代表前面的线程仍然被挂起(只有在前面的线程释放锁的时候,才会改变waitStatus为0),所以自己也会挂起。
所以,个人认为:如果一个线程执行太久了,后面的线程都被是有可能都被挂起的,那么就需要一个一个去唤醒他,就完全变成了一个重量级锁。
在Java里面,锁除了让临界区互斥外,还可以让释放锁的线程向获取同一个锁的线程发送消息。
锁的释放和获取的内存语义
接下来,看一下线程在锁的释放和获取究竟对内存是怎样的操作。
锁的释放
当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
锁的获取
当线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得被监视器monitor保护的临界区代码必须从主内存中读取共享变量(前面已经提到过,synchroniced会在字节码上加上monitor与monitor exit)。
这两个过程相当于线程A向线程B发送了通信消息,让B读取最新的共享变量。
与volatile的读写内存语义比较
通过与前面学习的volatile的读写内存语义比较:
锁内存语义的实现
锁内存语义的实现其实就是ReentrantLock的底层实现。
final这个修饰可以加在类、方法、变量上
但其实final也是可以解决一些并发重排序问题的。
final域的重排序规则
final域也有自己的重排序规则
这两个规则分别对应final的读写的重排序规则
写final域的重排序规则
写final域的重排序规则其实就是上面的,初始化不可以与引用赋值发生重排序,必须先初始化,然后再进行引用赋值,但如果对于普通变量来说,也就是普通域,很可能会发生这两个步骤的重排序。
这个规则可以确保,在对象引用为任意线程可见之时,对应的final域已经被正确初始化了。
写final域的重排序规则是使用内存屏障来实现的。
编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障,这个屏障禁止了处理器将final域的写命令重排序到return之后,也就是构造函数之外。
读final域的重排序规则
读final域的重排序规则就是,初次读对象引用与初次读对象里面的final引用是不可以发生重排序的,必须先读对象引用然后再读final引用。
这个规则可以确保,在读一个final域之前,一定会先读包含这个final域的对象的引用,这是因为final域是依赖于对象的。
读final域的重排序规则也是使用内存屏障来实现的。
编译器会在都final域操作的前面插入一个LoadLoad屏障,确保前面如果有初次读对象操作时,要先读对象,然后再读final域。
final域的引用类型
如果final域的变量是一个引用类型,那么对于读的重排序规则是没有变的,但写的重排序规则会增加多一条。
在构造函数内对一个final引用的对象的成员域写入时,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作是不能重排序的。
即构造函数在初始化final引用对象时,构造函数之外的其他地方可能正在修改这个final引用对象,那么这两个操作是不可以发生重排序的,也就是一定要先构造函数初始化完final引用对象后才可以允许其他地方进行修改。
所以总的final域的引用类型的写规则如下:
为什么final引用不能从构造函数内溢出?即发生重排序
我个人觉得,可能是因为final的引用每次改变都会成为一个新的对象,所以必须要确保改变是要按照顺序的。
如果从构造函数内溢出,很有可能这个final引用还没从构造函数里面初始化好,外面的其他线程就会将其修改,就会导致了顺序扰乱现象。
final的底层实现
前面已经对此提到过
下面我们来谈一下双重检查锁定与延迟初始化
volatile解决重排序问题
在前面学习懒汉模式实现单例模式的时候,我们已经使用过下面的这一套优化流程了
1.代码块中加锁判断单例对象是否已经初始化。
2.再进行判断多一次单例对象是否已经初始化,来判断单例模式对象是否已经初始化,因为可能同时有多个线程判断出单例对象未初始化,这时上一把锁,让一个线程进去初始化了,初始化了之后,应该再让其他线程再判断一次,看前面一个线程初始化没有(感觉这个方案可以解决一下缓存雪崩)。
3.给单例对象加volatile修饰,防止其构造指令出现重排序。
代码如下(instance记得要被volatile修饰)
类初始化解决重排序问题
上面使用volatile可以解决重排序问题,在这里也是可以用类来解决重排序问题的。
JVM在类的初始化阶段时,即在Class被加载后,且正在被线程使用之前,会执行类的初始化(初始化静态变量),在执行类的初始化期间,JVM会去获取一个锁,这个锁可以同步多个线程对同一个类的初始化,总的来说,就是利用类的初始化这个机制,让实例变量初始化的时候可以发生重排序,但其他线程看不到这个重排序,必须要等待完成整个类初始化过程才可以被访问这个类。
还是以单例模式为例
要用类初始化来实现单例模式,其实就是使用静态内部类
当多个线程调用getInstance时候,会发生阻塞(JVM获得锁),只有一个线程可以去加载初始化这个InnerSIngle类,然后该线程初始化里面的single静态变量。
类初始化过程中的同步机制
下面来看一下JVM是怎么保证类初始化过程中的同步
第一阶段
第一阶段是:通过在Class对象上进行同步(即获取Class对象的初始化锁),来控制类或者接口的初始化,当多个线程初始化同一个类的时候,只有一个线程可以获得这个Class对象的初始化锁,其他线程会一直等待获取锁的线程去释放锁。
第二阶段
第二阶段是:抢到类的初始化锁的线程去执行初始化,未抢到锁的线程在初始化锁对应的condition上等待,相当于是等待抢到类的初始化锁的线程去完成初始化动作。
初始化的动作先简单理解成是执行类的静态初始化代码和初始化类中声明的静态字段,底层方面的知识是涉及到JVM相关知识的。
过程如下所示
第三阶段
第三阶段是:获取到锁的线程去唤醒在condition中等待的所有线程,提醒这些线程,类已经初始化好了。
第四阶段
第四阶段是:被唤醒的线程结束自己对类的初始化处理。
第五阶段
第五阶段是:后面的线程如果再对类进行初始化,那么只会简单地获取锁,发现已经被初始化好了,释放锁,直接获取类。
volatile与类初始化的两个方案之间存在什么区别?
区别如下
更多资料,私信回复【1】……
今日份分享已结束,请大家多多包涵和指点!
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!