Java多线程编程(六)——volatile、原子性

关于volatile-问题

案例描述——取钱

原因分析

解决方法

(1)使用Volatile关键字

(2)使用synchronized线程锁

原子性

概述

案例描述——送礼物

思考:现在能不能使用之前的volatile关键字来解决?

使用同步代码块解决“送礼物”问题

原子性_AtomicInteger

AtomicInteger的2个构造方法

AtomicInteger的5个方法

使用AtomicInteger解决“送礼物”问题

AtomicInteger内存解析

AtomicInteger原理 

CAS算法:

CSA+自旋总结

AtomicInteger-源码解析

乐观锁与悲观锁

synchronized和CAS的区别 

相同点:

不同点:


关于volatile-问题

案例描述——取钱

我们现在来看一个案例,小路同学小皮同学同时往账户存钱,账户余额时100000元,此时小皮同学从账户取10000元,此时账户上还剩下90000元,小路同学去查看会怎么样呢?(即MyThread1执行)

public class Demo {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        t1.setName("小路同学");
        t1.start();

        MyThread2 t2 = new MyThread2();
        t2.setName("小皮同学");
        t2.start();
    }
}
public class Money {
    public static int money = 100000;
}
public class MyThread1 extends Thread {
    @Override
    public void run() {
        while(Money.money == 100000){
        }
        System.out.println("已经不是十万了");
    }
}
public class MyThread2 extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Money.money = Money.money - 10000;
    }
}

上述案例可以用代码模拟,其中MyThread1,MyThread2,Money分别代表小路同学,小皮同学,钱,Demo是测试类。

运行后发现,代码会一直卡在MyThread1的循环中,即:while(Money.money == 100000)

原因分析

当MyThread2线程修改了共享数据时,MyThread1线程没有及时获取到最新的值,如果还在使用原先的值,就会出现问题。

站在内存的角度来看,Java的内存模型来看,堆内存是唯一的,而栈内存是线程独有的(即每一个线程都有自己的线程栈)

每一个线程在使用里面变量的时候,都会先拷贝一份到变量副本中。

在线程中,使用共享数据的时候,每一次使用是从变量副本中获取的(为的是提高效率)

MyThread1 将数值改为9万,再将其推送到堆的共享数据中。而 MyThread2 栈中的数据还是10万,这就导致了上述Bug的发生,当然 MyThread2 中的值也不会一直是10万,其也会定时去访问共享数据,但是什么时候查看是我们无法控制的。

解决方法

(1)使用volatile关键字

(2)使用synchronized线程锁

(1)使用Volatile关键字

Volatile关键字 : 强制线程每次在使用的时候,都会看一下共享区域最新的值

使用方法非常非常简单,我们修改一下Money类,加上volatile关键字即可

public class Money {
    public volatile static int money = 100000;
}

运行结果:

(2)使用synchronized线程锁

1 ,线程获得锁

2 ,清空变量副本

3 ,拷贝共享变量最新的值到变量副本中

4 ,执行代码

5 ,将修改后变量副本中的值赋值给共享数据

6 ,释放锁

Coding:

public class Demo {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        t1.setName("小路同学");
        t1.start();

        MyThread2 t2 = new MyThread2();
        t2.setName("小皮同学");
        t2.start();
    }
}
public class Money {
    public static Object lock = new Object();
    public static int money = 100000;
}
public class MyThread1 extends  Thread {
    @Override
    public void run() {
        while(true){
            synchronized (Money.lock){
                if(Money.money != 100000){
                    System.out.println("结婚基金已经不是十万了");
                    break;
                }
            }
        }
    }
}
public class MyThread2 extends Thread {
    @Override
    public void run() {
        synchronized (Money.lock) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Money.money = Money.money - 10000;
        }
    }
}

所以同步代码块也有这样的一个功能,就是强制线程去“查看”共享数据里的内容。

原子性

概述

所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。(要么同时成功,要么同时失败)

案例描述——送礼物

设计一个送礼物的案例,一共要送10000个礼物,使用Runnable的方式,开启100个线程,在每个线程送100个礼物。

public class AtomDemo {
    public static void main(String[] args) {
        MyAtomThread atom = new MyAtomThread();
        for (int i = 0; i < 100; i++) {
            new Thread(atom).start();
        }
    }
}
public class MyAtomThread implements Runnable{
    private int count = 0;
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            count++;
            System.out.println("已经送了" + count + "个");
        }
    }
}

