Java 并发编程CAS、volatile、synchronized原理详解

CAS(CompareAndSwap)

什么是CAS?

在Java中调用的是Unsafe的如下方法来CAS修改对象int属性的值(借助C来调用CPU底层指令实现的):

    /**
     * 
     * @param o 对象所在类本身的对象(一般这里是对一个对象的属性做修改,才会出现并发)
     * @param offset 属性在对象中的相对偏移量位置(获取偏移量也是通过unsafe的⼀个⽅法: objectFieldOffset(Fieldfield)来获取属性在对象中的偏移量;)
     * @param expected 修改前期待的值,原来的值和修改之前的值需要一致
     * @param x 修改的目标值
     * @return 修改是否成功
     */
    public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);

对象的引用进行比较后交换,交换成功则返回true,交换失败返回false

为什么需要CAS?

CAS能够保证性能的同时保证数据的可见性,可以说是一个非阻塞的轻量级锁,性能高于synchronized

CAS 原理

执行逻辑类似:

if (this == expect) {
	this = update
	return true;
} else {
	return false;
}

交换过程完全是原子的,基本流程如下:

  1. 在CPU上计算完结果后,都会对比内存的结果是否还是原先的值
  2. 如果不是原先的值,则认为已被其它线程修改,不能替换
  3. 如果是原先的值,则认为没有其它线程去修改,则可以修改,因为变量是volatile类型,所以最终写入的数据会被其它线程看到,所以一个线程修改成功后,其它线程就发现自己修改失败了重新尝试修改

这明明是好几步的操作,怎么会是原子操作呢?
整体过程:Java通过JNI来调用native方法,而native方法的实现是C,C调用的是CPU底层指令实现。

在CPU底层指令中,是通过加锁的方式实现,实现方式有如下三种:

  1. 处理器自动保证基本内存操作的原子性:一个处理器访问某一个字节时,其它处理器不能访问这个字节的内存地址
  2. 使用总线锁保证原子性:当一个处理器在总线上输出LOCK#信号时,其它处理器的请求将被阻塞,那么该处理器可以独占使用共享内存
  3. 缓存锁保证原子性:频繁使用的内存会缓存在处理器的L1、L2、L3高速缓存中,那么原子操作可以直接在处理器内部缓存中进行,不需要声总线锁。当一个处理器修改缓存行中的i变量使用缓存锁定,那么其它处理器就不能同时缓存i变量的缓存行

CAS 优缺点

优点:

  1. 高性能:无锁的方式实现原子操作
  2. 使用简单:调用compareAndSwapxx()方法即可实现

缺点:

  1. ABA问题:由于CAS是检测值是否被改变,但一个变量的值原来是A,然后修改成B,然后又修改成A,这是实际上变量已被修改,但CAS检查的时候会发现它的值没有发生变化,还是能够被正常的CAS。解决思路就是通过对变量增加版本号,没修改一次版本号加1,所有原本ABA就是1A-2B-3A。在atomic包提供了一个类AtomicStampedReference就是采用类似的思路解决ABA问题
  2. 循环时间长开销大:在CAS更新失败会进入自旋,一旦长时间更新失败,就会占用较多的CPU
  3. 只能保证一个共享变量的原子操作:在只有一个变量的时候,可以使用CAS来保证原子操作,但如果有多个变量,CAS无法保证操作的原子性。解决方案就是用锁来保证多个变量的原子性;也可以使用位的方式,将多个变量合并成一个变量进行CAS操作,如x&F0|y&0F

CAS 应用

我们拿java.util.concurrent.atomic.AtomicInteger举例:

