Java-线程安全
0x01 什么是线程安全
线程安全是针对某个对象来说,如果当多线程访问此对象时,不用再用额外方式如同步锁等,总能运行获得正确结果,那就可以说这个对象代码线程安全。
0x02 Java中的线程安全
Java中线程安全强度由强到弱是:
不可变 -> 绝对线程安全 -> 相对线程安全 -> 线程兼容 -> 线程对立
2.1 不可变
就是指那些一旦初始化就不可改变的对象,这当然是线程安全的:
- final修饰的基本数据类型
- final修饰的不可变对象如String, 基本类型的封装类型(Long,Integer等)(注意this指针不可逃逸)
这类对象永远不会改变,多线程状态下也永远处于一致的状态。
这种方式实现不可变最简洁,但使用场景较少。
2.2 绝对线程安全
不用考虑运行时环境,调用着不用加任何同步措施,就能保证多线程情况下拥有正确结果。这很难达到,Java中很多所谓线程安全类都不是绝对线程安全。
比如Vector
,他每个方法都是synchronized
的,但如果多线程同时读写,可能会刚刚某线程remove
了某个序号上的元素,另一个线程去访问这个序号的元素就可能导致报错。解决方案就是读写方法都加入synchronized
这个vector实例。
2.3 相对线程安全
我们上面例子中的Vector就是相对线程安全,就是在单独考虑是是线程安全,但在某些特定的组合顺序执行时需要额外手段处理。
2.4 线程兼容
就是说本身并不线程安全,需要额外的如同步锁之类的手段确保线程安全。
常用的容器都是此类,如HashMap
, HashSet
等。
2.5 线程对立
指那些无法通过手段达到线程安全目的的代码。
如suspend
的时候加同步锁,那他会一直持有锁直到resume
。而当对这个线程调用resume
之前也申请同步锁,那就肯定发生死锁。
0x03 线程安全实现
3.1 互斥同步(悲观同步)
- 互斥是实现同步的手段,主要互斥方法有临界区, Mutex, Semaphore
- 同步指多线程访问共享数据时,只能被一个线程使用(如果用
Semaphore
就是若干个)
常见实现方法如下:
synchronized
java.util.concurrent
如ReentrantLock
本方式无论如何都是先加锁,所以也称为悲观同步。
3.2 非阻塞同步(乐观同步)
3.2.1 概念
上述互斥同步方式最大的开销是线程阻塞和唤醒,而且属于是悲观策略。
乐观同步常见的就是CAS(offset, expectVal, newVal)
。
该方法思想是,如果当前内存offset
的值还是为expectVal
,那就设为newVal
;如果值不是我们期望的,就不改变。而且在这个过程是原子的。
3.2.2 实现
-
java.util.concurrent.atomic
包内的一些类就有如compareAndSet
这样封装好了的cas方法。 -
Unsafe
类也有许多cas的方法,并在jdk源码中大量采用,但最好不要直接去用这个类。
3.2.3 问题
CAS有个问题就是ABA问题,详见Java-并发-CAS
3.3 无同步实现线程安全
主要是一些不会访问共享变量的情况,他们是线程安全的。
3.3.1 可重入代码
一个方法的结果只依赖参数,也就是说结果完全可以依据传入参数预测。
3.3.2 线程本地存储
ThreadLocal, 即线程独有变量。详见Java-多线程-ThreadLocal全解析
0x04 关于锁
关于锁的内容,可以参考另一篇文章Java-并发-LockLike
参考文档
《深入理解Java虚拟机-第二版》 作者周志明