Java多线程技术
Java多线程技术主要从以下3个方面展开讲述:
- 单线程:线程的实现方式、线程提供的基本方法;多线程:线程池实现;
- 线程同步:同步锁Synchronized、ReentrantLock,无锁实现CAS;
- 线程通信:Object的wait/notify方法、Lock.condition变量,以及ThreadLocal、Volatile变量;
Java多线程技术还有几个概念:
- Java内存模型
- 锁优化
- 原子性
单线程/多线程
Java的线程实现方式有两种,通过继承Thread类或则实现Runnable接口。其中Thread类也是实现了Runnable接口。Thread构造函数接受一个实现了Runnable接口的Runnable对象,通过调用Thread的start方法启动线程,执行run方法。
Thread类常见方法:
- yield 当前线程让出CPU执行时间片,主动挂起等待。
- sleep 当前线程休眠指定时间,但是并不会让出CPU执行时间片。
- start 启动线程,创建一个新的线程,将线程状态更改为Runnable可执行状态,如果获取到了执行权则直接执行。
- run 执行run方法,不会创建新的线程,使用当前线程执行。
- interrupt 配合sleep等操作执行,将处于休眠或则等待、挂起的线程自动退出,并释放锁。
- join 主线程等待子线程执行完。
- setDaemon 将线程设置为后台线程,不会随着主线程退出而退出
线程同步
多线程环境下对共享资源操作会引入线程安全问题,对于共享资源需要做好线程同步操作。线程安全主要通过同步来实现,Java中线程同步的实现方式主要有两种,一种是基于锁实现的同步,比如Synchronized、ReentrantLock,另一种是基于无锁的CAS算法实现。
Synchronized
Synchronized变量可以作用于方法名、代码块、类名、静态方法等,通过获取对象锁或则类锁来实现线程间同步的。
修饰方法名
public void synchronized methodA() {}; public void synchronized methodB() {};
多线程访问同一对象实例的methodA方法只能同步执行,synchronized作用于方法名获取的是对象锁。一个对象只要一个对象锁,所以当一个线程执行methodA方法获取到了当前对象的对象锁后,其他线程执行methodB也要进行等等,直到methodA方法执行完,对象锁被释放。
修饰代码块
synchronized (object) {}
synchronzied可以直接作用于一段代码块上,通过获取指定对象的对象锁。这个对象可以是当前对象也可以是其他对象。
这里要注意,不同对象的对象锁是不会互相影响的,所以一个类的多个方法,获取的是不同对象锁的话,这些方法是可以同时执行的。
修饰静态方法或则静态类
synchronized (ObjectA.class) {};
一个类只有一个Class对象,所以获取类的锁和静态方法锁是同一个锁,这两个地方的代码是同步的,无法同时执行。
Synchronized是可重入锁 当一个线程获取到了当前对象的对象锁后,可以再次获取该对象的对象锁。
public void synchronized methodA() { methodB() }; public void synchronized methodB() {};
Synchronized子类可以获取到父类的锁。
ReentrantLock
JDK1.5引入了ReentrantLock类,可重入锁,实现了Lock接口,需要手动加解锁,特别是在代码异常情况下一定要释放锁。
ReentrantLock常见方法:
- lock 加锁,获取对象锁
- tryLock 尝试加锁
- unlock 解锁
- newCondition 作用于线程间通信,类似于wait/notify。可以创建多个condition(即对象监视器)实例。注意调用condition对象方法也需要先获取对象锁,否者会抛异常。
ReentrantLock
- 方法
- lock方法 - Synchronized关键字作用
- condition对象 - wait/notify方法作用
- 实现
- 等待/通知实现
- 生产/消费实现
- 锁
- 公平锁,先到先得,FIFO顺序。
- 非公平锁,抢占式,随机获取锁。
ReentrantReadWriteLock
读写锁,锁分离技术,实现读写分离。读操作为共享锁,读写、写写操作为排它锁。readLock允许多个线程同时执行,writeLock只允许一个线程执行。
无锁实现的CAS
上面介绍的基于锁实现的Synchronized和ReentrantLock是Java语言层面提供的解决方案,CAS是硬件层面提供的技术方案,JVM层可以直接调用CAS指令。
线程通信
wait/notify
wait/notify是Object类提供的方法,wait使当前线程挂起进入等待队列并释放锁,notify从等待队列唤醒一个线程进入就绪队列获取锁。notify方法执行完并不会立即释放锁,notify可以调用多次,每次都会唤醒线程,也可以直接调用notifyAll方法唤醒等待队列里的所有线程进入就绪队列。
wait/notify方法执行必须要先获取到当前对象的对象锁,否者会抛异常。
condition
Java内存模型
内存模型
volatile
volatitle变量用于解决多个线程之间的变量可见性问题。
synchronized除了解决同步问题,同时也解决了可见性问题。
ThreadLocal
线程内的局部变量,内部通过一个map集合实现。
原子类型
Atomic基于CAS实现。
锁优化
对象锁(对象监视器)即monitor监视器锁,底层是依赖操作系统的Mutex Lock来实现的。Mutex Lock称为“重量级锁”,效率低。所以JDK对Synchronized进行优化,引入了“轻量级锁”和“偏向锁”。
获取锁JVM层面是通过CAS操作实现的。
JDK锁优化策略: 1)锁粗化,同一系列操作会反复对同一把锁进行上锁和解锁操作,编译器会扩大锁的边界,从而只使用一次上锁和解锁。 2)锁清除,代码块中没有涉及共享数据,编译器会清除锁。 3)自旋锁,锁竞争失败线程会被挂起、阻塞都要被转入内核态,进行上下文切换。自旋线程竞争失败时并不会立即进去阻塞状态,而会继续持有CPU执行一段时间,效率高,但会造成CPU资源浪费。 4)降低获取锁和释放锁带来的性能开销,引入了“偏向锁”和“轻量级锁”。
偏向锁
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,状态会升级,但是是单向升级,只能低到高,不能降级。JDK1.6默认开启偏向锁和轻量级锁。-XX:-UserBiasedLocking 关闭偏向锁。锁对象保存在对象的头文件中。
锁升级膨胀方向为 无锁(偏向锁)=》 轻量级锁 =》 重量级锁,这个过程为单向,对象头中的markword记录了当前锁的状态信息。
偏向锁
在无多线程竞争的情况下,减少不必要的轻量级锁执行路径,。用于只有一个线程执行的同步块,轻量级锁获取释放需要依赖多次CAS原子指令,偏向锁只依赖一次CAS。偏向锁为无锁的一种。
加锁:线程第一次进入同步块,CAS更新对象的Mark Word(偏向锁标志位为“1”,记录线程的ID) 释放:当有另一个线程来竞争锁时,就不再使用偏向锁,升级为轻量级锁。拥有偏向锁的线程不会主动释放锁,每次执行同步代码的时候直接判断偏向线程ID是否等于自己ID,等于就执行代码;不等于就说明有竞争了。
1)每次退出同步块释放锁,偏向锁只有在发生竞争时才释放锁。 2)每次进入和退出都需要CAS更新对象头。 3)竞争锁失败时,适应性自旋尝试抢占锁。
轻量级锁
在无多线程竞争的情况下,减少传统重量级锁产生的性能消耗。适用线程交替执行同步块的情况,同步块执行速度非常快。
Java代码层面锁优化的建议:
- 减少锁的持有时间
- 减小锁的粒度 - 例如ConcurrentHashMap的实现
- 使用读写锁替代独占锁 - 例如ReentrantReadWriteLock的实现
- 锁分离技术 - 例如LinkedBlockingQueue的实现
Java中的锁实现
AQS
AQS是JDK中为“线程同步”提供的一套基础工具类,标准同步器类的实现方式,基于AQS可以实现Java中的非常多“锁”。
java.util.concurrent并发包中,有非常多的同步类都是基于AQS实现的,比如
- ReentrantLock
- ReentrantReadWriteLock
- Semaphore
- CountDownLatch
- FutureTask
AQS功能说明:
- AQS内部是通过state整数状态值来维护锁是否可用,默认0表示锁可用,不等于0表示锁被占用;
- AQS封装了基本的操作,包括获取操作和释放操作;获取操作就是一个获取状态的操作,通过状态判断锁是否可用;
- 通过getState、setState以及compareAndSetState方法来操作状态值;状态值可以用来表示任意的状态;AQS不同的实现者,可以自定义状态值的作用;
- 获取操作,一般的步骤是先获取状态值,判断是否允许获取;如果允许,就尝试更新状态值;
根据AQS实现者的不同,实现的方法不同: 如果是独占锁,则要实现AQS的tryAcquire、tryRelease等方法; 如果是共享锁,则需要实现AQS的tryAcquireShared、tryReleaseShared方法。 AQS最终是调用子类实现的这些try方法来判断是否可执行。
- AQS内置提供了一个ConditionObject类,实现了AQS的基本方法,可以作为一个基本的同步器类来使用。
ReentrantLock里的AQS实现
ReentrantLock是独占锁(独占锁是悲观锁),所以要实现AQS的tryAcquire和tryRelease方法;
- ReentrantLock中的维护的状态信息:包括锁被获取到次数,以及通过owner变量保存持有锁的当前线程信息;
- tryAcquire获取操作逻辑:首先获取state状态值,判断锁是否可用;如果可用,通过CAS更新状态值,并持有当前线程,返回成功;如果锁不可用,通过owner变量判断锁是否被自己占用;如果是,则更新状态值,记录锁获取次数,返回成功;如果不是,说明锁被其他线程占用,返回失败。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取锁状态,0 标识锁可用
int c = getState();
// c == 0 说明锁可以用
if (c == 0) {
// 通过CAS更新状态,表明锁被占用
if (compareAndSetState(0, acquires)) {
// 保存持有锁的当前线程状态信息
owner = current;
return true;
}
}
// 如果 c != 0,说明锁已被占用
// 通过owner判断,是否是被自己占用了锁
else if (current == owner) {
// 如果是,则说明这次是一次重入操作,状态++
setState(c+acquires);
return true;
}
// 如果owner不等于自己,说明锁被其他线程占用,return false 获取锁失败。
return false;
}
- tryRelease释放操作逻辑:获取state状态值,判断是否是持有锁的线程在执行这个方法,如果不是,直接抛异常;更新锁状态,返回成功;
protected final boolean tryRelease(int releases) {
// 获取锁状态
int c = getState() - releases;
// 必须是持有锁的线程才能执行release操作
if (Thread.currentThread() != owner)
throw new IllegalMonitorStateException();
boolean free = false;
// c == 0 说明锁可以被释放,如果是非0,说明这个锁是共享锁,要等其他持有锁的线程都释放,每次释放state-1。
if (c == 0) {
free = true;
owner = null;
}
// 更新锁状态
setState(c);
return free;
}
- newCondition方法:使用AQS提供的ConditionObject类来实现wait/notify方法功能。
final ConditionObject newCondition() {
// 直接使用AQS提供的ConditionObject类
return new ConditionObject();
}
Semaphore和CountDownLatch
Semaphore和CountDownLatch都是非独占锁,它们将state状态值用于保存当前可用许可的数量。
FutureTask
FutureTask不是一个同步器类,但是它的作用就是一个线程一直等待某件事件的发生(即任务的结束),这里就涉及到任务状态的维护。所以FutureTask利用AQS的state来维护任务的状态,还额外保存任务结束的结果信息。
/** State value representing that task is running */
private static final int RUNNING = 1;
/** State value representing that task ran */
private static final int RAN = 2;
/** State value representing that task was cancelled */
private static final int CANCELLED = 4;
/** The result to return from get() */
private V result;
ReentrantReadWriteLock
ReentrantReadWriteLock内部有两个锁,读锁和写锁,但都是基于一个AQS子类实现的。读锁用于共享操作,写锁用于独占操作。
无锁实现
CAS
实现“线程同步”有两种方式:第一种是基于“锁”方式实现的,第二种是基于“无锁”方法实现的。
基于“锁”的常见实现方案有:Synchronized和ReentrantLock等;
基于“无锁”实现的方案为:CAS算法。
CAS(Compare and Swap)无锁并发控制的一种技术,是一种乐观锁。CAS的优势:
- 无锁操作,节省锁竞争带来的开销;
- 没有线程间频繁调度的开销。
CAS的工作原理:
- CAS是基于硬件层面实现的技术,是一条CAS指令;
- CAS包括三个值,V 内存中的值(当前正确的值),A 比较的值(期望内存中当前的值),B 新值;CAS首先判断V是否等于A,如果等于则更新V为B;如果不等于,说明值已被更改过,直接返回失败;
- 线程执行CAS操作并不会阻塞,CAS更新失败并不会挂起线程,调用者可以自行决定后续的操作,可以继续尝试更新(获取锁操作失败,线程会被挂起,CAS并不会)。
CAS存在问题:
ABA问题:即V值先由A变为了B,后又由B变为了A,对于V来说值还是A,无法知道是否有更新过。 解决方案:引入版本号,更新值的同时一起更新版本号,这样即使V值相同,版本号记录了更新记录。
JDK中基于CAS的实现
Atomic原子类 JDK1.5开始支持CAS操作,Atomic原子类基于volatile变量和CAS操作来实现。