Java并发中的可见性和原子性

简介: Java并发中的可见性和原子性

一、可见性



1、实例讲解


先看这样一段代码:

public class Test {
  static boolean a = true;
  public static void main(String[] args) {
    a = false;          //对a执行写操作
    System.out.println(a);    //对a执行读操作
  }
}


f1d77287d3914ab9b8def869d22efd9c.png


我们在单线程中,对a执行了写操作,并且读取到了最新写的值,也就是说,单线程中对a的写操作时可见的。


那么我们再开启一个线程 :

public class Test {
  static boolean a = true;
  public static void main(String[] args) throws InterruptedException {
    new Thread(()-> {
      while(a) {} //死循环
    }).start();
    Thread.sleep(1000); //为了保证不会影响,停一秒再写    
    a = false;          //对a执行写操作
    System.out.println(a);    //对a执行读操作
  }
}


cc57a8af1dfd4bde82cdf8beb968d9e6.png


可以看到,虽然a仍然打印出为false,但是程序没有结束,就说明在我们新开启的线程中a的值始终为true,他才可以一直执行while循环。换句话说,我们的主线程对a的写操作对于新开的线程的读操作来说是不可见的。


为什么这么长时间了,新开线程中的a还是true呢?那是因为新线程中一直在执行循环,使得线程没有机会去拿到主存中a的最新值,而是一直读取缓存中a的值。


那么,我们让循环沉睡一会儿,给他去读最新值的机会:


public class Test {
  static boolean a = true;
  public static void main(String[] args) throws InterruptedException {
    new Thread(()-> {
      while(a) {
        try {
          Thread.sleep(1);  //睡1ms,给线程去读新值的机会
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      } //死循环
    }).start();
    Thread.sleep(1000); //为了保证不会影响,停一秒再写    
    a = false;          //对a执行写操作
    System.out.println(a);    //对a执行读操作
  }
}


91fe63e00df8484bb87b4c17bc5f255b.png


可以看到,程序很快就结束了,说明新线程已经读到了a的新值为false,结束了循环。


2、如何理解Java线程中的不可见性?


简单来说:线程1读,线程2写,而线程1读不到线程2写的值,这就是不可见性。


3、那么如何实现可见性呢?


就需要用到volatile关键字了:


public class Test {
  static volatile boolean a = true;
  public static void main(String[] args) throws InterruptedException {
    new Thread(()-> {
      while(a) {} //死循环
    }).start();
    Thread.sleep(1000); //为了保证不会影响,停一秒再写    
    a = false;          //对a执行写操作
    System.out.println(a);    //对a执行读操作
  }
}


a0a1288a0b0c486a8d48fb0338b06c0b.png


二、原子性



保证可见性可以保证一个线程写之后,另一个线程可以读到。


那假如一个进程既读取变量,又依赖读到的变量进行写操作呢?我们来看下面的例子 :


1、实例讲解


用两个线程分别执行10000次a++的操作,按道理来说,a的结果应该会增加20000


public class Test {
  static int a = 0;
  public static void main(String[] args) throws InterruptedException {
    for(int i=0; i<10000; i++) {
      new Thread(()-> {
        a++;
      }).start();
      new Thread(()-> {
        a++;
      }).start();
    }   
    Thread.sleep(1000); //为了保证不会影响,停一秒再写    
    System.out.println(a);    //对a执行读操作
  }
}


4ae4bc43f4ae4bd3a2297209007c4f4a.png

可以看到,结果和我们的预期对不上,那我们加上volatile关键字试一下:


bf020e6b3b124949bbb0a5b659f6db9b.pngd1b6dd17895245a38eee877778cea203.png

结果还是对不上,这是为什么呢?

这就要探究a++的本质了


2、a++的本质

     

a++可以拆分为三个操作:1、读取a; 2、a+1; 3、将加之后的值赋给a


有可能会出现这种情况:


       1、当a=0时,线程1读取a值,线程2也读取a值;

       2、线程1将它读到的a值+1,此时为1,线程2也将它读到的a值+1,此时为1;