可以发现运行结果不一定是10000!这是因为count++不是原子操作,在执行的过程中,有可能被其他线程打断。

 count++在JMM中的执行流程如下

1,从共享数据中读取数据到本线程栈中.
2,修改本线程栈中变量副本的值
3,   会把本线程栈中变量副本的值赋值给共享数据.

在执行这三步的时候,任何一步都有可能被打断,CPU的执行权都有可能被其他线程抢走。

思考:现在能不能使用之前的volatile关键字来解决?

不行,volatile只能保证线程每次使用共享数据是最新值,但是不能保证原子性。

比如 :

有A,B两个线程,A先取得共享数据count的值100,再将其count++(注:还没有写回共享数据) ,CPU的执行权就被B线程抢走了,而B取得的仍然共享数据中的count=100的值。此时错误就发生了!

使用同步代码块解决“送礼物”问题

当然解决方法也很简单,直接使用之前的方法用同步代码块即可,修改如下:

public class MyAtomThread implements Runnable{
    private int count = 0;
    private Object lock = new Object();
    
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            synchronized(lock) {
                count++;
                System.out.println("已经送了" + count + "个");
            }
        }
    }
}

但是用这一种方式,效率比较低,不仅代码量比较大,而且执行效率也低。

原子性_AtomicInteger

我们可以使用在Java的JDK1.5中提供的原子包——java.util.concurrent.atomic包这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。因为变量的类型有很多种,所以在Atomic包里一共提供了13个类,属于4种类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性(字段)

本次我们只讲解使用原子的方式更新基本类型,使用原子的方式更新基本类型Atomic包提供了以下3个类:

AtomicBoolean: 原子更新布尔类型

AtomicInteger: 原子更新整型

AtomicLong: 原子更新长整型

以上3个类提供的方法几乎一模一样,以AtomicInteger为例,AtomicInteger的常用方法如下:

public AtomicInteger():                       初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue):  初始化一个指定值的原子型Integer

int get():                               获取值
int getAndIncrement():         以原子方式将当前值加1,注意,这里返回的是自增前的值。
int incrementAndGet():        以原子方式将当前值加1,注意,这里返回的是自增后的值。
int addAndGet(int data):      以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
int getAndSet(int value):      以原子方式设置为newValue的值,并返回旧值。

AtomicInteger的2个构造方法

public class AtomDemo01 {
    public static void main(String[] args) {
        AtomicInteger ac = new AtomicInteger();
        System.out.println(ac);
        //  0

        AtomicInteger ac2 = new AtomicInteger(10);
        System.out.println(ac2);
        //  10
    }
}

AtomicInteger的5个方法

如下Coding就是其5个方法的使用样例与结果

public class AtomDemo02 {
    public static void main(String[] args) {
        //get()
        AtomicInteger ac1 = new AtomicInteger(10);
        System.out.println(ac1.get());
        //  10

        //getAndIncrement()  返回自增+1前的值
        AtomicInteger ac2 = new AtomicInteger(10);
        int andIncrement = ac2.getAndIncrement();
        System.out.println(andIncrement);
        //  10
        System.out.println(ac2.get());
        //  11

        //incrementAndGet()   返回自增+1后的值
        AtomicInteger ac3 = new AtomicInteger(10);
        int ac3i = ac3.incrementAndGet();
        System.out.println(ac3i);
        //  11
        System.out.println(ac3.get());
        //  11

        //addAndGet   与实例的值相加后输出
        AtomicInteger ac4 = new AtomicInteger(10);
        int ac4j = ac4.addAndGet(20);
        System.out.println(ac4j);
        //  30
        System.out.println(ac4.get());
        //  30

        //getAndSet()   设置实例的新值,返回旧值
        AtomicInteger ac5 = new AtomicInteger(100);
        int andSet = ac5.getAndSet(20);
        System.out.println(andSet);
        //  旧值  100
        System.out.println(ac5.get());
        //  新值  20
    }
}

使用AtomicInteger解决“送礼物”问题

改写如下:

public class MyAtomThread implements Runnable{
    AtomicInteger ac = new AtomicInteger(0);

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            int count = ac.incrementAndGet();
            System.out.println("已经送了" + count + "个");
        }
    }
}

这样子写code,不仅实现了功能,还极大程度的提高了运行效率(不用像synchronized那样需要申请锁,释放锁耗费大量的时间)。

AtomicInteger内存解析

AtomicInteger原理 :

自旋锁 + CAS 算法

CAS算法:

有3个操作数(内存值V, 旧的预期值A,要修改的值B

当旧的预期值A == 内存值 此时修改成功,将V改为B

当旧的预期值A!=内存值 此时修改失败,不做任何操作

并重新获取现在的最新值(这个重新获取的动作就是自旋)

我们用下面这个例子来说明CAS算法

A、B两个线程,它们都使用了AtomicInteger类,两个线程都执行自增+1的操作。

A线程先将100读入线程栈区,然后再将100自增+1存入自己的线程栈区,即101。

于此同时,B线程也执行了A线程的上述操作,如下图:

A线程先对比其自身的 (旧值 == 内存值),两者相等,则修改成功,再将需改后的值写回内存,即此时堆内存中的值变为101。

B线程想将修改后的值写回堆中时,由于  旧值 != 新值 则修改失败(发生自旋),B线程需要将堆中的值 101再次读入线程栈区中,自增+1为102,再写回堆中,如下:

CSA+自旋总结

CAS的缺点

首先作为技术,没有什么好不好,只有“ 适合不适合解决某个问题 “来评判。 

缺点有:ABA问题自旋锁消耗问题、多变量共享一致性问题。

ABA问题

问题描述

线程t1 将它的值从A变为B,再从B变为A。同时有线程t2 要将值从A变为C。但CAS检查的时候会发现没有改变,但是实质上它已经发生了改变 。可能会造成数据的缺失。

解决方法

CAS还是类似于乐观锁,同数据乐观锁的方式给它加一个版本号或者时间戳,如AtomicStampedReference

自旋消耗资源

问题描述

多个线程争夺同一个资源时,如果自旋一直不成功,将会一直占用CPU。

解决方法

破坏掉for死循环,当超过一定时间或者一定次数时,return退出。JDK8新增的LongAddr,和ConcurrentHashMap类似的方法。当多个线程竞争时,将粒度变小,将一个变量拆分为多个变量,达到多个线程访问多个资源的效果,最后再调用sum把它合起来。

虽然base和cells都是volatile修饰的,但感觉这个sum操作没有加锁,可能sum的结果不是那么精确。

AtomicInteger-源码解析

我们可以查看一下源码,在idea中显示如下:

public AtomicInteger(int initialValue) {
        this.value = initialValue;
}

public final int incrementAndGet() {
        return U.getAndAddInt(this, VALUE, 1) + 1;
}

public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!weakCompareAndSetInt(o, offset, v, v + delta));
        return v;
}

public final boolean weakCompareAndSetInt(Object o, long offset, int expected, int x) {
    return this.compareAndSetInt(o, offset, expected, x);
}

public final native boolean compareAndSetInt(Object var1, long var2, int var4, int var5);
//先自增,然后获取自增后的结果
public final int incrementAndGet() {
        //+ 1 自增后的结果
        //this 就表示当前的atomicInteger(值)
        //1    自增一次
        return U.getAndAddInt(this, VALUE, 1) + 1;
}

public final int getAndAddInt(Object o, long offset, int delta) {
        //v 旧值
        int v;
        //自旋的过程
        do {
            //不断的获取旧值
            v = getIntVolatile(o, offset);
            //如果这个方法的返回值为false,那么继续自旋
            //如果这个方法的返回值为true,那么自旋结束

            //o 表示的就是内存值
            //v 旧值
            //v + delta 修改后的值
        } while (!weakCompareAndSetInt(o, offset, v, v + delta));
            //作用:比较内存中的值,旧值是否相等,如果相等就把修改后的值写到内存中,返回true。表示修改成功。
            //                                 如果不相等,无法把修改后的值写到内存中,返回false。表示修改失败。
            //如果修改失败,那么继续自旋。
        return v;
}

乐观锁与悲观锁

synchronized和CAS的区别 :

相同点:

在多线程情况下,都可以保证共享数据的安全性。

不同点:

synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都有可能修改。所以在每 次操作共享数据之前,都会上锁。(悲观锁)

CAS是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁。只不过在修改共享数据的时候,会检查一下,别人有没有修改过这个数据。

如果别人修改过,那么我再次获取现在最新的值(自旋)。

如果别人没有修改过,那么我现在直接修改共享数据的值 。(乐观锁)

阅读下章可见

Java多线程编程(七)——并发工具类https://blog.csdn.net/weixin_43715214/article/details/122465954

posted @ 2022-01-11 17:56  金鳞踏雨  阅读(56)  评论(0编辑  收藏  举报  来源