《实战Java高并发程序设计》——无锁编程

文章目录

  • 一、悲观锁和乐观锁(CAS)
    • 1.1 悲观锁
    • 1.2 乐观锁
  • 二、比较并交换(CAS)
    • 2.1 步骤
    • 2.2 优点
    • 2.3 缺点
  • 三、AtomicInteger
    • 3.1 介绍
    • 3.2 相关方法
    • 3.3 代码示例
    • 3.4 AtomicInteger和使用锁的性能比较
  • 四、AtomicReference
    • 4.1 简介
    • 4.2 示例
  • 五、AtomicStampReference
    • 5.1 简介
    • 5.2 新增API(相对于AtomicReference)
    • 5.3 示例
  • 六、AtomicIntegerArray
    • 6.1 简介
    • 6.2 核心API
    • 6.3 示例
  • 七、AtomicIntegerFieldUpdater
    • 7.1 简介
    • 7.2 示例
    • 7.3 注意事项
  • 参考文章


一、悲观锁和乐观锁(CAS)

1.1 悲观锁

总是假设每一次的临界区操作会产生冲突,如果有多个线程同时访问临界区的资源,则会让没拿到锁的线程进行等待。所以说锁会阻塞线程的执行

1.2 乐观锁

总是假设对资源的访问都是没有冲突的,所有线程都可以在不停顿的状态下执行。如果出现了冲突怎么办呢?无锁的策略使用一种叫做比较交换(CAS)的方式来处理冲突,一旦检测到冲突,就一直重试当前操作直到没有冲入未知。


二、比较并交换(CAS)

2.1 步骤

参数
CAS 一般包含三个参数

  • V : 内存地址
  • E : 旧的预期值
  • N : 要修改的新值

步骤
1、当地址V处的值和旧的预期值相等时(V == E),就可以把地址V处的值设置为要修改的新值N(V = N);
2、当地址V处的值和旧的预期值不相等(V != E),说明已经有其它线程做了更新,此时再次尝试步骤1,直到成功为止。

2.2 优点

1、对锁问题天生免疫,线程间的相互影响远远比基于锁的方式要小
2、没有锁竞争带来的开销
3、没有线程间频繁调度带来的开销

2.3 缺点

1、CPU开销较大。在并发量较高的情况下,如果许多线程尝试更新某一个变量的值,却又一直更新不成功,就一直重复,会给CPU带来很大的压力。
2、不保证代码块的原子性。CAS机制保证的是一个变量的原子性操作,但是不能保证整个代码块的原子性。
3、存在ABA问题。当获取数据后,准备修改为新值前,数据被其它线程连续修改了两次,并且经过这两次修改后,数据的值又恢复为旧值。这样的话,当前线程无法判断这个数据究竟是否被修改过。

ABA 问题的解决
我们可以为数据添加一个版本号,每次对数据进行操作时,版本号都会自增。我们就可以通过比较数据和版本号来判断数据是否被修改过。

三、AtomicInteger

3.1 介绍

可以把AtomicInteger看作一个整数;与Integer不同,它是可变的;并且对其进行的修改等任何操作都是使用CAS指令进行的,所以是线程安全的。

3.2 相关方法

public final int get();										// 获取当前值
public final void set(int newValue);						// 设置当前值
public final int getAndSet(int newValue);					// 设置新值,获取旧值
public final boolean compareAndSet(int expect, int update);	// 若当前值为 expect,将其设置为 update
public final int getAndIncrement();							// 当前值+1,获取旧值
public final int getAndDecrement();							// 当前值-1,获取旧值
public final int getAndAdd(int delta);						// 当前值+delta,获取旧值
public final int incrementAndGet();							// 当前值+1,获取新值
public final int decrementAndGet();							// 当前值-1,获取新值
public final int addAndGet(int delta);						// 当前值+delta,获取新值

3.3 代码示例

1、代码

public class AtomicIntegerDemo {
     
    // 原子类
    private static AtomicInteger i = new AtomicInteger();

    // 任务类
    public static class addTask implements Runnable {
     
        @Override
        public void run() {
     
            for (int j = 0; j < 1000; j++) {
     
                // 使用 CAS 操作自增
                i.incrementAndGet();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
     
        Thread[] ts = new Thread[10];
        // 1、初始化线程池
        for (int j = 0; j < 10; j++) {
     
            ts[j] = new Thread(new addTask());
        }
        // 2、启动线程,执行任务
        for (int j = 0; j < 10; j++) {
     
            ts[j].start();
        }
        // 3、等待所有线程执行完毕,再输出 i 的值
        // 也可以使用: for (int j = 0; j < 10; j++) { ts[j].join(); }
        Thread.currentThread().sleep(500);
        System.out.println("i 的值为 : " + i);
    }
}

2、执行结果

i 的值为 : 10000

如果不是线程安全的,那么输出的值应该小于1000;但实际输出的值是1000,说明AtomicInteger类是安全的。

3.4 AtomicInteger和使用锁的性能比较

1、代码

public class IntegerDemo {
     
