第三章 - 共享模型之管程(四)

第三章 - 共享模型之管程(四)

线程状态

重新理解线程状态转换 (重点)

Untitled

Untitled

Untitled

假设有线程 Thread t

情况 1 NEW --> RUNNABLE

  • 当调用 t.start( ) 方法时,由 NEW --> RUNNABLE

情况 2 RUNNABLE <–> WAITING

  • t 线程用 synchronized(obj) 获取了对象锁后
  • 调用 obj.wait( ) 方法时,t 线程从 RUNNABLE --> WAITING
  • 调用 obj.notify( ) , obj.notifyAll( ) , t.interrupt( ) 时
    • 竞争锁成功,t 线程从 WAITING --> RUNNABLE
    • 竞争锁失败,t 线程从 WAITING --> BLOCKED
@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
    final static Object obj = new Object();

    public static void main(String[] args) {

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t1").start();

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t2").start();

        // 主线程两秒后执行
        sleep(0.5);
        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
            obj.notifyAll(); // 唤醒obj上所有等待线程
        }
    }
}

Monitor示意图:

Untitled

示例调试步骤

  • 设置3个断点

Untitled

  • 顺序执行代码,线程t1、t2都wait了,断点来到主线程的notifyAll( )

Untitled

  • 向下执行,主线程notifyAll( )唤醒所有waiting的线程,这些线程从锁对象obj的waitSet移动到EntryList竞争对象锁

Untitled

  • 主线程向下执行,出了synchronized块,释放了锁。t1、t2可以竞争锁。可以看到t2竞争锁成功,成为锁对象obj的owner,t1仍然在entryList中阻塞。

Untitled

  • t2线程向下执行,出了synchronized块,释放锁,t1线程拿到锁

Untitled

情况 3 RUNNABLE <–> WAITING

  • 当前线程调用 t.join( ) 方法时,当前线程从 RUNNABLE --> WAITING
    • 注意是当前线程t 线程对象的监视器上等待
  • t 线程运行结束,或调用了当前线程的 interrupt( ) 时,当前线程从 WAITING --> RUNNABLE

情况 4 RUNNABLE <–> WAITING

  • 当前线程调用 LockSupport.park( ) 方法会让当前线程从 RUNNABLE --> WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt( ) 会让目标线程从 WAITING --> RUNNABLE

情况 5 RUNNABLE <–> TIMED_WAITING

t 线程用 synchronized(obj) 获取了对象锁后

  • 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING
  • t 线程等待时间超过了 n 毫秒,或调用 obj.notify( ) ,obj.notifyAll( ) , t.interrupt( ) 时
    • 竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
    • 竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED

情况 6 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING
    • 注意是当前线程在 t 线程对象的监视器上等待
  • 当前线程等待时间超过了 n 毫秒,或 t 线程运行结束,或调用了当前线程的 interrupt( ) 时,当前线程从TIMED_WAITING --> RUNNABLE

情况 7 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
  • 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE

情况 8 RUNNABLE <–> TIMED_WAITING

  • 当前线程调用 LockSupport.parkNanos(long nanos) LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt( ) ,或是等待超时,会让目标线程从TIMED_WAITING –> RUNNABLE

情况 9 RUNNABLE <–> BLOCKED

  • t 线程用 synchronized(obj) 获取了对象锁时,如果竞争失败,从 RUNNABLE --> BLOCKED
  • 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED

情况 10 RUNNABLE <–> TERMINATED

  • 当前线程所有代码运行完毕,进入 TERMINATED

多把锁

多把不相干的锁

  • 一间大屋子有两个功能:睡觉、学习,互不相干。
  • 现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低
    • 小南获得锁之后,学习完之后,小女才能进来睡觉
  • 解决方法是准备多个房间(多个对象锁)

代码示例

@Slf4j(topic = "c.BigRoomTest")
public class BigRoomTest {

    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);
        }
    }

}

Untitled

改进方法:让小南, 小女获取不同的锁即可

@Slf4j(topic = "c.BigRoomTest")
public class BigRoomTest {

    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 static final BigRoom bedRoom = new BigRoom(); //卧室
    private static final BigRoom studyRoom = new BigRoom(); //书房

    public void sleep() {
        synchronized (bedRoom) {
            log.debug("sleeping 2 小时");
            Sleeper.sleep(2);
        }
    }

