【无标题】线程学习(18)-多把锁下的线程问题,死锁,活锁,饥饿

多把锁的应用

减小锁粒度,提交并发度。

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排查。

【无标题】线程学习(18)-多把锁下的线程问题,死锁,活锁,饥饿_第1张图片

【无标题】线程学习(18)-多把锁下的线程问题,死锁,活锁,饥饿_第2张图片

【无标题】线程学习(18)-多把锁下的线程问题,死锁,活锁,饥饿_第3张图片

这种比较损毁性能,相对而言,正常生产环境采用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看到了死锁现象。这里的问题,就是线程各自持有各自的资源无法释放。

 【无标题】线程学习(18)-多把锁下的线程问题,死锁,活锁,饥饿_第4张图片

那么,这个问题,解决,只需要它们有序执行就可以了。在创建对象时,控制下锁的获取顺序。

【无标题】线程学习(18)-多把锁下的线程问题,死锁,活锁,饥饿_第5张图片

这种方案当然可以,但这样性能比较低,因为假如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会讲到,先过吧。也明白是什么情况。

不行,这些东西,我得找点业务场景练练。得实操。

你可能感兴趣的:(线程,java,开发语言)