一:线程的安全性分析
- 如何理解线程安全:
当多个线程访问某个共享对象时,不管运行环境采用何种调度方式,或者这些线程如何交替执行,并且主调代码中不需要任何的额外同步操作或者协同操作,这个类都能表现出正确的行为,那么就称这个类是线程安全的类。 - 正确的行为:就是这个类的执行结果总是符合我们预期的结果,称正确的行为。
一:线程不安全导致的原因:
线程安全主要有三个问题:
- 原子性:保证一系列操作要么全部执行,要么不执行。
- 可见性:一个线程对共享变量的修改对其他线程可见,可以立马被其他线程发觉。
- 有序性:保证代码的执行顺序不会被改变。
首先来说说计算机发展时所做的一些优化:但是这些优化的同时会导致线程不安全
- Cpu上增加了高速缓存,用于平衡内存与cpu速度的差异。(导致了原子性和可见性)
- 操作系统增加了进程、线程、以及分时服用cpu,用于均衡cpu与io设备的速度差异。
- 编译程序优化指令的执行顺序,使得能够更加合理地利用缓存。(导致了有序性问题)
导致原子性问题的原因:
说明:JVM会保证JVM的汇编指令具有原子性。
实验demo:
public class AtomicityDemo {
static int count = 0;
public static void incr(){
try {
//这里睡眠1毫秒是为了让出cpu时间片,为了更好的看到现象
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0;i<1000;i++){
new Thread(new Runnable() {
public void run() {
incr();
}
}).start();
}
//此处睡眠4s是为了让子线程全部完成
TimeUnit.SECONDS.sleep(4);
System.out.println(count);
}
}
执行结果:
第一次:912
第二次:936
第三次:910
但是我们的期望值是1000,由于原子性问题导致。线程的不安全。
从而我们没有能获得期望的值。
分析上面代码的原因:
1.通过javap -v AtomicityDemo.class命令反编译得到的代码部分如下:
public static void incr();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=1, args_size=0
0: getstatic #2 // Field java/util/concurrent/TimeUnit.MILLISECONDS:Ljava/util/concurrent/TimeUnit;
3: lconst_1
4: invokevirtual #3 // Method java/util/concurrent/TimeUnit.sleep:(J)V
7: goto 15
10: astore_0
11: aload_0
12: invokevirtual #5 // Method java/lang/InterruptedException.printStackTrace:()V
15: getstatic #6 // Field count:I
18: iconst_1
19: iadd
20: putstatic #6 // Field count:I
23: return
//这是反编译得到的汇编指令,JVM保证了每个指令具有原子性,但是我们上一段代码中的
//count++代码实际上会被拆分成三个汇编指令完成,分别是:
//15行:getstatic #6 //从内存中读count值。
//19行:iadd //对count值加一。
//20行:putstatic #6 //将count值写入
//这三个指令每一个单独都是具有原子性,但是合在一起就不具备了。
//再看下面图片:
现代计算机部分架构:
以上是现代计算机的部分结构,L1、L2高速缓存是内嵌在cpu内核的,属于每个cpu核心独有,cpu会把数据从内存读取到高速缓存中,非原子操作的话就会某个线程读取了另外线程没能会写到内存中的脏数据。导致线程不安全。
导致可见性问题的原因:
代码Demo:
public class VisibilityDemo {
static boolean flag = false;
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
while (!flag){
}
System.out.println("结束循环");
}
}).start();
try {
TimeUnit.SECONDS.sleep(5);
flag = true;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//实际的执行结果是子线程里面的循环一直无限。原因是主线程对flag的修改对子线程不可见,
//子线程中的flag一直是flase。
以上原因是因为每个cpu有着独有的高速缓存导致的。
导致有序性问题的原因:
java多线程有序性问题是编译器,处理器,执行器都可能对代码进行重排序使得代码进行优化而导致的有序性问题。但是这种重排序是有数据依赖性的。
比如:
1. int a = 5;
2. int b = 3;
3. a = a + 5;
4. b = a + 3;
//这段代码可能会被指令重排序使得执行顺序是2、1、3、4,但是不可能会重排成
//2、1、4、3,因为4是依赖3的数据a的,所以4是不可能重排序到3前面。
//指令重排序不会影响在单线程环境下的程序,但是可能会影响在多线程环境下的程序的正确性。