    // 原子类
    private static AtomicInteger i = new AtomicInteger();
    // 包装类
    private static Integer k = new Integer("0");
    // 锁对象
    private static IntegerDemo lock = new IntegerDemo();

    // 原子类 自增任务类
    public static class addTask implements Runnable {
     
        @Override
        public void run() {
     
            for (int j = 0; j < 10000000; j++) {
     
                // 使用 CAS 操作自增
                i.incrementAndGet();
            }
        }
    }

    // 包装类 自增任务类
    public static class addIntTask implements Runnable {
     
        @Override
        public void run() {
     
            for (int j = 0; j < 10000000; j++) {
     
                synchronized (lock) {
     
                    k++;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
     
        Thread[] ts = new Thread[10];
        // ******************** 原子类测试 ***************************
        // 1、初始化线程池
        for (int j = 0; j < 10; j++) {
     
            ts[j] = new Thread(new addTask());
        }
        // 2、启动线程,执行任务
        long start01 = System.currentTimeMillis();
        for (int j = 0; j < 10; j++) {
     
            ts[j].start();
        }
        // 3、等待所有线程执行完毕,再输出 i 的值
        for (int j = 0; j < 10; j++) {
     
            ts[j].join();
        }
        long end01 = System.currentTimeMillis();
        System.out.println("原子类时间:" + (end01 - start01) + "ms");
        System.out.println("原子类 i 的值为 : " + i);


        // ******************** 原子类测试 ***************************
        // 1、初始化线程池
        for (int j = 0; j < 10; j++) {
     
            ts[j] = new Thread(new addIntTask());
        }
        // 2、启动线程,执行任务
        long start02 = System.currentTimeMillis();
        for (int j = 0; j < 10; j++) {
     
            ts[j].start();
        }
        // 3、等待所有线程执行完毕,再输出 i 的值
        for (int j = 0; j < 10; j++) {
     
            ts[j].join();
        }
        long end02 = System.currentTimeMillis();
        System.out.println("包装类时间:" + (end02 - start02) + "ms");
        System.out.println("包装类 k 的值为 : " + k);
    }
}

2、执行结果

原子类时间:1976ms
原子类 i 的值为 : 100000000
包装类时间:2835ms
包装类 k 的值为 : 100000000

可以看到,使用原子类比使用锁的速度要快上很多。


四、AtomicReference

4.1 简介

  • AtomicInteger是对整数的封装,而AtomicReference是对普通的对象的引用,可以保证在修改对象引用时的线程安全性。
  • 但是它存在ABA问题,当获取对象数据后,准备修改为新值前,对象被其它线程连续修改了两次,并且经过这两次修改后,对象的数据又恢复为旧值。这样的话,当前线程无法判断这个对象究竟是否被修改过。

4.2 示例

1、举例
现在有一个蛋糕店在做活动,决定为贵宾卡里余额小于20元的客户一次性赠送20元,但条件是每位客户只能赠送一次。
2、代码

public class AtomicReferenceDemo {
     
    // 账户余额
    private static AtomicReference<Integer> money = new AtomicReference<>();

    // 设置初始账户余额为 19
    static {
     
        money.set(19);
    }

    /**
     * 充值
     */
    public static class RechargeTask implements Runnable {
     
        @Override
        public void run() {
     
            while (true) {
     
                // 1. 获取money
                Integer m = money.get();
                // 2. 小于20则充值,使用CAS设置值
                if (m < 20) {
     
                    if (money.compareAndSet(m, m + 20)) {
     
                        System.out.println("** 余额小于20,充值成功,余额: " + money.get() + "元");
                    }
                } else {
     
                    System.out.println("余额大于20,无需充值");
                }
            }
        }
    }

    /**
     * 消费
     */
    public static class ConsumeTask implements Runnable {
     
        @Override
        public void run() {
     
            while (true) {
     
                // 1. 获取
                Integer m = money.get();
                // 2. 大于20则消费,使用CAS设置值
                if (m > 20) {
     
                    if (money.compareAndSet(m, m - 20)) {
     
                        System.out.println("余额大于20,消费成功,余额: " + (m - 20) + "元");
                    }
                } else {
     
                    System.out.println("余额小于20,无法消费");
                }
            }
        }
    }

    public static void main(String[] args) {
     
        ExecutorService pool = Executors.newFixedThreadPool(10);
        new Thread(new ConsumeTask()).start();
        for (int i = 0; i < 1; i++) {
     
            pool.execute(new RechargeTask());
        }
    }
}

2、执行结果

** 余额小于20,充值成功,余额: 39元
余额小于20,无法消费
余额大于20,消费成功,余额: 19元
余额小于20,无法消费
余额小于20,无法消费
余额小于20,无法消费
** 余额小于20,充值成功,余额: 39

从执行结果可以看出,这个账户被反复充值多次。原因是账户余额被反复修改,且修改后的值等于原有的数值,使得CAS操作无法正确判断当前数据的状态。
AtomicReference无法解决此问题的原因是,对象在修改的过程中丢失了状态信息,对象值与状态被画上了等号。


五、AtomicStampReference

在上面的例子中,对象在修改的过程中丢失了状态信息,对象值与状态被画上了等号,所以AtomicReference无法解决上面的问题。但是,只要我们能够记录对象在修改过程中的状态值,就可以解决因为对象被反复修改导致线程无法正确判断对象状态的问题。

5.1 简介

AtomicStampReference内部不仅维护了对象值,还维护了一个时间戳(也可以称其为版本号,实际上可以使用任何一个整数来表示状态值)。

  • 当AtomicStampReference对应的数值被修改时,要同时更新数据和时间戳;
  • 当AtomicStampReference设置对象值时,对象值和时间戳都必须满足期望值,写入才会成功。

因此,即时对象被反复读写且写回原值,只要时间戳发生变化,就可以防止不恰当的写入。

5.2 新增API(相对于AtomicReference)

// 1、比较并设置,参数依次为:期望值、写入新值、期望时间戳、新时间戳
public boolean compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp);
// 2、获得当前对象引用
public V getReference();
// 3、获得当前时间戳
public int getStamp();
// 4、设置当前对象引用和时间戳
public void set(V newReference, int newStamp);

5.3 示例

1、举例
现在有一个蛋糕店在做活动,决定为贵宾卡里余额小于20元的客户一次性赠送20元,但条件是每位客户只能赠送一次。
2、代码

public class AtomicStampReferenceDemo {
     