    public void study() {
        synchronized (studyRoom) {
            log.debug("study 1 小时");
            Sleeper.sleep(1);
        }
    }

}

Untitled

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

活跃性

  • 因为某种原因,使得代码一直无法执行完毕,这样的现象叫做 活跃性
  • 活跃性相关的一系列问题都可以用 ReentrantLock 进行解决。

死锁 (重点)

  • 有这样的情况:一个线程需要 同时获取多把锁,这时就容易发生死锁

线程1获取A对象锁, 线程2获取B对象锁; 此时线程1又想获取B对象锁, 线程2又想获取A对象锁; 它们都等着对象释放锁, 此时就称为死锁

@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();
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                log.debug("lock A");
                sleep(1);
                synchronized (B) {
                    log.debug("lock B");
                    log.debug("操作...");
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (B) {
                log.debug("lock B");
                sleep(0.5);
                synchronized (A) {
                    log.debug("lock A");
                    log.debug("操作...");
                }
            }
        }, "t2");
        t1.start();
        t2.start();
    }

}

Untitled

发生死锁的必要条件 (重点)

  • 互斥条件
    • 在一段时间内,一种资源只能被一个进程所使用
  • 请求和保持条件
    • 进程已经拥有了至少一种资源,同时又去申请其他资源。因为其他资源被别的进程所使用,该进程进入阻塞状态,并且不释放自己已有的资源
  • 不可抢占条件
    • 进程对已获得的资源在未使用完成前不能被强占,只能在进程使用完后自己释放
  • 循环等待条件
    • 发生死锁时,必然存在一个进程——资源的循环链。

定位死锁的方法

方式一、JPS + JStack 进程ID

  • jps先找到JVM进程
  • jstack 进程ID
    • 在Java控制台中的Terminal中输入 jps 指令可以查看正在运行中的进程ID,使用 jstack 进程ID 可以查看进程状态。
jps

Untitled

jstack 59114

Untitled

  • 避免死锁要注意加锁顺序
  • 另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查

方式二、 jconsole检测死锁

Untitled

Untitled

哲学家就餐问题

Untitled

有五位哲学家,围坐在圆桌旁。

  • 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
  • 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
  • 如果筷子被身边的人拿着,自己就得等待

代码示例

public class TestDeadLock {

    public static void main(String[] args) {
        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();
    }

}

@Slf4j(topic = "c.Philosopher")
// 哲学家类
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.debug("eating...");
        Sleeper.sleep(0.5);
    }
}

// 筷子类
class Chopstick {
    String name;

    public Chopstick(String name) {
        this.name = name;
    }

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

执行一会儿,就不再执行下去了

Untitled

  • 使用 jconsole 检测死锁,发现

Untitled

  • 这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁和饥饿者两种情况

活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,(死锁是谁也无法执行)例如

@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();
    }
}

避免活锁的方法

  • 在线程执行时,中途给予 不同的间隔时间, 让某个线程先结束即可。

死锁与活锁的区别

  • 死锁是因为线程互相持有对方想要的锁,并且都不释放,最后到时线程阻塞停止运行的现象。
  • 活锁是因为线程间修改了对方的结束条件,而导致代码一直在运行,却一直运行不完的现象。

饥饿

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

Untitled

顺序加锁的解决方案

Untitled

虽然不会发生死锁了,但是会发生饥饿

public class TestDeadLock {
    public static void main(String[] args) {
        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("阿基米德", c1, c5).start();
    }
}
  • 由之前的
new Philosopher("阿基米德", c5, c1).start();
  • 改为了
new Philosopher("阿基米德", c1, c5).start();
  • 可以看到,程序没有发生死锁,但可以看到有些线程根本没有机会执行

Untitled

ReentrantLock (重点)

ReentrantLock 的特点 (synchronized不具备的)

  • 支持可重入
    • 可重入锁是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此 有权利再次获取这把锁
  • 可中断
    • lock.lockInterruptibly( ) : 可以被其他线程打断的中断锁
  • 可以设置超时时间
    • lock.tryLock(时间) : 尝试获取锁对象, 如果超过了设置的时间, 还没有获取到锁, 此时就退出阻塞队列, 并释放掉自己拥有的锁
  • 可以设置为公平锁
    • (先到先得) 默认是非公平, true为公平 new ReentrantLock(true)
  • 支持多个条件变量( 有多个waitSet )
    • (可避免虚假唤醒) - lock.newCondition( )创建条件变量对象; 通过条件变量对象调用 await/signal方法, 等待/唤醒

