Java并发编程序列之线程间通讯-synchronized关键字-volatile关键字

1,356 阅读5分钟

Java并发编程序列之线程间通讯-synchronized关键字-volatile关键字

Hello,大家好,今天开始Java并发编程序列的第二篇,该篇主要讲解如何使用synchronized实现同步,以及volatile关键字的作用,最后讲解线程间如何进行通讯。文章结构:

  1. synchronized关键字
  2. volatile关键字
  3. 线程间通讯(wait,notify)

1. synchronized关键字

synchronized关键字的用法我就不多说了。网上烂大街,N年前的技术了。我先说下结论,然后说下底层实现原理:

  1. 对于普通方法,锁是当前实例对象。
  2. 对于static方法,锁是当前类的Class对象。
  3. 对于同步代码块,锁是括号里配置的对象。
  4. 抛出异常会自动释放锁。
  5. synchronized锁是可以重入的。

实现原理:

synchronized同步代码块原理很简单,两个字节码指令,一个monitorenter,一个monitorexit,无论多少个线程,一次只能一个进入到monitorenter,其他的进入BLOCKED状态阻塞。
synchronized同步方法使用的ACC_synchronized标志位,其实是一样的效果。

然后来张效果图:

2. volatile关键字

volatile关键字也比较简单,还是老样子,说下结论,讲下原理:

  1. volatile保证多线程共享变量可见性。
  2. 禁止指令重排序
  3. 不保证原子性

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时专门讲。还是老样子,先列知识点,再讲原理:

  1. wait,notify这类方法是定义在Object上的。
  2. 调用wait或者notify时必须首先通过synchronized关键字获取到对象的锁。
  3. 调用wait方法时会释放锁。调用notify时不会释放锁,代码走完才会释放锁。
  4. join方法内部使用wait.join方法可以实现简单的线程等待。
  5. wait和sleep遇到interrept会抛出异常。
  6. ThreadLocal保存线程隔离数据。

注意图中两条触发线:

  • 调用notify时,线程从等待队列转移到同步队列。此时线程从Waiting/TIMED_WAITING状态到BLOCKED状态。
  • 代码走出synchronized时,同步队列的线程开始竞争锁。

然后丢一个典型的 等待/通知的模型:

等待方:
synchronized(对象) {
    while(条件不满足) {
    对象.wait();
    }
    对应的处理逻辑
}

通知方:
synchronized(对象) {
    改变条件
    对象.notifyAll();
}

注意点:

  • 一个对象拥有一个等待队列和同步队列
  • synchronized锁的对象一致才会互斥。

结语

好了,其实JUC之前的线程的知识并不难,所以我写的也不是很细。不过重点都出来了。Have a good day .后期讲JUC的时候可就没这么Easy了。