zoukankan      html  css  js  c++  java
  • Java多线程(四):volatile

    volatile

    volatile是一种轻量同步机制。请看例子
    MyThread25类

    public class MyThread25 extends Thread{
        private boolean isRunning = true;
    
        public boolean isRunning()
        {
            return isRunning;
        }
    
        public void setRunning(boolean isRunning)
        {
            this.isRunning = isRunning;
        }
    
        public void run()
        {
            System.out.println("进入run了");
            while (isRunning == true){}
            System.out.println("线程被停止了");
        }
    
        public static void main(String[] args) throws InterruptedException {
    
            MyThread25 mt = new MyThread25();
            mt.start();
            Thread.sleep(1000);
            mt.setRunning(false);
            System.out.println("已设置为false");
    
        }
    }
    

    输出结果如下

    进入run了
    已设置为false
    

    为什么程序始终不结束?说明mt.setRunning(false);没有起作用。
    这里我们说下Java内存模型(JMM)
    java虚拟机有自己的内存模型(Java Memory Model,JMM),JMM可以屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。
    JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本,线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存中的变量。这三者之间的交互关系如下

    出现上述运行结果的原因是,主内存isRunning = true, mt.setRunning(false)设置主内存isRunning = false,本地内存中isRunning仍然是true,线程用的是本地内存,所以进入了死循环。

    在isRunning前加上volatile
    private volatile boolean isRunning = true;
    输出结果如下

    进入run了
    已设置为false
    线程被停止了
    

    volatile不能保证原子类线程安全

    先看例子
    MyThread26_0类,用volatile修饰num

    public class MyThread26_0 extends Thread {
        public static volatile int num = 0;
        //使用CountDownLatch来等待计算线程执行完
        static CountDownLatch countDownLatch = new CountDownLatch(30);
    
        @Override
        public void run() {
            for(int j=0;j<1000;j++){
                num++;//自加操作
            }
            countDownLatch.countDown();
        }
    
        public static void main(String[] args) throws InterruptedException {
            MyThread26_0[] mt = new MyThread26_0[30];
            //开启30个线程进行累加操作
            for(int i=0;i<mt.length;i++){
                mt[i] = new MyThread26_0();
            }
            for(int i=0;i<mt.length;i++){
                mt[i].start();
            }
            //等待计算线程执行完
            countDownLatch.await();
            System.out.println(num);
        }
    }
    

    输出结果如下

    25886
    

    理论上,应该输出30000。原子操作表示一段操作是不可分割的,因为num++不是原子操作,这样会出现线程对过期的num进行自增,此时其他线程已经对num进行了自增。
    num++分三步:读取、加一、赋值。
    结论:
    volatile只会对单个的的变量读写具有原子性,像num++这种复合操作volatile是无法保证其原子性的
    解决方法:
    用原子类AtomicInteger的incrementAndGet方法自增

    public class MyThread26_1 extends Thread {
        //使用原子操作类
        public static AtomicInteger num = new AtomicInteger(0);
        //使用CountDownLatch来等待计算线程执行完
        static CountDownLatch countDownLatch = new CountDownLatch(30);
    
        @Override
        public void run() {
            for(int j=0;j<1000;j++){
                num.incrementAndGet();//原子性的num++,通过循环CAS方式
            }
            countDownLatch.countDown();
        }
    
        public static void main(String []args) throws InterruptedException {
            MyThread26_1[] mt = new MyThread26_1[30];
            //开启30个线程进行累加操作
            for(int i=0;i<mt.length;i++){
                mt[i] = new MyThread26_1();
            }
            for(int i=0;i<mt.length;i++){
                mt[i].start();
            }
            //等待计算线程执行完
            countDownLatch.await();
            System.out.println(num);
        }
    }
    

    输出结果如下

    30000
    

    原子类方法组合使用线程不安全

    例子如下
    ThreadDomain27类

    public class ThreadDomain27 {
        public static AtomicInteger aiRef = new AtomicInteger();
    
        public void addNum()
        {
            System.out.println(Thread.currentThread().getName() + "加了100之后的结果:" + aiRef.addAndGet(100));
            aiRef.getAndAdd(1);
        }
    }
    

    MyThread27类

    public class MyThread27 extends Thread{
        private ThreadDomain27 td;
    
        public MyThread27(ThreadDomain27 td)
        {
            this.td = td;
        }
    
        public void run()
        {
            td.addNum();
        }
    
        public static void main(String[] args)
        {
            try
            {
                ThreadDomain27 td = new ThreadDomain27();
                MyThread27[] mt = new MyThread27[5];
                for (int i = 0; i < mt.length; i++)
                {
                    mt[i] = new MyThread27(td);
                }
                for (int i = 0; i < mt.length; i++)
                {
                    mt[i].start();
                }
                Thread.sleep(1000);
                System.out.println(ThreadDomain27.aiRef.get());
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }
    

    输出结果如下

    Thread-2加了100之后的结果:100
    Thread-3加了100之后的结果:200
    Thread-0加了100之后的结果:302
    Thread-1加了100之后的结果:403
    Thread-4加了100之后的结果:504
    505
    

    理想的输出结果是100,201,302...,因为addAndGet方法和getAndAdd方法构成的addNum不是原子操作。
    解决该问题只需要在addNum加上synchronized关键字。
    输出结果如下

    Thread-1加了100之后的结果:100
    Thread-0加了100之后的结果:201
    Thread-2加了100之后的结果:302
    Thread-3加了100之后的结果:403
    Thread-4加了100之后的结果:504
    505
    

    结论:
    volatile解决的是变量在多个线程之间的可见性,但是无法保证原子性。
    synchronized不仅保障了原子性外,也保障了可见性。

    volatile和synchronized比较

    先看实例,使用volatile是什么效果
    CountDownLatch保证10个线程都能执行完成,当然你也可以在System.out.println(test.inc);之前使用Thread.sleep(xxx)

    public class MyThread28 {
        //使用CountDownLatch来等待计算线程执行完
        static CountDownLatch countDownLatch = new CountDownLatch(10);
        public volatile int inc = 0;
        public void increase() {
            inc++;
        }
    
        public static synchronized void main(String[] args) throws InterruptedException {
            final MyThread28 test = new MyThread28();
            for(int i=0;i<10;i++){
                new Thread(){
                    public void run() {
                        for(int j=0;j<1000;j++)
                            test.increase();
                        countDownLatch.countDown();
    
                    }
                }.start();
            }
            countDownLatch.await();
            System.out.println(test.inc);
        }
    
    }
    

    运行结果如下

    9677
    

    每次运行结果都不一致。刚才我已经解释过,这里我再解释一遍。
    使用volatile修饰int型变量i,多个线程同时进行i++操作。比如有两个线程A和B对volatile修饰的i进行i++操作,i的初始值是0,A线程执行i++时从本地内存刚读取了i的值0(i++不是原子操作),就切换到B线程了,B线程从本地内存中读取i的值也为0,然后就切换到A线程继续执行i++操作,完成后i就为1了,接着切换到B线程,因为之前已经读取过了,所以继续执行i++操作,最后的结果i就为1了。同理可以解释为什么每次运行结果都是小于10000的数字。

    解决方法:
    使用synchronized关键字

    public class MyThread28 {
        //使用CountDownLatch来等待计算线程执行完
        static CountDownLatch countDownLatch = new CountDownLatch(10);
        public int inc = 0;
        public synchronized void increase() {
            inc++;
        }
    
        public static synchronized void main(String[] args) throws InterruptedException {
            final MyThread28 test = new MyThread28();
            for(int i=0;i<10;i++){
                new Thread(){
                    public void run() {
                        for(int j=0;j<1000;j++)
                            test.increase();
                        countDownLatch.countDown();
    
                    }
                }.start();
            }
            countDownLatch.await();
            System.out.println(test.inc);
        }
    
    }
    

    输出结果如下

    10000
    

    synchronized不管是否是原子操作,它能保证同一时刻只有一个线程获取锁执行同步代码,会阻塞其他线程。
    结论:
    volatile只能用在变量,synchronized可以在变量、方法上使用。
    volatile不会造成线程阻塞,synchronized会造成线程阻塞。
    volatile效率比synchronized高。

  • 相关阅读:
    关于返回上一页功能
    Mybatis Update statement Date null
    SQLite reset password
    Bootstrap Validator使用特性,动态(Dynamic)添加的input的验证问题
    Eclipse使用Maven2的一次环境清理记录
    Server Tomcat v7.0 Server at localhost failed to start
    PowerShell一例
    Server Tomcat v7.0 Server at libra failed to start
    商标注册英语
    A glance for agile method
  • 原文地址:https://www.cnblogs.com/Java-Starter/p/11130632.html
Copyright © 2011-2022 走看看