基本语法

//获取ReentrantLock对象
private ReentrantLock lock = new ReentrantLock();
//加锁
lock.lock();
try {
	// 临界区
}finally {
	// 释放锁
	lock.unlock();
}

支持可重入

  • 可重入锁是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
  • 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
/**
 * @author xiexu
 * @create 2022-02-01 8:56 AM
 */
@Slf4j(topic = "c.ReentrantTest")
public class ReentrantTest {

    // 获取ReentrantLock对象
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        // 如果有竞争就进入"阻塞队列",一直等待着,不能被打断
        lock.lock();
        try {
            log.debug("entry main...");
            m1();
        } finally {
            lock.unlock();
        }
    }

    private static void m1() {
        lock.lock();
        try {
            log.debug("entry m1...");
            m2();
        } finally {
            lock.unlock();
        }
    }

    private static void m2() {
        lock.lock();
        try {
            log.debug("entry m2....");
        } finally {
            lock.unlock();
        }
    }

}

Untitled

可中断 (针对于lockInterruptibly( )方法获得的中断锁) 直接退出阻塞队列, 获取锁失败

synchronizedreentrantlock.lock( ) 的锁, 是不可被打断的; 也就是说别的线程已经获得了锁, 我的线程就需要一直等待下去,不能中断

  • 可被中断的锁, 通过 lock.lockInterruptibly( ) 获取的锁对象, 可以通过调用阻塞线程的interrupt( ) 方法
  • 如果某个线程处于阻塞状态,可以调用其interrupt方法让其停止阻塞获得锁失败
    • 处于阻塞状态的线程,被打断了就不用阻塞了,直接停止运行
  • 可中断的锁, 在一定程度上可以被动的减少死锁的概率, 之所以被动, 是因为我们需要手动调用阻塞线程的interrupt方法;

测试使用lock.lockInterruptibly()可以从阻塞队列中,打断

/**
 * 演示RenntrantLock中的可打断锁方法 lock.lockInterruptibly();
 *
 * @author xiexu
 * @create 2022-02-01 10:38 AM
 */
@Slf4j(topic = "c.ReentrantTest")
public class ReentrantTest {

    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            log.debug("t1线程启动...");
            try {
                // 如果没有竞争那么此方法就会获取 lock 对象锁
                // 如果有竞争就进入阻塞队列,可以被其他线程用 interrupt 方法打断
                log.debug("尝试获得锁");
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("等锁的过程中被打断了,没有获得锁,返回"); //没有获得锁就不需要往下走了
                return;
            }
            try {
                log.debug("t1线程获得了锁");
            } finally {
                log.debug("t1线程释放了锁");
                lock.unlock();
            }
        }, "t1");

        // 主线程获得锁(此锁不可被打断)
        lock.lock();
        log.debug("main线程获得了锁");
        // 启动t1线程
        t1.start();
        try {
            Sleeper.sleep(1);
            log.debug("打断 t1 线程");
            t1.interrupt();            //打断t1线程
        } finally {
            lock.unlock();
        }
    }
}

Untitled

测试使用lock.lock()不可以从阻塞队列中打断, 一直等待别的线程释放锁

@Slf4j(topic = "c.ReentrantTest")
public class ReentrantTest {

    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            log.debug("t1线程启动...");
            log.debug("尝试获得锁");
            lock.lock();
            try {
                log.debug("t1线程获得了锁");
            } finally {
                log.debug("t1线程释放了锁");
                lock.unlock();
            }
        }, "t1");

        // 主线程获得锁(此锁不可被打断)
        lock.lock();
        log.debug("main线程获得了锁");
        // 启动t1线程
        t1.start();
        try {
            Sleeper.sleep(1);
            log.debug("打断 t1 线程");
            t1.interrupt();            //打断t1线程
        } finally {
            log.debug("main线程释放了锁");
            lock.unlock();
        }
    }
}
  • lock( )锁不能被打断, 在主线程中调用t1.interrupt( ), 没用, 当主线程释放锁之后, t1线程获得了锁

Untitled

锁超时 (lock.tryLock( )) 直接退出阻塞队列, 获取锁失败

