Java多线程核心技术一:volatile关键字

1 volatile关键字概述

        volatile关键字在使用上有3个特性:

        1、可见性:B线程能马上看到A线程更改的数据。

        2、原子性:volatile原子性体现在赋值原子性,在32位JDK中对64位数据类型执行赋值操作时会写两次,高32位和低32位,如果写两次的这组操作被打断,导致写入的数据被其他线程的写操作所覆盖,获得错误的结果,就是非原子性的。如果写两次的操作是连续的,不允许被打断,就是原子性的。在32位JDK中针对未使用volatile声明的long或double64位数据类型没有实现写原子性,如果想实现,需要在声明变量时添加volatile。而在64位JDK中,是否具有原子性取决于具体的实现,在X86架构64位JDK版本中,写double或long是原子性的。针对用volatile声明的int变量进行i++操作时是非原子性的。这些会在下面的内容验证。

        3、禁止代码重排序。

2 可见性测试

        volatile关键字具有可见性,可以提高软件(系统)的灵敏度。具体测试过程如下:

        2.1 单线程出现死循环

        

public class PrintString {
    private boolean isContinuePrint = true;

    public boolean isContinuePrint() {
        return isContinuePrint;
    }

    public void setContinuePrint(boolean continuePrint) {
        isContinuePrint = continuePrint;
    }

