《Java 多线程编程核心技术》笔记——第2章 对象及变量的并发访问(四)

声明:

本博客是本人在学习《Java 多线程编程核心技术》后整理的笔记,旨在方便复习和回顾,并非用作商业用途。

本博客已标明出处,如有侵权请告知,马上删除。

2.3 volatile 关键字

关键字 volatile 的主要作用是使变量在多个线程间可见

2.3.1 同步死循环

  1. 创建一个公共类

    public class Service1 {
        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("run printStringMethod threadName = " +Thread.currentThread().getName());
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    
  2. 测试类

    public class Service1Test {
        public static void main(String[] args) {
            Service1 service1 = new Service1();
            service1.printStringMethod();
            System.out.println("我要停止它!stopThread=" + Thread.currentThread().getName());
            service1.setContinuePrint(false);
        }
    }
    

    运行结果

    run printStringMethod threadName = main
    run printStringMethod threadName = main
    run printStringMethod threadName = main
    run printStringMethod threadName = main
    run printStringMethod threadName = main
    run printStringMethod threadName = main
    ...
    

分析:程序运行后,根本停不下来。这是因为 main 线程一直在处理 while 循环,导致不能继续执行后面的代码。解决办法当然是使用多线程技术

2.3.2 解决同步死循环

  1. 创建一个自定义线程类

    public class Service2 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("run printStringMethod threadName = " +Thread.currentThread().getName());
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        @Override
        public void run() {
            printStringMethod();
        }
    }
    
  2. 测试类

    public class Service2Test {
        public static void main(String[] args) throws InterruptedException {
            Service2 service2 = new Service2();
            new Thread(service2).start();
            Thread.sleep(2000);
            System.out.println("我要停止它!stopThread=" + Thread.currentThread().getName());
            service2.setContinuePrint(false);
        }
    }
    

    运行结果

    run printStringMethod threadName = Thread-0
    run printStringMethod threadName = Thread-0
    我要停止它!stopThread=main
    

