并发编程之六:ReentrantLock线程活跃性问题(死锁、活锁、饥饿),线程执行顺序

线程的活跃性

活跃性:线程的代码是有限的,但是由于某些原因线程的代码一直执行不完。如死锁。
活跃性包括3种现象:死锁、活锁、饥饿。
解决方案:
活锁:线程运行时间交错开(两个线程都睡眠随机的时间,达到一个线程运行完毕,另一个线程再运行的目的)
死锁,饥饿:ReentrantLock

多把锁(细粒度的锁)

我们前几篇博客都是使用一把锁,这样会有一些并发度上的问题。
多把不相干的锁
栗子:-间大屋子有两个功能:睡觉、学习,互不相干。
现在小南要学习,小女要睡觉,但如果只用一间屋子(- 个对象锁)的话,那么并发度很低就变成了串行的,但是小南学习与小女睡觉是完全不影响的,串行显然不是太好。
解决方法是准备多个房间(多个对象锁)。一个学习的房间,一个睡觉的房间,不同的锁保护不同的操作,这样能够增强并发度。
代码如下:因为他们是给不同的对象上的锁,所以他们之间的操作是互不干扰的,几乎是同时运行的。
注意:要做多把锁,的保证多个锁之间是没有业务关联的。
在这里插入图片描述
注意:要做多把锁,的保证多个锁之间是没有业务关联的。
将锁的粒度细分:

  • 好处,是可以增强并发度
  • 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁

死锁(概念及排查工具)

死锁:两个线程都在等待对方执行完毕,才能再往下执行。
有这样的情况: 一个线程需要同时获取多把锁,这时就容易发生死锁。
t1线程获得A对象锁,接下来想获取B对象的锁。
t2线程获得B对象锁,接下来想获取A对象的锁。
如下代码:t1线程一上来就获得了A对象锁,t2一上来就获得了B对象的锁,然后在t1线程里无法获取B对象锁,因为B对象锁已经被线程t2所占用,而t2想要运行结束,的获取A锁,但是A被t1所占用于是双方都无法再继续执行。它们各自持有一把锁,但是想要获取对方的锁的时候就发生了死锁。
在这里插入图片描述

定位死锁
1、jstack(基于命令行)
2、jconsole(基于图形界面)
检测死锁可以使用jconsole工具, 或者使用jps定位进程id, 再用jstack定位死锁

点击idea的terminal窗口,
第一步:输入jps,第一列为线程id,第二列为线程所在的java类名称。
第二步:输入jstack 线程id
在这里插入图片描述
待补充…

哲学家就餐问题(导致死锁的著名问题)

哲学家就餐问题可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。哲学家就餐问题有时也用米饭和筷子而不是意大利面和餐叉来描述,因为很明显,吃米饭必须用两根筷子。
ps:哲学家们要是知道他们和别人共用筷子,会被恶心死吗?
如下代码:会发生死锁问题

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author: llb
 * @Date: 2021/7/13 16:53
 */
public class ReentrantLockTest4 {
   
   

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
   
   
        ChopsTick c1 = new ChopsTick("1");
        ChopsTick c2 = new ChopsTick("2");
        ChopsTick c3 = new ChopsTick("3");
        ChopsTick c4 = new ChopsTick("4");
        ChopsTick c5 = new ChopsTick("5");
        new Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c5, c1).start();
    }
}


class Philosopher extends Thread {
   
   
    ChopsTick left;
    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) {
   
   
                    eat();
                }
            }
        }
    }

    private void eat() {
   
   
        log.log("eating...");
        try {
   
   
            Thread.sleep(1000);
        } catch (InterruptedException e) {
   
   
            e.printStackTrace();
        }
    }
}


class ChopsTick {
   
   
    public ChopsTick(String name) {
   
   
        this.name = name;
    }

    String name;

    @Override
    public String toString() {
   
   
        return "筷子{" + name + "}";
    }
}