public class AtomicInteger extends Number implements java.io.Serializable {

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            // 属性在这个对象⾥⾯的相对偏移量位置
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) {
            throw new Error(ex);
        }
    }

    // 要更新的变量
    private volatile int value;

    /**
     * Creates a new AtomicInteger with the given initial value.
     *
     * @param initialValue the initial value
     */
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

    /**
     * 如果当前值是期望值expect,则原子地将值设置为给定的更新值update
     *
     * @param expect 期望值
     * @param update 更新值
     * @return 如果true表示更新成功,false表示实际值不等于期望值
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    
    /**
     * 将当前值原子的加1
     *
     * @return the 加1之前的值
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    ...
}

java.util.concurrent.atomic包中Atomic*基本都是使用相同的思路来实现原子操作的

volatile

volatile的主要作用是:

  1. 可见性:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对其它线程来说是立即可见的
  2. 有序性:禁止指令重排序

volatile 可见性

可见性表示一个变量的值被更新后,是否能够在每个线程中可见

public class ExampleVisibility {
    private boolean stop = false;

    private void stop() {
        this.stop = true;
    }

    private void run() {
        while (!stop) {
            // run
        }
        System.out.println("stop!!");
    }

    public static void main(String[] args) throws InterruptedException {
        ExampleVisibility self = new ExampleVisibility();
        new Thread(() -> {
            self.run();
        }).start();

        Thread.sleep(1000 * 4);

        new Thread(() -> {
            // 停止while
            self.stop();
        }).start();

    }
}

执行多次,你会发现有的时候无法停止while,也就是stop一直为false

问题原因:
在cpu中有多级cache,如果每次在内存中取效率不高,所有cpu中有自己的cache以提高效率(在缓存中,每个变量在缓存行上都有一个2bit的状态,那就是CPU EMSI协议)
Java 并发编程CAS、volatile、synchronized原理详解_第1张图片
有如下两种可能原因:

  1. 线程将变量stop修改后没有更新到主存中
  2. stop在cpu一级缓存或二级缓存有缓存,当线程要读取stop变量的时候发现已存在缓存中,没有再去主存中重新读取stop变量

解决方案:
在stop变量中增加volatile关键字保证可见性

private volatile boolean stop = false;

volatile 有序性

volatile在一定程度上保证有序性。其中主要就是通过禁止指令重排的方式实现有序性
指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果是正确的,但是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现问题。

代码转换为机器执行的指令所经过的步骤:
在这里插入图片描述
流水线优化问题:
Java 并发编程CAS、volatile、synchronized原理详解_第2张图片
在cpu内部不同的操作是由不同的硬件来做,比如专门ADD操作的硬件、INC的硬件、MOV的硬件,它们之间同时执行多个指令,上一级处理完成流到下一级,所以叫做流水线

在未排序之前需要阻塞等待ADD指令完成写入后INC才能继续执行,INC执行后才能MOV
在重排序之后,先执行了MOV,然后INC,指令之间没有阻塞(一般的重排序不会对结果产生影响)

public class ExampleReordering {
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        int count = 0;
        while (true) {
            count++;
            x = 0;
            y = 0;
            a = 0;
            b = 0;//clear
            Thread one = new Thread(new Runnable() {
                public void run() {
                    shortWait(100000);
                    a = 1;
                    x = b;
                }
            });

            Thread other = new Thread(new Runnable() {
                public void run() {
                    b = 1;
                    y = a;
                }
            });
            one.start();
            other.start();
            one.join();
            other.join();
            String result = "第" + count + "次 (" + x + "," + y + ")";
            if (x == 0 && y == 0) {
                System.err.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }
    }


    public static void shortWait(long interval) {
        long start = System.nanoTime();
        long end;
        do {
            end = System.nanoTime();
        } while (start + interval >= end);
    }
}

代码正确执行情况下,x和y不会同时等于0,在指令重排序的情况下,就可能发生x和y都等于0的异常情况

解决方案:
在每个变量中增加volatile关键字禁止指令重排序

    private volatile static int x = 0, y = 0;
    private volatile static int a = 0, b = 0;

volatile 原理

在对比加入volatile关键字与未加入volatile关键字所生成的汇编代码发现,加入volatile关键字时会多出一个lock前缀指令,lock前缀指令相当于一个内存屏障(或内存栅栏),内存屏障主要提供如下功能:

  1. 确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障之后;
  2. volatile会强制将对缓存的修改操作立即写入主内存
  3. 如果是写操作的时候,volatile会导致其它CPU中对应的此缓存行失效

volatile 应用

synchronized关键字是防止多个线程执行同一段代码,而volatile作用却不同,使用前需要具备如下两个条件:

  1. 对变量的写操作不依赖于当前变量的值
  2. 该变量没有包含在具有其它变量的不变式中

volatile能保证原子性吗?

不能,volatile关键字保证了操作的可见性,但无法保证变量操作的原子性,原因如下:
Java 并发编程CAS、volatile、synchronized原理详解_第3张图片
在多个线程对i变量进行i++操作时,实际上需要做如上4步,在线程A还没有将add后的i值putfield,上下文就切换到了线程B,线程B执行了add操作。
两个线程同时对一个老的i进行add,所有原来应该+2的变成只有+1,最终导致数据的偏差。

解决方案:在incr()方法中增加synchronized关键字

    private synchronized void incr() {
        i++;
    }

synchronized

什么是synchronized?

是一种同步锁,通过synchronized对方法或代码块修饰,确保多个线程在同一个时刻,只能有一个线程处于方法/代码块中,它确保了线程对变量访问的可见性和排他性

synchronized 实现原理

JVM是通过进入、退出对象监视器(Monitor)来实现对方法、同步块的同步的;而对象监视器的本质依赖于底层操作系统的互斥锁(Mutex Lock)实现。
Java 并发编程CAS、volatile、synchronized原理详解_第4张图片
基本过程:

  1. 代码编译之后会在同步方法调用前加入一条monitor.enter指令,在退出方法和异常处插入monitor.exit指令
  2. 当有一个线程进入后会获取锁(Mutex Lock),如果有另一个线程调用方法,则会阻塞在方法入口处,直到获取锁的线程monitor.exit之后才能继续尝试获取锁

synchronized 锁升级过程

在JDK1.6之前,synchronized是一个重量级锁,开销很大,所以经常被建议说少用点。
但在JDK1.6之后,该关键字进行了很多的优化,主要就是锁升级,会自动根据不同的资源竞争情况升级锁(锁膨胀)。

锁升级过程:

  1. 偏向锁:是指一段同步代码一直被一个线程所访问,假如该锁没有被其他线程所获取,没有其他线程来竞争该锁,那么持有偏向锁的线程将永远不需要进行同步操作。降低获取锁的代价。
  2. 轻量级锁:是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能(比如自旋锁)。
  3. 重量级锁:是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞(对象监视器Monitor),性能降低。

synchronized 应用

synchronized 有如下四种方式修饰对象来实现代码同步:

  1. 修饰一个代码块对象锁,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象
  2. 修饰一个方法对象锁,其作用的范围是整个方法,作用的对象是调用这个方法的对象
  3. 修饰一个静态的方法类锁,其作用的对象是这个类的所有对象
  4. 修饰一个类锁,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象
  • 示例代码:
package com.muse.thread;

import java.util.concurrent.TimeUnit;

public class SynchronizedDemo {
    public static void main(String[] args) {
        /** case1:无Synchronized,乱序输出 */
        //        NoneSyncDemo noneSyncDemo = new NoneSyncDemo();
        //        Thread thread1 = new Thread(noneSyncDemo);
        //        Thread thread2 = new Thread(noneSyncDemo);

        /** case2:synchronized修饰代码块, 对象锁 */
        // 加锁有效
        //        SyncBlockDemo syncBlockDemo = new SyncBlockDemo();
        //        Thread thread1 = new Thread(syncBlockDemo);
        //        Thread thread2 = new Thread(syncBlockDemo);

        // 加锁无效
        //        SyncBlockDemo syncBlockDemo1 = new SyncBlockDemo();
        //        SyncBlockDemo syncBlockDemo2 = new SyncBlockDemo();
        //        Thread thread1 = new Thread(syncBlockDemo1);
        //        Thread thread2 = new Thread(syncBlockDemo2);

        /** case3:synchronized修饰方法,对象锁 */
        // 加锁有效
        //        SyncMethodDemo syncMethodDemo = new SyncMethodDemo();
        //        Thread thread1 = new Thread(syncMethodDemo);
        //        Thread thread2 = new Thread(syncMethodDemo);

        // 加锁无效
        //        SyncMethodDemo syncMethodDemo1 = new SyncMethodDemo();
        //        SyncMethodDemo syncMethodDemo2 = new SyncMethodDemo();
        //        Thread thread1 = new Thread(syncMethodDemo1);
        //        Thread thread2 = new Thread(syncMethodDemo2);

        /** case4:synchronized修饰静态方法,类锁 */
        // 加锁有效
        //        SyncStaticMethodDemo syncStaticMethodDemo = new SyncStaticMethodDemo();
        //        Thread thread1 = new Thread(syncStaticMethodDemo);
        //        Thread thread2 = new Thread(syncStaticMethodDemo);

        // 加锁有效
        //        SyncStaticMethodDemo syncStaticMethodDemo1 = new SyncStaticMethodDemo();
        //        SyncStaticMethodDemo syncStaticMethodDemo2 = new SyncStaticMethodDemo();
        //        Thread thread1 = new Thread(syncStaticMethodDemo1);
        //        Thread thread2 = new Thread(syncStaticMethodDemo2);

        /** case5:synchronized修饰类,类锁 */
        // 加锁有效
        //        SyncClassDemo syncClassDemo = new SyncClassDemo();
        //        Thread thread1 = new Thread(syncClassDemo);
        //        Thread thread2 = new Thread(syncClassDemo);

        // 加锁有效
        SyncClassDemo syncClassDemo1 = new SyncClassDemo();
        SyncClassDemo syncClassDemo2 = new SyncClassDemo();
        Thread thread1 = new Thread(syncClassDemo1);
        Thread thread2 = new Thread(syncClassDemo2);

        thread1.start();
        thread2.start();
    }
}