2.3.3 解决异步死循环

  1. 创建一个自定义线程类

    public class RunThread extends Thread{
        private boolean isRunning = true;
    
        public boolean isRunning() {
            return isRunning;
        }
    
        public void setRunning(boolean running) {
            isRunning = running;
        }
    
        @Override
        public void run() {
            super.run();
            try {
                System.out.println("进入 run 了");
                while (isRunning == true) {
                    System.out.println("run printStringMethod threadName = " +Thread.currentThread().getName());
                    Thread.sleep(1000);
                }
                System.out.println("线程被停止了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
  2. 测试类

    public class RunThreadTest {
        public static void main(String[] args) throws InterruptedException {
            RunThread runThread = new RunThread();
            runThread.start();
            Thread.sleep(2000);
            runThread.setRunning(false);
            System.out.println("已经赋值为 false");
        }
    }
    

    运行结果

    进入 run 了
    run printStringMethod threadName = Thread-0
    run printStringMethod threadName = Thread-0
    已经赋值为 false
    线程被停止了
    

但上面的代码格式,一旦运行在 -server 服务器模式中 64bit 的 JVM 上时,会出现死循环。

  1. 打开 Edit Configurations --> RunThreadTest --> VM options,配置如下:

    -server
    
  2. 再次运行

    进入 run 了
    run printStringMethod threadName = Thread-0
    run printStringMethod threadName = Thread-0
    已经赋值为 false
    run printStringMethod threadName = Thread-0
    run printStringMethod threadName = Thread-0
    ...
    

分析:可以看到出现了死循环的效果。代码 System.out.println("线程被停止了"); 从未被执行。

是什么样的原因造成将 JVM 设置为 -server 时就出现死循环呢

在启动 RunThread.java 线程时,变量 private boolean isRunning = true; 存在于公共堆栈及线程的私有堆栈中。在 JVM 被设置为 -server 模式是为了线程运行的效率,线程一直在私有堆栈中取得 isRunning 的值是 true。而代码 thread.setRunning(false); 虽然被执行,更新的却是公共堆栈中的 isRunning 变量值 false,所以一直就是死循环状态。内存结构如图所示:

在这里插入图片描述

这个问题是私有堆栈中的值和公共堆栈中的值不同步造成的。解决这样的问题就要使用 volatile 关键字,它的作用就是当线程访问 isRunning 这个变量时,强制性从公有堆栈中进行取值

  1. 修改自定义线程类

    public class RunThread extends Thread{
        volatile private boolean isRunning = true;
    
        public boolean isRunning() {
            return isRunning;
        }
    
        public void setRunning(boolean running) {
            isRunning = running;
        }
    
        @Override
        public void run() {
            super.run();
            try {
                System.out.println("进入 run 了");
                while (isRunning == true) {
                    System.out.println("run printStringMethod threadName = " +Thread.currentThread().getName());
                    Thread.sleep(1000);
                }
                System.out.println("线程被停止了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
  2. 再次运行

    进入 run 了
    run printStringMethod threadName = Thread-0
    run printStringMethod threadName = Thread-0
    已经赋值为 false
    线程被停止了
    

通过使用 volatile 关键字,强制的从公共内存中读取变量的值,内存结构如图所示:

在这里插入图片描述

使用 volatile 关键字增加了实例变量在多个线程之间的可见性,但 volatile 关键字有一个致命的缺陷是不支持原子性

原子性:即一个操作不会被线程调度器中断,要么执行完,要么就不执行

volatile 和 synchronized 的区别

  • 关键字 volatile 是线程同步的轻量级实现,所有 volatile 性能肯定比 synchronized 要好,并且 volatile 只能修饰于变量,而 synchronized 可以修饰方法以及代码块。随着 JDK 新版本的发布,synchronized 在执行效率上得到了很大的提升,在开发中使用 synchronized 的比率还是比较大的。
  • 多线程访问 volatile 不会发生阻塞,而 synchronized 会出现阻塞。
  • volatile 能保证数据的可见性,但不能保证原子性;而 synchronized 可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。
  • 最后再次强调,volatile 解决的是变量在多个线程之间的可见性;而 synchronized 解决的是多个线程之间访问资源的同步性。

2.3.4 voletile 非原子的特性

下面来验证一下 voletile 的非原子性。

  1. 创建一个自定义线程类,其中 count 变量是 voletile 修饰的静态变量

    public class MyThread1 extends Thread {
        volatile public static int count;
    
        private static void addCount() {
            for (int i = 0; i <100; i++) {
                count++;
            }
            System.out.println(count);
        }
    
        @Override
        public void run() {
            super.run();
            addCount();
        }
    }
    
  2. 创建测试类

    public class MyThread1Test {
        public static void main(String[] args) {
            MyThread1[] myThread1Arr = new MyThread1[1000];
            for (int i = 0; i < 100; i++) {
                myThread1Arr[i] = new MyThread1();
            }
            for (int i = 0; i < 100; i++) {
                myThread1Arr[i].start();
            }
        }
    }
    

    运行结果

    ...
    7115
    7015
    6815
    6615
    9881
    9481
    9181
    8881
    8615
    

分析:如果 voletile 具有原子性的话,那每一次 count++ 都不会被线程调度器中断并成功执行,count 最后应该是 10000 才对,可是这里运行结果最大的值为 9881,显然证明了 voletile 不具有原子性。

  1. 修改自定义线程类,在 addCount 方法前加上 synchronized 关键字,这样相当于给 MyThread1.class 上锁

    public class MyThread1 extends Thread {
        volatile public static int count;
    
        synchronized private static void addCount() {
            for (int i = 0; i <100; i++) {
                count++;
            }
            System.out.println(count);
        }
    
        @Override
        public void run() {
            super.run();
            addCount();
        }
    }
    
  2. 再次运行,得到 10000 的正确结果

    ...
    9300
    9400
    9500
    9600
    9700
    9800
    9900
    10000
    

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

  1. 从内存中取出 i 的值。
  2. 计算 i 的值。
  3. 将 i 的值写到内存中。

假如在第 2 步计算值的时候,另外一个线程也修改 i 的值,那么这个时候就会出现脏读数据。解决的办法其实就是使用 synchronized 关键字,这个知识点在前面的案例中已经介绍过了。所以说 volatile 本身并不处理数据的原子性,而是强制对数据的读写及时影响到主内存的。
用图来演示一下使用关键字 volatile 时出现非线程安全的原因。变量在内存中工作的过程如下图所示:

在这里插入图片描述

  1. read 和 load 阶段:从主存复制变量到当前线程工作内存。
  2. use 和 assign 阶段:执行代码,改变共享变量值。
  3. store 和 write 阶段:用工作内存数据刷新主存对应变量的值。

在多线程环境中,use 和 assign 是多次出现的,但这一操作并不是原子性,也就是在 read 和 load 之后,如果主内存 count 变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,也就是私有内存和公共内存中的变量不同步,所以计算出来的结果汇合预期不一样,也就出现了非线程安全问题

2.3.5 使用原子类进行 i++

除了在 i++ 操作时使用 synchronized 关键字实现同步外,还可以使用 AtomicInteger 原子类进行实现。

原子操作是不能分割的整体,没有其他线程能够中断或检查正在原子操作中的变量。一个原子类型就是一个原子操作可用的类型,它可以在没有锁的情况下做到线程安全

  1. 修改自定义线程类,使用 AtomicInteger 类型的 count

    public class MyThread1 extends Thread {
        volatile public static AtomicInteger count = new AtomicInteger();
    
        private static void addCount() {
            for (int i = 0; i <100; i++) {
                count.incrementAndGet();
            }
            System.out.println(count.get());
        }
    
        @Override
        public void run() {
            super.run();
            addCount();
        }
    }
    
  2. 再次运行,得到 10000 的正确结果。

    ...
    9400
    9500
    9600
    9700
    10000
    9900
    9800
    

2.3.6 原子类也不完全安全

从上面例子中虽然得到 10000 的正确结果,但打印的顺序有问题。这是因为虽然 count.incrementAndGet() 是原子的,但 addCount() 方法不是原子的,解决这个问题就要在 addCount() 前加 synchronized 就行了。

2.3.7 synchronized 代码块具有 volatile 同步的功能

关键字 synchronized 可以使多个线程访问同一个资源具有同步性,而且它还具有将线程工作内存中的私有变量与公共内存中的变量同步的功能。

它包含两个特征:互斥性和可见性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

bm1998

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

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

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

打赏作者

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

抵扣说明:

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

余额充值