并发编程6 - 无锁并发CAS 与 Volatile

文章目录

    • 一. Java内存模型
      • 1. 可见性问题
      • 2. Balking 模式
      • 3. 有序性问题
      • 4. double-checked locking 问题
    • 二. 共享模型之无锁
      • 1. CAS 与 Volatile
      • 2. 原子类

一. Java内存模型

JMM(Java Memory Model),它定义了主存、工作内存抽象概念。体现在

  • 原子性:保证指令不会受到线程上下文切换的影响;(Monitor)
  • 可见性:保证指令不会受到 cpu 缓存的影响;(volatile、Monitor)
  • 有序性:保证指令不会受到 cpu 指令并行优化的影响。(volatile、Monitor)

1. 可见性问题

示例:主线程通过修改公共变量,使t1线程退出,但下面的 t1 线程不会退出:

public class VisiblityTest {
    public static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while(flag) {
                //
            }
        });

        t1.start();
        Thread.sleep(1000);
        flag = false;
        System.out.println("修改flag……");
    }
}

原因:
内存划分为主存和工作内存,主存是共享信息存储的位置,工作内存是线程私有信息存放的位置。因为 t 线程要频繁从主内存中读取 run 的值,所以 run 会被放入高速缓存区,即使主线程修改了主内存中的 run 值,也对 t 线程不可见。
并发编程6 - 无锁并发CAS 与 Volatile_第1张图片
解决方式:

(1)将变量声明为 volatile

volatile static boolean flag = true;

(2)加锁:加锁也能解决可见性问题。

注意:volatile 只能保证可见性(每次读到的都是最新的值),不能保证原子性,适用于一个线程写一个线程读的情况(如两阶段终止模式)。Synchronized 既可以保证可见性又能保证原子性。但是 Synchronized 属于重量级锁,volatile 是轻量级的。

2. Balking 模式

犹豫模式,用于一个线程发现另一个线程已经做了某件事,就直接退出避免重复执行。经常用于避免线程重复启动。

示例避免线程重复启动:

class MonitorService {
    private volatile boolean isStart = false;
    private volatile boolean isStop = false;

    public void start() {
        synchronized (this) {
            if (isStart) {
                System.out.println("线程已启动……");
                return;
            }
            isStart = true;
        }
        new Thread(() -> {
            //前面已经保证了只有一个线程能执行下面的代码
            while (!isStop) {
                System.out.println("执行监控……");
            }
            //这里没有用锁,需要通过 volatile 保证可见性
            isStart = false;
            System.out.println("监控线程已退出……");
        }).start();
    }

    public synchronized void stop() {
        isStart = false;
        isStop = true;
    }

}

3. 有序性问题

指令重排:JVM 在不影响正确性的前提下,可以调整语句的执行顺序。多线程下指令重排会影响正确性。

示例:下列两个方法分别使用两个线程调用,result 的值除了 1 和 10,还可能出现 0 的情况。这就是指令重排在多线程下所产生的问题:

int num = 0;
boolean flag = false;

public void method1(int result) {
	if(flag) {
		result = num;
	} else {
		result = 1;
	}
}

public void method2() {
	num = 10;
	flag = true; // flag 为 true 时,num 不一定为 10
}

解决方式:volatile。加在某个变量前可以保证该变量之前的代码都禁止重排序。

// 由于 flag 赋值在 num 之后,所以这里只需要给 flag 变量加 volatile
volatile boolean flag = false;

4. double-checked locking 问题

以下单例模式通过 double-checked locking 保证了只有首次获取实例对象时才加锁:

final class Singleton {
    private Singleton() { }
    private static Singleton instance = null;

    public static Singleton getInstance() {
        // double-checked locking 保证只有第一次创建对象时才进入同步代码块,提高效率
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    return new Singleton();
                }
            }
        }
        return instance;
    }
}

但是,有序性问题会使上述代码出错,虽然 synchronized 也能保证有序性,但是在锁之外的 instance 对象没有受到保护。解决方式也是加 volatile

