欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JUC之volatile关键字详解

程序员文章站 2022-07-13 14:55:03
...
一、JUC简介

  •     在Java5.0提供了java.util.concurrent(简称JUC包),在此包中增加了在并发编程中很常用的工具类,在用于定义类似于线程的自定义子系统,包括线程池,异步IO和轻量级任务框架;还提供了设计用于多线程上下文中的Collection实现等。

二、volatile关键字

  • volatile关键字:当多个线程进行共享数据时,可以保证内存中的数据时可见的;相比较于syschronized是一种较为轻量级的同步策略。
  • volatile不具备“互斥性”。
  • volatile不能保证变量的“原子性”。
    下面举一个列子:
package com.itszt;

public class Test {
    public static void main(String[] args) throws InterruptedException {

        MyThread myThread = new MyThread();
        System.out.println("开启子线程");
        myThread.start();
        System.out.println("子线程开启完毕");
        Thread.sleep(2000);
        System.out.println("在主线程中终止子线程");
        myThread.setCanRun(false);
        System.out.println("main里面的----"+myThread.canRun);

    }

    public static class MyThread extends Thread{

        private  boolean canRun = true;

        public boolean isCanRun() {
            return canRun;
        }

        public void setCanRun(boolean canRun) {
            this.canRun = canRun;
        }

        @Override
        public void run() {
            super.run();
            while (canRun) {


            }
            System.out.println("子线程里面的:"+canRun);
            System.out.println("子线程终止。。。。");

        }
    }
}
运行结果:

JUC之volatile关键字详解

上面例子中,我们的main方法的主线程与子线程MyThread同时对变量canRun变量进行操作,canRun就是多线程的共享数据,通过运行结果可以看出,主线程更改了canRun的值为false,按理说根据循环条件,子线程应该会终止,但是结果并没有终止,而是仍然处在死循环中,可见子线程中的canRun并没有被更改,仍然是true。

为什么会出现这样的问题呢?我们明明已经更改了变量的值啊?下面,我们来看一下这个变量的值是如何的一个存在,和两个线程是如何取值的。


JUC之volatile关键字详解


可以看出,多个线程在共享这个数据时,每个线程会自己备份这个数据,并单独进行对数据的操作,这样,线程之间对应这个共享数据时不可见的。


  •     解决办法一
        当我们用关键字volatile修饰canRun后


package com.itszt;
public class Test {
    public static void main(String[] args) throws InterruptedException {

        MyThread myThread = new MyThread();
        System.out.println("开启子线程");
        myThread.start();
        System.out.println("子线程开启完毕");
        Thread.sleep(2000);
        System.out.println("在主线程中终止子线程");
        myThread.setCanRun(false);
        System.out.println("main里面的----"+myThread.canRun);

    }

    public static class MyThread extends Thread{

        private volatile boolean canRun = true;

        public boolean isCanRun() {
            return canRun;
        }

        public void setCanRun(boolean canRun) {
            this.canRun = canRun;
        }

        @Override
        public void run() {
            super.run();
            while (canRun) {


            }
            System.out.println("子线程里面的:"+canRun);
            System.out.println("子线程终止。。。。");

        }
    }
}
    运行结果:


JUC之volatile关键字详解


  •         解决办法二
                   使用同步锁sychronized


package com.itszt;

public class Test {
    public static void main(String[] args) throws InterruptedException {

        MyThread myThread = new MyThread();
        System.out.println("开启子线程");
        myThread.start();
        System.out.println("子线程开启完毕");
        Thread.sleep(2000);
        System.out.println("在主线程中终止子线程");

        myThread.setCanRun(false);
        System.out.println("main里面的----"+myThread.canRun);

    }

    public static class MyThread extends Thread{

        private  boolean canRun = true;

        public boolean isCanRun() {
            return canRun;
        }

        public void setCanRun(boolean canRun) {
            this.canRun = canRun;
        }


        @Override
        public void run() {
            super.run();
            while (true){
                synchronized (Test.class){
                    if (!canRun){
                        break;
                    }
                }
            }
            System.out.println("子线程里面的:"+canRun);
            System.out.println("子线程终止。。。。");

        }
    }
}
  运行结果:


JUC之volatile关键字详解


三、i++原子性问题

  1.     i++实际分为三步:读取--->修改--->写入
  2.     原子性:“读-改-写”三步为一体的,是不能被拆分的
  3.     原子变量:JDK5以后,java.util.concurrent.atomic包下,提供了常用的原子变量
  •         原子变量的值用volatile修饰,确保变量内存可见性
  •         CAS(Compare-And-Swap)算法保证数据的原子性
下面的例子来看volatile关键字不能确保操作的原子性问题:


package com.itszt;

