10-Java中共享内存可见性以及synchronized和volatile关键字
Java中共享变量的内存可见性
-
我们首先来看一下在多线程下处理共享变量时Java的内存模型,如图所示
Java内存模型规定,将所有的变量都存放在主存中,当线程使用变量的时候,会把主内存里面的变量赋值到自己的工作区间或者叫工作内存,线程读写变量时操作的是自己的工作内存中的变量,Java内存模型是一个抽象的概念,那么在实际中线程的工作内存是什么呢?
图中显示的是一个双核CPU系统架构,每一个核都有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辑运算。每一个核都有自己的一级缓存。
当一个线程操作共享变量的时,它首先从主存复制共享变量到自己的工作内存(私有内存)中,然后对工作内存的变量进行处理,处理完之后将变量值更新到主存中。假如线程A和线程B同时处理一个共享变量,会出现什么情况呢?我们使用上图2-5所示的CPU架构,假设线程A和B使用不同的CPU执行,并且当前两级cache都为空,那么由于这个时候cache的存在,将会导致内存不可见问题:
- 线程A首先获取到共享变量X的值,由于两级cache都没有命中,所以加载主内存中X的值,假如为0。然后把X=0值缓存到两级cache中,线程A修改X=1,然后将其写入两级cache中,并且刷新到主存中。线程A操作完毕后,线程A所在的CPU的两级cache和主存中的X都为1。
- 线程B获取到X的值,首选一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回了一个X=1;到这里一切都是正常的,因为这时候主内存中X=1,然后线程B修改X=2,并将其放到线程B所在的一级cache和二级cache中,最后更新主存中X=2。
- 线程A再次要修改X的值,获取时一级缓存中命中,并且X=1,到这里问题就出现了,明明线程B已经把X修改为2了,为何线程A读取X的值还是1呢?这就是共享变量的内存不可见问题。也就是线程B写入的值对线程A不可见。那么如何解决共享变量线程不可见的问题呢?这里就需要使用java中的volatile关键字解决这个问题,下面会讲到。
Java中Synchronized关键字
-
synchronized关键字介绍
synchronized块是Java提供的一种原子性内置锁,Java中的每一个对象都可以看成一个同步锁来使用。这些Java内置的使用者看不到的锁被称为内置锁,也叫监视器锁。线程的执行代码块在进入synchronized代码块前会自动的获取到内部锁,这时候其他线程访问该同步代码块会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步代码块内调用了该内置锁资源的wait系列方法时会释放该内置锁。内置锁是排它锁,也就是当一个线程获取到这个锁之后,其他线程必须等待该线程释放锁后才能获得该锁。
-
synchronized的内存语义
前面介绍了共享变量内存可见性问题主要是由于线程当中工作内存所导致的。下面我们来讲解synchronized的一个内存语义,这个内存语义就是解决共享变量内存可见性问题。进入synchronized块的内存语义是把synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时候就不会从工作内存中取,而是直接从主存中取,退出synchronized块的内存语义是把sunchronized块对共享变量的修改刷新到主存中。其实这也是加锁和释放锁的概念。当获取锁后会清空本地内存中将会用到的共享变量,在使用这些共享内存会从主存中加载,在释放锁时会将本地内存中修改的共享变量刷新到主存中。synchronized除了用来解决共享变量内存不可见问题,还可以用来实现原子性操作。另外注意的是,synchronized关键字会不会引起线程上下文切换并带来线程调度开销。
Java中volatile关键字
-
上面介绍的是使用锁的方式可以解决共享变量内存不可见问题。但是使用锁太笨重,因此它会带来线程上下文切换问题。对于解决内存可见性问题,Java还提供了一种弱形式的同步,也就是使用volatile关键字,该关键字确保一个变量的更新对其他线程马上可见。当一个变量被声明为volatile时,线程在写入变量的时候不会把值缓存到寄存器或者其他地方,而是会把值刷新返回到主存中。当其他线程读取该共享变量的时候,会直接从主存中重新获取到最新值。而并不是使用工作内存中的值。voltile内存语义和synchronized语义有相似之处,当线程写入volatile变量值的时候就等于线程退出synchronized同步块(把写入工作内存中共享变量的值同步到主内存),读取volatile变量值时就相当于进入进入同步代码块(先清空本地内存中共享变量值,再从主存中获取到最新值)。
-
下面使用volatile关键字解决内存可见性问题的例子,如下代码中的共享变量value就是不安全的,因为这里没有适当的同步措施。
public class ThreadNotSafeInteger { private int value; public int getValue() { return value; } public void setValue(int value) { this.value = value; } }
-
首先来看使用synchronized关键字进行同步的方式
public class ThreadNotSafeInteger { private int value; public synchronized int getValue() { return value; } public synchronized void setValue(int value) { this.value = value; } }
-
然后使用volatile进行同步
public class ThreadNotSafeInteger { private volatile int value; public int getValue() { return value; } public void setValue(int value) { this.value = value; } }
-
在这里使用volatile和synchronized是等价的。都解决的共享内存变量value不可见问题。但是前者是独占锁,其他线程调用会被阻塞等待,同时还存在线程上下文切换个线程重现调度的开销。这也是使用锁方式不好的地方。后者使用的是非阻塞算法,不会造成线程上下文切换的开销。
Java中原子性操作
-
所谓原子操作,是指执行一系列操作要么一次性全部执行完,要么全部都不执行。如果不能保证操作室原子性操作,那么就会出现线程安全问题,如下:
public class ThreadNotSafeCount { private Long value; public Long getValue() { return value; } public void setValue(Long value) { this.value = value; } private void inc() { ++value; } }
首先执行javac ThreadNotSafeCount.java命令
然后执行javap -c ThreadNotSafeCount.class命令
Compiled from "ThreadNotSafeCount.java" public class com.heiye.learn2.ThreadNotSafeCount { public com.heiye.learn2.ThreadNotSafeCount(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public java.lang.Long getValue(); Code: 0: aload_0 1: getfield #2 // Field value:Ljava/lang/Long; 4: areturn public void setValue(java.lang.Long); Code: 0: aload_0 1: aload_1 2: putfield #2 // Field value:Ljava/lang/Long; 5: return }
-
我们该如何保证多个操作的原子性呢?最简单的办法就是使用synchronized关键字进行同步,代码如下
public class ThreadNotSafeCount { private Long value; public synchronized Long getValue() { return value; } public synchronized void setValue(Long value) { this.value = value; } private synchronized void inc() { ++value; } }
使用synchronized关键字的确可以实现线程安全性,即内存可见性和原子性,但是synchronized是独占锁,内有获取到内部锁的线程会被阻塞掉,但是getValue()只是读操作,多个线程同时调用这个方法并不会引发线程安全问题,但是加了synchronized关键字后,同一时间只能有一个线程可以调用,这显然是不合理的,没有必要。也许会有这样一个疑惑,可以不可把这个方法上的synchronized关键字去掉呢?答案是不能的,因为这里是靠synchronized来实现共享内存可见性的,那么有没有什么更好的办法呢?,答案是有的,下面讲到的在内部使用非阻塞CAS算法实现的原子性操作类AtomicLong就是一个不错的选择。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· DeepSeek 解答了困扰我五年的技术问题
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 用 C# 插值字符串处理器写一个 sscanf
· [翻译] 为什么 Tracebit 用 C# 开发
· Deepseek官网太卡,教你白嫖阿里云的Deepseek-R1满血版
· DeepSeek崛起:程序员“饭碗”被抢,还是职业进化新起点?
· 2分钟学会 DeepSeek API,竟然比官方更好用!
· .NET 使用 DeepSeek R1 开发智能 AI 客户端