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

Java并发--ReentrantReadWriteLock饥饿问题 前言 通过阅读ReentrantReadWriteLock的源码我们会发现ReentrantReadWriteLock内部通过AbstractQueuedSynchronizer来实现同步语义,在ReentrantReadWriteLock中拥有读锁与写锁。读锁是一种共享锁,允许多个线程同时获取锁,写锁是一种独占锁,同一时刻只允许一个线程获取。如果在读多写少的情况下,利用ReentrantReadWriteLock很容易造成饥饿问题。 饥饿问题:ReentrantReadWriteLock实现了读写分离 阅读详情

线程的活跃性

活跃性:线程的代码是有限的,但是由于某些原因线程的代码一直执行不完。如死锁。
活跃性包括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{
                System.out.println("获取到锁");
            }finally {
                lock.unlock();
            }
        }, "t1");

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

        // 让主线程睡1s,然后打断t1
        // 打断线程的方法,在其它线程里,运行该线程的interrupt方法
        Thread.sleep(1000);
        t1.interrupt();
    }
}

打印结果:代码里我们可以看到,主线程并没有释放锁的代码,也就是说t1是不可能获取锁的,然后我们在主线程里把t1打断,让t1退出等待锁。防止线程无限的等待下去,这也是避免死锁的一种方法。如果不可被打断,就有可能产生死锁。
在这里插入图片描述

如果这里用的是lock而不是lockInterruptibly,线程会一直等待下去,不会被打断
如下代码:等待中的线程没有抛出异常,说明没有被打断,意味着,lock()的锁是不可被打断的。

import java.util.concurrent.locks.ReentrantLock;

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

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            // 如果没有竞争,那么此方法就会获取lock对象锁
            // 如果有竞争就进入阻塞队列,可以被其它线程用 interrupt 方法打断,不在等待锁
            System.out.println("尝试获得锁");
            lock.lock();
            try {
                System.out.println("获取到锁");
            } finally {
                lock.unlock();
            }
        }, "t1");

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

        // 让主线程睡1s,然后打断t1
        // 打断线程的方法,在其它线程里,运行该线程的interrupt方法
        Thread.sleep(1000);
        t1.interrupt();
    }
}

打印结果:一直等待下去
在这里插入图片描述

可超时(主动解决死锁)

ReentrantLock的可打断是为了避免线程死等,但是可打断毕竟是一种被动的避免死等,由其它线程调用该线程的interrupt不让该线程死等,而锁超时,是主动的去避免死等。
可超时:如果其它线程一直持有锁不释放,我也不会一直死等,等待一段时间,如果这段时间过了,对方仍旧没有释放锁,那么我就放弃等待,表示这次获取锁失败了。可以避免线程无限制的等待下去。

lock.tryLock()方法:

  1. 无参:不超时等待。获取不到锁立即退出等待。
  2. 参数1:时间,参数2:时间单位。时间返回内获取不到锁退出等待。会提抛出InterruptedException异常。
  3. 支持被打断

以上两个方法都是避免死锁的解决方法

如下代码:

场景1:没有其它线程竞争锁

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author: llb
 * @Date: 2021/7/13 16:06
 */
public class ReentrantLockTest3 {

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println("尝试获取锁...");
            // tryLock方法,获取到锁返回true,反之false
            if (!lock.tryLock()) {
                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 16:06
 */
public class ReentrantLockTest3 {

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println("尝试获取锁...");
            // tryLock方法,获取到锁返回true,反之false
            if (!lock.tryLock()) {
                System.out.println("获取不到锁");
                return;
            }
            try {
                System.out.println("获取到锁");
            } finally {
                lock.unlock();
            }
        }, "t1");
        
        lock.lock();
        System.out.println("主线程获取到锁");
        t1.start();
    }
}

打印结果:
在这里插入图片描述
场景3:有时限的锁-获取不到锁


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

/**
 * @Author: llb
 * @Date: 2021/7/13 16:06
 */
public class ReentrantLockTest3 {

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.log("尝试获取锁...");
            try {
                // tryLock方法,获取到锁返回true,反之false.tryLock也是支持被打断的
                // 等待1s,如果获取到锁,返回true,等待1s,获取不到锁返回false
                if (!lock.tryLock(1, TimeUnit.SECONDS)) {
                   log.log("获取不到锁");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.log("获取不到锁");
                return;
            }
            try {
                log.log("获取到锁");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        log.log("主线程获取到锁");
        t1.start();
    }
}

以上代码,在线程获取到锁的时候会返回true,打印“获得到锁”,如果获取不到锁等待1s,1s后获取到锁,同上,如果还是获取不到则返回false,打印“获取不到锁”。但是主线程根本就没有释放锁,所以t1永远都得不到锁。

打印结果:我们可以看到22秒时等待锁,1s后等待不到,于是放弃等待,在23s时,打印“获取不到锁”
在这里插入图片描述

场景3:有时限的等待-获取到锁

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

/**
 * @Author: llb
 * @Date: 2021/7/13 16:06
 */
public class ReentrantLockTest3 {

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.log("尝试获取锁...");
            try {
                // tryLock方法,获取到锁返回true,反之false.tryLock也是支持被打断的
                // 等待1s,如果获取到锁,返回true,等待1s,获取不到锁返回false
                if (!lock.tryLock(2, TimeUnit.SECONDS)) {
                    log.log("获取不到锁");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.log("获取不到锁");
                return;
            }
            try {
                log.log("获取到锁");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        log.log("主线程获取到锁");
        t1.start();
        // 主线程睡1s,然后释放锁
        Thread.sleep(1000);
        log.log("主线程释放了锁");
        lock.unlock();
    }
}

打印结果:以上代码让主线程1s后释放锁,然后t1等待2s去获取锁资源,当然获取的到了
在这里插入图片描述

解决哲学家就餐问题(tryLock无参方法)

我们在死锁章节介绍了哲学家就餐问题的死锁现象,现在我们来解决了它。
synchronized虽然也能解决该问题,但是synchronized的等待不可以被中断,所以解决起来比较麻烦,比如让哲学家们有顺序的获取 筷子,或者在获取做左筷子之后,如果获取不到右筷子,可以等待一端时间(但是真实情况中,这个等待的时间是不好把握的),但是显然ReentrantLock的可中断等待解决起来更方便。

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) {
            // 尝试获取左手筷子
            if (left.tryLock()) {
                try {
                    // 尝试获取右手筷子
                    if (right.tryLock()) {
                        try {
                            // 两只筷子都有了,就可以吃饭了
                            eat();
                        } finally {
                            right.unlock();
                        }
                    }
                } finally {
                    // 如果哲学家得不到右手的筷子,那么他会放下左手的筷子
                    // 然后就不会像synchronized一样去死等了。
                    // 也就避免的死锁的产生
                    // 释放手里的筷子
                    left.unlock();
                }
            }

            // 尝试获得左手筷子
            synchronized (left) {
                // 尝试获得右手筷子
                synchronized (right) {
                    eat();
                }
            }
        }
    }

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