活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如
在这里插入图片描述

活锁与死锁的区别:
死锁,两个线程互相持有对方想要的锁,导致两个线程都无法继续向下运行,两个线程都阻塞住了。
活锁:两个线程没有阻塞,它们都不断的使用cpu不断的运行,互相改变了对方的结束条件导致对方结束不了。
解决活锁的方法,让两个线程执行的时间交错,或者将睡眠时间改为随机数,达到把他们的执行时间交错开,第一个线程执行完了,第二个线程开始执行。

饥饿

接下来我们来看线程活跃性中的饥饿问题。
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到CPU调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题
下面我讲一下我遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题。
在这里插入图片描述

解决方案: 顺序加锁的解决方案

ReentrantLock(解决死锁、活锁)

知己知彼,百战不殆,我们看下从它的单词意思学期。
entrant中文意思是重入,en表示:可。Lock:锁。
ReentrantLock:属于juc并发包下的一个重要类。

synchronized与ReentrantLock的区别

区别:与synchronized相比的不同点
相对于synchronized它具备如下特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量

解释:
可中断:synchronized里:比如a线程拥有锁,b线程在等在锁,但是在a不释放所资源的前提下,没有方法让b线程不等待。synchronized不可以被中断,并不指synchronized方法不可中断,而是指,synchronized的等待不可以被中断。但是ReentrantLock可以。
可以设置超时时间:synchronized,如果一个线程获取一个锁,其他的没有获取锁的线程就一直等待下去了,直到获取到锁位置。但是ReentrantLock可以设置超时时间,到了一定时间我争取不到锁,我就去执行其他的逻辑,不能在一颗树上吊死啊。
可以设置为公平锁:所谓公平锁就是先进先出,防止线程饥饿的情况,比如大家排队,公平锁是按排队一个个来,而不是随机来,如果线程过多,随机来,有些线程可能一直得不到运行。
支持多个条件变量:这里的条件变量就是,相当于synchronized里有一个waitset(当条件不满足时线程等待的一个地方),当条件不满足时,线程就在waitset里等待。而ReentrantLock是指,你不满足条件1的时候可以在一个地方等,不满足条件2的时候可以在另一个地方等,不满足条件3的时候…而synchronized相当于不管你不满足啥条件你都只能在一个地方等。当notifyAll叫醒线程的时候,它就叫醒一屋子的线程。不像ReentrantLock可以细分,可以指定叫醒哪些线程。

可重入:(ReentrantLock与synchronized的相同点)

可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。

基本语法
先获取一个reentrantLock对象。从这里就可以看出它它和synchronized不一样,synchronized是在关键字的级别去保护临界区,而reentrantLock是在对象的级别去保护临界区。

1、先获取一个reentrantLock对象
2、调用它的lock方法进行一个锁的获取。
3、将临界区写入到一个try-finally块里,然后在finally里不管有没有异常都释放掉锁。
注意:
1、一定要保证lock与unlock是成对出现的。其次要在finally里去释放锁。
至于加锁的lock方法放在try外面还是里面效果都是一样的,按自己喜欢来。
2、reentrantLock.lock();就取代了之前的普通对象+monitor,如果线程没有得到锁,就会进入reentrantLock的头里去等待。
3、以前我们把synchronized的对象当成锁,但是真正的锁是monitor所关联的对象,但是现在呢,我们创建出来的这个ReentrantLock对象它就是一把锁。

        private static ReentrantLock lock = new ReentrantLock();

在这里插入图片描述

如下代码
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。所以正确结果就是,main线程可以在main方法里lock进入m1,当进入m1时执行lock.lock();方法时如果能成功执行,进入try,就说明可重入,m1调用m2也是同理。

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author: llb
 * @Date: 2021/7/13 14:54
 */
