多线程编程(六)——volatile关键字和原子类

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/swadian2008/article/details/99696399

目录

一、有序性、可见性、原子性

1、有序性

2、可见性

3、原子性

二、volatile关键字的作用——可见性

1、Volatile关键字使用条件

2、Volatile关键字使用场景

3、Volatile关键字使用缺点

4、Volatile关键字和synchronized关键字的比较

5、volatile的非原子性

三、原子类——原子性

1、原子类使用场景

(1)多线程环境中统计次数和递增操作

(2)限制接口数量,超过数量进行熔断

2、CAS(比较并替换)操作和ABA问题

(1)CAS操作

(2)ABA问题

3、原子类Atomic的不安全性


一、有序性、可见性、原子性

1、有序性

多线程指令重排序:

指令重排分为硬件层面和虚拟机层面,此处只讲虚拟机层面

目的:解决内存操作速度慢于CPU操作速度所带来的CPU空置问题,尽可能充分利用CPU

表现形式:虚拟机会按照特定的规则将程序打乱,使得写在后面的程序可能会先执行,写在前边的程序可能会后执行。

2、可见性

所谓的可见性,是线程A可以读取到线程B修改后的变量的值,改变线程从私有工作内存取变量值的方式,强制线程从主内存取值。

3、原子性

原子是世界上最小的单位,具有不可分割的特性

二、volatile关键字的作用——可见性

关键字volatile的主要作用是使变量在多个线程中可见。通过使用volatile关键字,强制从公共内存中读取变量的值,不允许线程内部对变量进行缓存和重排序、不加锁、不阻塞,实现更轻量级的线程同步。

内存结构图:

1、Volatile关键字使用条件

(1)对变量的写操作不依赖于当前值

(2)该变量没有包含在具有其他变量的不变式中

2、Volatile关键字使用场景

(1)用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机

(2)用作状态标志

状态标记的公有特性,通常只有一种状态转换:比如标志从false转换为true,然后停止

3、Volatile关键字使用缺点

只有在状态真正独立于程序内其他内容时才能使用Volatile,但大多数编程环境都不满足上述条件,因此使用volatile比使用锁更加容易出错(谨慎)

4、Volatile关键字和synchronized关键字的比较

1、多线程访问volatile不会发生阻塞,而synchronized会出现阻塞

2、volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性和可见性。

注:线程安全包括原子性和可见性两个方面,java的同步机制都是围绕这两个方面来确保线程安全的。

5、volatile的非原子性

关键字volatile虽然实现变量在线程之间的可见性,但是不能保障同步性(原子性),所以执行的结果也是存在问题的。

测试代码如下:

public class MyThread extends Thread {

    public volatile static int count;

    private static void addCount() {
        for (int i = 0; i < 100; i++) {
            count++;
        }
        System.out.println("打印相加结果:" + count);
    }

    @Override
    public void run() {
        addCount();
    }

    public static void main(String[] args) {
        // 定义100个线程
        MyThread[] myThreads = new MyThread[100];
        for (int i = 0; i < 100; i++) {
            myThreads[i] = new MyThread();
        }
        for (int i = 0; i < 100; i++) {
            myThreads[i].start();
        }
    }
}

执行结果:

这是因为,多个线程执行相加时,虽然可以相互读到其他线程修改后的值,但是,如果有多个线程同时修改count值为100,假如10个线程同时修改,那么最终只有一个线程修改的值有效,这个时候另外的9个线程相加效果就失效了,所以,我们可以看到,打印的结果总是小于正确数:10000。

原理:i++操作不是同步的

给addCount方法加锁:

private synchronized static void addCount() {
        for (int i = 0; i < 100; i++) {
            count++;
        }
        System.out.println("打印相加结果:" + count);
    }

加锁后的执行结果:

结论:

volatile关键字解决的是变量读取时的可见性问题,但无法保证原子性,对于多线程访问同一个实例变量还是需要加锁

三、原子类——原子性

原子类基于CAS操作,不使用锁或同步机制来保护对其值的并发访问,避免死锁,提高性能。但一次只能同步一个值。