/**
 * 以上代码中我们知道,哲学家们要先获取左筷子,再获取右筷子,然后才能吃饭,所以锁是加在筷子上的
 * 我们要把筷子当成锁,但是又不想使用synchronized,那就只能,让它继承ReentrantLock
 */
class ChopsTick extends ReentrantLock {
    public ChopsTick(String name) {
        this.name = name;
    }

    String name;

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



运行结果:你看他们每个人都吃到了饭,也不管筷子上有没有别人的口水,也不管恶心不恶心…
在这里插入图片描述

公平锁(解决饥饿问题)

公平锁的本意是用来解决饥饿问题的
synchronized的monitor锁属于不公平锁:所谓不公平锁就是,当拥有锁的线程释放锁资源时,其它等待的线程就会一拥而上,谁先抢到了,谁就是锁的主人。
所谓公平锁就是先进先出,防止线程饥饿的情况,当拥有锁的线程释放锁资源时,等待中的线程先等待的先得到锁,即先来先得到锁。比如大家排队,公平锁是按排队一个个来,而不是随机来,如果线程过多,随机来,有些线程可能一直得不到运行。
ReentrantLock默认是不公平锁。

首先我们看下ReentrantLock得源码,它得构造函数里有一个boolean类型得参数
在这里插入图片描述
有参构造函数里得fair(公平)默认为false。
如果fair为真它就创建一个FairSync的对象,为假,它就创建一个NonfairSync的对象。
在这里插入图片描述
注:公平锁一般没有必要,会降低并发度,等到后面分析原理时我们会啃源码,看公平锁是按先入先得来获取锁的。

ReentrantLock中的条件变量(避免虚假唤醒)

synchronized中也有和条件变量完全等价的概念,就是我们讲原理时那个waitSet休息室,当条件不满足时进入waitSet等待
ReentrantL ock的条件变量比synchronized强大之处在于,它是支持多个条件变量的,这就好比

  1. synchronized 是那些不满足条件的线程都在1间休息室等消息(所以唤醒时也只能随机唤醒或全部唤醒,哪些不满足唤醒的线程就成了虚假唤醒,他们还得再去循环再去等待)
  2. 而ReentrantL ock支持多间休息室,有专门]等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒。

使用流程

  1. await 前需要获得锁(和synchronized一样,想要进入这个休息室,你必须的先获得锁)
  2. await执行后,会释放锁,进入conditionObject等待
  3. await的线程被唤醒调用signal/signalAll(或打断、或超时)取重新竞争lock锁
  4. 竞争lock锁成功后,从await后继续执行
  5. ReentrantLock的await其实和synchronized的wait方法是类似的

代码示例:

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

/**
 * @Author: llb
 * @Date: 2021/7/15 17:48
 */
public class ReentrantLockTest5 {
    static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        // 休息室:不同业务逻辑的线程可以放在不同的休息室里
        // 我们在唤醒它们的时候,就可以更准确的唤醒线程
        // 而不是之前的类似于Synchronized的notifyAll唤醒所有的线程
        // 或者notify随机的唤醒某一个线程
        // 创建一个新的条件变量(相当于休息室)
        Condition condition1 = lock.newCondition();
        Condition condition2 = lock.newCondition();
        Condition condition3 = lock.newCondition();

        // 加锁
        lock.lock();
        // 等待:线程进入休息室等待
        condition1.await();
        // 唤醒:
        // 其它线程想要唤醒,休息室1里的线程,就找到condition1
        // 唤醒该休息室里随机的一个线程
        condition1.signal();
        // 唤醒该休息室里所有的线程
        condition1.signalAll();

    }
}

使用条件变量的例子:
收外卖和等烟的例子,我们用ReentrantLock来解决第四章中的等外卖和等烟的例子。
背景:一些线程他们要使用一个共享的房间来达到一个线程安全的目的。所以他们都要使用加锁的方式去进入到room房间去做些线程安全的代码。
前提条件:
人物 小南:正所谓,一根烟,一杯酒,一个bug调一宿,小南就是典型的有烟才能干活。所以小南的在有烟的前提下才能去干活。
下面我们就用一段代码来模拟这个过程。

条件变量(Condition)的使用例子
import com.carrotsearch.hppc.CharScatterSet;

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

/**
 * @Author: llb
 * @Date: 2021/7/15 18:04
 */
public class ReentrantLockTest6 {
    static final Object room = new Object();
    static boolean hasCigarette = false;// 有没有烟
    static boolean hasTakeout = false; // 外卖

    static ReentrantLock ROOM = new ReentrantLock();
    // 等待烟的休息室
    static Condition waitHasCigarette = ROOM.newCondition();
    // 等待外卖的休息室
    static Condition waitTakeout = ROOM.newCondition();

