并发编程(四)-线程常见方法的使用与分析

一、线程常见方法

方法名static功能说明注意
start()启动一个新线程,在新的线程运行 run 方法中的代码start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException
run()新线程启动后会调用的方法如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为
join()等待线程运行结束
join(long n)等待线程运行结束,最多等待 n毫秒
getId()获取线程长整型的 idid 唯一
getName()获取线程名
setName(String)修改线程名
getPriority()获取线程优先级
setPriority(int)修改线程优先级java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率 ,默认优先级是5
getState()获取线程状态Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING,TIMED_WAITING, TERMINATED
isAlive()线程是否存活(还没有运行完毕)
isInterrupted()判断是否被打断不会清除 打断标记
interrupt()打断线程如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记;如果打断的正在运行的线程,则会设置 打断标记;park 的线程被打断,也会设置打断标记
interrupted()static判断当前线程是否被打断会清除 打断标记
currentThread()static获取当前正在执行的线程
sleep(long n)static让当前执行的线程休眠n毫秒,休眠时让出 cpu的时间片给其它线程
yield()static提示线程调度器让出当前线程对CPU的使用主要是为了测试和调试

不推荐的方法

  • 这些方法已过时,容易破坏同步代码块,造成线程死锁
方法名功能说明
stop()停止线程运行
suspend()挂起(暂停)线程运行
resume()恢复线程运行

1. start 与 run

  • 调用 run
  • 程序仍在 main 线程运行,FileReader.read() 方法调用还是同步的
public static void main(String[] args) {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug("running...");
                FileReader.read(Constants.PDF_FULL_PATH);
            }
        };

        t1.run();
        log.debug("do other things...");
    }
10:14:02.886 c.Test4 [main] - running...
10:14:02.890 c.FileReader [main] - read [Java核心技术 卷2 高级特性 原书第10.pdf] start ...
10:14:03.703 c.FileReader [main] - read [Java核心技术 卷2 高级特性 原书第10.pdf] end ... cost: 813 ms
10:14:03.704 c.Test4 [main] - do other things...
  • 调用 start
  • 程序在 t1 线程运行,FileReader.read() 方法调用是异步的
public static void main(String[] args) {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug("running...");
                FileReader.read(Constants.PDF_FULL_PATH);
            }
        };

        t1.start();
        log.debug("do other things...");
    }
10:15:08.613 c.Test4 [main] - do other things...
10:15:08.613 c.Test4 [t1] - running...
10:15:08.618 c.FileReader [t1] - read [Java核心技术 卷2 高级特性 原书第10.pdf] start ...
10:15:09.422 c.FileReader [t1] - read [Java核心技术 卷2 高级特性 原书第10.pdf] end ... cost: 804 ms

小结

  • 直接调用 run 是在主线程中执行了 run,没有启动新的线程
  • 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码

start 源码:

public synchronized void start() {     
        group.add(this);
        boolean started = false;
        try {
        	// 调的是start0()
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
	// native 方法start0() 底层是C++实现的
    private native void start0();
java语言本身底层是C++语言,只是在C++基础上进行了简化
可以通过OpenJDK查看底层源码
java线程是通过start方法启动执行的,主要内容在native方法start0中
OpenJDKJNI一般是一一对应的,Thread.java对应的就是Thread.c
start0其实就是JVM_StartThread

在这里插入图片描述
在这里插入图片描述


在jvm.cpp中有JVM_StartThread的实现

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


在thread.cpp中有Thread::start的实现
最终由操作系统开启了一个线程

在这里插入图片描述

2. sleep 与 yield

  • sleep
  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行,需要等到CPU分配时间片
  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
  • 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
public static void main(String[] args) {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        t1.start();
        log.debug("t1 state: {}", t1.getState());

        try {
        	// 让main线程休眠是为了能获取到 t1 休眠后的状态
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        log.debug("t1 state: {}", t1.getState());
    }
10:42:19.292 c.Test6 [main] - t1 state: RUNNABLE
10:42:19.810 c.Test6 [main] - t1 state: TIMED_WAITING
  • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug("enter sleep...");
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    log.debug("wake up...");
                    e.printStackTrace();
                }
            }
        };
        t1.start();

        TimeUnit.SECONDS.sleep(1);
        log.debug("interrupt...");
        t1.interrupt();
    }