/**
 * [case1:无Synchronized]
 */
class NoneSyncDemo implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            try {
                System.out.println(Thread.currentThread().getName() + ":" + i);
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * [case2:synchronized修饰代码块]
 * 一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。
 **/
class SyncBlockDemo implements Runnable {
    @Override
    public void run() {
        synchronized(this) {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                    TimeUnit.MILLISECONDS.sleep(200);
                } catch (Throwable e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

/**
 * [case3:synchronized修饰方法]
 * 一个线程访问一个对象中的synchronized修饰的方法时,其他试图访问该对象的线程将被阻塞。
 **/
class SyncMethodDemo implements Runnable {
    @Override
    public synchronized void run() {
        for (int i = 0; i < 5; i++) {
            try {
                System.out.println(Thread.currentThread().getName() + ":" + i);
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * [case4:synchronized修饰静态方法]
 * 修饰一个静态的方法,类锁,其作用的对象是这个类的所有对象;
 **/
class SyncStaticMethodDemo implements Runnable {
    @Override
    public void run() {
        method();
    }

    public synchronized static void method() {
        for (int i = 0; i < 5; i++) {
            try {
                System.out.println(Thread.currentThread().getName() + ":" + i);
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * [case5:synchronized修饰类]
 * 修饰一个类,类锁,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象;
 **/
class SyncClassDemo implements Runnable {
    @Override
    public void run() {
        synchronized(SyncClassDemo.class) {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                    TimeUnit.MILLISECONDS.sleep(200);
                } catch (Throwable e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

思考

volatile与synchronized的区别

区别点 volatile synchronized
修饰 只能用于修饰变量 可以用于修饰方法、代码块、类
线程阻塞 不会发生线程阻塞 发生阻塞
原子性 不能保证变量的原子性 可以保证变量原子性
可见性 可以保证变量在线程之间访问资源的可见性 可以间接保证可见性,因为它会将私有内存中和公共内存中的数据做同步
同步性 能保证变量在私有内存和主内存间的同步 synchronize是多线程之间访问资源的同步性(同时只能有一个线程访问)
编译器优化 不会被编译器优化 标记的变量可以被编译器优化

synchronized与ReentrantLock的区别

区别点 Synchronized ReentrantLock
使用方式 关键字 实现类
实现方式 JVM实现控制 AQS实现控制
是否自动 yes no
锁的获取 如果资源被锁,会一直等待 如果资源被锁,可以有多种处理方式 详情参见“面试题2”
锁的释放 被锁的代码执行完or发生异常 finally中手动编程释放
锁的状态 无法判断 可以判断,isLocked()
锁的类型 可重入,不可中断,非公平锁 可重入,**可中断(lockInterruptibly),公平锁or非公平锁

总结

对CAS、volatile、synchronized原理分析,并对各自的特点、应用场景、使用方式做了举例。在最后比较了这几种保证线程安全机制的区别。希望通过本文能够帮你们揭开它们神秘的面纱。

扩展

CPU EMSI协议

MESI的四个字⺟分别代表了四个可以被标记在缓存⾏上的独⽴状态。(也就是⽤2bit来编码)

  • Modified (M)
    当缓存⾏处于Modified状态时,它表明该缓存⾏只存在于当前缓存中。并且这个缓存⾏中的数
    据是脏数据,也就是说这个缓存⾏中的数据与主存中的数据不⼀致。缓存被要求在未来将缓存
    ⾏的数据写于主存中,但不⽤⽴即写⼊。但如果别的缓存向该缓存请求这个数据,那必须保证
    该数据写⼊主存已经完成。当回写回主存完成后,缓存⾏状态会有Modified变为Shared状
    态。
  • Exclusive (E)
    当缓存⾏处于Exclusive状态时,它表明该缓存⾏只存在于当前缓存中,不过其中的数据与主存
    中的数据是⼀致的。当别的缓存向该缓存请求read当前缓存⾏时,缓存⾏状态会变成
    Shared。或者当有write操作时,他会变成Modified状态。
  • Shared (S)
    当缓存⾏处于Shared状态时,它表明该缓存⾏可能同时存在与别的缓存中,并且其中的数据与
    主存中⼀致。这个缓存⾏随时可能被丢弃(改变为Invalid状态)。
  • Invalid (I)
    当缓存⾏处于Invalid 状态时,表明该缓存⾏是⽆效的。

基本过程:

  1. CPU1从内存读取变量x。 CPU1会向总线发送⼀条读的消息, CPU1读取到数据之后,会将该缓
    存⾏cache1的状态设置为E(独享,在底层汇编中加lock信号,保证缓存的⼀致性)。
  2. CPU2读取变量x。再CPU1还没有将x会写⼊内存的时候, CPU2也发送了⼀条读x变量的信号,
    通过总线嗅探机制, CPU1会嗅探到CPU2要读取CPU1中的缓存⾏对应于内存的区域,那么
  3. CPU1的缓存⾏的状态将会由E转换为S(共享状态),并且CPU2对应的缓存⾏也是S状态。
    CPU1修改数据。 CPU1⼜向总线发送消息要求修改x变量,那么CPU1将会锁住该缓存⾏,并将
    状态由S改为M(修改状态); CPU2嗅探到CPU1要修改变量x,那么CPU2会将相应的缓存⾏
    的状态由S改为I(⽆效)。
  4. 同步数据。当CPU1将数据x写回内存后,其对应缓存⾏状态由M转换为E,当CPU2再次发送读
    消息时, CPU1状态由E改为S, CPU2的状态由I改为S。

参考

  • Java 并发编程ReentrantLock–源码解析(底层AQS实现)
  • Java 并发编程概念深入理解
  • 各种锁的介绍、锁之间的区别
  • https://blog.csdn.net/Hsuxu/article/details/9467651

你可能感兴趣的:(java,java,多线程)