    public static void main(String[] args) throws InterruptedException {

        new Thread(new Runnable() {
            @Override
            public void run() {
                ROOM.lock();
                try {
                    log.log(Thread.currentThread().getName() + ":有烟没? " + hasCigarette);
                    while (!hasCigarette) {
                        log.log(Thread.currentThread().getName() + ":没烟先歇会: " + hasCigarette);
                        // 这里不用之前的wait等待了,我们直接进入休息室等待
                        // 避免虚假唤醒,来个外卖,把两人都叫醒了,但是另一个人一看不是自己的,他有的去等待
                        waitHasCigarette.await();
                    }
                    log.log(Thread.currentThread().getName() + ":可以干活了: " + hasCigarette);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    ROOM.unlock();
                }
            }
        }, "小南").start();

        // 该线程与小南线程几乎一样,但是它等的是外卖而不是烟
        new Thread(new Runnable() {
            @Override
            public void run() {
                ROOM.lock();
                try {
                    log.log(Thread.currentThread().getName() + ":外面送到没? " + hasTakeout);
                    while (!hasTakeout) {
                        log.log(Thread.currentThread().getName() + ":没等待外卖先歇会: " + hasTakeout);
                        // 进入等待外卖的休息室:
                        // 避免虚假唤醒,来个外卖,把两人都叫醒了,但是另一个人一看不是自己的,他有的去等待
                        // 类似与synchronized里的wait但是这里,ReentrantLock分的更细,让该线程指定的到一个地方去等待
                        waitTakeout.await();
                    }
                    log.log(Thread.currentThread().getName() + ":可以干饭了: " + hasTakeout);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    ROOM.unlock();
                }
            }
        }, "小女").start();

        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (room) {
                        log.log(Thread.currentThread().getName() + ":可以干活了 " + hasCigarette);
                    }
                }
            }, "其他人").start();
        }
        Thread.sleep(1000);

        // 该送外卖的线程到底将谁叫醒了呢?
        new Thread(new Runnable() {
            @Override
            public void run() {

                ROOM.lock();

                try {
                    hasTakeout = true;
                    // 我们这里没有用signalAll方法是因为只有小女一个人在等待外卖
                    waitTakeout.signal();
                    log.log(Thread.currentThread().getName() + ":外卖到了 " + hasTakeout);
                } finally {
                    ROOM.unlock();
                }
            }
        }, "送外卖的").start();

        // 该送烟的线程到底将谁叫醒了呢?
        new Thread(new Runnable() {
            @Override
            public void run() {

                ROOM.lock();

                try {
                    hasCigarette = true;
                    // 我们这里没有用signalAll方法是因为只有小男一个人在等待烟
                    waitHasCigarette.signal();
                    log.log(Thread.currentThread().getName() + ":外卖到了 " + hasTakeout);
                } finally {
                    ROOM.unlock();
                }
            }
        }, "送外卖的").start();
    }

}

运行结果:
在这里插入图片描述

其实synchronized与ReentrantLock很相似,前者的wait对应后者的await,前者的notify/notifyAll对应后者的signal/signalAll。只不过ReentrantLock将条件更细的划分了,synchronized里所有的线程都调用wait,但是ReentrantLock更细分了,它通过Condition(条件变量)让不同的线程在不同的Condition里等待,等到唤醒的时候,也是可以指定Condition去唤醒里面一个或者所有线程,避免唤醒条件不满足的线程,造成虚假唤醒。

我们再来看一下synchronized的解决方法对比一下

public class WaitTest4 {

    static final Object room = new Object();
    static boolean hasCigarette = false;// 有没有烟
    static boolean hasTakeout = false; // 外卖
    public static void main(String[] args) throws InterruptedException {

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (room) {
                    logger.log(Thread.currentThread().getName() + ":有烟没? " + hasCigarette);
                    while (!hasCigarette) {
                        logger.log(Thread.currentThread().getName() + ":没烟先歇会: " + hasCigarette);
                        try {
                            room.wait();
                        } catch (InterruptedException e) {
                            // 该异常什么时候会抛出呢?就是其他线程调用interrupt方法
                            // 它也会让正在wait的线程被打断,打断之后,我们这边接到异常
                            // 就知道该线程被打断了,也可以对打断标记进行判断,于是可以继续进行处理。
                            // 当然了,更合理的方法是其他线程调用notify方法来唤醒
                            // 正在wait的线程
                            e.printStackTrace();
                        }
                    }
                    logger.log(Thread.currentThread().getName() + ":有烟没? " + hasCigarette);
                    if (hasCigarette) {
                        logger.log(Thread.currentThread().getName() + ":可以干活了: " + hasCigarette);
                    } else {
                        logger.log(Thread.currentThread().getName() + ":没干成活: " + hasCigarette);
                    }
                }
            }
        }, "小南").start();

        // 该线程与小南线程几乎一样,但是它等的是外卖而不是烟
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (room) {
                    logger.log(Thread.currentThread().getName() + ":外面送到没? " + hasTakeout);
                    while (!hasTakeout) {
                        logger.log(Thread.currentThread().getName() + ":没等待外卖先歇会: " + hasTakeout);
                        try {
                            room.wait();
                        } catch (InterruptedException e) {
                            // 该异常什么时候会抛出呢?就是其他线程调用interrupt方法
                            // 它也会让正在wait的线程被打断,打断之后,我们这边接到异常
                            // 就知道该线程被打断了,也可以对打断标记进行判断,于是可以继续进行处理。
                            // 当然了,更合理的方法是其他线程调用notify方法来唤醒
                            // 正在wait的线程
                            e.printStackTrace();
                        }
                    }
                    logger.log(Thread.currentThread().getName() + ":外面送到没? " + hasTakeout);
                    if (hasTakeout) {
                        logger.log(Thread.currentThread().getName() + ":可以干饭了: " + hasTakeout);
                    } else {
                        logger.log(Thread.currentThread().getName() + ":没干成饭: " + hasTakeout);
                    }
                }
            }
        }, "小女").start();

        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (room) {
                        logger.log(Thread.currentThread().getName() +":可以干活了 " + hasCigarette);
                    }
                }
            },"其他人").start();
        }
        Thread.sleep(1000);

        // 该送外卖的线程到底将谁叫醒了呢?
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (room) {
                    hasTakeout = true;
                    logger.log(Thread.currentThread().getName() +":外卖到了 " + hasTakeout);
                    // 这里要要注意,
                    // notify/notifyAll只能唤醒wait状态下的线程
                    // 对blocked状态下的线程毫无作用。
                    // 所以不管是wait还是notify/notifyAll,
                    // 他们都有一个前提就是线程都必须获得了对象锁。成为了owner之后才能调用这3个方法
                    // 所以这里要在synchronized代码块里调用notify,否则将抛出非法状态异常
                    //room.notify();
                    room.notifyAll();
                }
            }
        },"送外卖的").start();
    }
}

