当前位置:   article > 正文

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

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 单线程出现死循环

        

  1. public class PrintString {
  2. private boolean isContinuePrint = true;
  3. public boolean isContinuePrint() {
  4. return isContinuePrint;
  5. }
  6. public void setContinuePrint(boolean continuePrint) {
  7. isContinuePrint = continuePrint;
  8. }
  9. public void printStringMethod(){
  10. try {
  11. while(isContinuePrint = true){
  12. System.out.println("运行 printStringMethod()方法的线程是:" + Thread.currentThread().getName());
  13. Thread.sleep(1000);
  14. }
  15. }catch (InterruptedException e){
  16. e.printStackTrace();
  17. }
  18. }
  1. public class Run1 {
  2. public static void main(String[] args) {
  3. PrintString printString = new PrintString();
  4. printString.printStringMethod();
  5. System.out.println("我要停止它。停止的线程是:" + Thread.currentThread().getName());
  6. printString.setContinuePrint(false);
  7. }
  8. }

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

2.2 使用多线程解决死循环

  1. public class PrintString implements Runnable{
  2. private boolean isContinuePrint = true;
  3. public boolean isContinuePrint() {
  4. return isContinuePrint;
  5. }
  6. public void setContinuePrint(boolean continuePrint) {
  7. isContinuePrint = continuePrint;
  8. }
  9. public void printStringMethod(){
  10. try {
  11. while(isContinuePrint == true){
  12. System.out.println("运行 printStringMethod()方法的线程是:" + Thread.currentThread().getName());
  13. Thread.sleep(100);
  14. }
  15. }catch (InterruptedException e){
  16. e.printStackTrace();
  17. }
  18. }
  19. @Override
  20. public void run() {
  21. printStringMethod();
  22. }
  23. }
  1. public class Run1 {
  2. public static void main(String[] args) throws InterruptedException {
  3. PrintString printString = new PrintString();
  4. new Thread(printString).start();
  5. System.out.println("我要停止它。停止的线程是:" + Thread.currentThread().getName());
  6. printString.setContinuePrint(false);
  7. }
  8. }

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

  1. public class RunThread extends Thread{
  2. private boolean isRunnint = true;
  3. public boolean isRunnint() {
  4. return isRunnint;
  5. }
  6. public void setRunnint(boolean runnint) {
  7. isRunnint = runnint;
  8. }
  9. @Override
  10. public void run(){
  11. System.out.println("进入run方法");
  12. while (isRunnint == true){
  13. }
  14. System.out.println("线程被停止了");
  15. }
  16. }

  1. public class Run2 {
  2. public static void main(String[] args) {
  3. try {
  4. RunThread thread = new RunThread();
  5. thread.start();
  6. Thread.sleep(1000);
  7. thread.setRunnint(false);
  8. System.out.println("已经赋值为false了");
  9. }catch (InterruptedException e){
  10. e.printStackTrace();
  11. }
  12. }
  13. }

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

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

  1. public class RunThread extends Thread{
  2. volatile private boolean isRunnint = true;
  3. public boolean isRunnint() {
  4. return isRunnint;
  5. }
  6. public void setRunnint(boolean runnint) {
  7. isRunnint = runnint;
  8. }
  9. @Override
  10. public void run(){
  11. System.out.println("进入run方法");
  12. while (isRunnint == true){
  13. }
  14. System.out.println("线程被停止了");
  15. }
  16. }

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

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

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

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

  1. public class Service {
  2. private boolean isContinue = true;
  3. public void runMethod(){
  4. while(isContinue == true){
  5. }
  6. System.out.println("停下来了");
  7. }
  8. public void stopMethod(){
  9. isContinue = false;
  10. }
  11. }
  1. public class ThreadA extends Thread{
  2. private Service service;
  3. public ThreadA(Service service) {
  4. this.service = service;
  5. }
  6. @Override
  7. public void run(){
  8. service.runMethod();
  9. }
  10. }
  1. public class ThreadB extends Thread{
  2. private Service service;
  3. public ThreadB(Service service) {
  4. this.service = service;
  5. }
  6. @Override
  7. public void run(){
  8. service.stopMethod();
  9. }
  10. }
  1. public class Run3 {
  2. public static void main(String[] args) {
  3. try {
  4. Service service = new Service();
  5. ThreadA a = new ThreadA(service);
  6. a.start();
  7. Thread.sleep(1000);
  8. ThreadB b = new ThreadB(service);
  9. b.start();
  10. System.out.println("已经发起停止的命令了");
  11. }catch (InterruptedException e){
  12. e.printStackTrace();
  13. }
  14. }
  15. }

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

  1. public class Service {
  2. private boolean isContinue = true;
  3. public void runMethod(){
  4. String s = new String();
  5. while(isContinue == true){
  6. synchronized (s){
  7. }
  8. }
  9. System.out.println("停下来了");
  10. }
  11. public void stopMethod(){
  12. isContinue = false;
  13. }
  14. }

 

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

3 原子性与非原子性测试

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

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

  1. public class MyThread extends Thread{
  2. volatile public static int count;
  3. private static void addCount(){
  4. for (int i = 0;i<100;i++){
  5. count ++;
  6. }
  7. System.out.println("count = " + count);
  8. }
  9. @Override
  10. public void run(){
  11. addCount();
  12. }
  13. }
  1. public class Run {
  2. public static void main(String[] args) {
  3. MyThread[] array = new MyThread[100];
  4. for (int i = 0; i < 100; i++) {
  5. array[i] = new MyThread();
  6. }
  7. for (int i = 0; i < 100; i++) {
  8. array[i].start();
  9. }
  10. }
  11. }

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

        修改 MyThread.java如下:

  1. public class MyThread extends Thread{
  2. volatile public static int count;
  3. synchronized private static void addCount(){
  4. for (int i = 0;i<100;i++){
  5. count ++;
  6. }
  7. System.out.println("count = " + count);
  8. }
  9. @Override
  10. public void run(){
  11. addCount();
  12. }
  13. }

        在本示例中,如果在方法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)类型就是一个原子操作可用的类型,它可以在没有锁的情况下做到线程安全。

  1. public class AddCountThread extends Thread{
  2. private AtomicInteger count = new AtomicInteger(0);
  3. @Override
  4. public void run(){
  5. for (int i = 0; i < 10000; i++) {
  6. System.out.println(count.incrementAndGet());
  7. }
  8. }
  9. }
  1. public class Run1 {
  2. public static void main(String[] args) {
  3. AddCountThread addCountThread = new AddCountThread();
  4. Thread t1 = new Thread(addCountThread);
  5. t1.start();
  6. Thread t2 = new Thread(addCountThread);
  7. t2.start();
  8. Thread t3 = new Thread(addCountThread);
  9. t3.start();
  10. Thread t4 = new Thread(addCountThread);
  11. t4.start();
  12. Thread t5 = new Thread(addCountThread);
  13. t5.start();
  14. }
  15. }

        成功累加到50000。

