Java并发编程序列之线程间通讯-synchronized关键字-volatile关键字
Hello,大家好,今天开始Java并发编程序列的第二篇,该篇主要讲解如何使用synchronized实现同步,以及volatile关键字的作用,最后讲解线程间如何进行通讯。文章结构:
- synchronized关键字
- volatile关键字
- 线程间通讯(wait,notify)
1. synchronized关键字
synchronized关键字的用法我就不多说了。网上烂大街,N年前的技术了。我先说下结论,然后说下底层实现原理:
- 对于普通方法,锁是当前实例对象。
- 对于static方法,锁是当前类的Class对象。
- 对于同步代码块,锁是括号里配置的对象。
- 抛出异常会自动释放锁。
- synchronized锁是可以重入的。
实现原理:
synchronized同步代码块原理很简单,两个字节码指令,一个monitorenter,一个monitorexit,无论多少个线程,一次只能一个进入到monitorenter,其他的进入BLOCKED状态阻塞。 synchronized同步方法使用的ACC_synchronized标志位,其实是一样的效果。然后来张效果图:
2. volatile关键字
volatile关键字也比较简单,还是老样子,说下结论,讲下原理:
- volatile保证多线程共享变量可见性。
- 禁止指令重排序
- 不保证原子性
1. volatile保证多线程共享变量可见性。
先看下JMM内存模型。
再来看下volatile达到的效果:2. 禁止指令重排序
int a = 0;bool flag = false;public void write() { a = 2; //1 flag = true; //2}public void multiply() { if (flag) { //3 int ret = a * a;//4 }}复制代码
这个代码,1和2步骤不一定是先执行1,后执行2.有可能先执行2,再执行1,多线程环境下,会导致ret的值为0(不是预期的4),达不到预期效果。所以我们要避免重排序。把a变量设置为volatile变量,这样就是顺序执行了,先执行1,再执行2.
3. 不保证原子性
这个更easy了。比如有一个共享volatile变量value=0;两个线程同时读取出去,然后都执行++操作为1,赋值的时候A线程赋值为1了,B线程又去赋值,把之前的1给覆盖为1了。所以两个线程++后的结果不是预期的2,而是1.
说下volatile的底层原理: 其实就是在volatile变量的前面加上了一个lock汇编指令,有如下效果:
- 重排序时不能把后面的指令重排序到内存屏障之前的位置
- 使得本CPU的Cache写入内存(相当于直接写入主内存)
- 写入动作也会引起别的CPU或者别的内核无效化其Cache(读取的时候去主内存读取),相当于让新写入的值对别的线程可见。
说下volatile的适用场景:
- 多线程共享变量只读操作。根据只读变量当做标志位。
- 防止重排序
上面提到了volatile不保证原子性,so,怎么办?其一,比较粗暴的加锁.其次就是比较出名的Cas算法了。这里顺带说一下Cas算法。Cas算法的意思就是Compare and set .意思就是,在设置一个变量的值的时候,先拿旧值和它比一下,如果一样,再set.这样就避免了多线程间的脏写。比如一个变量值为1 ,被A线程更改成了2,B线程在更改时判断它之前拿到的1和现在的2不一样,就不进行写操作了。JVM中的Cas操作是利用了一个处理器指令CMPXCHG。 自旋Cas:不断的获取,进行Cas操作。只到成功: JUC中提供了类似于AtomicInteger的原子类,提供了compareAndSet方法,来支持Cas算法。需要注意的是AutomicReference可以变向的支持多变量原子操作. 下面我来写一个能够保证多线程安全的原子++操作的方法:
AutomicInteger safeI =0;private void safeAddOne(){ for(;;){ int i =safeI.get(); boolean suc = safeI.compareAndSet(i,++i); if(suc){ break; } }}复制代码
这个方法,无论被多个个线程并发调用,最终的结果都是依次+1后的结果,不会存在覆盖。
3. 线程间通讯(wait,notify)
线程间通讯wait,notify也是必须需要掌握的。这一篇只讲wait和notify通讯。其实JUC之后有更好的通讯方式。后期讲JUC时专门讲。还是老样子,先列知识点,再讲原理:
- wait,notify这类方法是定义在Object上的。
- 调用wait或者notify时必须首先通过synchronized关键字获取到对象的锁。
- 调用wait方法时会释放锁。调用notify时不会释放锁,代码走完才会释放锁。
- join方法内部使用wait.join方法可以实现简单的线程等待。
- wait和sleep遇到interrept会抛出异常。
- ThreadLocal保存线程隔离数据。
注意图中两条触发线:
- 调用notify时,线程从等待队列转移到同步队列。此时线程从Waiting/TIMED_WAITING状态到BLOCKED状态。
- 代码走出synchronized时,同步队列的线程开始竞争锁。
然后丢一个典型的 等待/通知的模型:
等待方:synchronized(对象) { while(条件不满足) { 对象.wait(); } 对应的处理逻辑}通知方:synchronized(对象) { 改变条件 对象.notifyAll();}复制代码
注意点:
- 一个对象拥有一个等待队列和同步队列
- synchronized锁的对象一致才会互斥。
结语
好了,其实JUC之前的线程的知识并不难,所以我写的也不是很细。不过重点都出来了。Have a good day .后期讲JUC的时候可就没这么Easy了。