    // 账户余额(设置初始余额为 19,初始时间戳为 0)
    private static AtomicStampedReference<Integer> money = new AtomicStampedReference<>(19, 0);

    // 获取初始的时间戳
    private static final int timeStamp = money.getStamp();

    /**
     * 充值
     */
    public static class RechargeTask implements Runnable {
     
        @Override
        public void run() {
     
            while (true) {
     
                // 1. 获取money
                Integer m = money.getReference();
                // 2. 小于20则充值判断并使用CAS设置值
                if (m < 20) {
     
                    if (money.compareAndSet(m, m + 20, timeStamp, timeStamp + 1)) {
     
                        System.out.println("** 余额小于20,充值成功,余额: " + money.getReference() + "元"); // 万一阻塞了怎么办?
                    }
                } else {
     
                    System.out.println("余额大于20,无需充值");
                }
            }
        }
    }

    /**
     * 消费
     */
    public static class ConsumeTask implements Runnable {
     
        @Override
        public void run() {
     
            while (true) {
     
                // 1. 获取余额和时间戳
                int timeStamp00 = money.getStamp();
                Integer m = money.getReference();
                // 2. 小于20则充值判断并使用CAS设置值
                if (m > 20) {
     
                    if (money.compareAndSet(m, m - 20, timeStamp00, timeStamp00 + 1)) {
     
                        System.out.println("余额大于20,消费成功,余额: " + (m - 20) + "元"); // 万一阻塞了怎么办?
                    }
                } else {
     
                    System.out.println("余额小于20,无法消费");
                }
            }
        }
    }

    public static void main(String[] args) {
     
        ExecutorService pool = Executors.newFixedThreadPool(10);
        new Thread(new ConsumeTask()).start();
        for (int i = 0; i < 1; i++) {
     
            pool.execute(new RechargeTask());
        }
    }
}

3、执行结果

余额小于20,无法消费
** 余额小于20,充值成功,余额: 39元
余额大于20,消费成功,余额: 19元
余额小于20,无法消费
余额小于20,无法消费
余额小于20,无法消费
余额小于20,无法消费

从执行结果可以看出,只进行了一次充值


六、AtomicIntegerArray

6.1 简介

1、原子数组的种类