private static volatile Singleton instance = null;

二. 共享模型之无锁

1. CAS 与 Volatile

CAS 即 compare and set,它配合 Volatile 可以实现无锁并发。原理是通过不断地尝试获取共享变量的最新值直至成功,来解决并发带来的安全性问题。 CAS 其实采用了乐观锁的思想。

CAS 必须 配合 Volatile 才能发挥效果(因为需要时刻获取最新值),下面实例中的 AtomicInteger 的实现就采用了 Volatile。

public class CASTest {
    public static void main(String[] args) {
        List<Thread> list = new ArrayList<>();
        AccountCAS accountCAS = new AccountCAS(1000);
        for (int i = 0; i < 1000; i++) {
            Thread t = new Thread(()->{
               accountCAS.withdraw(1);
            });
            list.add(t);
        }
        list.forEach(Thread::start);
        list.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println("余额: " + accountCAS.getBalance());
    }



}

class AccountCAS {
    //余额
    private AtomicInteger balance;

    public AccountCAS(int balance) {
        this.balance = new AtomicInteger(balance);
    }

    public Integer getBalance() {
        return balance.get();
    }
    //取款操作
    public void withdraw(Integer amount) {
        while(true) {
            int prev = balance.get();
            int next = prev - amount;
            //比较并设值,只有返回值为 true 才算成功,并退出循环
            if(balance.compareAndSet(prev, next)) {
                break; 
            }
        }
    }
}

并发编程6 - 无锁并发CAS 与 Volatile_第2张图片
原理图:
并发编程6 - 无锁并发CAS 与 Volatile_第3张图片
CAS的性能分析:无锁情况下,即使重试失败,也不会阻塞发生上下文切换,能够提高运行效率。但是由于无锁时线程一直在运行,如果没有 CPU 资源,分不到时间片的情况下,也会发生上下文切换。CAS 适合于线程数小于 CPU 核数的情况。但时如果竞争激烈,重试必然发生时,采用 CAS 效率反而会受到影响。

2. 原子类

由 Java.util.current 包提供的一些原子类。这些类中的方法的操作都是原子性的。

(1)原子整数
AtomicInteger、AtomicBoolean、AtomicLong

上方示例代码中 compareAndSet 必须配合 while(true) 使用,但是 AtomicInteger 提供了更为简单的写法:

//取款操作
public void withdraw(Integer amount) {
    balance.getAndAdd(-1 * amount);
}

更加通用的方法:

//取款操作
public void withdraw(Integer amount) {
    balance.updateAndGet(x -> x-1); //传入一个IntUnaryOperator参数
}

(2)原子引用
AtomicReference(判断原子引用的值是否发生了变化)、AtomicStampedReference(带版本号的原子引用、追踪原子引用的整个变化过程)、AtomicMarkableReference(stamp的简化版,只判断原子引用是否被其他线程更改过)
示例:

class DecimalAccountCAS {
    private AtomicReference<BigDecimal> balance;

    public DecimalAccountCAS(BigDecimal balance) {
        this.balance = new AtomicReference<>(balance);
    }

    public BigDecimal getBalance() {
        return balance.get();
    }

    public void withdraw(BigDecimal amount) {
        balance.updateAndGet( x -> x.subtract(amount));
    }
}

(3)原子数组
AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

(4)字段更新器
原子更新类中的字段:AtomicReferenceFieldUpdater、AtomicLongFieldUpdater、AtomicIntegerFieldUpdater

字段需要声明为 volatile:

private volatile String name;

使用:

AtomicReferenceFieldUpdater updater =
                AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
updater.compareAndSet(.....)

(5)累加器
LongAdder、DoubleAdder的 increment() 方法性能优于前面的 getAndIncrement() 方法。它的原理是在有竞争时,设置多个累加单元,Thread-0 累加 Cell[0] 的结果,Thread-1 累加 Cell[1] 的结果……(上限为CPU核数)。

你可能感兴趣的:(并发编程)