避免多线程错误三个方法:
(1)不共享变量(ThreadLocal);//空间换时间
(2)共享变量设定为不可变(Immutable);//空间换时间
(3)使用同步(Synchronized).//时间换空间
tips:
java8stream在parallel中,不能操作非线程安全的类。
Synchronized
内置锁.
非静态方法synchronized: 对当前对象加锁;
静态方法synchronized: 对Class对象加锁.
同步块:
1 | synchronized(this){ |
因为锁是加在某个对象上,所以叫做内置锁.线程在进入同步块的时候获得锁,出来的时候释放锁.
释放锁的情况:
- 正常退出
- 异常退出.
优点:
同步块中的代码能作为一个原子操作.缺点:
只有一个线程能获得锁.性能较低.
可重入
重入: 当某个线程试图获取它自己已经持有的锁.
Synchronized
内置锁是可重入的,意味着线程可以多次获取自己持有的内置锁,都可以获得成功.
实现:
锁在对象里,锁中记录持有者的线程和获取计数器.
计数器为0时,锁被认为不被任何线程所持有;
当线程请求一个计数器为0的锁时,锁中记录下锁的持有者,并且将计数器置为1.如果再一次进入同步块,计数器+1,退出一次同步块计数器-1,当计数器为0,锁被释放.
可见性
不加任何同步控制的变量,由于指令重排,多线程等因素,可见性无法保证,读线程可能读到的不是最新的值(读到失效数据).这种级别是最低安全性.
对于大部分基本类型来说,最低安全性是可以容忍的.但对于double,long来说,由于被更新的可能是数据的一半,除非使用volatile关键字等机制,否则读出来的可能不仅是失效数据,而是一个混合了失效数据\最新数据的近乎随机的值,可能带来毁灭性的后果.
volatile
作用: 保持可见性
缺点: 不保证操作的原子性. (需要原子性,应该使用锁或原子类)
使用场景:
- boolean值,状态变量;
- 只有一个线程写,其他线程读.
或 写的时候不依赖原先的值(不是自增这种).
编译优化与变量
jvm在server模式下对循环内没有改变的变量进行优化,将其提出循环.
;在client模式下则不会有这种优化.
示例代码:
1 | boolean otherDone;//此处应该volatile |
上述代码如果不加volatile,则在server模式下可能会死锁,因为!otherDone的判断可能会被优化到循环外面.导致无限循环.
线程封闭
不在线程间共享变量,因此可以达到线程安全.
例如使用ThreadLocal.
栈封闭
线程封闭的特例.
由于每个线程有自己的栈,而局部变量在栈上,因此只使用局部变量的话,就是栈封闭.(达到线程安全)
由于基本类型无法获取引用,因此基本类型无法逸出,是安全的;
而对象引用可能逸出,因此需要编程人员自行保证不会逸出.
(不把引用乱传递)
发布与逸出
- 线程安全的叫发布;
- 不安全的叫逸出.
安全发布的两种途径:
- 发布的对象是不可变的;
- 发布的时候使用同步方法.
途径1
其中,不可变的方法:
private final,且返回clone或Arrays.copy的值.
途径2
把不可变对象安全发布的方法:
- 静态初始化函数中初始化一个对象引用;
- 把对象引用存放到volatile或AtomicReference;
- 把对象引用存放到正确构造对象的final类型域;
- 把对象引用存放到一个由锁保护的域中.(如放入同步容器中,包括HashTable,Vector,sychronizedMap,synchronizedMap,concurrentMap,CopyOnWriteArrayList,BlockingQueue,ConcurrentLinkedQueue).
- 方法1: 静态初始化函数
1
public static Holder holder=new Holder(42);
对于不可变对象,安全发布后就可以用了;
对于可变对象,安全发布以后还需要安全得使用,也就是使用的时候也需要同步.
- 总结:
不可变对象: 任意发布
事实不可变: 安全发布
可变: 安全发布且安全使用.
建议
- 尽量使用final,private.
其中final能保证初始化值过程中的安全性.
疑问
- immutable和threadLocal选哪个?