5 逻辑混乱与解决方案 

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

        

  1. public class MyService {
  2. public static AtomicLong aiRef = new AtomicLong();
  3. public void addNum(){
  4. System.out.println(Thread.currentThread().getName() + "加了100之后的值是:" + aiRef.addAndGet(100L));
  5. aiRef.addAndGet(1L);
  6. }
  7. }
  1. public class MyThread extends Thread{
  2. private MyService myService;
  3. public MyThread(MyService myService) {
  4. this.myService = myService;
  5. }
  6. @Override
  7. public void run(){
  8. myService.addNum();
  9. }
  10. }
  1. public class Run1 {
  2. public static void main(String[] args) {
  3. try {
  4. MyService service = new MyService();
  5. MyThread[] threads = new MyThread[5];
  6. for (int i = 0; i < threads.length; i++) {
  7. threads[i] = new MyThread(service);
  8. }
  9. for (int i = 0; i < threads.length; i++) {
  10. threads[i].start();
  11. }
  12. Thread.sleep(1000);
  13. System.out.println(service.aiRef.get());
  14. }catch (InterruptedException e){
  15. e.printStackTrace();
  16. }
  17. }
  18. }

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

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

 

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

6 禁止代码重排序

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

  1. A 代码 - 重耗时
  2. B 代码 - 轻耗时
  3. C 代码 - 重耗时
  4. D 代码 - 轻耗时

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

  1. B 代码 - 轻耗时
  2. D 代码 - 轻耗时
  3. A 代码 - 重耗时
  4. C 代码 - 重耗时

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

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

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

  1. A 变量的操作
  2. B 变量的操作
  3. volatile Z 变量的操作
  4. C 变量的操作
  5. D 变量的操作

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

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

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

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

总结:

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

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

        synchronzied和volatile使用场景:

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

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

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号