    public void printStringMethod(){
        try {
            while(isContinuePrint = true){
                System.out.println("运行 printStringMethod()方法的线程是:" + Thread.currentThread().getName());
                Thread.sleep(1000);
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
public class Run1 {
    public static void main(String[] args) {
        PrintString printString = new PrintString();
        printString.printStringMethod();
        System.out.println("我要停止它。停止的线程是:" + Thread.currentThread().getName());
        printString.setContinuePrint(false);
    }
}

        出现死循环的原因是main线程一直处理while循环,导致程序不能继续执行后面的代码,解决办法是使用多线程。 

2.2 使用多线程解决死循环

public class PrintString implements Runnable{
    private boolean isContinuePrint = true;

    public boolean isContinuePrint() {
        return isContinuePrint;
    }

    public void setContinuePrint(boolean continuePrint) {
        isContinuePrint = continuePrint;
    }

    public void printStringMethod(){
        try {
            while(isContinuePrint == true){
                System.out.println("运行 printStringMethod()方法的线程是:" + Thread.currentThread().getName());
                Thread.sleep(100);
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        printStringMethod();
    }
}
public class Run1 {

    public static void main(String[] args) throws InterruptedException {
        PrintString printString = new PrintString();
        new Thread(printString).start();
        System.out.println("我要停止它。停止的线程是:" + Thread.currentThread().getName());
        printString.setContinuePrint(false);
    }
}

2.3 使用多线程有可能出现死循环

public class RunThread extends Thread{
    private boolean isRunnint = true;

    public boolean isRunnint() {
        return isRunnint;
    }

    public void setRunnint(boolean runnint) {
        isRunnint = runnint;
    }

    @Override
    public void run(){
        System.out.println("进入run方法");
        while (isRunnint == true){

        }
        System.out.println("线程被停止了");
    }
}

public class Run2 {
    public static void main(String[] args) {
        try {
            RunThread thread = new RunThread();
            thread.start();
            Thread.sleep(1000);
            thread.setRunnint(false);
            System.out.println("已经赋值为false了");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

        是什么原因导致死循环呢?在启动线程时,因为变量private boolean isRunning = tue;分别存储在公共内存和线程的私有内存中,线程运行后再线程的私有内存中取得isRunning的值一直是true,而代码 thread.setRunning(false) 虽然被执行了,却是把公共内存中的isRunning变量改成false,操作的是两块内存地址中的数据,所以一直处于死循环的状态。内存结果如下图:
         

        这个问题其实就是私有内存中的值与公共内存中的值不同导致的,可以通过使用volatile关键字来解决,volatile的主要作用就是当线程访问isRunning变量时,强制地从公共内存中取值。修改RunThread.java文件

public class RunThread extends Thread{
    volatile private boolean isRunnint = true;

    public boolean isRunnint() {
        return isRunnint;
    }

    public void setRunnint(boolean runnint) {
        isRunnint = runnint;
    }

    @Override
    public void run(){
        System.out.println("进入run方法");
        while (isRunnint == true){

        }
        System.out.println("线程被停止了");
    }
}

        通过使用volatile关键字,强制地从公共内存读取变量的值,在同步到线程的私有内存中,内存结结构如下图。并且使线程被正确的终止了,这种方式就是之前提到的一种停止线程的方法:使用退出标志使线程正常退出。 

总结:使用volatile关键字是增加了实例变量在多个线程之间的可见性。

2.4 synchronzied代码块也具有增加可见性作用

        synchronzied关键字可以使多个线程访问同一个资源时,具有可见性,也可以使线程私有内存中的变量与公共内存中的变量同步,也就是可见性,下面对其进行验证。

public class Service {
    private boolean isContinue = true;
    public void runMethod(){
        while(isContinue == true){

        }
        System.out.println("停下来了");
    }
    public void stopMethod(){
        isContinue = false;
    }
}
public class ThreadA extends Thread{
    private Service service;

    public ThreadA(Service service) {
        this.service = service;
    }

    @Override
    public void run(){
        service.runMethod();
    }
}
public class ThreadB extends Thread{
    private Service service;

    public ThreadB(Service service) {
        this.service = service;
    }
    @Override
    public void run(){
        service.stopMethod();
    }
}
public class Run3 {
    public static void main(String[] args) {
        try {
            Service service = new Service();
            ThreadA a = new ThreadA(service);
            a.start();
            Thread.sleep(1000);
            ThreadB b = new ThreadB(service);
            b.start();
            System.out.println("已经发起停止的命令了");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

运行时出现死循环。原因是各个线程间的数据值没有可见性造成的,而synchronzied关键字可以使数据具有可见性,修改Service.java代码:

public class Service {
    private boolean isContinue = true;
    public void runMethod(){
        String s = new String();
        while(isContinue == true){
            synchronized (s){

            }
        }
        System.out.println("停下来了");
    }
    public void stopMethod(){
        isContinue = false;
    }
}

 

        synchronzied关键字会把私有内存中的数据同公共内存同步,使私有内存中的数据和公共内存中的数据一致。

3 原子性与非原子性测试

        在32位JDK中针对未使用volatile声明的long或double的64位数据类型没有实现赋值与原子性,如果想实现,声明变量时添加volatile。如果在64位JDK中,是否原子取决于具体的实现,在X86架构64位JDK版本中,写double或long是原子的。 

        另外,volatile最致命的缺点是不支持运算原子性,也就是多个线程对用volatile修饰的变量i执行 i ++/i --操作时,还是会被分成三步,造成非线程安全问题。

public class MyThread extends Thread{
    volatile public static int count;
    private static void addCount(){
        for (int i = 0;i<100;i++){
            count ++;
        }
        System.out.println("count = " + count);
    }
    @Override
    public void run(){
        addCount();
    }

}
public class Run {
    public static void main(String[] args) {
        MyThread[] array = new MyThread[100];
        for (int i = 0; i < 100; i++) {
            array[i] = new MyThread();
        }
        for (int i = 0; i < 100; i++) {
            array[i].start();
        }
    }
}

        运行结果不是10000,说明在多线程环境下,volatile 修饰的变量的 ++运算是非原子性的。

        修改 MyThread.java如下:

public class MyThread extends Thread{
    volatile public static int count;
    synchronized  private static void addCount(){
        for (int i = 0;i<100;i++){
            count ++;
        }
        System.out.println("count = " + count);
    }
    @Override
    public void run(){
        addCount();
    }

}

        在本示例中,如果在方法private static  void addCount()前加入synchronzied同步关键字,就没有必要在使用volatile关键字来声明count变量了。

        volatile关键字主要是在多个线程中可以感知实例变量被更改了,并且可以获得最新的值时使用,也就是增加可见性时使用。例如,在32位JDK中增加赋值操作的原子性。

        volatile关键字提示线程每次从公共内存中区读取变量,而不是从私有内存中去读取,这样就保证了同步数据的可见性。但需要注意的是:如果修改实例变量中的数据,比如i++,则这样的操作其实并不是一个原子操作,也就是非线程安全的。表达式i++的操作步骤分解如下:

        1、从内存中取出i的值。

        2、计算i的值。

        3、将i的值写到内存中。

        假设在第二步计算的时候,另一个线程也修改i的值,那么结果就是错误的,解决办法是使用synchronzied关键字保证原子性。所以,volatile本身并不处理int i ++ 运算操作的原子性。

        总结:volatile保证数据在线程之间的可见性,但不保证同步性,同时在32位JDK中保证赋值操作的原子性。

4 使用Atomic原子类进行i++操作实现原子性

        除了在i++操作时使用synchronzied关键字实现同步外,还可以使用AtomicInteger原子类实现。原子操作是不可分割的整体,没有其他线程能够中断或检查正在原子操作中的变量。一个原子(atomic)类型就是一个原子操作可用的类型,它可以在没有锁的情况下做到线程安全。

public class AddCountThread extends Thread{
    private AtomicInteger count = new AtomicInteger(0);
    @Override
    public void run(){
        for (int i = 0; i < 10000; i++) {
            System.out.println(count.incrementAndGet());
        }
    }
}
public class Run1 {
    public static void main(String[] args) {
        AddCountThread addCountThread = new AddCountThread();
        Thread t1 = new Thread(addCountThread);
        t1.start();
        Thread t2 = new Thread(addCountThread);
        t2.start();
        Thread t3 = new Thread(addCountThread);
        t3.start();
        Thread t4 = new Thread(addCountThread);
        t4.start();
        Thread t5 = new Thread(addCountThread);
        t5.start();
    }
}

        成功累加到50000。

5 逻辑混乱与解决方案 

        即使在有逻辑性的情况下,原子类的输出结果也具有随机性。

        

public class MyService {
    public static AtomicLong aiRef = new AtomicLong();
    public void addNum(){
        System.out.println(Thread.currentThread().getName() + "加了100之后的值是:" + aiRef.addAndGet(100L));
        aiRef.addAndGet(1L);

    }
}
public class MyThread extends Thread{
    private MyService myService;

    public MyThread(MyService myService) {
        this.myService = myService;
    }

    @Override
    public void run(){
        myService.addNum();
    }
}
public class Run1 {
    public static void main(String[] args) {
        try {
            MyService service = new MyService();
            MyThread[] threads = new MyThread[5];
            for (int i = 0; i < threads.length; i++) {
                threads[i] = new MyThread(service);
            }
            for (int i = 0; i < threads.length; i++) {
                threads[i].start();
            }
            Thread.sleep(1000);
            System.out.println(service.aiRef.get());
        }catch (InterruptedException e){
            e.printStackTrace();
        }


    }
}

        执行结果如上图,输出顺序错了,应该每加1次100再次加1,出现这种情况的原因是addAndGet()方法是原子的,但方法和方法之间的调用确实非原子的,此时可以用同步解决该问题。更改MyService.java

public class MyService {
    public static AtomicLong aiRef = new AtomicLong();
    synchronized public void addNum(){
        System.out.println(Thread.currentThread().getName() + "加了100之后的值是:" + aiRef.addAndGet(100L));
        aiRef.addAndGet(1L);

    }
}

 

        从运行结果可以看到,输出信息依次加100再加1,是正确的计算过程。 

6 禁止代码重排序

        volatile关键字可以禁止代码重排序。什么是重排序?在Java程序运行时,JIT(即时编译器)为了优化程序的运行,可以动态地改变程序代码运行的顺序。比如有如下代码:

A 代码 - 重耗时
B 代码 - 轻耗时
C 代码 - 重耗时
D 代码 - 轻耗时

        在多线程的环境中,JIT有可能进行代码重排,重排后的代码顺序可能如下:

B 代码 - 轻耗时
D 代码 - 轻耗时
A 代码 - 重耗时
C 代码 - 重耗时

        这样做的主要原因是CPU流水线中这4个指令时同时执行的,轻耗时的代码在很大程度上会先执行完,以让出CPU流水线资源供其他指令使用,所以代码重排是为了追求更好的程序运行效率。

        重排发生在没有依赖关系时,比如上面ABCD代码,相互之间没有依赖关系时就会发生重排,如果存在依赖则代码不会重排序。

        而volatile关键字可以禁止代码重排序,比如下面这段代码:

A 变量的操作
B 变量的操作
volatile Z 变量的操作
C 变量的操作
D 变量的操作

        根据以上代码,会有下面2种情况

        1、AB可以重排,但不能排到Z的后面

        2、CD可以重排,但不能排到Z的前面

        也就是说,变量Z是一道屏障,Z变量之前或之后的代码不能跨越Z。同样,synchronzied关键字也具有同样的特性。

总结:

        synchronzied关键字的主要作用是保证同一时刻,只有一个线程可以执行某一个方法或者某一个代码块。synchronzied可以修改方法以及代码块,随着JDK版本的升级,synchronzied在执行效率上得到很大提升,它包含三个特征:可见性、原子性和禁止代码重排序。

        volatile关键字的主要作用是让其他线程可以看到最新的值,volatile只能修改变量。同样也具有可见性、原子性和禁止代码重排序三个特征。

        synchronzied和volatile使用场景:

        1、当想实现一个变量的值被更改,而其他线程能取到最新的值时,就要对变量使用volatile。

        2、如果多个线程对同一个对象中的同一个实例变量进行写操作,为了避免出现非线程安全问问题,就要使用synchronzied。

  • 29
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

geminigoth

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值