一、多八锁
多把不相干的锁
一间大屋子有两个功能:睡觉、学习,互不相干。
现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低
解决方法就是准备多个房间(对个对象锁)
例如
import cn.itcast.n2.util.Sleeper;
import lombok.extern.slf4j.Slf4j;
public class TestMultiLock {
public static void main(String[] args) {
BigRoom bigRoom = new BigRoom();
new Thread(() -> {
bigRoom.study();
},"小南").start();
new Thread(() -> {
bigRoom.sleep();
},"小女").start();
}
}
@Slf4j(topic = "c.BigRoom")
class BigRoom {
public void sleep() {
synchronized (this) {
log.debug("sleeping 2 小时");
Sleeper.sleep(2);
}
}
public void study() {
synchronized (this) {
log.debug("study 1 小时");
Sleeper.sleep(1);
}
}
}
运行结果:(并发度较低)
改进:(加入多把锁)
import cn.itcast.n2.util.Sleeper;
import lombok.extern.slf4j.Slf4j;
public class TestMultiLock {
public static void main(String[] args) {
BigRoom bigRoom = new BigRoom();
new Thread(() -> {
bigRoom.study();
},"小南").start();
new Thread(() -> {
bigRoom.sleep();
},"小女").start();
}
}
@Slf4j(topic = "c.BigRoom")
class BigRoom {
private final Object studyRoom = new Object();
private final Object bedRoom = new Object();
public void sleep() {
synchronized (bedRoom) {
log.debug("sleeping 2 小时");
Sleeper.sleep(2);
}
}
public void study() {
synchronized (studyRoom) {
log.debug("study 1 小时");
Sleeper.sleep(1);
}
}
}
改进后运行结果:用多八把锁/细粒度的锁提高程序的并发性
(需保证业务间无关联)
将锁的粒度细分
class BigRoom {
//额外创建对象来作为锁
private final Object studyRoom = new Object();
private final Object bedRoom = new Object();
}
● 好处:可以增强并发度
● 坏处:如果一个线程需要同时获取多把锁,就容易发生死锁
二、活跃性
2.1 死锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
t1 线程 获得 A对象 锁,接下来想获取 B对象 的锁
t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁
例:各自均有一把锁,都想获取对方锁时会产生死锁,代码最终无法得到执行
import lombok.extern.slf4j.Slf4j;
import static cn.itcast.n2.util.Sleeper.sleep;
@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
public static void main(String[] args) {
test1();
}
private static void test1() {
Object A = new Object();
Object B = new Object();
// t1线程获取A对象上的锁
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
sleep(1);
// t1线程经1s后尝试获取锁B
synchronized (B) {
log.debug("lock B");
log.debug("操作...");
}
}
}, "t1");
// t2线程获取B对象上的锁
Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("lock B");
sleep(0.5);
// t2线程经0.5s后尝试获取锁A
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
}
2.2 定位死锁
死锁在多线程是较常见的。检测死锁可以使用 jconsole
工具,或者使用 jps 定位进程 id,再用 jstack
定位死锁:
①:使用 jps 定位进程 id,再用 jstack
定位
打印死锁信息
②:使用jconsole工具
运行jconsole
监测出出现错误的代码行数
【活跃性】问题,除了死锁以外,还有活锁和饥饿者两种情
况
2.3 哲学家就餐问题
有五位哲学家,围坐在圆桌旁。
● 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
● 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
● 如果筷子被身边的人拿着,自己就得等待
测试结果:
正常就餐几轮后不能再继续就餐出现无限等待(每个哲学家各持有一根筷子,等待对方放下筷子)
2.4 活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束。
例如
import lombok.extern.slf4j.Slf4j;
import static cn.itcast.n2.util.Sleeper.sleep;
@Slf4j(topic = "c.TestLiveLock")
public class TestLiveLock {
static volatile int count = 10;
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
// 期望减到 0 退出循环
while (count > 0) {
sleep(0.2);
count--;
log.debug("count: {}", count);
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
sleep(0.2);
count++;
log.debug("count: {}", count);
}
}, "t2").start();
}
}
运行结果:(不断在10左右进行加加减减操作,线程失踪无法停止)
死锁与活锁
的区别:
死锁是两个线程持有对方需要的锁,导致线程都无法继续向下运行,两个线程均陷入阻塞;而活锁两个线程均未阻塞,都在使用CPU不断运行,但由于改变对方的结束条件导致两个线程都无法结束
解决活锁的办法
:
● 使两个线程的执行时间有一定的交错(不集中在一起执行/设置睡眠的时间为一个随机数===>将其指令交错开,第一个线程很快运行完,第二个线程将没有机会改变对方的结束条件)
在开发中遇到活锁的情况,可增加随机睡眠时间来避免活锁的产生
2.5 饥饿
很多教程中将饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束
,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题
观察一个线程饥饿的例子,可以先观察使用顺序加锁的方式解决之前的死锁问题
相同顺序加锁的解决方案
线程1按AB的顺序加锁,先获得锁A,线程2也是按AB的顺序进行加锁,线程2此时想获取对象A的锁时获取失败(进入对象A的EntryList中阻塞),这时线程1再尝试获取B对象的锁。这样线程1均可以将AB两个锁获取到。等其释放完后线程2也按相同的顺序获取AB对象。