当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。——Brain Goetz
一、java中的线程安全
多个线程之间存在共享数据竞争是线程安全解决问题的前提。
如果将线程安全归为非真即假的二元选项,按照线程安全的程度由强至弱,可以分为以下五类
- 不可变
被final修饰的数据类型一但被正确构建出来之后就是不可变的。
String, Long, Double等 - 绝对线程安全
javaAPI提供的标榜自己是线程安全的类大多情况下不上绝对线程安全的。如Vector的使用,有时需要手动添加同步措施。 - 相对线程安全
调用端需要采取一些额外的同步措施,如Vector, HashTable,Collections的synchronizedCollection()方法包装的集合等。
二、常见的线程安全实现的方法
1. 互斥同步
何为同步?
多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用。
何为互斥?
互斥是实现同步的手段,临界区,互斥量,信号量都是主要的互斥实现方式。因此互斥是因,同步是果。互斥是方法,同步是目的。
如何实现同步互斥?
(1)synchronized
该关键字经过编译后会在同步块的前后分别形成moniterenter和monitorexit这两个字节码指令。
这两个字节码指令都需要一个reference类型的参数来指定要锁和解锁的对象。如果java程序中明确指明了这个对象参数,那就是这个对象的reference。如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例货class对象作为锁对象。
执行monitorenter时锁计数器加1,执行monitorexit时锁计数减1,锁计数器为0时就释放锁,如果获取锁失败就阻塞等待,知道对象锁被另外一个线程释放。
(2)ReentrantLock
lock和unlock配合try/finally使用
synchronized和reentrantlock两者都是可重入锁。
何为可重入锁?:一个线程或得了某对象锁后,后续执行该对象上的其他加锁方法不会阻塞而会直接获取锁,即一个线程获取锁的时候不会自己把自己锁死。
(3)reentrantlock与synchronized的区别
- reentrantlock等待可中断
- reentrantlock可以实现公平锁
- reentrantlock可以绑定多个condition
两者性能并无多少差别,使用时以合适的场景和方便为主。
2. 无同步方案
(1)ThreadLocal
线程本地私有变量。是以map的形式存储变量Key—Value。其中key为当前变量,value为要存储的值。用数组来存储,本质上是拉链法来解决hash冲突。