防止无限制等待, 减少死锁

  • 使用 lock.tryLock( ) 方法会返回获取锁是否成功。如果成功则返回true,反之则返回false。
  • 并且tryLock方法可以设置指定等待时间,参数为:tryLock(long timeout, TimeUnit unit) , 其中timeout为最长等待时间,TimeUnit为时间单位
  • 获取锁的过程中, 如果超过等待时间或者被打断, 就直接从阻塞队列移除, 此时获取锁就失败了, 不会一直阻塞着 ! (可以用来实现死锁问题)

不设置等待时间, 立即失败

/**
 * 演示RenntrantLock中的tryLock()
 *
 * @author xiexu
 * @create 2022-02-01 10:38 AM
 */
@Slf4j(topic = "c.ReentrantTest")
public class ReentrantTest {

    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("尝试获得锁");
            // 此时肯定获取失败, 因为主线程已经获得了锁对象
            if (!lock.tryLock()) {
                log.debug("获取不到锁,返回");
                return;
            }
            try {
                log.debug("获得到锁");
            } finally {
                log.debug("释放了锁");
                lock.unlock();
            }
        }, "t1");

        lock.lock(); // main线程先获得锁
        log.debug("获得到锁");
        t1.start();
        // 主线程2s之后才释放锁
        Sleeper.sleep(2);
        log.debug("释放了锁");
        lock.unlock();
    }

}

Untitled

设置等待时间, 超过等待时间还没有获得锁, 失败, 从阻塞队列中移除该线程

/**
 * 演示RenntrantLock中的tryLock(long mills), 
 * 超过锁设置的等待时间或者被打断,就从阻塞队列移除
 *
 * @author xiexu
 * @create 2022-02-01 10:38 AM
 */
@Slf4j(topic = "c.ReentrantTest")
public class ReentrantTest {

    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("尝试获得锁");
            try {
                // 设置等待时间, 超过等待时间或者被打断, 都会获取锁失败; 退出阻塞队列
                if (!lock.tryLock(3, TimeUnit.SECONDS)) {
                    log.debug("获取不到锁,返回");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("被打断了, 获取锁失败, 返回");
                return;
            }
            try {
                log.debug("获得到锁");
            } finally {
                log.debug("释放了锁");
                lock.unlock();
            }
        }, "t1");

        lock.lock(); // main线程先获得锁
        log.debug("获得到锁");
        t1.start();
        t1.interrupt(); // 打断t1线程
        // 主线程5s之后才释放锁
        Sleeper.sleep(5);
        lock.unlock();
        log.debug("释放了锁");
    }

}

Untitled

Untitled

通过lock.tryLock( )来解决, 哲学家就餐问题 (重点)

lock.tryLock(时间) : 尝试获取锁对象, 如果超过了设置的时间, 还没有获取到锁, 此时就退出阻塞队列, 并释放掉自己拥有的锁

/**
 * 使用了ReentrantLock锁, 该类中有一个tryLock()方法, 在指定时间内获取不到锁对象, 就从阻塞队列移除,不用一直等待。
 * 当获取了左手边的筷子之后, 尝试获取右手边的筷子, 如果该筷子被其他哲学家占用, 获取失败, 此时就先把自己左手边的筷子,
 * 给释放掉. 这样就避免了死锁问题
 */
@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {

    public static void main(String[] args) {
        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();
    }

}

@Slf4j(topic = "c.Philosopher")
// 哲学家类
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 {
                    // 此时发现它的right筷子被占用了, 使用tryLock(),
                    // 尝试获取失败, 此时它就会将自己左筷子也释放掉
                    // 临界区代码
                    if (right.tryLock()) { //尝试获取右手边筷子, 如果获取失败, 则会释放左边的筷子
                        try {
                            eat();
                        } finally {
                            right.unlock();
                        }
                    }
                } finally {
                    left.unlock(); //释放自己手里左手的筷子
                }
            }
        }
    }

    private void eat() {
        log.debug("eating...");
        Sleeper.sleep(0.5);
    }
}

// 继承ReentrantLock, 让筷子类称为锁
class Chopstick extends ReentrantLock {

    String name;

    public Chopstick(String name) {
        this.name = name;
    }

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

}

Untitled

公平锁 new ReentrantLock(true)

  • ReentrantLock默认是非公平锁, 但是可以指定为公平锁。
  • 在线程获取锁失败,进入阻塞队列时,先进入的会在锁被释放后先获得锁。这样的获取方式就是公平的。
  • 一般不要设置ReentrantLock为公平的, 会降低并发度
  • Synchronized底层的Monitor锁就是不公平的, 和谁先进入阻塞队列是没有关系的。