10:49:41.025 c.Test7 [t1] - enter sleep...
10:49:42.026 c.Test7 [main] - interrupt...
10:49:42.026 c.Test7 [t1] - wake up...
java.lang.InterruptedException: sleep interrupted
Disconnected from the target VM, address: '127.0.0.1:64497', transport: 'socket'
	at java.lang.Thread.sleep(Native Method)
	at cn.itcast.test.Test7$1.run(Test7.java:14)
  • yield
  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器,Runnable状态的线程仍然有机会被执行,所以yield不一定能让出时间片

3. 线程优先级

  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

4. 防止 CPU 占用100%

(1)sleep 实现

  • 在没有利用 cpu 来计算时,不要让 while(true) 空转浪费 cpu,这时可以使用 yield 或 sleep 来让出 cpu 的使用权给其他程序
while(true) {
	try {
			Thread.sleep(50);
 		} catch (InterruptedException e) {
			e.printStackTrace();
  		}
}
  • 可以用 wait 或 Lock + 条件变量 达到类似的效果
  • 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
  • sleep 适用于无需锁同步的场景

(2)wait 实现

synchronized(锁对象) {
    while(条件不满足) {
        try {
            锁对象.wait();
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
    }
    // do sth...
}

(3)Lock + 条件变量实现

lock.lock();
try {
    while(条件不满足) {
        try {
            条件变量.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    // do sth...
} finally {
    lock.unlock();
}

5. join 同步等待线程运行结束

(1)同步等待一个结果

  • 不加 join
@Slf4j(topic = "c.Test10")
public class Test10 {
    static int r = 0;
    public static void main(String[] args) throws InterruptedException {
        test1();
    }
    private static void test1() throws InterruptedException {
        log.debug("开始");
        Thread t1 = new Thread(() -> {
            log.debug("开始");
            sleep(1);
            log.debug("结束");
            r = 10;
        },"t1");
        t1.start();
        log.debug("结果为:{}", r);
        log.debug("结束");
    }
}
11:26:10.757 c.Test10 [main] - 开始
11:26:10.887 c.Test10 [t1] - 开始
11:26:10.887 c.Test10 [main] - 结果为:0
11:26:10.890 c.Test10 [main] - 结束
11:26:11.899 c.Test10 [t1] - 结束

分析

  • 因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10
  • 而主线程一开始就要打印 r 的结果,所以只能打印出 r=0

解决方法

  • 用 join,加在 t1.start() 之后即可

  • 加 join

@Slf4j(topic = "c.Test10")
public class Test10 {
    static int r = 0;
    public static void main(String[] args) throws InterruptedException {
        test1();
    }
    private static void test1() throws InterruptedException {
        log.debug("开始");
        Thread t1 = new Thread(() -> {
            log.debug("开始");
            sleep(1);
            log.debug("结束");
            r = 10;
        },"t1");
        t1.start();
        t1.join();
        log.debug("结果为:{}", r);
        log.debug("结束");
    }
}
11:29:15.751 c.Test10 [main] - 开始
11:29:15.832 c.Test10 [t1] - 开始
11:29:16.843 c.Test10 [t1] - 结束
11:29:16.843 c.Test10 [main] - 结果为:10
11:29:16.846 c.Test10 [main] - 结束

图解
在这里插入图片描述

(2)同步等待多个结果

@Slf4j(topic = "c.TestJoin")
public class TestJoin {
    static int r = 0;
    static int r1 = 0;
    static int r2 = 0;

	public static void sleep(int i) {
        try {
            TimeUnit.SECONDS.sleep(i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        test2();
    }

    private static void test2() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            sleep(1);
            r1 = 10;
        });
        Thread t2 = new Thread(() -> {
            sleep(2);
            r2 = 20;
        });
        t1.start();
        t2.start();
        long start = System.currentTimeMillis();
        log.debug("join begin");
        t1.join();
        log.debug("t1 join end");
        t2.join();
        log.debug("t2 join end");
        long end = System.currentTimeMillis();
        log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
    }

}
11:44:53.981 c.TestJoin [main] - join begin
11:44:54.985 c.TestJoin [main] - t1 join end
11:44:55.987 c.TestJoin [main] - t2 join end
11:44:55.987 c.TestJoin [main] - r1: 10 r2: 20 cost: 2009

分析

  • t1.join():等待 t1 时, t2 并没有停止, 也在运行
  • t2.join():1s 后, 执行到此, t2 也运行了 1s, 因此也只需再等待 1s

图解
在这里插入图片描述

(3)限定时间的等待

  • 等够时间的join
@Slf4j(topic = "c.TestJoin")
public class TestJoin {
    static int r = 0;
    static int r1 = 0;

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

    public static void test3() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            sleep(2);
            r1 = 10;
        });

        long start = System.currentTimeMillis();
        t1.start();

        log.debug("join begin");
        t1.join(3000);
        long end = System.currentTimeMillis();
        log.debug("r1: {} cost: {}", r1, end - start);
    }

}
11:57:28.643 c.TestJoin [main] - join begin
11:57:30.652 c.TestJoin [main] - r1: 10 cost: 2011
  • 没等够时间的join
