JAVA并发编程
3. 线程同步
3.1 相关概念
3.1.1 含义
线程同步机制是一套用于协调线程之间的数据访问的机制,用来保证线程安全。
3.1.2 方法
- 锁
- volatile关键字
- final, static关键字
- 相关API,如wait(), sleep(), notify()等
3.2 锁
3.2.1 含义
锁可以理解为对共享数据进行保护的一个许可证,线程在访问共享数据前必须先获得锁,一个锁只能同时被一个线程持有,线程在结束访问之后释放锁。
3.2.2 作用
- 保障原子性:通过互斥保障原子性
- 保障可见性:获得锁和释放锁会刷新处理器缓存
- 保障有序性:
3.2.2 重进入
- 可重入锁:线程可以继续成功申请已经占有的锁。
- 内部锁(synchronized)是可重进入的,属于非公平锁;显示锁既支持公平锁又支持非公平锁。
3.3.3 粒度
- 一个锁可以保护的共享数据的数量大小称为锁的粒度。
- 锁保护共享数据量大,称该锁的粒度粗, 否则就称该锁的粒度细。
- 锁的粒度过粗会导致线程在申请锁时会进行不必要的等待,锁的粒度过细会增加锁调度的开销 。
3.3.4 减少锁的竞争
减少锁的竞争能够改进性能和可伸缩性,有3种方式减少锁的竞争
- 减少持有锁的时间
- 减少持有锁的频率
- 用协调机制取代独占锁
3.3 synchronized
3.3.1 含义
- synchronized块是Java提供的内置锁机制,包括锁对象的引用和锁保护的代码块。
- 每个对象都可以隐式地作为同步锁,称作内部锁或监视器锁。内部锁是互斥锁(mutex)。
3.3.2 同步代码块
-
代码实现
synchronized(anObject){ //do something synchronized }
-
只有使用同一个锁对象才能实现同步
3.3.3 同步方法
-
代码实现
synchronized returnType aMethod{ //do something synchronized }
-
synchronized修饰实例方法,默认this作为锁对象
-
synchronized修饰实例方法,默认运行时类对象(.class)作为锁对象
3.4 volatile
3.4.1 概念
-
作用:使共享变量在多个线程之间可见
-
原理:当域被声明为volatile之后,编译器会随时监控这个变量,且对其操作不会被重排序,变量不会缓存在工作内存(对其他线程隐藏)而是在公共内存中。
-
典型用途:标识重要的生命周期事件
3.4.2 对比
性质 | volatile | synchronized |
---|---|---|
特点 | 轻量级的同步机制,开销小 | 开销较大 |
修饰对象 | 变量 | 方法,代码块 |
阻塞 | 不会 | 会 |
可见性 | 能 | 能 |
原子性 | 不能 | 能 |
3.5 硬件对并发的支持
3.5.1 比较并交换CAS
-
CAS有三个操作数,内存位置V、旧的预期值A和新值B,当且仅当V符合预期值A时,CAS用新值原子化地更新V的值。
-
代码模拟
@ThreadSafe public class SimulatedCAS { @GuardedBy("this") private int value; public synchronized int get() { return value; } public synchronized int compareAndSwap(int expectedValue, int newValue) { int oldValue = value; if (oldValue == expectedValue) value = newValue; return oldValue; } public synchronized boolean compareAndSet(int expectedValue, int newValue) { return (expectedValue == compareAndSwap(expectedValue, newValue)); } }
-
不足:不能避免ABA问题。即共享变量的当前值与当前线程提供的期望值相同, 就认为这个变量没有被其他线程修改过。
(可以引入修订号判断是否被其他线程修改过,例如AtomicStampedReference类)
3.6 原子变量类
3.6.1 含义
- 原子变量类基于CAS操作实现了volatile变量的原子性
3.6.2 种类
类型 | 类名 |
---|---|
数据类型 | AtomicInteger, AtomicLong, AtomicBoolean |
数组 | AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray |
字段更新器 | AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater |
引用型 | AtomicReference, AtomicStampedReference, AtomicMarkableReference |
3.6.3 使用方法
3.7 活跃度危险
3.7.1 死锁
-
含义:多个线程因为环路的锁依赖关系而永远等待。
-
原因:多线程程序同步时可能需要使用多个锁,如果获得锁的顺序不一致可能会导致死锁 。(锁顺序死锁)
-
类别
类型 描述 锁顺序死锁 多线程获得锁的顺序不一致 动态锁顺序死锁 获得锁的顺序与参数有关 协作对象间的死锁 持有锁的时候调用外部方法,外部方法可能会获得其他锁 资源死锁 多个线程因为环路的资源依赖关系而永远等待 -
解决方法
- 使用定时锁,例如显示锁Lock类的tryLock方法
- 通过线程转储分析死锁
3.7.2 饥饿
- 含义:线程被永久拒绝访问所需的资源
- 原因:不当使用线程优先级;在锁中无休止地循环或等待。
3.7.3 活锁
- 含义:线程未被阻塞,但不断失败地重试相同的操作。
- 原因:过渡的错误恢复代码
- 解决:对重试机制引入随机性、
4 线程间通信
4.1 等待通知
4.1.1 概念
- 线程中某一条件暂时没有满足便先等待进入阻塞,稍后其他线程通知此线程条件满足,原线程恢复。
4.1.2 wait()
-
wait()方法只能在同步代码块中由锁对象调用
-
调用wait()方法,当前线程会释放锁 ,直到被另一线程调用该锁的notify()或notifyAll()。
-
带参方法
- wait(long):就算未被通知,在指定毫秒时间内也会恢复
- wait(long, int):指定毫秒和纳秒
-
代码实现
synchronized(aLock){ aLock.wait(); }
-
当线程处于等待状态时, 调用线程对象的interrupt()方法会中断线程的等待状态, 并产生InterruptedException异常。
4.1.3 notify()
- notify()方法只能在同步代码块中由锁对象调用,否则会抛出异常。
- 调用notify()方法,会唤醒某一个等待的线程,并且要等到同步代码块执行完才会释放锁对象。
- notifyAll()可以唤醒所有线程
4.2 ThreadLocal
4.2.1 概念
- 作用:使每个线程与持有数值的对象关联在一起。
- 优点:可以避免每次都重新分配对象
4.4.2 实现
-
方法
- get()
- set(obj)
- initialValue():在子类中重写改方法,可设置get()初值
-
代码
//example_1 ThreadLocal threadLocal = new ThreadLocal(); threadLocal.set(t1); T t2 = threadLocal.get(); //example_generic ThreadLocal<T> threadLocal = new ThreadLocal()<>; //example_setInitialValue class MyLocal extends ThreadLocal<T>{ @override protected T initialValue(){ return new T(); } }