JUC之volatile关键字详解

一、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("子线程终止。。。。");

        }
    }
}
运行结果:

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

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




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


  •     解决办法一
        当我们用关键字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("子线程终止。。。。");

        }
    }
}
    运行结果:




  •         解决办法二
                   使用同步锁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("子线程终止。。。。");

        }
    }
}
  运行结果:




三、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));
            }
        }
    }
}
运行结果:




可以看出,使用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));
            }
        }
    }
}
运行结果:




解决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,否则,将重新操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值