提到“Java并发”,很多人直接会想到“线程安全”,咱现在就坐下来聊聊这个话题。不过,我的切入点是啥叫“线程不安全”。
先简单说一说 "Java内存模型"
我们在写并发程序的时候,对于线程间可以共享的变量,我们在线程中拿过来就用了,就像在同一个池子里捞同一条鱼一样。然而,在实际内存模型中可不是这么直接。为了提升程序性能,Java引入了缓存机制。线程间共享的变量都被储存在“主内存”中,主内存对所有有关线程均可见。每个线程都有自己的缓存(工作内存),且对其它线程不可见。当线程需要用到共享的变量时,会从主内存中copy一份,放到自己的缓存(工作内存)中,然后再对变量进行操作。来,上张图:
注意:线程中的指令对共享变量的所有操作,本质上都是对工作内存的操作,而非直接和主内存打交道。
虚拟机对主内存和工作内存的操作有如下8个(每个操作都具备原子性):
lock:锁定主内存中某变量的内存区域;
unlock:和lock对应的解锁;
read:从主内存中度变量的值;
load:将read操作读来的值加载到工作内存;
use:将工作内存中变量的值传入执行引擎(注意:只是指这个传输动作,不包括计算,这和代码层面的“使用”不同);
assign:将执行引擎计算的结果写到工作内存;
store:将变量的值从工作内存中拿出;
write:将store拿出的值写到主内存.
现在开始正式聊聊 "线程不安全"
package com.test.thread;
public class Test
{
private static int v=0;
public static void main(String[] args){
TestThread thread0 = new TestThread();
TestThread thread1 = new TestThread();
thread0.start();
thread1.start();
}
private static class TestThread extends Thread
{
public void run(){
v++;
}
}
}
最后Test类中的v属性的值是多少?是2?未必。
package com.test.thread;
public class Test
{
private static volatile int v=0;
public static void main(String[] args){
TestThread thread0 = new TestThread();
TestThread thread1 = new TestThread();
thread0.start();
thread1.start();
}
private static class TestThread extends Thread
{
public void run(){
v++;
}
}
}
volatile关键字通过使其修饰的变量的读与写具有原子性,保证了当需要用到某共享变量时,工作内存中的数据是最新的(普通变量的读会被分成read,load,use三个操作,写会被分成assign,store,write三个操作)。用前文提到的5个描述来说话就是在指令流中,
操作1和
操作2必须连续排列,
操作4和
操作5必须连续排列,
thread1的操作也是同理。
package com.test.thread;
public class Test
{
private static int v=0;
public static void main(String[] args){
TestThread thread0 = new TestThread();
TestThread thread1 = new TestThread();
thread0.start();
thread1.start();
}
private synchronized void increment(){
v++;
}
private static class TestThread extends Thread
{
public void run(){
increment();
}
}
}
在加了synchronized之后,volatile就可以省略了,因为synchronized语句块在进入时会强制从主内存中刷新数据,退出时会强制将工作内存中的数据写到主内存。