减小锁粒度,提交并发度。
package com.bo.threadstudy.four;
import lombok.extern.slf4j.Slf4j;
/**
* 多把锁的情况,以及后期的死锁,活锁,饥饿现象,哲学家就餐
*/
@Slf4j
public class ManyLockTest {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
//使用多把锁,其目的就是将锁的粒度划分的更细,增强并发度,但在嵌套环境下就是死锁
public static void main(String[] args) {
//假设有一个房间,我现在需要四个人做工,一个房间只能容纳一个人,每个人耗时1秒
//这个房子我切割一下,切成两个房子,然后让两四个人分开做工,就提升了并发度,例子不太好,老师的也不咋地,将就把
//原本一间方4秒的任务,换成2间两秒
for (int i = 0; i < 4; i++) {
if(i%2==0){
new Thread(() -> {
synchronized (lock1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("在做一号工");
}
},"t1_"+i).start();
}else{
new Thread(() -> {
synchronized (lock2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("在做二号工");
}
},"t2_"+i).start();
}
}
}
}
满足死锁的条件:
每个资源只能被一个线程所持有。
线程在阻塞等待其它线程持有的资源的时候,自己的资源不会主动释放。
线程持有资源正常运行的情况下,也不会被其它线程的请求释放资源。
多个线程嵌套持有各个线程需要的资源,形成一个闭环。
在这种情况下会出现死锁。写一个简单的死锁例子。
@Slf4j
class DeadLock{
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
while(true){
synchronized (lock1){
log.debug("t1线程进入lock1");
synchronized (lock2){
log.debug("t1线程进入lock2");
}
}
}
},"t1").start();
new Thread(() -> {
while(true){
synchronized (lock2){
log.debug("t2线程进入lock2");
synchronized (lock1){
log.debug("t2线程进入lock1");
}
}
}
},"t2").start();
}
}
synchronized确实会造成死锁。那么怎么排查死锁问题。
老师讲课过程中,说了两种,第一种:jconolse,第二种,jps查询进程ID使用jsack排查。
这种比较损毁性能,相对而言,正常生产环境采用arthas来进行查询即可。启动arthas-boot.jar后通过thread -b命令查询即可。
既然涉及到死锁,那么最经典的问题就是哲学家吃饭问题了,先写一个错误场景。
package com.bo.threadstudy.four;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
/**
* 哲学家吃饭问题
*/
public class PhilosopherEatTest {
public static void main(String[] args) {
Philosopher t1 = new Philosopher("亚里士多德", 1, 2);
Philosopher t2 = new Philosopher("柏拉图", 2, 3);
Philosopher t3 = new Philosopher("苏格拉底", 3, 4);
Philosopher t4 = new Philosopher("但丁", 4, 5);
Philosopher t5 = new Philosopher("拿破仑", 5, 1);
ArrayList philosophers = new ArrayList<>();
philosophers.add(t1);
philosophers.add(t2);
philosophers.add(t3);
philosophers.add(t4);
philosophers.add(t5);
for (Philosopher philosopher : philosophers) {
new Thread(() -> {
philosopher.eat();
},philosopher.getName()).start();
}
}
}
/**
* 哲学家类
*/
@Slf4j
class Philosopher{
//姓名
private String name;
//肯定是需要一双筷子的
private Chopsticks chopsticks;
public String getName() {
return name;
}
Philosopher(String name, Integer left, Integer right){
this.name = name;
chopsticks = new Chopsticks();
chopsticks.setLeft(left);
chopsticks.setRight(right);
}
//吃饭操作
public void eat(){
synchronized (chopsticks.getLeft()){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(this.name+"拿起了左手的筷子");
synchronized(chopsticks.getRight()){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(this.name+"拿起了右手的筷子");
}
}
}
}
/**
* 筷子类
*/
class Chopsticks{
private Integer left;
private Integer right;
public Integer getLeft() {
return left;
}
public void setLeft(Integer left) {
this.left = left;
}
public Integer getRight() {
return right;
}
public void setRight(Integer right) {
this.right = right;
}
}
老师的做法和我大同小异,差距比较大的地,也是筷子我定义成一双,老师定义的一根,不重要。
通过jps配合jstack看到了死锁现象。这里的问题,就是线程各自持有各自的资源无法释放。
那么,这个问题,解决,只需要它们有序执行就可以了。在创建对象时,控制下锁的获取顺序。
这种方案当然可以,但这样性能比较低,因为假如t1-t4一块拿起筷子,得等t4吃完后,t3才能吃,接下来就是t2,t1。最终是t5,是按顺序执行的。
在这里共耗费了6秒。
解决方法,改造一下eat()方法。
//吃饭操作
public void eat(){
synchronized (chopsticks.getLeft()){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(chopsticks.getLeft()%2 == 0){
//左手边筷子是偶数号的,先放下,等其他人吃完
try {
chopsticks.getLeft().wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug(this.name+"拿起了左手的筷子");
synchronized(chopsticks.getRight()){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(this.name+"拿起了右手的筷子");
//吃完后唤醒(先吃完的这批,左手号筷子是偶数的,它们右手的筷子是奇数,唤醒也该唤醒左手的筷子)
chopsticks.getRight().notifyAll();
}
}
}
这里耗费了3秒。没有什么问题。性能确实有所提升。但再换个场景,类似问题我能快速解决吗,也不确定啊?还是不够强。
后续会用ReetrantLock中的tryLock()方法再试试。
多个线程之间,在互相改变对方的执行条件,导致谁都没有办法结束。这里的话用双重校验锁保证了下线程安全。
package com.bo.threadstudy.four;
import lombok.extern.slf4j.Slf4j;
/**
* 活锁测试样例
*/
@Slf4j
public class AliveLockTest {
private static Integer count = 1000;
private static Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
while(count > 0){
synchronized (lock){
//双重校验,保证线程安全
if(count>0){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//synchronized应该是保证结果写到主存里了
count--;
log.debug("t1线程count递减值"+count);
}
}
}
},"t1").start();
new Thread(() -> {
while(count < 2000){
synchronized (lock){
if(count<2000){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
log.debug("t2线程count递加值"+count);
}
}
}
},"t2").start();
}
}
线程饥饿,很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题。
虽然老师也讲了样例,就是多个线程执行,有的线程频繁获取锁资源,有的线程就是获取不到锁资源。这个的话,后期ReadWriteLock会讲到,先过吧。也明白是什么情况。
不行,这些东西,我得找点业务场景练练。得实操。