1、JAVA中的线程安全的问题
谈到线程安全的问题,就不得不提共享资源。所谓的共享资源,其实就是说这个资源被多个线程持有,或者说多个线程可以访问该资源。
线程安全问题其实就是指,多个线程同时多谢一个共享资源并且没有任何同步措施的时候,导致了读脏数据,或者其他不可预见的问题。
Java内存模型规定,所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间,或者叫做本地内存,线程读写其实操作的是本地内存里面的变量。如果不太理解,我们来看下图:
如上图,线程A和线程B可以同时操作主内存中的共享变量。如果上面两个线程同时读取共享变量而不去修改,那么就不会存在线程安全问题,只有当至少一个线程修改共享变量才有可能出现线程安全问题。最典型的就是计数变量count。计数变量count本身是一个共享变量,然后多个线程对其进行递增,如果不使用同步措施,由于递增操作是分为三个步骤的:获取-计算-保存。因此可能导致计数不准。下面我么来看一个表格。
t1 | t2 | t3 | t4 | |
---|---|---|---|---|
线程A | 从主内存读取count值到线程本地内存 | 递增本线程count的值 | 写回主内存 | |
线程B | 从主内存读取count值到线程本地内存 | 递增本线程count的值 | 写回主内存 |
加入当前count=0,t1时刻线程A从主内存读取count值到本地内存countA。然后在t2时刻递增countA值为1,同时线程B从主内存读取count值到本地内存countB,此时countB为0(因为countA还没有被写入主内存)。t3时刻线程A才把countA的值写入主内存,至此线程A的一次计数完毕,同时线程B递增countB的值为1,然后在t4时刻线程B把countB的值写入主内存。至此线程B的一次计数完成。
这个时候,问题就来了,明明是做了两次计数,但是主内存里面的count却为1。其实这就是线程安全问题。如何解决这个问题呢?这就要用synchronized关键字来进行同步了。后面会讲解。
2、JAVA中的共享变量的内存可见性的问题
上图只是JAVA内存模型的一个抽象概念,那么实际实现中,线程的工作内存是怎么样的呢?我们来看下图:
图中所示是一个双核CPU系统架构,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行逻辑运算。每个核都有自己的一级缓存L1 Cache,还有一个所有核都共享的2级缓存L2 Cache。
当操作一个共享变量时,他首先从主内存复制到自己的本地内存,处理完后在更新到主内存。
那么假如线程A和线程B同时处理一个共享变量,会出现什么情况呢。如果我们使用上图的架构,将会导致内存不可见问题。具体分析如下:
- 线程A首先获得了共享变量X的值,由于两级缓存都没有命中,所以加载主内存中X的值,假设为0。然后将X=0缓存到两级缓存,线程A修改两级缓存里面的X的值,修改为1,并刷新到主内存。线程A操作完毕后,线程A的两级缓存里面的内容为X=1。
- 线程B要获得X的值,首先L1 Cache没有命中,然后查看L2 Cache,第二级缓存命中,所以返会X=1。到这里都是正常的,因为主内存中此时的X也为1。然后线程B开始修改X,将其修改为2,并将结果修改到两级缓存,然后更新到主内存中,主内存中X的值就为2。
- 如果这时候线程A又要修改X的值,在获取时就会命中L1 Cache,并且X=1,到这里就出现了问题。明明线程B已经将X修改为2了,为何线程A拿到的X值还是1。也就是共享内存不可见的问题。可见性是指一个线程对共享变量的修改,对于另一个线程来说是否是可以看到的。
出现内存不可见的问题是因为线程每次获取变量的顺序为:L1 Cache > L2 Cache > 主内存。那么如何解决这个问题呢。使用JAVA中的关键字volatile
3、synchronized介绍
synchronized是JAVA中提供的一种原子性内内置锁。JAVA中每一个对象都可以把它当作一个同步锁来使用,这些Java内置的使用者看不到的锁被称为内部锁,也叫监视器锁。线程在执行synchronized块时会自动获得监视器锁。如果该锁已被其他线程获取了,那么当前线程会被阻塞。
3.1、synchronized主要有3种使用方式
- 修饰实例方法,作用于当前实例,进入同步代码前需要先获取实例的锁
- 修饰静态方法,作用于类的Class对象,进入修饰的静态方法前需要先获取类的Class对象的锁
- 修饰代码块,需要指定加锁对象(记做lockobj),在进入同步代码块前需要先获取lockobj的锁
无论synchronized加在哪里,他锁住的是永远对象。
3.2、synchronized内存语义
进入synchronized块内存语义:把synchronized块中使用到的变量从线程本地内存中清除,这样synchronized块用到该变量时就会直接去主内存中拿。
退出synchronized块内存语义:把synchronized块内对共享变量的修改刷新回主内存。
4、volatile介绍
当一个变量被声明为volatile时,线程在写入变量时,不会把值缓存到两级缓存,而是会把值直接刷新到主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用本地内存的值。
那么一般在什么时候使用volatile关键字呢?
- 写入变量值不依赖变量当前值。因为如果依赖当前值,将是获取-计算-保存这三步,这三步操作不是原子性的。volatile不保证操作的原子性。(原子性:一系列操作要么全部成功,要么全部失败)
- 读写变量时没有加锁。因为加锁本身已经保证了内存可见性,这个时候不需要把变量声明为volatile。