线程安全问题
1. 什么是线程安全
要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享(Shared)的、可变(Mutable)的状态的访问。
共享:变量可以由多个线程同时访问。可变:变量的值在其生命周期内可以发生变化。
一个对象是否是线程安全的,取决于他是否被多个线程同时访问。这指的是程序中访问对象的方式,而不是对想要实现的功能。要使得对象是线程安全的,需要采用同步机制来协同对对象可变状态的访问。
2. 竟态条件
当某就算的正确性取决于多个线程的交替执行时序时,那么就会发生竟态条件。
最常见的竟态条件类型就是“先检查后执行”。即通过一个可能失效的观测结果来决定下一步的动作。
1 2 3 4 5 6
| # 举一个实际生活中的例子 你和你的朋友约好在某一饭店见面,当你到达那里是,发现这个地方有两个相同的饭店,并且你和你的朋友并不知道是哪一家。 于是,你选择先去A饭店去看朋友是否在A饭店,但你并没有发现他。那么你可以等待或者去看看你的朋友是否在饭店B。 问题是:当你在街上走时,你的朋友可能已经离开饭店B,从后门进入饭店A了。
因为要想和朋友会面。必须取决于时间的发生时序(比如,你朋友要在饭店B待多久)。当你走出前门时,你就不知道饭店中是否有你朋友,意思就是,当你离开饭店A之后,你对饭店A的观察结果就无效了。
|
上面实际例子就是一种竟态条件,大多数竟态条件的本质是基于一种可能失效的观察结果来做出判断或者某个计算。
1 2 3
| # 单例模式中延迟性加载的竟态条件 单例模式:确保单个对象被创建 延迟性加载:将对象的初始化操作推迟到实际被使用时才进行。
|
1 2 3 4 5 6 7 8 9 10
| public class LazyInitRace{ private Object obj = null; public Object getInstance(){ if(obj == null) obj = new Object(); return obj; } }
|
在getInstance()方法中存在一个竟态条件,它可能会破坏这个类的正确性。
假如有两个线程A,B同时需要某一个对象,同时调用getInstance()方法。A看到obj == null,因而创建一个新的Object对象,B同样判断obj是否为空。
但是此时的obj是否为空呢?要去取决于不可预测的时序 ,比如线程的调度方式,以及线程A需要多久来初始化obj。如果当线程B检查时,obj为null,那么就会两次创建对象,就会创建两个不同的对象,这不符合单例模式的规则。
竟态条件并不总是产生错误,还需要某种不恰当的执行时序。
如果你去饭店B的时候,你的朋友正好在等你,这样就不会不会出现错误。
3.复合操作
要避免竟态条件,就必须在某个线程修改该变量的值时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在修改状态中。
假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。原子操作是指:对于访问同一个状态的所有操作(包括操作本身)来说,这个操作是一个原子方式执行的操作。
比如,我们说过,最常见的竟态条件就是,先检查后执行,要想保证线程安全性。就必须保证,这个操作必须是原子的。我们将这种操作统称为复合操作
。
比如i++
这个操作中,包括三步:读取–修改–写入。这个操作是原子的,称为复合操作。
4. 线程安全问题
原子性问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class ThreadDemo implements Runnable{ private int count = 0;
@Override public void run() { for (int i = 0; i < 1000; i++) { count++; System.out.println(Thread.currentThread().getName() + "======" + count);
}
} }
public class UnsafeDemo1 {
public static void main(String[] args) { Runnable runnable = new ThreadDemo(); for (int i = 0; i < 100; i++) { new Thread(runnable,"线程" + i).start(); }
} }
|

可见性问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class ThreadDemo2 extends Thread{ public boolean bool = false;
@Override public void run() { try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } bool = true; } }
public class UnsafeDemo2 { public static void main(String[] args) { ThreadDemo2 thread = new ThreadDemo2(); thread.start();
while(true){ if (thread.bool == true){ System.out.println(thread.bool + "变为true"); } } } }
|

- 重排序问题
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排
1 2
| # 为什么指令重排序可以提高性能? 每个指令都会包含多个步骤,每个步骤可能使用不同的硬件。因此产生了流水线技术。它的原理是指令1还没有执行完,就可以开始执行指令2。并不需要等到指令1执行结束后在执行指令2,这样就大大提高了效率
|
流水线技术大大提高了程序的效率。但有时会降低效率。比如:流水线技术恢复中断的代价比较大。所以我们会想尽办法不让流水线中断。指令重排就是减少中断的一种技术。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| package com.jiang.ThreadSafe.unsafe;
import java.util.concurrent.CountDownLatch;
public class UnsafeDemo3 { private static int x = 0, y = 0; private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException { int i = 0; for (; ; ) { i++; x = 0; y = 0; a = 0; b = 0;
CountDownLatch latch = new CountDownLatch(3);
Thread one = new Thread(new Runnable() { @Override public void run() { try { latch.countDown(); latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } a = 1; x = b; } }); Thread two = new Thread(new Runnable() { @Override public void run() { try { latch.countDown(); latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } b = 1; y = a; } }); two.start(); one.start(); latch.countDown(); one.join(); two.join();
String result = "第" + i + "次(" + x + "," + y + ")"; if (x == 1 && y == 1) { System.out.println(result); break; } else { System.out.println(result); } } }
}
|
