面试中最常被虐的地方必定有并发编程这块知识点,不管你是刚刚入门的大四萌新仍是2-3年经验的CRUD怪,也就是说这类问题你最起码会被问3年,何不花时间死磕到底。消除恐惧最好的办法就是面对他,奥利给!(这一系列是本人学习过程当中的笔记和总结,并提供调试代码供你们玩耍java
1.java线程生命周期和java线程状态都有哪些?git
2.java线程生命周期之间是如何转换的?github
3.Thread.start()都作了哪些事情?面试
请自行回顾以上问题,若是还有疑问的自行回顾上一章哦~编程
本章学习完成,你将会掌握Thread经常使用API接口的使用,包括sleep、yield和join,而且会详细解析join源码和用法。同时配合上一章的start()方法,本章还会介绍一下应该如何去关闭一个线程。鉴于interrupt字段内容较多,咱们放到下一章讲哦。(老规矩,熟悉这块的同窗能够选择直接关注点赞👍完成本章学习哦!)bash
本章代码下载并发
本节开头先打个预防针,针对每个API会用和精通是两个水准哦,这里咱们的目标是彻底吃透,因此章节内容会比较干,可是我会加油写的有代入感,你们一块儿加油~👏app
sleep一共有两个重载方法ide
因为这两个实现精度不一样,内部调用的都是同一个方法,因此咱们这里就挑public static void sleep(long millis, int nanos) throws InterruptedException
来看下。函数
/**
* Causes the currently executing thread to sleep (temporarily cease
* execution) for the specified number of milliseconds plus the specified
* number of nanoseconds, subject to the precision and accuracy of system
* timers and schedulers. The thread does not lose ownership of any
* monitors.
*
* @param millis
* the length of time to sleep in milliseconds
*
* @param nanos
* {@code 0-999999} additional nanoseconds to sleep
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative, or the value of
* {@code nanos} is not in the range {@code 0-999999}
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public static void sleep(long millis, int nanos)
throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
sleep(millis);
}
复制代码
官方的描述是这样的,使线程暂时中止执行,在指定的毫秒数上再加上指定的纳秒数,可是线程不会失去监视器
。这里的关键是不会失去持有的监视器,上一章咱们讲过这时线程处于BLOCKED
阶段。
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
复制代码
当millis<0或者nanos不在0-999999范围中的时候就会抛出IllegalArgumentException
@throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
复制代码
sleep
可被中断方法打断,可是会抛出InterruptedException
异常。
好啦到这里咱们介绍完了这个API了,是否是感受很简单呢?哈哈光这样可不行,实践是检验真理的惟一标准下面咱们来验证一下sleep
以后对象监视锁到底有没有释放。
别犯困啦,划重点啦
/**
* 建立一个独占锁
*/
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
System.out.println("我是" + Thread.currentThread().getName() + ",lock在我手中");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "不须要lock了");
}
}
}, "一号线程").start();
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
System.out.println("我是" + Thread.currentThread().getName() + ",lock在我手中");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "不须要lock了");
}
}
}, "二号线程").start();
}
复制代码
输出结果:
我是一号线程,lock在我手中
一号线程不须要lock了
我是二号线程,lock在我手中
二号线程不须要lock了
复制代码
同窗们能够用各类姿式来run咱们的代码,不关你是坐着run,躺着run仍是倒立run,结果始终是连续的,不会出现一号线程和二号线程交替打印的情景。这就证实了sleep
确实不会释放其获取的监视锁,可是他会放弃CPU执行权。实践也实践完了,可是每次都要计算毫秒也着实费劲,有没有什么好的办法呢?
⚠️会玩的都这么写
假如如今有需求要求咱们让线程sleep
1小时28分19秒33毫秒咱们要咋办?手脚快的同窗可能已经掏出了祖传的计算器滴滴滴地操做起来了。可是咱们通常不这么作,JDK1.5为咱们新增了一个TimeUnit
枚举类,请你们收起心爱的计算器,其实咱们能够这么写
//使用TimeUnit枚举类
TimeUnit.HOURS.sleep(1);
TimeUnit.MINUTES.sleep(28);
TimeUnit.SECONDS.sleep(19);
TimeUnit.MILLISECONDS.sleep(33);
复制代码
这样咱们的代码更加优雅,可读性会更强
写累了,锻炼下身体,给同窗们挖个坑。咱们已经知道millis
的范围是大于等于0,sleep(1000)
咱们知道是什么意思,那么sleep(0)
会有做用吗?
sleep
的第二个点,
sleep(0)的做用是“触发操做系统马上从新进行一次CPU竞争
,竞争结果多是当前线程继续获取到CPU的执行权,也有多是别的线程获取到了当前线程的执行权。
两个点但愿你们能够牢记
1.sleep不会释放mointor lock。
2.sleep的做用是触发操做系统马上从新进行一次CPU竞争。
仍是老套路,咱们先来看API接口描述是怎么定义这个接口的
/**
* A hint to the scheduler that the current thread is willing to yield
* its current use of a processor. The scheduler is free to ignore this
* hint.
*
* <p> Yield is a heuristic attempt to improve relative progression
* between threads that would otherwise over-utilise a CPU. Its use
* should be combined with detailed profiling and benchmarking to
* ensure that it actually has the desired effect.
*
* <p> It is rarely appropriate to use this method. It may be useful
* for debugging or testing purposes, where it may help to reproduce
* bugs due to race conditions. It may also be useful when designing
* concurrency control constructs such as the ones in the
* {@link java.util.concurrent.locks} package.
*/
public static native void yield();
复制代码
这个方法的描述是这样的提示调度程序,当前线程愿意放弃CPU执行权。调度程序能够无条件忽略这个提示,打个比方就是说,A暗恋B,A说我愿意怎么怎么样,B能够接受A,可是也能够彻底无条件的忽略A,,嗯嗯额~大概就是这么个场景,卑微A。
API接口描述中也明确说明了,这个接口不经常使用,可能用于调试或测试的目的,可能用于重现因为竞争条件而致使的bug,还有就是在java.util.concurrent.locks
包中有用到这个API,总的来讲就是在实际生产开发过程当中是不用的。可是它又不像stop
同样已经被废弃不推荐使用,讲这个API的目的是应由于它很容易和sleep
混淆。
1.调用yield
并生效以后线程会从RUNNING阶段转变为RUNNABLE,固然被无条件忽略的状况除外。而sleep
则是进入BLOCKED阶段,并且是几乎百分百会进入。
2.sleep
会致使线程暂停,可是不会消耗CPU时间片,yield
一旦生效就会发生线程上下文切换,会带来必定的开销。
3.sleep
能够被另外一个线程调用interrupt
中断,而yield
就不会,yield
得等到CPU轮询给到执行权的时候才会再次被唤醒,也就是从RUNNABLE
阶段编程RUNNING
阶段。
光说不练假把式,虽然不经常使用,可是是驴子是马总归仍是要溜一溜。
private static class MyYield implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
if (i % 5 == 0) {
System.out.println(Thread.currentThread().getName()+"线程,yield 它出现了");
// Thread.yield();
}
}
System.out.println(Thread.currentThread().getName()+"结束了");
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new MyYield());
t1.start();
Thread t2 = new Thread(new MyYield());
t2.start();
Thread t3 = new Thread(new MyYield());
t3.start();
}
复制代码
屡次执行,按到最多的输出是连续的,相似下面这种输出结果:
Thread-0线程,yield 它出现了
Thread-0结束了
Thread-1线程,yield 它出现了
Thread-1结束了
Thread-2线程,yield 它出现了
Thread-2结束了
复制代码
如今咱们把注释打开,发现输出结果变了
Thread-0线程,yield 它出现了
Thread-1线程,yield 它出现了
Thread-2线程,yield 它出现了
Thread-0结束了
Thread-1结束了
Thread-2结束了
复制代码
那是应为在调用到yield
到时候当前线程让出了执行权,因此等到你们都出现了
以后,你们再分别结束了
。
在本小节中咱们介绍一下join
API
sleep
API十分相像,可是join
除了两个设置超时等待时间的API外,还额外提供了一个不设置超时时间的方法,可是经过追踪第一个API咱们发现内部其实调用的就是第二个API的join(0)
,设置纳秒的内部调用也是第二个API。因此咱们这边就拿第二个API来说解。/**
//设置一段时间等待当前线程结束,若是超时还未返回就会一直等待
* Waits at most {@code millis} milliseconds for this thread to
* die. A timeout of {@code 0} means to wait forever.
*
这个方法调用的前提就是当前线程仍是处于alive状态的
* <p> This implementation uses a loop of {@code this.wait} calls
* conditioned on {@code this.isAlive}. As a thread terminates the
* {@code this.notifyAll} method is invoked. It is recommended that
* applications not use {@code wait}, {@code notify}, or
* {@code notifyAll} on {@code Thread} instances.
*
* @param millis
* the time to wait in milliseconds
*
//超时时间为负数
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
复制代码
经过该方法咱们能够看到他的逻辑是经过当前运行机器的时间,判断线程是否isAlive
来决定是否须要继续等待,而且内部咱们能够看到调用的是wait()
方法,直到delay<=0
的时刻,就会跳出当前循环,从而结束中断。
又要给同窗们讲一个悲伤的故事了
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println("周末都要加班,终于回家了,洗个手吃饭了");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("洗完手,");
});
Thread t2 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
// t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("拿起筷子,");
});
t1.start();
t2.start();
// t2.join();
System.out.println("我要吃饭了");
}
复制代码
输出:
周末都要加班,终于回家了,洗个手吃饭了
我要吃饭了
拿起筷子,洗完手,
复制代码
显然这个结果不是咱们想要的结果,也可是不排除加班加的已经意识模糊,手抓饭了,这里咱们仍是但愿按照正常习惯来执行。咱们把注释打开
输出:
周末都要加班,终于回家了,洗个手吃饭了
洗完手,拿起筷子,我要吃饭了
复制代码
这个才是咱们须要的结果。
相信同窗们经过这个例子已经大概了解join
的做用了,没错join
是可让程序能按照必定的次序来完成咱们想完成的工做,他的工做原理就是阻塞当前调用join
的线程,让新join
进来的线程优先执行。
线程关闭大体上能够分为三种状况
1.线程正常关闭
2.线程异常退出
3.进程假死
这里咱们着重讲一下线程正常关闭的状况,也是实际开发生产中经常使用方法。
这个没什么好说的,就是线程逻辑单元执行完成而后本身正常结束。
早期JDK中还提供有一个stop
函数用于关闭销毁线程,可是后来发现会存在monitor锁没法释放的问题,会致使死锁,因此如今强烈建议你们不要用这个方式。这里咱们使用捕获线程中断的方式来结束线程。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println("我要自测代码啦~~");
while (!Thread.currentThread().isInterrupted()) {
System.out.println("目前来看是好好的");
}
System.out.println("代码中断中止了");
});
t1.start();
TimeUnit.SECONDS.sleep(1);
t1.interrupt();
}
复制代码
输出:
我要自测代码啦~~
...
目前来看是好好的
目前来看是好好的
代码中断中止了
复制代码
能够看到,咱们经过判断当前线程的isInterrupted()
状态来捕获线程是否已经被中断,从而能够来控制线程正常关闭。同理,若是咱们在线程内部已经执行来某中断方法,好比sleep
就能够经过捕获中断异常来退出sleep
状态,从而也能让线程正常结束。
因为interrupt
颇有可能被擦除,或者整个逻辑单元中并有调用中断方法,这样咱们上一种方法就不适用了,这里咱们使用volatile关键字来设置一个开关,控制线程的正常退出。
private static class MyInterrupted extends Thread {
private volatile boolean close = false;
@Override
public void run() {
System.out.println("我要开始自测代码啦~~");
while (!close) {
System.out.println("目前来看好好的");
}
System.out.println("close已经变成了" + close + ",代码正常关闭了");
}
public void closed() {
this.close = true;
}
}
public static void main(String[] args) throws InterruptedException {
MyInterrupted myInterrupted = new MyInterrupted();
myInterrupted.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("我要开始关闭线程了");
myInterrupted.closed();
}
复制代码
输出:
我要开始自测代码啦~~
...
目前来看好好的
目前来看好好的
目前来看好好的
我要开始关闭线程了
close已经变成了true,代码正常关闭了
复制代码
能够看到咱们调用closed方法时候把close设置为了true,从而正常关闭代码,关于volatile
关键字咱们以后的章节会详细讲哦,请同窗们继续关注,和我一块儿学习😁,但愿能够同窗们帮忙关注下和点点赞👍哦~
下一章也已经出来咯,此次内容稍多,因此拖了比较久,可是内容都是妥妥的,下一章详细讲解了interrupt的执行逻辑,一步一步带同窗们调试,感兴趣的同窗记得进入下一章的学习哦~