/**
* 
*
*
* 关键字:volatile:保证该数据是被所有子线程共享的
*
* 与static的区别:所有的对象共享同一个数据(volatile是作用于多线程的,static是作用于多对象的)
*
*
*/
public class Test1 {

    private static volatile int i=0;
    private static long timeBegin;

    public static void main(String[] args){
        timeBegin = System.currentTimeMillis();
        for (int a = 0; a < 50; a++) {
            new MyThread().start();
        }
    }
    private static class MyThread extends Thread{
        @Override
        public void run() {
            super.run();
            for (int b = 0; b < 10000; b++) {
                i++;
            }
            System.out.println("最后的值: "+i);
            if (i==500000) {
                System.out.println("结束的时间为:"+(System.currentTimeMillis()-timeBegin));
            }
        }
    }
}
运行结果:


JUC之volatile关键字详解


可以看出,使用volatile关键字修饰的i,经过50个线程处理,并没有自加到500000,出现这种情况的原因正是因为volatile不能保证操作的原子性,当一个线程读取到当前的i值,在进行读-改-写操作的过程中,已有其他线程更改过了i的值,这样导致更改数据的不一致性。

当我们用原子变量修饰共享数据,就能很好的保证原子性。


package com.itszt;

import java.util.concurrent.atomic.AtomicInteger;

/**
* 
*
* i++  的原子性   读-改-写
*
* 锁:
*
*      悲观锁:
*
*          我们使用的时候别人是不能用的,只有当我们使用完释放了资源,别人才能用
*          synchronized、lock:可以解决原子性问题,但是运行效率都不高
*
*      乐观锁:
*
*          多个线程可以对同一个数据同时操作,或者同一段代码进行操作,只不过操作结果是否生效就不一定了。
*
*  JUC是如何帮我们解决这个问题的呢?
*
*      JUC基于CAS算法来做
*
*      多个线程对同一个数据进行操作时会留一个版本号,每个线程每操作一次,就会更新版本号,将版本号自增1
*      比如:A操作之前的版本号为0,那么当A操作完之后,A将更改这个版本号为1
*      加入A操作完成之后,在A需要更新版本之前,会检查当前的版本号,发现版本号发生变化不在是0的时候,
*      或者已经为1,那么肯定有另外的线程,比如B对该数据进行了更改,并且生成了新的版本号,此时A就需要重新执行操作了(读改写)
*
*
*
*/
public class Test2 {

    //使用原子变量进行改进
    private static volatile AtomicInteger i = new AtomicInteger(0);
    //声明一个时间戳
    private static long timeBegin;

    public static void main(String[] args){

        timeBegin = System.currentTimeMillis();
        //模拟多线程去操作共享数据i
        for (int b = 0; b < 50; b++) {
            new MyThread().start();
        }
    }

    //定义个线程
    public static class MyThread extends Thread{
        @Override
        public void run() {
            super.run();
            //循环执行i++
            for (int a = 0; a < 10000; a++) {
                i.incrementAndGet();//等同于i++
            }
            System.out.println("输出i的值:"+i);
            //计算执行完毕的时间
            if (i.get()==500000) {
                System.out.println("完成时间: "+(System.currentTimeMillis()-timeBegin));
            }
        }
    }
}
运行结果:


JUC之volatile关键字详解


解决volatile关键字存在不能保证原子性的不足,另一种解决方案是使用同步锁synchronized,但是执行效率远远不及乐观锁的执行效率


package com.itszt;

/**
* 
*
*
* 关键字:volatile:保证该数据是被所有子线程共享的
*
* 与static的区别:所有的对象共享同一个数据(volatile是作用于多线程的,static是作用于多对象的)
*
*
*/
public class Test1 {

    private static volatile int i=0;

    private static long timeBegin;

    public static void main(String[] args){
        timeBegin = System.currentTimeMillis();
        for (int a = 0; a < 50; a++) {
            new MyThread().start();
        }
    }
    private static class MyThread extends Thread{
        @Override
        public void run() {
            super.run();
            for (int b = 0; b < 10000; b++) {
                //使用同步锁
                synchronized (Test1.class){
                    i++;
                }
            }
            System.out.println("最后的值: "+i);
            if (i==500000) {
                System.out.println("结束的时间为:"+(System.currentTimeMillis()-timeBegin));
            }
        }
    }
}
运行结果:

    

四、CAS算法

  •     CAS(Compare-And-Swap)算法是硬件对于并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问;
  •     CAS是一种无锁的非阻塞算法实现
  •     CAS包含三个操作数:
    • 需要读写的内存值:V
    • 进行比较的预估值:A
    • 拟写入的更新值:B
    • 当且仅当V==A时,V=B,否则,将重新操作。
相关标签: JUC

上一篇: volatile关键字

下一篇: WAS常见问题