同步模式之顺序控制

至上面的一小节,我们已经学会了synchronized的wait,notify/notifyAll以及ReentrantLock的lock,unlock,await,single,singleAll,已经学会了线程之间的一些基本的同步控制了。经常会有一些面试题目,以及现实开发中遇到的一些问题。就是,在多个线程之间对它们的执行顺序进行一些协调,我们我们要学会下面这种模式,就是控制线程的运行次序。

场景:现在有两个线程一个线程打印1,另一个线程打印2,现在要求必须先打印2在打印1
下面我们就以不同的方式去实现它。

固定运行顺序1:wait&notify版
/**
 * @author diao 2021/7/15 22:42
 */
public class Test2 {

    static final Object lock = new Object();
    // 表示t2是否运行过
    static boolean t2runned = false;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            // 为什么写在while里,这是我们前面学到的
            // synchronized的wait的正确的使用姿势
            synchronized (lock) {
                // 当条件满足的时候,就跳出while打印结果
                while (!t2runned) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.log("1");
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                log.log("2");
                // 更改标记位
                t2runned = true;
                // 唤醒t1
                lock.notifyAll();
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

代码分析:

  • 情况1

:是t1先运行,那么t2runned初始值为false,不满足条件,所以进入到while里执行wait释放锁资源,然后t1、t2竞争锁资源。如果是t1竞争到了,重复上述步骤,当t2竞争到所资源之后,打印结果,修改标记位为true,然后释放所资源,唤醒t1,t1不满足while条件,不会再wait,直接打印结果。

  • 情况2

:t2先运行打印结果,修改标记位为true,然后释放所资源,唤醒t1(此时是个空唤醒,因为t1没有得到过锁,也就不会执行wait:注wait方法会释放线程拥有的所资源,所以调用wait的线程,一定是得到过锁资源的线程),t1不满足while条件,不会再wait,直接打印结果。
打印结果:总是先2再1
在这里插入图片描述

固定运行顺序2:await&signal&condition版

代码分析和上面简直一模一样,这里不在比比

import java.awt.*;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author diao 2021/7/15 22:42
 */
public class Test3 {

    static final ReentrantLock lock = new ReentrantLock();
    static Condition condition1 = lock.newCondition();
    // 表示t2是否运行过
    static boolean t2runned = false;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                if(!t2runned) {
                    condition1.await();
                }
                log.log("1");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }

        }, "t1");

        Thread t2 = new Thread(() -> {
           lock.lock();
           try {
               log.log("2");
               t2runned = true;
               // 这里就一个线程
               condition1.signalAll();
           }finally {
               lock.unlock();
           }
        }, "t2");
        t1.start();
        t2.start();
    }
}

打印结果:总是先2再1
在这里插入图片描述

固定运行顺序3:park&unpark
import java.util.concurrent.locks.LockSupport;

/**
 * @author diao 2021/7/15 23:10
 */
public class Test4 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            // 暂停当前线程,park之后线程对应的是wait状态
            LockSupport.park();
            log.log("1");
        }, "t1");

        Thread t2 = new Thread(() -> {
            log.log("2");
            // 恢复某个线程的运行
            LockSupport.unpark(t1);
        }, "t2");
        t1.start();
        t2.start();
    }
}

  • 情况1

先运行线程1,线程1得到资源了,但是park,会是它暂停当前线程直到其它线程调用了unpark方法后它才会去执行。然后线程2就执行了,先打印2,然后在唤醒线程1,于是线程1会接着park之后的代码运行,于是打印1.

  • 情况2
    首先要明白unpark可以在park执行之前或者执行之后调用。不懂的请看并发编程之五中有关LockSupport的park&unpark部分。

原理之park & unpark
每个线程都有自己的-一个Parker对象(java层面是看不到得),由三部分组成_ counter, cond 和 _mutex 打个比喻

线程就像一一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。counter 就好比背包中的备用干粮(0
为耗尽,1为充足)
调用park就是要看需不需要停下来歇息
1、如果备用干粮耗尽,那么钻进帐篷歇息
2、如果备用干粮充足,那么不需停留,继续前进
调用unpark,就好比令干粮充足
1、如果这时线程还在帐篷,就唤醒让他继续前进
2、如果这时线程还在运行,那么下次他调用park时,仅是消耗掉备用干粮,不需停留继续前进
2.1、因为背包空间有限,多次调用unpark仅会补充一份备用干粮

情况2:先执行线程2:此时线程2 在运行,打印2,然后调用unpark,然后线程1执行,当其调用park时,因为线程2已经调用过unpark了,所以线程1不会暂停,而是直接往下运行。简单的理解为,当有线程先调用unpark再调用park时,线程不会暂停而是继续执行。多次调用unpark,也只能保证紧跟着的下一次park不会暂停。

