java并发系列(2)——线程共享,synchronized与volatile

本文深入探讨了java并发编程中的线程共享问题,包括CPU缓存模型、Java内存模型(JMM)、并发的三大特性(原子性、可见性、有序性)以及synchronized和volatile的语义和对并发特性的保证。通过实例分析,解释了synchronized如何保证原子性和可见性,以及volatile如何防止重排序并确保可见性,但不保证原子性。
摘要由CSDN通过智能技术生成

接上一篇《java并发系列(1)——基本概念与Thread API

2.3 线程共享

多个线程并发访问共享资源时,有可能产生线程安全问题。

线程安全包括三个方面:原子性、可见性、有序性,也可以说是并发三大特性。

2.3.1 cpu 缓存模型
2.3.1.1 背景

cpu 的运算是在一个容量较小但读写速度很快的寄存器上进行的,而数据是存储在计算机物理内存(主存)的。所以 cpu 需要从内存读取数据到寄存器进行运算,然后再把数据更新到内存。

随着技术发展,cpu 运算速度飞速提升,而内存访问速度一直没有太大的突破。于是内存访问速度就成了一个瓶颈。

怎么办?上缓存。(就跟 mysql 访问速度很慢,我们通常会一次性把数据全都拉到内存操作一个道理。)

2.3.1.2 cpu 缓存模型

上了缓存之后,就有了这样一个模型:

每个 cpu 核心有自己的寄存器、L1 缓存、L2 缓存,而 L3 缓存是共享的,然后通过总线连接其它组件。

如果数据在缓存里面,就直接使用缓存数据,缓存里面没有才去内存里面找,这样就解决了内存访问速度太慢的问题。

然而,又引入了新的问题:缓存一致性。

2.3.1.3 cpu 缓存一致性问题

每个 cup 核心都缓存了一份自己的数据副本,这样就会出现一个问题,比如:

  • core1,core2 都从 memory 加载了数据到 L1;
  • core1 经过运算改动了数据,写到了 L1,但还没有刷新到 memory;
  • 或者 core1 已经刷新数据到 memory,但 core2 还没有从 memory 重新加载数据;
  • 那么此时,core1 和 core2 在各自缓存里的数据就不一样。

解决办法有两个:

  • 总线锁:总线在同一时间只能被一个 cpu 核心使用;
  • 缓存一致性协议:对于共享变量,读操作不做特殊处理;写操作发信号通知其它 cpu 共享变量缓存失效。
2.3.2 java 内存模型(JMM)

如果程序员直接面向操作系统编程,不同操作系统很多特性存在差异,那么代码移植性就会很差。所以 java 对底层操作系统做了一层封装,也就是 JVM。同时 java 定义了自己的内存规范,也就是 JMM。这样,对 java 程序员来说,就只需要关注 JMM 即可,而 JMM 的实现对 java 程序员来说则是透明的。

java 内存模型与 cpu 缓存模型类似,它规定了若主存由多个线程更新时,对主存的读操作能看到哪些值。

具体如下:

  • 主内存(堆内存)线程共享;
  • 每个线程有自己的工作内存(本地内存、栈内存);
  • 线程必须先把需要的数据(基本类型、对象引用)从堆加载到栈;
  • 用完之后把数据刷新到堆,而什么时候刷新数据是不确定的。
2.3.3 并发三大特性

现在借助于 JMM 来看并发的三大特性。

2.3.1.1 原子性

指一个操作要么还没执行,要么已经执行完,执行到一半的中间状态不可能被其它线程看到。

java 原生的原子性操作:

  • 所有引用类型的读写操作;
  • 除 long/double 以外的基本类型的读写操作。

对于 long 和 double 类型, java 不强制要求 JVM 保证其读写操作的原子性。

(1)x=1 是原子性操作

某线程先在本地内存写入 x=1,再把 x=1 写入主内存。其它线程要么看到 1 被写入之前的值,要么 1 被写入之后的值,也就是 1,不可能看到其它值。

(2)x=1L 非原子性操作

某线程可能刚写入了高位 32 位,这时切换到了其它线程,其它线程可能会看到写到一半的值。

(3)x++ 非原子操作

分三步:先从主内存把 x 读取到线程本地内存,在本地内存中把 x + 1,再把 x 的值写到主内存。

未保证原子性导致的意外结果,代码示例

package per.lvjc.concurrent.sync;

public class AtomicityTest {
   

    private static int sum = 0;

    public static void main(String[] args) throws InterruptedException {
   
        Runnable runnable = () -> {
   
            for (int i = 0; i < 10000; i++) {
   
                sum++;
            }
        };
        int size = 10;
        Thread[] threads = new Thread[size];
        for (int i = 0; i < size; i++) {
   
            Thread thread = new Thread(runnable);
            thread.start();
            threads[i] = thread;
        }
        for (int i = 0; i < size; i++) {
   
            threads[i].join();
        }
        System.out.println("sum = " + sum);
    }
}

10 个线程个做 1万次自增,执行结果经常不等于 10 万。

2.3.1.2 可见性

指一个线程对共享变量进行了修改,其它线程能立刻看到最新值。

java 原生不保障可见性。一个线程修改了共享变量的值之后,最新的值什么时候会被写入主内存,什么时候其它线程会从主内存重新读取最新的值都是不确定的。

当然,java 提供了一些措施来保证可见性。

未保证可见性导致的意外结果,代码示例

package per.lvjc.concurrent.sync;

import java.util.concurrent.TimeUnit;

public class VisibilityTest {
   

    private static int i = 0;

    public static void main(String[] args) throws InterruptedException {
   
        Thread write = new Thread(() -> {
   
            while (i < 5) {
   
                i++;
                System.out.println("write: i =" + i);
               
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值