       3、线程1将1这个值刷入主存,此时主存中的a=1;线程2也将1这个值刷入主存,此时为1


这显然是不对的,两个线程各执行一次a++,a的值应该+2才对。刚刚我们得到的值为19998,可能就是有两个线程在其他线程+之前读取到了a值。


那加上volatile关键字之后呢?


volatile关键字只能保证可见性,即一个线程写过之后,另一个线程能够立马读到。而假如线程2操作在线程1操作写之前就已经读了,那还是没办法改变这个情况。这两个线程都需要读取主存的值,并且每个线程都依赖自己读取的值进行写操作。这就需要保证原子性了。


3、使用Atomicxxxx保证原子性:


public class Test {
  static AtomicInteger a = new AtomicInteger(0);
  public static void main(String[] args) throws InterruptedException {
    for(int i=0; i<10000; i++) {
      new Thread(()-> {
        a.getAndAdd(1);
      }).start();
      new Thread(()-> {
        a.getAndAdd(1);
      }).start();
    }   
    Thread.sleep(1000); //为了保证不会影响,停一秒再写    
    System.out.println(a);    //对a执行读操作
  }
}


540047ec940743c988600d02d3a63dd0.png

可以看到,a++的操作改成了getAndAdd(),读和写是一起执行的,这就不会在读值之后写值之前被其他线程插一杠子。


需要注意的一点是,原子性和可见性并不是相互独立的,保证原子性的前提是保证可见性,那为什么我们没有再用volatile修饰a来保证可见性呢?这就需要去看看AtomicInteger的源码了:

6e7d51c461154ee2af35713e6192ec2f.png


其实它的内部也使用了volatile关键字。


4、使用synchronized同步代码段强制实现原子性和可见性


除了Atomic,也可以使用synchronized同步代码段强制实现原子性。


public class Test {
  static AtomicInteger a = new AtomicInteger(0);
  static int b = 0;
  public static void main(String[] args) throws InterruptedException {
    for(int i=0; i<10000; i++) {
      new Thread(()-> {
//        a.getAndAdd(1);
        synchronized(Test.class) {
          b++;
        }
      }).start();
      new Thread(()-> {
//        a.getAndAdd(1);
        synchronized(Test.class) {
          b++;
        }
      }).start();
    }   
    Thread.sleep(1000); //为了保证不会影响,停一秒再写    
//    System.out.println(a);    //对a执行读操作
    System.out.println(b);    //对b执行读操作
  }
}


相比较Atomic,synchronized就更加重量级了。


另外:volatile不具有传染性,用volatile修饰的对象的内部属性不具有可见性,反之用volatile修饰的内部属性也不能保证所在对象的可见性。


参考:【java】并发之可见性与原子性_哔哩哔哩_bilibili 




相关文章
|
3月前
|
安全 Java 编译器
揭秘JAVA深渊:那些让你头大的最晦涩知识点,从泛型迷思到并发陷阱,你敢挑战吗?
【8月更文挑战第22天】Java中的难点常隐藏在其高级特性中,如泛型与类型擦除、并发编程中的内存可见性及指令重排,以及反射与动态代理等。这些特性虽强大却也晦涩,要求开发者深入理解JVM运作机制及计算机底层细节。例如,泛型在编译时检查类型以增强安全性,但在运行时因类型擦除而丢失类型信息,可能导致类型安全问题。并发编程中,内存可见性和指令重排对同步机制提出更高要求,不当处理会导致数据不一致。反射与动态代理虽提供运行时行为定制能力,但也增加了复杂度和性能开销。掌握这些知识需深厚的技术底蕴和实践经验。
75 2
|
3月前
|
安全 Java 调度
解锁Java并发编程高阶技能:深入剖析无锁CAS机制、揭秘魔法类Unsafe、精通原子包Atomic,打造高效并发应用
【8月更文挑战第4天】在Java并发编程中,无锁编程以高性能和低延迟应对高并发挑战。核心在于无锁CAS(Compare-And-Swap)机制,它基于硬件支持,确保原子性更新;Unsafe类提供底层内存操作,实现CAS;原子包java.util.concurrent.atomic封装了CAS操作,简化并发编程。通过`AtomicInteger`示例,展现了线程安全的自增操作,突显了这些技术在构建高效并发程序中的关键作用。
68 1
|
3天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
7天前
|
Java 数据库连接 数据库
如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面
本文介绍了如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面。通过合理配置初始连接数、最大连接数和空闲连接超时时间,确保系统性能和稳定性。文章还探讨了同步阻塞、异步回调和信号量等并发控制策略,并提供了异常处理的最佳实践。最后,给出了一个简单的连接池示例代码,并推荐使用成熟的连接池框架(如HikariCP、C3P0)以简化开发。
21 2
|
24天前
|
Java
【编程进阶知识】揭秘Java多线程:并发与顺序编程的奥秘
本文介绍了Java多线程编程的基础,通过对比顺序执行和并发执行的方式,展示了如何使用`run`方法和`start`方法来控制线程的执行模式。文章通过具体示例详细解析了两者的异同及应用场景,帮助读者更好地理解和运用多线程技术。
25 1
|
2月前
|
Java API 容器
JAVA并发编程系列(10)Condition条件队列-并发协作者
本文通过一线大厂面试真题,模拟消费者-生产者的场景,通过简洁的代码演示,帮助读者快速理解并复用。文章还详细解释了Condition与Object.wait()、notify()的区别,并探讨了Condition的核心原理及其实现机制。
|
3月前
|
存储 Java
Java 中 ConcurrentHashMap 的并发级别
【8月更文挑战第22天】
50 5
|
3月前
|
存储 算法 Java
Java 中的同步集合和并发集合
【8月更文挑战第22天】
39 5
|
3月前
|
缓存 Java 调度
【Java 并发秘籍】线程池大作战:揭秘 JDK 中的线程池家族!
【8月更文挑战第24天】Java的并发库提供多种线程池以应对不同的多线程编程需求。本文通过实例介绍了四种主要线程池:固定大小线程池、可缓存线程池、单一线程线程池及定时任务线程池。固定大小线程池通过预设线程数管理任务队列;可缓存线程池能根据需要动态调整线程数量;单一线程线程池确保任务顺序执行;定时任务线程池支持周期性或延时任务调度。了解并正确选用这些线程池有助于提高程序效率和资源利用率。
51 2
|
3月前
|
Java 开发者
【编程高手必备】Java多线程编程实战揭秘:解锁高效并发的秘密武器!
【8月更文挑战第22天】Java多线程编程是提升软件性能的关键技术,可通过继承`Thread`类或实现`Runnable`接口创建线程。为确保数据一致性,可采用`synchronized`关键字或`ReentrantLock`进行线程同步。此外,利用`wait()`和`notify()`方法实现线程间通信。预防死锁策略包括避免嵌套锁定、固定锁顺序及设置获取锁的超时。掌握这些技巧能有效增强程序的并发处理能力。
26 2