交替输出1:wait&notify

场景:线程1输出a 5次,线程2输出b 5次,线程3输出c 5次。现在要求输出abcabcabcabcabc怎么实现?

代码如下:

/**
 * @Author: llb
 * @Date: 2021/7/16 23:12
 */
public class Test3 {

    public static void main(String[] args) {
        // 多个线程用同一把锁才能起到同步的作用
        // 这里表示,初始打印flag为1的,一共打印5次
        WaitNotify waitNotify = new WaitNotify(1, 5);
        new Thread(()->{
            waitNotify.print("a", 1 , 2);
        }, "t1").start();
        new Thread(()->{
            waitNotify.print("b", 2 , 3);
        }, "t2").start();
        new Thread(()->{
            waitNotify.print("c", 3 , 1);
        }, "t3").start();
    }

}


/**
 * 输出内容     等待标记      下一个标记
 *    a           1             2
 *    b           2             3
 *    c           3             1
 */
class WaitNotify{
    // 打印
    public void print(String str, int waitFlag, int nextFlag) {
        for (int i = 0; i < loopNumber; i++) {
            // 因为要同步,所以要加锁
            synchronized (this) {
                // 为什么加while见wait/notify的正确使用姿势
                // 判断传进来的waitFlag是不是应该运行的flag
                while (waitFlag != flag) {
                    try {
                        // 如果得到锁资源的不是当前该运行的线程
                        // 那么就进行wait释放锁资源,让其他线程去竞争
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 打印内容
                System.out.print(str);
                // 将标志位设置为下一个要运行的标志
                flag = nextFlag;
                // 唤醒所有等待的线程
                this.notifyAll();
            }
        }
    }

    // 等待标记
    private int flag;
    // 循环次数
    private int loopNumber;

    public WaitNotify(int flag, int loopNumber) {
        this.flag = flag;
        this.loopNumber = loopNumber;
    }

}

打印结果:按照顺序abc共5次。
使用整数标记来控制该线程是等待还是继续向下运行,并且用了下一个标记,来控制下一个等待标记下一个该执行的线程。
在这里插入图片描述

交替输出2:ReentrantLock&Condition&await&signal

一下代码没啥好说的,有个小问题,为啥在主线程里不能直接调用a.signal呢?调用会报错。
在这里插入图片描述

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

/**
 * @author diao 2021/7/18 22:19
 */
public class Test5 {

    public static void main(String[] args) throws InterruptedException {

        AwaitSignal awaitSignal = new AwaitSignal(5);
        Condition a = awaitSignal.newCondition();
        Condition b = awaitSignal.newCondition();
        Condition c = awaitSignal.newCondition();
        new Thread(() -> {
            awaitSignal.print("a", a, b);
        }).start();
        new Thread(() -> {
            awaitSignal.print("b", b, c);
        }).start();
        new Thread(() -> {
            awaitSignal.print("c", c, a);
        }).start();

        // 上面的3个线程进入print方法后全部都await了
        // ReentrantLock会释放锁资源
        Thread.sleep(1000);
        // 为了确保上面3个线程全部陷入等待,我们让主线程睡眠1s
        // 然后我们唤醒a休息室中的线程
        // 小问题:既然是唤醒休息室中的线程,为啥要加锁呢,直接signal不就行了吗?
        // 原理,进入singl方法,查看源码:getExclusiveOwnerThread()方法返回的是当前持有锁的线程,
        // Thread.currentThread()取得的是当前的线程,所以当持有锁的线程不是当前线程时,
        // isHeldExclusively()方法就会返回false,继而抛出IllegalMonitorStateException异常。
        // 结论 调用signal()方法的线程一定要持有锁,否则会抛出IllegalMonitorStateException异常。
        // 这里调用signal的代码写在了main方法里,所以是主线程调用的signal,所以主线程需要先获取锁
        awaitSignal.lock();
        try {
            a.signal();
        } finally {
            awaitSignal.unlock();
        }
    }
}

class AwaitSignal extends ReentrantLock {
    private int loopNumber;

    public AwaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }

    public void print(String str, Condition current, Condition next) {
        for (int i = 0; i < loopNumber; i++) {
            lock();
            try {
                current.await();
                System.out.print(str);
                next.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                unlock();
            }
        }
    }
}

在这里插入图片描述

交替输出3:park&unpark
park与unpark。它没有对象锁的概念也没有ReentrantLock锁的概念。它停止和恢复线程的运行都是以线程自身为单位的。

import java.util.concurrent.locks.LockSupport;

/**
 * @author diao 2021/7/19 23:13
 */
public class Test6 {

    static Thread t1;
    static Thread t2;
    static Thread t3;

    public static void main(String[] args) {
        ParkUnpark pu = new ParkUnpark(5);
        t1 = new Thread(() -> {
            pu.print("a", t2);
        });
        t2 = new Thread(() -> {
            pu.print("b", t3);
        });
        t3 = new Thread(() -> {
            pu.print("c", t1);
        });
        // 他们刚开始运行的时候都是阻塞的
        t1.start();
        t2.start();
        t3.start();
        // 唤醒t1
        LockSupport.unpark(t1);
    }
}


class ParkUnpark {
    public void print(String str, Thread next) {
        for (int i = 0; i < loopNumber; i++) {
            // 让当前线程阻塞
            LockSupport.park();
            // 当它被唤醒的时候去打印内容
            System.out.print(str);
            // 唤醒下一个该执行的线程
            LockSupport.unpark(next);
        }

    }

    private int loopNumber;

    public ParkUnpark(int loopNumber) {
        this.loopNumber = loopNumber;
    }
}

在这里插入图片描述

本篇小结

  • 1、掌握死锁、活锁、饥饿问题产生的原因,及其解决方法