1、原子类使用场景

(1)多线程环境中统计次数和递增操作

public class ExecuteMethod {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger();
        ExecutorService ex = Executors.newFixedThreadPool(10);
        for (int j = 0; j < 10; j++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    atomicInteger.incrementAndGet();
                    System.out.println("atomicInteger的值" + atomicInteger);
                }
            };
            ex.execute(runnable);
        }
        ex.shutdown();
    }
}

执行结果:10个线程每个线程执行一次递增操作,一次值操作了一个值,但是如果在循环外再加一层循环,原子类操作就会出现线程安全问题

(2)限制接口数量,超过数量进行熔断

public class MyService {

    private AtomicInteger atomicInteger = new AtomicInteger();

    private void method() {
        int number = atomicInteger.incrementAndGet();// 递增
        int limit = 2;
        try {
            if (number > limit) {
                System.out.println("拒绝访问!超过线程允许访问数!");
            } else {
                System.out.println("允许访问");
            }
        } finally {
            atomicInteger.decrementAndGet();// 递减
        }
    }

    public static void main(String[] args) {
        MyService myService = new MyService();
        ExecutorService ex = Executors.newFixedThreadPool(100);
        for (int i = 0; i < 100; i++) {
            ex.execute(() -> myService.method());
        }
    }
}

执行结果:

2、CAS(比较并替换)操作和ABA问题

(1)CAS操作

一种基于硬件的处理方式,每个CAS操作都涉及到三个值:内存地址V,期望值A和新值B,如果给的值和期望值相等就赋值为B,不相等就不执行任何操作

(2)ABA问题

执行CAS操作时,其他线程将变量A改成了B又改成了A,当本线程用期望值和该变量进行比较时,发现A变量的值没有变就进行了数据的修改操作,这样与设计的初衷不相符。

举个栗子:

假如执行一次为20元以下的账户增加10元的存款操作。

A线程去扫描账户,发现账户的余额为15元,便执行加10元的操作,但是碰巧此时,该账户又消费了10元,因此账户余额又回到了15元;当B线程第二次去扫描账户的时候,发现账户值仍旧为15元,便又给账户进行增加10元的操作。

ABA问题的根本在于cas在修改变量的时候,无法记录变量的状态,比如修改的次数,否修改过这个变量。这样就很容易在一个线程将A修改成B时,另一个线程又会把B修改成A,造成casd多次执行的问题。

解决方法:每次变量更新时,赋值给变量一个版本号并加1递增,这样就避免了ABA问题。

3、原子类Atomic的不安全性

原子操作是不可分割的整体,它可以在没有锁的情况下做到线程安全,但是如果在具有逻辑性的多线程环境下(方法之间的调用不是同步的),原子类的操作也会存在不安全性。

如下同样是执行一个相加到10000的操作

测试代码:

public class MyThread extends Thread {

    AtomicInteger atomicInteger = new AtomicInteger();

    private void addCount() {
        for (int i = 0; i < 100; i++) {
            atomicInteger.incrementAndGet();
        }
        System.out.println("打印相加结果:" + atomicInteger);
    }

    @Override
    public void run() {
        addCount();
    }

    public static void main(String[] args) {
        // 定义100个线程
        MyThread myThread = new MyThread();
        for (int i = 0; i < 100; i++) {
            Thread thread = new Thread(myThread);
            thread.start();
        }
    }
}

执行结果1:

执行结果2:

上述结果可以看到,原子类在方法中一旦被多个线程调用(且方法不是同步方法),中间的执行逻辑是混乱的,执行中数据的正确性也得不到保障。

因此,解决这个问题的办法同样还是在方法上加同步锁!!!

给方法进行加锁操作:

private synchronized void addCount() {
        for (int i = 0; i < 100; i++) {
            atomicInteger.incrementAndGet();
        }
        System.out.println("打印相加结果:" + atomicInteger);
    }

执行结果:

猜你喜欢

转载自blog.csdn.net/swadian2008/article/details/99696399