文章目录
-
- 一. 再述线程状态转换
- 二. 多把锁与线程活跃性问题
-
- 1. 多把锁
- 2. 活跃性
- 三. ReEntrantLock
-
- 1. 基本用法
- 2. 可重入
- 3. 可打断
- 4. 锁超时
- 5. 公平锁
- 6. 条件变量
一. 再述线程状态转换
情况1:New ——》RUNNABLE
当调用 t.start() 方法时
情况2:RUNNABLE 《——》WAITING
线程 synchronized(obj) 获得锁以后,调用 obj.wait 方法,状态将从 RUNNABLE ——》WAITING;调用 notify、notifyAll、interrupt 方法,如果:
- 竞争锁失败,线程从 WAITING ——》 BLOCKED
- 竞争锁成功,线程从 WAITING ——》RUNNABLE
注意 BLOCKED 状态与 WAITING 状态的区别:
waiting:主动为之,wait()方法释放cpu执行权和释放锁进入等待队列,需要notify()唤醒进入同步队列竞争锁;
blocked:被动的,在竞争锁的时候失败,被阻塞,在同步队列里继续竞争锁。
情况3:RUNNABLE 《——》WAITING
- 当当前线程调用 t.join() 方法时,当前线程从 RUNNABLE ——》WAITING。
- t 线程运行结束,或调用了当前线程的 interrupt(),当前线程从 WAITING ——》RUNNABLE 。
情况4:RUNNABLE 《——》WAITING
- 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE ——》WAITING;
- 调用 LockSupport.unpark(t) 或调用了线程的 interrupt(),会让目标线程从 WAITING ——》RUNNABLE 。
情况5/6/7/8:RUNNABLE 《——》TIMED_WAITING
调用 obj.wait(n)、t.join(n)、Thread.sleep(n)、LockSupport.parkNanos(n)、LockSupport.parkUntil(n) 时,线程从 RUNNABLE ——》TIMED_WAITING。
注意:调用sleep(long)方法不会释放锁,时间到了自己返回原状态。
情况9:RUNNABLE 《——》BLOCKED
- t 线程用 synchronized(obj) 竞争对象锁失败时,从 RUNNABLE ——》BLOCKED;
- 获得锁的线程执行完毕后,会唤醒所有阻塞的线程重新竞争,竞争成功的从 BLOCKED ——》RUNNABLE
情况10:RUNNABLE ——》TERMINATED
线程中的代码全部执行完毕,RUNNABLE ——》TERMINATED。
二. 多把锁与线程活跃性问题
1. 多把锁
当两个业务完全不相干时,可以选择更加细粒度的锁,增加程序的并发性。
class MultiLock {
//两个对象锁
private Object bedRoom = new Object();
private Object studyRoom = new Object();
public void sleep() {
//不再锁住 this 对象,而是更加细粒度的锁
synchronized (bedRoom) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void study() {
synchronized (studyRoom) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2. 活跃性
(1)死锁
但是当一个程序中存在多把锁,而一个线程需要同时获得多把锁时,就容易发生死锁。
public class MultiLockTest {
final static Logger logger = LoggerFactory.getLogger(MultiLockTest.class);
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock1) {
try {
logger.info("lock 1 ……");
Thread.sleep(1000);
synchronized (lock2) {
logger.info("lock 2 ……");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (lock2) {
try {
logger.info("lock 2 ……");
Thread.sleep(2000);
synchronized (lock1) {
logger.info("lock 1 ……");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t2");
t1.start();
t2.start();
}
}
(2) 定位死锁
方式1:jps + jstack
方式2:jconsole
连接——》线程——》检测死锁
(3)哲学家就餐问题
程序模拟死锁问题:
筷子类
class Chopstick {
private String name;
public Chopstick(String name) {
this.name = name;
}
}
哲学家类
class Philosopher extends Thread {
Logger logger = LoggerFactory.getLogger(Philosopher.class);
//模拟左右两根筷子
private Chopstick left;
private Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while(true) {
//需要两根筷子才能吃饭
synchronized (left) {
synchronized (right) {
logger.info("eating ……");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
测试类
public class PhilosopherTest {
public static void main(String[] args) {
Chopstick c1 = new Chopstick("c1");
Chopstick c2 = new Chopstick("c2");
Chopstick c3 = new Chopstick("c3");
Chopstick c4 = new Chopstick("c4");
Chopstick c5 = new Chopstick("c5");
new Philosopher("p1", c1, c2).start();
new Philosopher("p2", c2, c3).start();
new Philosopher("p3", c3, c4).start();
new Philosopher("p4", c4, c5).start();
new Philosopher("p5", c5, c1).start();
}
}
jconsole检测结果,五个线程均陷入死锁:
(4)活锁
当两个线程都在改变对方的运行结束条件时会发生活锁。
示例:
public class LiveLockTest {
static int count = 10;
private static final Logger logger = LoggerFactory.getLogger(LiveLockTest.class);
public static void main(String[] args) {
new Thread(() -> {
while (count > 0) {
count--;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("count: " + count);
}
}, "t1").start();
new Thread(() -> {
while (count < 20) {
count++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("count: " + count);
}
}, "t2").start();
}
}
与死锁的不同:死锁是两个线程都陷入了阻塞状态,而活锁是两个线程都在运行,只是无法结束。
解决活锁问题,可以使两个线程交错运行,或者增加随机睡眠时间。
(5)饥饿
饥饿:一个线程由于优先级太低,始终得不到CPU调度执行,也不能够结束。
比如之前的哲学家就餐问题,为避免死锁问题,我们可以通过采用顺序获取锁的方式解决:
new Philosopher("p1", c1, c2).start();
new Philosopher("p2", c2, c3).start();
new Philosopher("p3", c3, c4).start();
new Philosopher("p4", c4, c5).start();
new Philosopher("p5", c1, c5).start(); //获取锁的方式改为 c1, c5
但是日志显示,P4和P3线程获取锁的几率较高,而P5线程几乎获取不到锁,这就是饥饿现象:
三. ReEntrantLock
1. 基本用法
//获取锁,只有获取到了锁后面的代码才会继续执行,否则将一直阻塞在这里
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
2. 可重入
可重入是指同一个线程如果已经获得了一把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。
如果是不可重入锁,那么第二次获取锁时,自己也会被锁挡住。
synchronized 也是可重入锁。
public class ReEntrantLockTest {
private static final Logger logger = LoggerFactory.getLogger(ParkTest.class);
public static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
logger.info("enter main……");
m1();
} finally {
lock.unlock();
}
}
public static void m1() {
lock.lock(); //可重入
try {
logger.info("enter m1……");
} finally {
lock.unlock();
}
}
}
3. 可打断
可打断表明可以打断一直处于等待锁状态的线程,避免死锁。
synchronized 是不可打断锁,因为interrupt() 方法只是将打断标记设置为 true,当线程未获取到锁处于阻塞状态下时,并没有显式抛出异常。而处于WAITING 状态下的线程可被打断是因为 wait、sleep、join 这些方法会检查打断标记,并抛出InterruptedException异常。为什么synchronized不可打断
ReentrantLock的 lock 方法也是不可打断的,而 lockInterruptibly() 方法 是可打断的。
示例:
public class ReEntrantLockTest {
private static final Logger logger = LoggerFactory.getLogger(ParkTest.class);
public static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
logger.info("t1线程尝试获取锁……");
lock.lockInterruptibly();
logger.info("t1获取到锁……");
lock.unlock();
} catch (InterruptedException e) {
logger.info("t1线程没有获取到锁……");
e.printStackTrace();
}
});
lock.lock();
logger.info("主线程获取到锁……");
t1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.interrupt();
}
}
查看 lockInterruptibly() 方法的源码发现,它其实也是通过检查打断标记抛出InterruptedException异常,来实现可打断的功能:
lockInterruptibly 源码:
4. 锁超时
tryLock() 与 tryLock(long timeout, TimeUnit unit) 方法允许线程自己设置锁等待时间,前者没有获取到锁立即返回,后者会等待一定的时间,超过时间没有获取到锁则返回。
public class TryLockTest {
private static final Logger logger = LoggerFactory.getLogger(TryLockTest.class);
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
logger.info("尝试获取锁");
try {
if (! lock.tryLock(2, TimeUnit.SECONDS)) {
logger.info("没有获取到锁");
return; //没获取到锁需要返回
}
} catch (InterruptedException e) { //带参数的tryLock方法也允许被打断
e.printStackTrace();
logger.info("没有获取到锁");
return;
}
//获取到了锁需要执行的代码
try {
logger.info("获取到了锁");
} finally {
lock.unlock();
}
}, "t1");
logger.info("获取了锁");
lock.lock();
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("释放了锁");
lock.unlock();
}
}
使用 tryLock() 解决哲学家就餐问题:
由于不能再使用 synchronized 锁住筷子对象,则筷子对象本身就需要具有锁特性。因此,这里将筷子类继承 ReentrantLock 类。
class Chopstick extends ReentrantLock {
private String name;
public Chopstick(String name) {
this.name = name;
}
}
将 synchronized 换成 tryLock 或者 tryLock(long timeout, TimeUnit unit) :
class Philosopher extends Thread {
Logger logger = LoggerFactory.getLogger(Philosopher.class);
private Chopstick left;
private Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while(true) {
//将synchronized换成 tryLock
if (left.tryLock()) {
try {
if (right.tryLock()) {
try {
logger.info("eating ……");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
right.unlock();
}
}
} finally {
//解决死锁的关键就在于右筷子拿不到时会释放左筷子
left.unlock();
}
}
}
}
}
5. 公平锁
synchronized 锁是不公平锁,即当锁被释放时,阻塞队列里的线程会同时去争抢锁,而不是按照进入阻塞队列的顺序获取锁。
ReentrantLock 默认是不公平锁,可以通过改变其构造方法将其设置为公平锁:
公平锁可以用来解决饥饿问题,但是会降低并发度,一般无需设置。
6. 条件变量
与 synchronized 的 wait/notify 类似的概念,当条件不满足时,为了不阻塞其他线程用锁,可以进入 waitSet 等待。
ReentrantLock 的条件变量更强大的地方在于,它支持多个条件变量,即等待不同条件的线程可以进入不同的等待室。
使用流程:
- await 前需要获得锁;
- await 执行后会释放锁,进入 conditionObject 等待;
- await 的线程被唤醒(或打断、或超时)后,需要重新竞争锁;
- 竞争锁成功后,从await 后继续执行。
await / signal 使用示例:
public class ReEntrantLockTest {
private static final Logger logger = LoggerFactory.getLogger(ParkTest.class);
public static ReentrantLock lock = new ReentrantLock();
static boolean condition1 = false;
static boolean condition2 = false;
public static void main(String[] args) throws Exception {
Condition waitSet1 = lock.newCondition();
Condition waitSet2 = lock.newCondition();
Thread t1 = new Thread(() -> {
lock.lock();
try {
while (!condition1) {
try {
logger.info("条件1不满足,进入等待");
waitSet1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
logger.info("t1 开始执行");
} finally {
lock.unlock();
}
}, "t1");
Thread t2 = new Thread(() -> {
lock.lock();
try {
while (!condition2) {
try {
logger.info("条件2不满足,进入等待");
waitSet2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
logger.info("t2 开始执行");
} finally {
lock.unlock();
}
}, "t2");
t1.start();
t2.start();
Thread.sleep(2000);
lock.lock();
try {
logger.info("通知t1执行……");
condition1 = true;
waitSet1.signal();
logger.info("通知t2执行……");
condition2 = true;
waitSet2.signal();
} finally {
lock.unlock();
}
}
}