  • 2、synchronized与ReentrantLock的区别与相同点

  • ReentrantLock与synchronized区别:

     1、可被打断					lock.lockInterruptibly();//被动解决死锁
     2、可以设置超时间        lock.tryLock();//主动解决死锁
     3、可以设置公平锁        new ReentrantLock(true);//解决饥饿问题
     4、支持多个条件变量    lock.newCondition();//避免虚假唤醒
    

本章小结

算是对第四、五、六章的一个小结

并发编程之四:并发之共享问题、线程安全、synchronized关键字

并发编程之五:synchronized底层原理、monitor、轻量级锁、偏向锁、wati/notify/notifyAll、join、状态转换

并发编程之六:RenentrantLock线程活跃性问题(死锁、活锁、饥饿),线程执行顺序
在这里插入图片描述
在这里插入图片描述
本章小结
本章我们需要重点掌握的是

  • 分析多线程访问共享资源时,哪些代码片段属于临界区

  • 使用synchronized互斥解决临界区的线程安全问题

    1、掌握synchronized锁对象语法
    2、掌握synchronzied 加载成员方法和静态方法语法
    3、掌握wait/notify 同步方法
    4、互斥与同步虽然都是synchronized但是解决的问题不一样
     互斥:解决临界区的代码由于线程上下文切换导致的指令交错的问题。
     同步:某一个条件不满足时,想让线程等待
    
  • 使用lock(ReentrantLock)互斥解决临界区的线程安全问题

    掌握lock的使用细节:可打断、锁超时(try-lock)、公平锁(ReentrantLock默认非公平)、条件变量
    
  • 学会分析变量的线程安全性、掌握常见线程安全类的使用(所谓线程安全性,他们的内部方法添加了synchronized方法内部代码就原子的,但是这么多个同步的方法放在一起缺不是原子的)

  • 了解线程活跃性问题:死锁、活锁、饥饿

  • 应用方面

    互斥:使用synchronized或Lock达到共享资源互斥效果(保证临界区内的代码是原子效果,不会受
    到线程上下文切换的干扰)。
    同步:使用wait/notify或Lock的条件变量来达到线程间通信效果。
    
  • 原理方面

    1、monitor(锁,它是jvm层面实现的,c++实现,ReentrantLock便是java级别实现的)、synchroized原理、 wait/notifv原理
    2、synchronized 进阶原理(重量级锁、轻量级锁、偏向锁、锁膨胀等)
    3、park & unpark原理
    
  • 模式方面