//默认是不公平锁,需要在创建时指定为公平锁
ReentrantLock lock = new ReentrantLock(true);

什么是公平锁? 什么是非公平锁?

  • 公平锁 (new ReentrantLock(true))
    • 公平锁, 可以把竞争的线程放在一个先进先出的阻塞队列上
    • 只要持有锁的线程执行完了, 唤醒阻塞队列中的下一个线程获取锁即可; 此时先进入阻塞队列的线程先获取到锁
  • 非公平锁 (synchronized, new ReentrantLock( ))
    • 非公平锁, 当阻塞队列中已经有等待的线程A了, 此时后到的线程B, 先去尝试看能否获得到锁对象. 如果获取成功, 此时就不需要进入阻塞队列了. 这样以来后来的线程B就先获得到锁了
  • 所以公平和非公平的区别就是 : 线程执行同步代码块时, 是否会去尝试获取锁, 如果会尝试获取锁, 那就是非公平的, 如果不会尝试获取锁, 直接进入阻塞队列, 再等待被唤醒, 那就是公平的

条件变量 (可避免虚假唤醒) - lock.newCondition( )创建条件变量对象; 通过条件变量对象调用await/signal方法, 等待/唤醒

  • Synchronized 中也有条件变量,就是Monitor监视器中的 waitSet 等待集合,当条件不满足时进入waitSet 等待
  • ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量。
    • synchronized 是那些不满足条件的线程都在一间休息室等通知; (此时会造成虚假唤醒)
    • ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒的; (可以避免虚假唤醒)

使用流程

  • await 前需要 获得锁
  • await 执行后,会释放锁,进入 conditionObject (条件变量)中等待
  • await 的线程被唤醒(或打断、或超时)去重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行
  • signal 方法用来唤醒某个条件变量(休息室)的某一个等待的线程
  • signalAll 方法用来唤醒某个条件变量(休息室)中的所有线程
/**
 * ReentrantLock可以设置多个条件变量(多个休息室), 相对于synchronized底层monitor锁中waitSet
 *
 * @author xiexu
 * @create 2022-02-03 10:18 AM
 */
@Slf4j(topic = "c.ConditionVariable")
public class ConditionVariable {

    private static boolean hasCigarette = false;
    private static boolean hasTakeout = false;
    private static final ReentrantLock lock = new ReentrantLock();
    // 创建新的条件变量(休息室)
    // 等待烟的休息室
    static Condition waitCigaretteSet = lock.newCondition();
    // 等外卖的休息室
    static Condition waitTakeoutSet = lock.newCondition();

    public static void main(String[] args) {

        new Thread(() -> {
            lock.lock();
            try {
                log.debug("有烟没?[{}]", hasCigarette);
                while (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        // 此时小南进入到 等烟的休息室
                        waitCigaretteSet.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("烟来咯, 可以开始干活了");
            } finally {
                lock.unlock();
            }
        }, "小南").start();

        new Thread(() -> {
            lock.lock();
            try {
                log.debug("外卖送到没?[{}]", hasTakeout);
                while (!hasTakeout) {
                    log.debug("没外卖,先歇会!");
                    try {
                        // 此时小女进入到 等外卖的休息室
                        waitTakeoutSet.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("外卖来咯, 可以开始干活了");
            } finally {
                lock.unlock();
            }
        }, "小女").start();

        Sleeper.sleep(1);
        new Thread(() -> {
            lock.lock();
            try {
                log.debug("送外卖的来咯~");
                hasTakeout = true;
                // 唤醒等外卖的小女线程
                waitTakeoutSet.signal();
            } finally {
                lock.unlock();
            }
        }, "送外卖的").start();

        Sleeper.sleep(1);
        new Thread(() -> {
            lock.lock();
            try {
                log.debug("送烟的来咯~");
                hasCigarette = true;
                // 唤醒等烟的小南线程
                waitCigaretteSet.signal();
            } finally {
                lock.unlock();
            }
        }, "送烟的").start();
    }

}

Untitled

同步模式之顺序控制

固定运行顺序

  • 假如有两个线程, 线程A打印1, 线程B打印2.
  • 要求: 程序先打印2, 再打印1

使用ReentrantLock的 await/signal

/**
 * 使用ReentrantLock的 await/sinal 来实现顺序打印 2, 1
 *
 * @author xiexu
 * @create 2022-02-03 10:42 AM
 */
@Slf4j(topic = "c.SyncPrintWaitTest")
public class SyncPrintWaitTest {

    public static final ReentrantLock lock = new ReentrantLock();
    public static Condition condition = lock.newCondition();
    // 表示 t2 线程是否运行过
    public static boolean t2Runned = false;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                // 临界区
                while (!t2Runned) {
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("1");
            } finally {
                lock.unlock();
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            lock.lock();
            try {
                log.debug("2");
                t2Runned = true;
                condition.signal();
            } finally {
                lock.unlock();
            }
        }, "t2");

        t1.start();
        t2.start();
    }
}

Untitled

Wait/Notify版本实现

/**
 * 使用wait/notify来实现顺序打印 2, 1
 *
 * @author xiexu
 * @create 2022-02-03 10:30 AM
 */
@Slf4j(topic = "c.SyncPrintWaitTest")
public class SyncPrintWaitTest {

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

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                while (!t2Runned) {
                    try {
                        // 进入等待(waitSet), 会释放锁
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("1");
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                log.debug("2");
                t2Runned = true;
                lock.notify();
            }
        }, "t2");