public class ReentrantLockTest1 {
   
   

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
   
   
        lock.lock();
        try {
   
   
            System.out.println("method main");
            m1();
        } finally {
   
   
            lock.unlock();
        }
    }

    public static void m1() {
   
   
        lock.lock();
        try {
   
   
            System.out.println("method m1");
            m2();
        } finally {
   
   
            lock.unlock();
        }
    }

    public static void m2() {
   
   
        lock.lock();
        try {
   
   
            System.out.println("method m2");
        } finally {
   
   
            lock.unlock();
        }
    }

}

打印结果
在这里插入图片描述

可打断(被动解决死锁)

线程在等待锁的过程中,其它线程可以用interrupt去终止该线程的等待。
这个我们就不能用刚才的lock.lock了,因为它是不可被打断的锁。
这里我们使用lock.lockInterruptibly();
下面代码,我们正常调用,因为没有其它线程和它竞争锁资源,所以它不会被打断,正常执行同步带代码块里的内容,打印日志。

场景1:正常执行

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author: llb
 * @Date: 2021/7/13 14:54
 */
public class ReentrantLockTest2 {
   
   

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
   
   
        Thread t1 = new Thread(() ->{
   
   
            try {
   
   
                // 如果没有竞争,那么此方法就会获取lock对象锁
                // 如果有竞争就进入阻塞队列,可以被其它线程用 interrupt 方法打断,不在等待锁
                System.out.println("尝试获得锁");
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
   
   
                // 如果被打断,则进入到catch里
                // 正在等待中的线程,在其他线程调用interrupt方法时会被打断,
                // 并且抛出打断异常,且将打断标志设置为false
                e.printStackTrace();
                System.out.println("没有获得锁");
                return;
            }
            try{
   
   
                System.out.println("获取到锁");
            }finally {
   
   
                lock.unlock();
            }
        }, "t1");
        t1.start();
    }
}

打印结果
在这里插入图片描述

场景2:被阻塞

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author: llb
 * @Date: 2021/7/13 14:54
 */
public class ReentrantLockTest2 {
   
   

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
   
   
        Thread t1 = new Thread(() ->{
   
   
            try {
   
   
                // 如果没有竞争,那么此方法就会获取lock对象锁
                // 如果有竞争就进入阻塞队列,可以被其它线程用 interrupt 方法打断,不在等待锁
                System.out.println("尝试获得锁");
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
   
   
                // 如果被打断,则进入到catch里
                // 正在等待中的线程,在其他线程调用interrupt方法时会被打断,
                // 并且抛出打断异常,且将打断标志设置为false
                e.printStackTrace();
                System.out.println("没有获得锁");
                return;
            }
            try{
   
   
                System.out.println("获取到锁");
            }finally {
   
   
                lock.unlock();
            }
        }, "t1");

        // 这里写lock.lock();是哪个线程获取的锁呢?
        // 代码是在main方法里所以是主线程获取了该锁,
        // 然后t1之后才启动,于是t1就被阻塞住了
        lock.lock();
        t1.start();
    }
}

打印结果:
在这里插入图片描述
场景3:打断操作,意义:防止线程无限的等待下去,这也是避免死锁的一种方法。如果不可被打断,线程一直等待,就有可能产生死锁。

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author: llb
 * @Date: 2021/7/13 14:54
 */
public class ReentrantLockTest2 {
   
   

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
   
   
        Thread t1 = new Thread(() ->{
   
   
            try {
   
   
                // 如果没有竞争,那么此方法就会获取lock对象锁
                // 如果有竞争就进入阻塞队列,可以被其它线程用 interrupt 方法打断,不在等待锁
                System.out.println("尝试获得锁");
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
   
   
                // 如果被打断,则进入到catch里
                // 正在等待中的线程,在其他线程调用interrupt方法时会被打断,
                // 并且抛出打断异常,且将打断标志设置为false
                e.printStackTrace();
                System.out.println("没有获得锁");
                return;
            }
            try{
   
   </
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值