@Slf4j(topic = "c.TestJoin")
public class TestJoin {
    static int r = 0;
    static int r1 = 0;

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

    public static void test3() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            sleep(2);
            r1 = 10;
        });

        long start = System.currentTimeMillis();
        t1.start();

        log.debug("join begin");
        t1.join(1000);
        long end = System.currentTimeMillis();
        log.debug("r1: {} cost: {}", r1, end - start);
    }

}
12:00:43.072 c.TestJoin [main] - join begin
12:00:44.088 c.TestJoin [main] - r1: 0 cost: 1019

6. interrupt

(1)打断 sleep,wait,join 的线程

  • 这几个方法都会让线程进入阻塞状态
  • 打断阻塞的线程, 会清空打断状态,以 sleep 为例
public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("sleep...");
            try {
                TimeUnit.SECONDS.sleep(3); // wait, join 同理
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t1");

        t1.start();
        TimeUnit.SECONDS.sleep(1);
        log.debug("interrupt");
        t1.interrupt();
        log.debug("打断标记:{}", t1.isInterrupted());
    }
12:14:15.286 c.Test11 [t1] - sleep...
12:14:16.293 c.Test11 [main] - interrupt
12:14:16.293 c.Test11 [main] - 打断标记:false
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at cn.itcast.test.Test11.lambda$main$0(Test11.java:14)
	at java.lang.Thread.run(Thread.java:748)

(2)打断正常运行的线程

  • 打断正常运行的线程, 不会清空打断状态
  • 只是告诉被打断的线程,想要打断它,被打断的线程自己决定是否继续运行或中断
public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while(true) {
                boolean interrupted = Thread.currentThread().isInterrupted();
                if(interrupted) {
                    log.debug("被打断了, 退出循环");
                    break;
                }
            }
        }, "t1");
        t1.start();

        TimeUnit.SECONDS.sleep(1);
        log.debug("interrupt");
        t1.interrupt();
    }
12:23:09.177 c.Test12 [main] - interrupt
12:23:09.181 c.Test12 [t1] - 被打断了, 退出循环

(3)打断 park 线程

  • 打断 park 线程, 不会清空打断状态
  • 打断park后,打断标记变为true,调用isInterrupted()方法判断打断状态后,再调用LockSupport.park()方法时,线程不会被阻塞
@Slf4j(topic = "c.Test14")
public class Test14 {

    private static void test3() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("park...");
            LockSupport.park(); // 阻塞,被打断后,打断标记为true再往下执行
            log.debug("unpark...");
            log.debug("打断状态:{}", Thread.currentThread().isInterrupted());

			 // 打断标记为true时,park不会阻塞
            LockSupport.park();
            log.debug("unpark...");
        }, "t1");
        t1.start();

        sleep(1);
        t1.interrupt();

    }

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

14:52:24.612 c.Test14 [t1] - park...
14:52:25.621 c.Test14 [t1] - unpark...
14:52:25.621 c.Test14 [t1] - 打断状态:true
14:52:25.623 c.Test14 [t1] - unpark...
  • 打断park后,调用Thread.interrupted()方法判断完打断状态后,接着会清除打断标记,再调用LockSupport.park()时,又会被阻塞