        t1.start();
        t2.start();
    }
}

Untitled

使用LockSupport中的 park/unpark

可以看到,wait/notify 在实现上很麻烦:

  • 首先,需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒。因此使用了『运行标记』来判断该不该wait
  • 第二,如果有些干扰线程错误地 notify 了 wait 线程,条件不满足时还要重新等待,使用了 while 循环来解决此问题
  • 最后,唤醒对象上的 wait 线程需要使用 notifyAll,因为『同步对象』上的等待线程可能不止一个
/**
 * 使用LockSupport中的park,unpark来实现, 顺序打印 2, 1
 *
 * @author xiexu
 * @create 2022-02-03 10:48 AM
 */
@Slf4j(topic = "c.SyncPrintWaitTest")
public class SyncPrintWaitTest {

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            LockSupport.park();
            log.debug("1");
        }, "t1");

        Thread t2 = new Thread(() -> {
            log.debug("2");
            LockSupport.unpark(t1);
        }, "t2");

        t1.start();
        t2.start();
    }

}

Untitled

  • park 和 unpark 方法比较灵活,他俩谁先调用,谁后调用无所谓。并且是以线程为单位进行『暂停』和『恢复』,不需要『同步对象』和『运行标记』
  • 因为wait-notify其实是运用了保护性暂停模式,而park-unpark的底层实现其实就是保护性暂停的体现

交替输出

线程1 输出 a 5次, 线程2 输出 b 5次, 线程3 输出 c 5次。现在要求输出abcabcabcabcabcabc

wait/notify 版本

/**
 * Description: 使用wait/notify来实现三个线程交替打印 abcabcabcabcabc
 *
 * @author xiexu
 * @create 2022-02-03 10:50 AM
 */
@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
    public static void main(String[] args) {
        WaitNotify waitNotify = new WaitNotify(1, 5);

        new Thread(() -> {
            waitNotify.print("a", 1, 2);

        }, "a线程").start();

        new Thread(() -> {
            waitNotify.print("b", 2, 3);

        }, "b线程").start();

        new Thread(() -> {
            waitNotify.print("c", 3, 1);

        }, "c线程").start();
    }
}

@Slf4j(topic = "c.WaitNotify")
@Data
class WaitNotify {

    // 等待标记
    private int flag;

    // 循环次数
    private int loopNumber;

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

    /*
            输出内容    等待标记    下一个标记
            a           1          2
            b           2          3
            c           3          1
         */

    /**
     * @param str      打印的内容
     * @param waitFlag 等待标记
     * @param nextFlag 下一个标记
     */
    public void print(String str, int waitFlag, int nextFlag) {
        for (int i = 0; i < loopNumber; i++) {
            synchronized (this) {
                while (waitFlag != this.flag) {
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print(str);
                this.flag = nextFlag;
                this.notifyAll();
            }
        }
    }

}

Untitled

await/signal版本

/**
 * 使用await/signal来实现三个线程交替打印 abcabcabcabcabc
 *
 * @author xiexu
 * @create 2022-02-03 12:38 AM
 */
@Slf4j(topic = "c.TestWaitNotify")
public class TestAwaitSignal {