  • AtomicIntegerArray:整型数组
  • AtomicLongArray:long型数组
  • AtomicReferenceArray:普通的对象数组

2、本质
AtomicIntegerArray本质上是对int[]类型的数据的封装,使用Unsafe类通过CAS的方式控制int[]在多线程下的安全性。

6.2 核心API

// 获取数组中下标为 i 的元素
public final int get(int i);
// 获取数组的长度
public final int length();
// 将数组下标 i 处的元素设置为新值,并返回旧值
public final int getAndSet(int i, int newValue);
// 进行CAS操作,若下标为 i 处的元素等于 expect,则更新为 update,更新成功返回 true
public final boolean compareAndSet(int i, int expect, int update);
// 将数组下标 i 处的元素 +1
public final int getAndIncrement(int i);
// 将数组下标 i 处的元素 -1
public final int getAndDecrement(int i);
// 将数组下标 i 处的元素 +delta (delta 可以为负数)
public final int getAndAdd(int i, int delta);

6.3 示例

  • 代码
public class AtomicIntegerArrayDemo {
     
    // 声明包含 10 个元素的数组
    private static AtomicIntegerArray arr = new AtomicIntegerArray(10);

    // 自增任务类
    public static class AddTask implements Runnable {
     
        @Override
        public void run() {
     
            for (int i = 0; i < 1000; i++) {
     
                arr.getAndIncrement(i % arr.length());  // 每个线程使每个数组元素自增了 100 次
            }
        }
    }

    public static void main(String[] args) {
     
        Thread[] ts = new Thread[10];
        for (int i = 0; i < 10; i++) {
     
            ts[i] = new Thread(new AddTask());
            ts[i].start();
        }
        // 等待所有线程运行完再打印
        for (int i = 0; i < 10; i++) {
     
            ts[i].join();
        }
        System.out.println(arr);
    }
}
  • 执行结果
[1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000]

若线程不安全,那么必定会有元素的值小于1000;然而实际上所有元素的值都是1000,说明这是线程安全的。


七、AtomicIntegerFieldUpdater

7.1 简介

AtomicIntegerFieldUpdater可以让普通变量也享受到原子操作。

Updater有三种

  • AtomicIntegerFieldUpdater:对 int 类型数据进行修改
  • AtomicLongFieldUpdater:对 long 类型数据进行修改
  • AtomicReferenceFieldUpdater:对 普通对象 进行修改

7.2 示例

1、举例
我们来模拟一个投票的场景,候选人得到一票,就记为1,否则记为0,最终选票就是所有数据的简单求和。
2、代码

/**
 * 候选人实体类
 */
class Candidate {
     
    int id;
    // 使用volatile 关键字,保证变量被正确读取
    volatile int score;
}

// --------------------------------------------------------------

public class AtomicIntegerFieldUpdateDemo {
     

    // 定义 AtomicIntegerFieldUpdater 实例,用来对 Candidate 的 score 进行写入
    public static final AtomicIntegerFieldUpdater<Candidate> scoreUpdater =
            AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");

    // 用于检查 AtomicIntegerFieldUpdater 的值是否正确
    public static AtomicInteger sumScore = new AtomicInteger();

    // 候选人对象
    public static final Candidate candidate = new Candidate();

    /**
     * 投票任务类
     */
    public static class AddTask implements Runnable {
     
        @Override
        public void run() {
     
            if (Math.random() > 0.4) {
     
                scoreUpdater.incrementAndGet(candidate);
                sumScore.incrementAndGet();
            }
        }
    }

    /**
     * 主函数
     */
    public static void main(String[] args) throws InterruptedException {
     
        Thread[] ts = new Thread[10000];
        for (int i = 0; i < ts.length; i++) {
     
            Runnable target;
            ts[i] = new Thread(new AddTask());
            ts[i].start();
        }
        // 等待所有线程执行完成之后,再打印数据
        for (int i = 0; i < ts.length; i++) {
     
            ts[i].join();
        }
        // 打印数据,比较它们是否相同
        System.out.println("score    : " + candidate.score);
        System.out.println("sumScore : " + sumScore);
    }
}

3、执行结果

score    : 6040
sumScore : 6040

通过执行结果我们可以看到,两个值相等,所以 AtomicIntegerFieldUpdater 可以保证 Candidate.score 的准确性。

7.3 注意事项

1、Updater 只可以修改它可见范围内的变量。因为 Updater 是使用反射得到这个变量的,如果不可见则会报错。例如,将score声明为 private则不行;
2、变量必须是 volatile 类型;
3、不支持static字段,因为 CAS 操作是通过对象实例中的偏移量直接进行赋值的(Unsafe.objectFieldOffset()方法不支持静态变量)。


参考文章

  • https://blog.csdn.net/qq_35571554/article/details/82892806
  • https://blog.csdn.net/qq_35571554/article/details/82906091

你可能感兴趣的:(并发编程,JavaSE,java,多线程)