线程安全及实现方法
何为线程安全
当多个线程同时访问一个对象时 如果不考虑这些线程在运行时环境下的调度和交替执行 也不需要进行额外的同步 或者在调用方进行任何其他的协调操作 调用这个对象的行为都可以获得正确的结果 那就称这个对象是线程安全的
java中的线程安全
按照线程安全的“安全程度”由强到弱来排序 可以将java语言中各种操作共享的数据分为以下五类:不可变 绝对线程安全 相对线程安全 线程兼容 线程对立
-
不可变
一定是线程安全的
如果对线程共享的数据是一个基本数据类型 只要在定义时使用final关键字修饰 就可以保证是不可变的
如果共享数据是一个对象需要对象自行保证其行为不会对其状态产生任何影响 比如java.lang.String类的对象实例 用户调用它的任何方法都不会影响它原来的值 只会返回一个新构造的字符串对象 还有一种最简单的方式是把对象里所有带有状态的变量都声明为final -
绝对线程安全
个人理解:所有方法都加上synchronized锁(包括调用方法) 但是效率不高 如java.util.Vector是一个线程安全的容器 它的add() get() size()等方法都被synchronized修饰 但是在调用其方法的时候 对于调用方法也需要加synchronized才能保证绝对线程安全 -
相对线程安全
通常意义上的线程安全 需要保证对这个对象单次的操作都是线程安全的 在调用时不需要进行额外的保障措施 但是对于一些特定顺序的连续调用 就可能需要在调用端使用额外的同步手段来保证调用的正确性
在java语言中大部分声称线程安全的类都属于这种 如Vector HashTable Collection 以及synchronizedCollection()方法包装的集合等 -
线程兼容
指对象本身并不是线程安全的 但是可以通过在调用端正确的使用同步手段来保证对象在并发环境中可以安全的使用(平时说的一个类不是线程安全的) 如ArrayList和HashMap -
线程对立
不管调用端是否采取了同步措施 都无法在多线程环境中并发使用代码
例如Thread类的suspend()方法和resume()方法 如果两个线程同时持有一个线程对象 一个尝试去中断线程另一个尝试去恢复线程 在并发进行的情况无论调用时是否进行了同步 目标线程都存在死锁的风险——假如suspend()中断的线程就是即将要执行resume()的那个线程 就会产生死锁 所以这两个方法已经被声明废弃了
线程安全的实现方法
-
互斥同步
是一种最常见也是最主要的并发正确性保障手段
同步是指在多个线程并发访问共享数据时 保证共享数据在同一个时刻只被一条线程使用 而互斥是实现同步的一种手段 常见的互斥实现方式有 临界区 互斥量 信号量
互斥是因 同步是果 互斥是方法 同步是目的
java中最基本的互斥同步手段就是synchronized关键字 这是一种块结构的同步语法 synchronized关键字经过javac编译后 在同步块前后分别形成monitorenter和monitorexit这两个字节码指令 这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象
在执行monitorenter指令时 首先要去尝试获取对象的锁 如果这个对象没被锁定 或者当前线程已经持有了那个对象的锁 就把锁的计数器的值加一 而在执行monitorexit指令时会将锁计数器的值减一 一旦计数器值为零 锁随即被释放 如果获取对象锁失败 那当前线程就应当被阻塞等待 直到请求锁定的对象被持有它的线程释放为止
两个关于synchronized的直接推论- 被synchronized修饰的同步块对同一条线程来说是可重入的 这意味着同一线程反复进入同步块也不会出现自己把自己锁死的情况
- 被synchronized修饰的同步块在持有锁的线程执行完毕并释放锁之前会无条件地阻塞后面其他线程的进入
从执行成本来看 持有锁是一个重量级操作
jdk5以后java.util.concurrent.locks.Lock接口成了java的另一种全新的互斥同步手段 基于Lock接口 用户能够以非块结构来实现互斥同步 改为在类库层面去实现同步**
重入锁**(ReentrantLock) 是Lock接口最常见的一种实现 与synchronized一样是可重入的(可重入性指一条线程能够反复进入被它自己持有锁的同步块的特性 即锁关联的计数器 如果持有锁的线程再次获得它 则将计数器的值加一 每次释放锁时计数器的值减一 当计数器的值为零时 才能真正释放锁)
在基本用法上ReentrantLock也与synchronized很相似 但是ReentrantLock与synchronized相比增加了一些高级功能:等待可中断 可实现公平锁 锁可以绑定多个条件
- 等待可中断:指当前持有锁的线程长期不释放锁的时候 正在等待的线程可以选择放弃等待 改为处理其他事情
- 公平锁 指多个线程在等待同一个锁时 必须按照申请锁的时间顺序来依次获得锁 而非公平锁则不保证这一点 ReentrantLock在默认情况下也是非公平的 但可以通过带布尔值的构造函数来要求使用公平锁 使用公平锁后性能会急剧下降
- 锁绑定多个条件 指一个ReentrantLock对象可以同时绑定多个Condition对象 在synchronized中 锁对象的wait()跟它的notify()或者notifyAll()方法配合可以实现一个隐含的条件 如果要和多个条件关联需要另外加锁 但是ReentrantLock只用多次调用newCondition()方法就好
ReentrantLock与synchronized对比:synchronized在java语法层面的同步 更清晰简单 只需要基础的同步功能时推荐synchronized
Lock应该确保在finally块中释放锁 否则一旦受同步保护的代码块中抛出异常则有可能永远不会释放持有的锁 需要程序员自己保证 而synchronized可以由java虚拟机来确保
从长远来看 java虚拟机更容易针对synchronized来进行优化
-
非阻塞同步
互斥同步面临的主要问题时进行线程阻塞和唤醒所带来的性能开销 因此这种同步也被称为 阻塞同步 互斥同步属于一种悲观的并发策略 总是认为只要不去做正确的同步措施(加锁) 就肯定会出现问题
而非阻塞同步:基于冲突检测的乐观并发策略 (不管风险 先进性操作 如果没有其他线程争用共享数据 那操作就直接成功 如果共享的数据被征用 产生了冲突 那再进行其他补偿措施 如不断重试)这种方式的实现不需要再把线程阻塞挂起 称为非阻塞同步 也称无锁编程
乐观并发策略需要“硬件指令集的发展” 因为必须要求操作和冲突检测这两个步骤具备原子性 只能依靠硬件来实现 硬件确保一些从语义上看来需要多次操作的行为可以只通过一条处理器指令来完成 常用指令:
1.测试并设置 (Test-and-Set)2. 获取并增加(Fetch-and-Increment) 3. 交换(swap)4. 比较并交换(Compare-and-Swap CAS)5. 加载连接/条件储存(Load-Linked/Store-Conditional LL/SC)
CAS: 没有锁的状态下 多线程访问时保证线程一致性去改动某个值
ABA问题:原数据是A要改成B 但是改动途中有人把A改成A
解决方式:对当前值加版本号 每次改动更新版本号
-
无同步方案
同步与安全两者没有必然的联系 如果一个方法本来就不涉及共享数据 那就不需要任何同步措施去保证正确性 因此有一些代码天生就是线程安全的:-
可重入代码 又称纯代码 指可以在代码执行的任何时刻中断它 转而去执行另外一段代码 而在控制权返回后原来的程序不会出现任何错误
可重入代码有一些共同的特征 如:不依赖全局变量 存储在堆上的数据和公用的系统资源 用到的状态量都由参数中传入 不调用非可重入的方法等(简单的判断 一个方法返回的结果是可以预测 只要输入了相同的数据就都能返回相同的结果 即满足可重入性要求) - 线程本地存储: 如果一段代码中所需要的数据必须与其他代码共享 就看这些共享数据是否能保证在同一个线程中执行 能保证的话可以把共享数据的可见范围限制在同一个线程内 这样无需同步也能保证安全 (如使用消费队列的架构模式)
-
可重入代码 又称纯代码 指可以在代码执行的任何时刻中断它 转而去执行另外一段代码 而在控制权返回后原来的程序不会出现任何错误