@Slf4j(topic = "c.Test14")
public class Test14 {
private static void test3() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("park...");
            LockSupport.park();
            log.debug("unpark...");
//            log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
            log.debug("打断状态:{}", Thread.interrupted());

            log.debug("Thread.interrupted()会清除打断标记,打断标记为{}, park在此处阻塞", Thread.currentThread().isInterrupted());
            LockSupport.park();
            log.debug("unpark...");
        }, "t1");
        t1.start();

        sleep(1);
        t1.interrupt();

    }

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

14:57:35.569 c.Test14 [t1] - park...
14:57:36.579 c.Test14 [t1] - unpark...
14:57:36.579 c.Test14 [t1] - 打断状态:true
14:57:36.581 c.Test14 [t1] - Thread.interrupted()会清除打断标记,打断标记为false, park在此处阻塞

7. 守护线程

  • 默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    break;
                }
            }
            log.debug("结束");
        }, "t1");
        // 设置守护线程
        t1.setDaemon(true);
        t1.start();

        // main线程结束,守护线程立刻结束
        Thread.sleep(1000);
        log.debug("结束");
    }
15:11:38.839 c.Test15 [main] - 结束

注意

  • 垃圾回收器线程就是一种守护线程
  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求

二、wait & notify

1. 小故事理解 wait & notify

  • 由于条件不满足,小南不能继续进行计算

  • 但小南如果一直占用着锁,其它人就得一直阻塞,效率太低
    在这里插入图片描述

  • 于是老王单开了一间休息室(调用 wait 方法),让小南到休息(WaitSet)等着去了,但这时锁释放开,其它人可以由老王随机安排进屋

  • 直到小M将烟送来,大叫一声 [ 你的烟到了 ] (调用 notify 方法)

    在这里插入图片描述

  • 小南于是可以离开休息室,重新进入竞争锁的队列
    在这里插入图片描述

2. wait & notify 原理

在这里插入图片描述

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

3. wait & notify API

  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待
  • wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到被 notify 为止
  • wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify 提前唤醒
  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法

@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被唤醒,执行其它代码....");
            }
        },"t1").start();

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

        // 主线程0.5秒后执行
        sleep(0.5);
        log.debug("唤醒 obj 上所有线程");
        synchronized (obj) {
//            obj.notify(); // 唤醒obj上一个线程
            obj.notifyAll(); // 唤醒obj上所有等待线程
        }
    }
}
14:44:13.682 c.TestWaitNotify [t1] - 执行....
14:44:13.684 c.TestWaitNotify [t2] - 执行....
14:44:14.194 c.TestWaitNotify [main] - 唤醒 obj 上所有线程
14:44:14.194 c.TestWaitNotify [t2] - t2被唤醒,执行其它代码....
14:44:14.194 c.TestWaitNotify [t1] - t1被唤醒,执行其它代码....

4. sleep 与 wait 区别

  • (1)sleep 是 Thread 方法,而 wait 是 Object 的方法
  • (2) sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用,并且需要先wait再notify
  • (3) sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
  • (4)它们状态都是 TIMED_WAITING

5. wait & notify 使用套路

// wait 线程
new Thread(() -> {
	synchronized(lock) {
	// 用 while + wait,当条件不成立时,可以再次 wait
	 while(条件不成立) {
	 	lock.wait();
	 }
	 // 被唤醒后再干活
	}
}, "t1").start();

// notifyAll 线程
new Thread(() -> {
	synchronized(lock) {
	 	lock.notifyAll();
	}
}, "t2").start();

案例

