JAVA多线程之同步

锁的使用,其实就是为了使用临界资源的时候来进行同步,即一个线程用完一个临界资源后,另一个线程再使用该临界资源,达到每次只有一个线程使用的目的。同时,也可以进行线程的同步,也就是使得多个线程的执行之间有顺序,比如线程1执行完set操作后,线程2再执行get操作等,这里我们就来学习一下线程的同步。

1. wait/notify

1.1 基本使用

wait/notify的特点如下:

  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为WAITING 状态
  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒
  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争
  • 调用wait方法后就直接加的是重量级锁

在这里插入图片描述

相关的方法如下:

API描述
wait()无限等待
wait(long n)等待n毫秒
notify()挑选等待队列中的一个线程进行唤醒
notifyAll()将等待队列中的线程全部唤醒

示例代码如下:

@Slf4j(topic = "c.TestBiased")
public class Test1 {
    final static Object obj = new Object();

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

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

        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
            obj.notify(); // 唤醒obj上一个线程
//            obj.notifyAll(); // 唤醒obj上所有等待线程
        }
    }
}

代码示例如上,上面代码都锁住了同一个对象obj,然后进行了wait操作,即将t1和t2都放入了WaitSet,然后针对同一个对象锁,调用notify会挑选一个线程进行唤醒,可能是t1也可能是t2,如果调用的是notifyAll则是唤醒全部线程。

wait与sleep对比:

  • 原理不同:sleep() 方法是属于 Thread 类,是线程用来控制自身流程的,使此线程暂停执行一段时间而把执行机会让给其他线程;wait() 方法属于 Object 类,用于线程间通信
  • 锁的处理机制不同:调用 sleep() 方法的过程中,线程不会释放对象锁,当调用 wait() 方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池(不释放锁其他线程怎么抢占到锁执行唤醒操作),但是都会释放 CPU
  • 使用区域不同:wait() 方法必须放在**同步控制方法和同步代码块(先获取锁)**中使用,sleep() 方法则可以放在任何地方使用

1.2 优化使用

虚假唤醒:notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程。

举个例子,比如有一个线程小南和一个线程小女,这两个线程锁住的都是一个对象,如果小南线程需要一根烟才能继续,小女需要一份外卖才能继续,否则都会进入WaitSet,这时,有一个外卖员线程,外卖员带了一份外卖,如果使用notify,那么小南小女随机唤醒一个,如果唤醒的是小南,那么其烟没到,还是无法工作,这就是虚假唤醒。

解决方法:采用 notifyAll

notifyAll 仅解决某个线程的唤醒问题,使用 if + wait 判断仅有一次机会,一旦条件不成立,无法重新判断

解决方法:用 while + wait,当条件不成立,再次 wait

可以修改后的代码如下:

@Slf4j(topic = "c.demo")
public class demo {
    static final Object room = new Object();
    static boolean hasCigarette = false;    //有没有烟
    static boolean hasTakeout = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                while (!hasCigarette) {//while防止虚假唤醒
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小南").start();

        new Thread(() -> {
            synchronized (room) {
                Thread thread = Thread.currentThread();
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (!hasTakeout) {
                    log.debug("没外卖,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (hasTakeout) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小女").start();


        Thread.sleep(1000);
        new Thread(() -> {
        // 这里能不能加 synchronized (room)?
            synchronized (room) {
                hasTakeout = true;
				//log.debug("烟到了噢!");
                log.debug("外卖到了噢!");
                room.notifyAll();
            }
        }, "送外卖的").start();
    }
}

2. Park & Unpark

2.1 基本使用

LockSupport 是用来创建锁和其他同步类的线程原语

LockSupport 类方法:

  • LockSupport.park():暂停当前线程,挂起原语

  • LockSupport.unpark(暂停的线程对象):恢复某个线程的运行

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("start...");	//1
    		Thread.sleep(1000);// Thread.sleep(3000)
            // 先 park 再 unpark 和先 unpark 再 park 效果一样,都会直接恢复线程的运行
            System.out.println("park...");	//2
            LockSupport.park();
            System.out.println("resume...");//4
        },"t1");
        t1.start();
       	Thread.sleep(2000);
        System.out.println("unpark...");	//3
        LockSupport.unpark(t1);
    }
    

LockSupport 出现就是为了增强 wait & notify 的功能:

  • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park、unpark 不需要
  • park & unpark 以线程为单位来阻塞和唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify。类比生产消费,先消费发现有产品就消费,没有就等待;先生产就直接产生商品,然后线程直接消费
  • wait 会释放锁资源进入等待队列,park 不会释放锁资源,只负责阻塞当前线程,会释放 CPU

2.2 基本原理

每个线程都有自己的一个Park对象,由三部分组成 _counter, _cond, _mutex ,我们来就先调用park方法和先调用unpark方法分别看看park对象的变化。

  • 先 park:

    1. 当前线程调用 Unsafe.park() 方法
    2. 检查 _counter ,本情况为 0,这时获得 _mutex 互斥锁
    3. 线程进入 _cond 条件变量挂起
    4. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
    5. 唤醒 _cond 条件变量中的 Thread_0,Thread_0 恢复运行,设置 _counter 为 0

    在这里插入图片描述

  • 先 unpark:

    1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
    2. 当前线程调用 Unsafe.park() 方法
    3. 检查 _counter ,本情况为 1,这时线程无需挂起,继续运行,设置 _counter 为 0

    在这里插入图片描述

3. 线程状态

我们重新看下面线程的10种状态转换,可以分析出每种状态转换的情况原因:

在这里插入图片描述

  1. NEW --> RUNNABLE

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

  2. RUNNABLE <–> WAITING

    • 调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING
    • 调用 obj.notify(), obj.notifyAll(), t.interrupt() 时:
      • 竞争锁成功,t 线程从 WAITING --> RUNNABLE
      • 竞争锁失败,t 线程从 WAITING --> BLOCKED
  3. RUNNABLE <–> WAITING

    • 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING
    • t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE
  4. RUNNABLE <–> WAITING

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

    • 调用 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
    • 当前线程等待时间超过了 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

4. 活跃性

4.1 死锁

4.1.1 死锁介绍

死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,由于线程被无限期地阻塞,因此程序不可能正常终止

Java 死锁产生的四个必要条件:

  1. 互斥条件,即当资源被一个线程使用(占有)时,别的线程不能使用
  2. 不可剥夺条件,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放
  3. 请求和保持条件,即当资源请求者在请求其他的资源的同时保持对原有资源的占有
  4. 循环等待条件,即存在一个等待循环队列:p1 要 p2 的资源,p2 要 p1 的资源,形成了一个等待环路

四个条件都成立的时候,便形成死锁。死锁情况下打破上述任何一个条件,便可让死锁消失

4.2.2 死锁定位

  1. 首先命令行输入 jps ,可以打印出Java进程的所有进程ID,然后使用 jstack PID 查看对应Java进程是否有死锁
  2. Linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack <pid>的输出来看各个线程栈
  3. 使用jconsole 工具检测

4.3 活锁

活锁:指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程

两个线程互相改变对方的结束条件,最后谁也无法结束,比如下面两个进程,一个进程希望count减到0结束,一个线程希望count加到20结束,由此count的值一直波动,但是始终无法波动到结束条件:

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) {
                Thread.sleep(200);
                count--;
                System.out.println("线程一count:" + count);
            }
        }, "t1").start();
        new Thread(() -> {
            // 期望超过 20 退出循环
            while (count < 20) {
                Thread.sleep(200);
                count++;
                System.out.println("线程二count:"+ count);
            }
        }, "t2").start();
    }
}

4.4 饥饿

饥饿: 一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值