    1、同步模式之保护性暂停:两个线程之间传递结果,比如线程1需要获得线程2的结果(一一对应)
    2、异步模式之生产者消费者:结果产生者和消费者不是一一对应
    3、同步模式之顺序控制:控制线程的执行顺序,让他们顺序执行或者交替执行
    
Java并发编程(五) : 死锁、哲学家就餐问题、ReentrantLock (锁重入、锁超时、可中断、公平锁、Condition 条件变量)、同步模式之顺序控制 4.9 活跃性 活跃性相关的一系列问题都可以用ReentrantLock进行解决。 4.9.1 死锁 有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁t1 线程获得A对象锁,接下来想获取B对象的锁;t2 线程获得B对象锁,接下来想获取A对象的锁例。Test28.java 4.9.2 检测死锁 检测死锁可以使用 jconsole工具;或者使用 jps 定位进程 id,再用 jstack 定位死锁:Test28.java 下面使用jstack工具进行演示 D:\我的项目\JavaLearing\ja 阅读详情

相关推荐

R语言WGCNA实战:从基因表达矩阵到模块可视化全流程(附避坑指南)

本文详细介绍了使用R语言进行WGCNA(加权基因共表达网络分析)的完整实战流程,涵盖从基因表达矩阵预处理、软阈值选择、网络构建与模块识别,到模块与表型关联分析及结果可视化的关键步骤。文章重点解析了代码实现,并提供了针对数据质量检查、参数调整等常见问题的避坑指南,帮助生物信息学研究者高效挖掘基因共表达模块的生物学意义。

weixin_29062823的博客 578

死锁和活锁和饥饿和ReentrantLock

死锁和活锁和饥饿和ReentrantLock

qq_53318060的博客 169

智能枕头

睡觉和吃饭是一样重要的,一个人想要健康的体魄,不仅要饮食得当,更需要有一个好的睡眠质量。随着人们生活品质的提高,人们对于睡眠的要求越来越高,那么就非常需要一款舒适的枕头。为了适应市场的需求,我爱物联网推出了智能枕头。因为智能枕头可以给头部及颈椎一个良好的承托力,舒适、柔软、放松,让人更好的进入睡眠。 目录 一、智能枕头方案介绍 二、智能枕头的优点 三、智能枕头功能介绍 四、智能枕头APP开发 五、...

6108我爱物联网方案开发优快云博客 3659

避免线程饥饿与死锁:tryLock超时设置的4个黄金法则

掌握Java Lock的tryLock超时设置,有效避免线程饥饿与死锁。适用于高并发场景,通过合理设定超时时间、配合重试机制,提升系统响应性与稳定性。四大实用法则助你优化锁策略,值得收藏。

CompiShoal的博客 1008

当MATLAB遇见ROS2:一场跨平台实时仿真的技术融合与创新实践

本文探讨了MATLAB/Simulink与ROS2在跨平台实时仿真中的技术融合与创新实践。重点介绍了在Windows+WSL2异构环境下构建高可靠性联合仿真系统的架构设计、PX4仿真环境与DDS中间件的集成方法,以及MATLAB R2024b对ROS2深度集成的实战技巧,为复杂机器人系统的开发提供了高效解决方案。

rum55的博客 244

UE5新手必看:3种UI定位方法实战(含蓝图配置截图)

本文为UE5新手详细解析了三种核心UI定位方法:屏幕空间、场景空间和世界空间。通过实战蓝图配置与控件操作,指导开发者如何根据HUD、角色血条、3D物体标记等不同应用场景,正确选择并配置UI的定位方式,以实现清晰的信息传递与沉浸式体验。

weixin_29018815的博客 473

ReentrantLock显著缺点?

存在一些缺点,但它仍然是一种非常有用的同步工具,特别是在需要更多控制和灵活性的情况下。在使用时,需要仔细考虑其使用场景和性能影响,并确保正确使用和释放锁以避免出现问题。是 Java 中用于实现可重入锁的类,虽然它提供了比内置的。

Gemini1995的博客 757

OCR文字识别领域经典论文总结

本篇将介绍文字识别经典论文,内容包括文字检测、文字识别、端到端识别等方法,具体将分别对论文算法简介、思路、代码等几个部分展开介绍。目前各部分只举例经典论文,算法没有全部写完,后续会将在此部分基础上更新更多论文综述及代码实战部分。.................................

专注大数据与人工智能技术分享,欢迎私信加群互相学习! 1万+

并发(三):ReentrantLock类

上文问题:为什么在DCL单例模式下,加了synchronized锁,代码块退出后,还要禁止指令重排序? 难道不是在持有锁的线程内,等重排序完之后,才会释放锁吗?在DCL单例模式下, 使用volatile是保证指令重排序,创建对象分为三步:第一,给对象分配内存空间,第二给对象初始化,第三变量与内存空间建立链接,其中第二第三步不存在数据依赖,固可以重排序,当线程2判断if(instance==null),这个等号是比较内存地址值的,判断当前变量的内存地址值是不是为null,这里就涉及了字节码知识,当store指

qq_37805943的博客 297

16、天文时间序列分类:基于人工神经网络的研究

本研究探讨了基于人工神经网络的天文时间序列分类方法,重点比较了MLP、LSTM和CNN等模型在BRITE和开普勒K2数据集上的表现。研究发现LSTM不适用于此类数据,而结合特征提取与PSO超参数优化的多层感知器(MLP)在开普勒K2数据上实现了98.55%的二分类准确率。通过精细的数据预处理和高置信度样本选择,显著提升了分类性能。研究成果为恒星变异性识别提供了高效工具,并对天文学与机器学习交叉领域具有重要参考价值。

pz8901234的博客 112

Pytorch实用教程:super(MLP,self).__init__()和super().__init__()有什么区别?

在 Python 中,super()函数用于调用父类(超类)的方法。它的使用在继承关系中非常常见,特别是在初始化继承自父类的属性时。在 PyTorch 中,这通常见于初始化nn.Module类的子类。super()的两种调用方式有微妙的差别,但在 Python 3 中常常用来达到同样的目的。这种写法在 Python 2 中非常常见。这里,super()被调用时需要指定当前类名(MLP)和当前实例(self。

若北辰 1025

死锁、活锁和饥饿是什么意思?

写在前面 本文隶属于专栏《100个问题搞定Java并发》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢! 本专栏目录结构和文献引用请见100个问题搞定Java并发 解答 死锁是指两个或者两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,他们将无法推进下去。 如果线程的智力不够, 且都秉承着“谦让”的原则,主动将资源释放给他人使用,那么就会导致资源不断地在两个线程间跳动,而没有一个线程可以同时拿到所有资源正常执行。这种情况就是活锁。

Shockang的博客 8757

OpenClaw实战系列01:OpenClaw接入飞书机器人全接入指南 + Ollama本地大模型

通过 OpenClaw + Ollama + 飞书的组合,我们成功搭建了一个完全私有、免费、可定制的AI助理。它不仅能在日常聊天中提供帮助,还能通过OpenClaw强大的“技能”体系,真正去操作你的电脑、管理文件、甚至执行代码。这套方案将数据留在本地,既保证了隐私安全,又锻炼了动手能力。现在,去给你的AI助理添加更多酷炫的技能吧!

PeterPan的博客 1万+

JUC并发编程(三)死锁/活锁/饥饿/ReenTrantLock基础使用

死锁是并发编程中一种常见的系统状态,指,若无外力干预,这些线程将无限期阻塞下去。网上看见一个例子(A等B,B等A)面试官问:跟我说说什么是死锁?回答:你给我offer我就跟你说。面试官:你说我才能给你offer。......二者一直僵持下去.....

小安同学的博客 1357

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

死锁、活锁、饥饿&定位死锁&解决死锁

文章目录1. 死锁2. 定位死锁2.1 jstack工具使用2.2 jconsole工具使用:3. 解决死锁3.1 哲学家就餐问题4. 活锁4.1 活锁原因4.2 活锁解决5. 饥饿6. 使用ReentrantLock解决死锁、活锁、饥饿6.1 解决哲学家就餐死锁问题 1. 死锁 死锁:一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。 说白了就是:两个线程互相持有对方所需的资源,互不释放且互相等待 t1 线程获得A对象锁,接下来想获取B对象的锁 t2 线程获得B对象锁,接下来想获取A对象的锁

fengxiandada的博客 1134

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

并发Bug图谱:死锁活锁饥饿10案例图解(实战踩坑)

并发Bug三大族├─ 死锁:环形等待,jcmd一键查├─ 活锁:无限重试,CPU白送└─ 饥饿:非公平+高优,线程饿死死锁有环,jcmd现形;活锁重试,退避救命;饥饿长队,公平+限速。

持续输出Java相关知识 1222

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

有关Java死锁和活锁的联系

本文主要探讨了多线程编程中的死锁和活锁问题。死锁是因线程相互等待资源而导致的系统停滞,需满足互斥、请求保持、不剥夺和循环等待四个条件;可通过预防、避免或检测来解决。活锁则是线程不断改变状态相互响应但无法推进任务,表现为CPU占用但无实际进展;可通过随机退避、限制重试次数或调整锁获取顺序来处理。两者虽表现不同,但都是线程协调不当所致。文章提供了Java代码示例和具体解决方案,强调了理解这些概念对开发健壮并发程序的重要性。

weixin_50055999的博客 1216

锁的活跃性(死锁、活锁、饥饿)

死锁 锁是个非常有用的工具,运用场景非常多,它使用起来非常简单,而且易于理解。但 同时它也会带来一些困扰,那就是可能会引起死锁,一旦产生死锁,就会造成系统功能不可 用。 定义 线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。 死锁的产生的一些特定条件: 1、互斥条件:即一个资源只能被一个线程占用,直到被该线程释放 。 2、请求和保持条件:一个线程因请求资源而发生阻塞时,对已获得的资源保持不放。 3、不剥夺条件:一个资源被一个线程

我的博客 638

深度剖析:Java 并发三大量难题 —— 死锁、活锁、饥饿全解

本文深入剖析Java并发中三大顽疾:死锁(线程永久阻塞)、活锁(线程忙等无效运行)、饥饿(低优先级线程长期得不到资源)。厘清其本质区别、触发条件、实战案例及jstack/Arthas等排查方案,并给出统一锁序、定时锁、公平锁等落地解决策略。

果酱 の 博客 622

java多线程(十)多把锁ReentrantLock

基于白嫖黑马程序员白嫖视频:全面深入学习java并发编程,java基础进阶中级必会教程 目录1 1

你佳哥 1391

Java JUC 源码分析 - 线程死锁、饥饿、活锁

Java并发编程中的线程死锁、饥饿和活锁是多线程环境下可能出现的活跃性问题,虽然它们不是JUC(Java Util Concurrency)包中具体的类或方法,但在使用JUC包内的同步工具时,理解和预防这些问题是非常关键的。下面分别对这三种现象进行简要说明,并结合JUC相关特性探讨如何在源码层面分析和避免这些问题。

qq_33240556的博客 492

重入锁、死锁、活锁、公平非公平锁……一下子都给你屡清楚了

目录 写在前面 重入锁 线程饥饿死锁 死锁 活锁(Livelock) 公平锁非公平锁 互斥锁 读-写锁 写在前面 每当听公司大佬提起来,死锁、活锁、公平锁、非公平锁……自己也是知其然而不知其所以然。 自己特意去阅读了《Java并发编程实战》,终于将这些概念屡清楚了,在这里将自己的理解分享给大家。 重入锁 所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。synchronized 和Ree...

秃了也弱了 1808

Java多线程-死锁,活锁,饥饿详解

文章目录什么是死锁?产生死锁的必要条件如何避免和处理死锁?死锁的例子什么是活锁什么是线程饥饿什么是无锁 什么是死锁? 定义:如果两个或者更多的线程因相互等待对方而被永远暂停(线程的生命周期状态为BLOCKED或者WAITING),那么我们就称这些线程产生了死锁. 由于产生死锁的线程的生命周期状态永远是非运行状态,因此这些线程索要执行的任务也永远无法进展. 通俗的说:当线程A持有独占锁a,并尝试去获...

尚凯辉的博客 763

java多线程(并发)夯实之路- ReentrantLock深入浅出(解决哲学家问题)

但是顺序加锁容易导致饥饿问题,如哲学家就餐问题会有死锁产生,用顺序加锁解决死锁问题后,会出现饥饿问题(一些哲学家总是优先抢到锁,一直进餐,导致其他哲学家一直不能进餐)如果一个线程需要同时获得多把锁,就容易发生死锁 t1线程获得了A对象的锁,接下来想获得B对象的锁 t2线程获得了B对象的锁,接下来想获得A对象的锁。方法前需要先获得锁,方法执行之后会释放锁,进入condtionObject等待,被唤醒(打断,超时)重新竞争锁,成功获得锁后继续执行。()方法尝试获取锁,获取到了返回真,否则返回假(不会等待)。

qq_61282807的博客 1008

死锁活锁饥饿阻塞无锁:并发问题五态实战解析

并发编程中的资源协调问题,本质是多线程环境下对共享资源的竞争与调度失衡。从操作系统级的互斥原理,到JVM线程模型、数据库事务锁机制及无锁原子操作,其技术价值在于保障系统高可用与数据一致性。典型应用场景涵盖电商大促、金融交易、IoT设备管理等高并发系统。深入理解死锁的循环等待、活锁的无效重试、饥饿的调度不公、阻塞的可控让渡以及无锁的CAS实现,是构建稳定后端服务的核心能力。本文结合jstack、Arthas、MySQL死锁图与JMH压测等工程实践,解析五类状态的本质差异与协同演化关系。

weixin_34293246的博客 328

Java高并发底层原理(二十七)—— 死锁、活锁与线程饥饿是如何发生的

本文深入分析了Java高并发编程中程序“卡住”的三种典型问题:死锁、活锁与线程饥饿。文章首先指出,线程卡住并非程序崩溃,而是执行条件未被满足,并给出了三种问题的表面现象与本质原因对比表。接着,通过两把锁的经典代码示例,详细解释了死锁的发生机制:多个线程各自持有部分资源并互相等待,形成循环等待环。死锁的四个必要条件(互斥、请求并保持、不可剥夺、循环等待)被逐一说明,其中循环等待是最易从代码层面控制的关键。文章重点介绍了两种避免死锁的实用方法:一是固定加锁顺序,通过让所有线程按全局统一顺序申请锁来破

m0_56222998的博客 312
上一篇: 并发编程之五:synchronized底层原理、monitor、轻量级锁、偏向锁、wati/notify/notifyAll、join、状态转换
下一篇: 并发编程之七:java内存模型(JMM)、volatile
java_xxxx
博客等级 码龄9年 127粉丝 99原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值