JUC并发编程——多把锁

一、多八锁

多把不相干的锁
一间大屋子有两个功能:睡觉、学习,互不相干。

现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低

解决方法就是准备多个房间(对个对象锁)

例如

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对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

new一个对象_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值