线程安全是开发者在开发多线程任务时最关心的问题,那么线程安全需要注意哪些呢?
一、思考:线程安全产生的原因是什么?
二、final,volatile关键字的作用?
三、1.5之前的javaDCL有什么缺陷?
四、如何编写线程安全的程序?
五、ThreadLocal使用的注意事项有哪些?
一、思考:线程安全产生的原因是什么?
原因:可变资源(内存)线程间共享
由Java的内存模型:各线程都有自己的工作内存 和 虚拟机的主内存。
工作内存中保存了该线程使用的变量的副本,各线程对变量的所有操作必须在工作内存中进行,即操作的是变量的副本。
看下面这个过程:
例如:对于 int a = 3; 线程A、B、C 都要操作变量a++;
- 当线程A操作a++后:主内存 a=3,A的工作内存 a = 4; B工作内存中 a=3; C 中 a=3;
- 然后B也操作a++: 主内存 a=3,A的工作内存 a = 4; B工作内存中 a=4; C 中 a=3;
- 然后A工作内存中的a同步到主内存:主内存 a=4,A的工作内存 a = 4; B工作内存中 a=4; C 中 a=3;
- 然后主内存同步到其他线程:主内存 a=4,A的工作内存 a = 4; B工作内存中 a=4; C 中 a=4;
这时就出问题了:a = 5才对,因为A,B线程都执行了a++,两次a++。
二、如何实现线程安全呢?
根据线程安全原因:可变资源(内存)线程间共享可得出:
- 不共享资源
- 共享不可变资源
- 共享可变资源(可见性、操作原子性、禁止重排序)
1、不共享资源
ThreadLocal:
如何使用ThreadLocal(看Loop.java中如何使用):
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
ThreadLocal原理:
ThreadLocal.java
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//这个map是绑定在线程上的
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocal使用建议:
- 声明为全局静态final成员,ThreadLocal在一个线程里有一个就够了,没必要每次初始化都创建一个。另外ThreadLocalMap是以ThreadLocal的引用为key,如果引用经常变的话,map中就匹配不到了。
- 避免存储大量对象,由底层数据结构决定。
- 用完后及时移除对象,如果线程一直存在,这个引用就一直不会被移除。
final的禁止重排序:
y不是final的,会被重排序,如果被重排序到构造函数之外,就会出现上面情况。
volatile的禁止重排序:
在JDK1.5的时候volatile的语义被增强了,增加了【禁止指令重排序】,JDK1.4之前下面的DCL单例是有问题的。
public class Singleton {
private static volatile Singleton singleton; //volatile很关键
private Singleton() {}
public static Singleton getInstance() {
if (null == singleton) { //提高程序的效率
synchronized (Singleton.class) {
if (null == singleton) { //解决多线程下的安全性问题,也就是保证对象的唯一
singleton = new Singleton();
}
}
}
return singleton;
}
}
jdk1.5之后的翻译为机器码指令是下面:
1、test(single == null)
2、lock
3、test(single == null)
4、$instance = <allocate memory> //分配内存
5、call constructor
6、singleton = $instance
7、unlock
而jdk1.4之前加volatile或者jdk1.5后不加volatile的指令可能是下面:
1、test(single == null)
2、lock
3、test(single == null)
4、$instance = <allocate memory> //分配内存
5、singleton = $instance
6、unlock
7、call constructor
结论:这样的指令造成构造函数的调用在锁外面了,当多线程时,其他线程得到锁并且判断单例不null,直接来用,但是可能这个单例并没有被构造完成,会造成不可预知的问题。
保证可见性的方法
- 使用final关键字
- 使用volatile关键字
- 加锁,锁释放时会强制将工作内存刷新至主内存。
对于3的解释:由【Happens-before规则第一条:内置锁的释放锁操作发生在该锁随后的加锁操作之前】
原子性:
例如a++,其实际代码如下,被拆分成了3行代码,不是原子操作。
int tmp = a;
tmp += 1;
a = tmp;
保证原子性的方法:
1、加锁,保证操作的互斥性
2、使用CAS指令(如,Unsafe.compareAndSwapInt)
3、使用原子数值类型(如,AtomicInteger)
4、使用原子属性更新器(AtoicReferenceFieldUpdater)
结论:如何编写线程安全的程序
- 不变性,能不共享就不共享,写可重入函数(见补充)
- 可见性,如果必须要共享,保证变量的可见性
- 原子性,保证操作的原子性
- 禁止指令重排序,保证代码执行的顺序
补充:
可重入函数,不涉及其他内存:
public static int add(int a){
return a + 2;
}