@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep5 {
    static final Object room = new Object(); // 对象锁最好使用final修饰,防止改变
    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) {
                    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) {
                log.debug("外卖送到没?[{}]", hasTakeout);
                while (!hasTakeout) {
                    log.debug("没外卖,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (hasTakeout) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小女").start();

        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
            synchronized (room) {
                hasTakeout = true;
                log.debug("外卖到了噢!");
                room.notifyAll();
            }
        }, "送外卖的").start();


    }

}
20:14:17.024 c.TestCorrectPosture [小南] - 有烟没?[false]
20:14:17.042 c.TestCorrectPosture [小南] - 没烟,先歇会!
20:14:17.042 c.TestCorrectPosture [小女] - 外卖送到没?[false]
20:14:17.043 c.TestCorrectPosture [小女] - 没外卖,先歇会!
20:14:18.029 c.TestCorrectPosture [送外卖的] - 外卖到了噢!
20:14:18.029 c.TestCorrectPosture [小女] - 外卖送到没?[true]
20:14:18.029 c.TestCorrectPosture [小女] - 可以开始干活了
20:14:18.029 c.TestCorrectPosture [小南] - 没烟,先歇会!

三、park & unpark

1. 基本使用

  • 它们是 LockSupport 类中的方法,LockSupport是用来创建锁和其他同步类的基本线程阻塞原语
  • LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的是Unsafe中的native代码
  • LockSupport不用加锁,程序性能好
  • 可以调换先后顺序(先park后unpark 或者 先unpark后park)
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)

案例一:先 park 再 unpark

@Slf4j(topic = "c.TestParkUnpark")
public class TestParkUnpark {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            // 先 park 再 unpark, t1线程休眠时间小于main线程休眠时间
            sleep(2);
            log.debug("t1线程先park...");
            LockSupport.park();
            log.debug("resume...");
        }, "t1");
        t1.start();


        sleep(3);
        log.debug("unpark...");
        LockSupport.unpark(t1);
    }
}

11:12:17.676 c.TestParkUnpark [t1] - start...
11:12:19.681 c.TestParkUnpark [t1] - t1线程先park...
11:12:20.676 c.TestParkUnpark [main] - unpark...
11:12:20.676 c.TestParkUnpark [t1] - resume...

案例二:先 unpark 再 park

  • 先 unpark 后,就算再 park,也不会阻塞住
@Slf4j(topic = "c.TestParkUnpark")
public class TestParkUnpark {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            sleep(2);
            log.debug("park...");
            LockSupport.park();
            log.debug("resume...");
        }, "t1");
        t1.start();

        // 先 unpark 再 park , main线程休眠时间小于t1线程休眠时间
        sleep(1);
        log.debug("main线程先unpark...");
        LockSupport.unpark(t1);
    }
}
11:09:13.101 c.TestParkUnpark [t1] - start...
11:09:14.111 c.TestParkUnpark [main] - main线程先unpark...
11:09:15.113 c.TestParkUnpark [t1] - park...
11:09:15.113 c.TestParkUnpark [t1] - resume...

2. 特点

与 Object 的 wait & notify 相比

  • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不需要
  • park & unpark 是以线程为单位来【阻塞】和【唤醒】线程(精确唤醒对应线程),而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify

3. 原理

  • 每个线程都有自己的一个 Parker 对象(类似许可证),由三部分组成 _counter (计数器), _cond(条件变量) 和 _mutex(互斥锁)。Parker对象是底层C语言实现的。

  • 调用park时,首先会检查需不需要阻塞:
    _counter = 1 运行,_counter = 0 阻塞。

  • 调用unpark时,会让_counter = 1:

    • 如果线程此时正在阻塞,则唤醒线程继续运行;
    • 如果线程此时没有阻塞,则下次调用park时,发现_counter = 1,线程不会阻塞而是继续运行;
  • 多次调用 unpark,_counter 仍为1,不会自增

  • 与Semaphore不同的是,_counter 累加上限是1

(1)分析 先 park 再 unpark

  1. 当前线程调用 Unsafe.park() 方法
  2. 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁
  3. 线程进入 _cond 条件变量阻塞
  4. 设置 _counter = 0
    在这里插入图片描述
  5. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
  6. 唤醒 _cond 条件变量中的 Thread_0
  7. Thread_0 恢复运行
  8. 设置 _counter 为 0

在这里插入图片描述

(2)分析 先 unpark 再 park

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

在这里插入图片描述
①. 为什么可以先唤醒线程后阻塞线程?

  • 因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞

②. 为什么唤醒两次后再阻塞两次,但最终结果还会阻塞线程?

  • 因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值