    public static void main(String[] args) throws InterruptedException {
        AwaitSignal awaitSignal = new AwaitSignal(5);
        // a线程的休息室
        Condition a_condition = awaitSignal.newCondition();
        // b线程的休息室
        Condition b_condition = awaitSignal.newCondition();
        // c线程的休息室
        Condition c_condition = awaitSignal.newCondition();

        new Thread(() -> {
            awaitSignal.print("a", a_condition, b_condition);
        }, "a").start();

        new Thread(() -> {
            awaitSignal.print("b", b_condition, c_condition);
        }, "b").start();

        new Thread(() -> {
            awaitSignal.print("c", c_condition, a_condition);
        }, "c").start();

        Thread.sleep(1000);
        System.out.println("==========开始=========");
        awaitSignal.lock();
        try {
            a_condition.signal();  //首先唤醒a线程
        } finally {
            awaitSignal.unlock();
        }
    }
}

class AwaitSignal extends ReentrantLock {

    // 循环次数
    private final int loopNumber;

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

    /**
     * @param str     打印的内容
     * @param current 当前线程的休息室
     * @param next    下一个线程的休息室
     */
    public void print(String str, Condition current, Condition next) {
        for (int i = 0; i < loopNumber; i++) {
            lock();
            try {
                try {
                    current.await();
                    //System.out.print("i:==="+i);
                    System.out.print(str);
                    next.signal();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } finally {
                unlock();
            }
        }
    }
}

Untitled

LockSupport的park/unpark实现

/**
 * 使用park/unpark来实现三个线程交替打印 abcabcabcabcabc
 *
 * @author xiexu
 * @create 2022-02-03 12:50 AM
 */
@Slf4j(topic = "c.TestWaitNotify")
public class TestParkUnpark {
    static Thread t1;
    static Thread t2;
    static Thread t3;

    public static void main(String[] args) {
        ParkUnpark parkUnpark = new ParkUnpark(5);

        t1 = new Thread(() -> {
            parkUnpark.print("a", t2);
        }, "t1");

        t2 = new Thread(() -> {
            parkUnpark.print("b", t3);
        }, "t2");

        t3 = new Thread(() -> {
            parkUnpark.print("c", t1);
        }, "t3");

        t1.start();
        t2.start();
        t3.start();

        LockSupport.unpark(t1); //首先唤醒 t1 线程
    }
}

class ParkUnpark {
    // 循环次数
    private final int loopNumber;

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

    /**
     * @param str
     * @param nextThread 下一个要唤醒的线程
     */
    public void print(String str, Thread nextThread) {
        for (int i = 0; i < loopNumber; i++) {
            LockSupport.park(); // 阻塞住当前线程
            System.out.print(str);
            LockSupport.unpark(nextThread); // 唤醒下一个线程
        }
    }
}

Untitled

本章小结

Untitled

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
现代操作系统第版是一本经典的操作系统教材,第三章主要讲解了进程的概念、进程控制块、进程状态以及进程调度等内容。以下是第三章的主要内容概述: 1.进程的概念:进程是程序在执行过程中分配和管理资源的基本单位,每个进程都有自己的地址空间、数据栈、指令计数器、寄存器和文件描述符等。 2.进程控制块:进程控制块是操作系统内核中用于管理进程的数据结构,包含了进程的状态、进程ID、优先级、程序计数器、寄存器、内存分配情况、打开文件列表等信息。 3.进程状态:进程状态包括运行态、就绪态、阻塞态和创建态等,进程在不同状态之间转换,操作系统根据进程状态来进行进程调度。 4.进程调度:进程调度是操作系统内核中的一个重要模块,负责决定哪个进程可以获得CPU的使用权,进程调度算法包括先来先服务、短作业优先、时间片轮转等。 5.进程同步:进程同步是指多个进程之间的协作,包括互斥、信号量、管程等机制,用于保证多个进程之间的正确性和一致性。 6.进程通信:进程通信是指多个进程之间的信息交换,包括共享内存、消息队列、管道等机制,用于实现进程之间的数据传输和共享。 以下是现代操作系统第第三章的相关代码示例: ```c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t pid; int status; pid = fork(); if (pid < 0) { printf("Fork error\n"); exit(1); } else if (pid == 0) { printf("Child process\n"); exit(0); } else { printf("Parent process\n"); wait(&status); printf("Child exit status: %d\n", WEXITSTATUS(status)); } return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

猿小羽

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

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

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

打赏作者

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

抵扣说明:

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

余额充值