解析Thread和Object类中的多线程难点
Posted kobebyrant
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了解析Thread和Object类中的多线程难点相关的知识,希望对你有一定的参考价值。
其实这一篇是想写jdk1.7和jdk1.8里面的concurrentHashMap的,毕竟是阿汤哥介绍的硬核方法,之前研究了一个多星期,大致的内容和难点都看懂了,但是还是有一些不满足的地方。应该是一种知识点的依赖关系的问题: 1.8里面的concurrentHashMap是依赖于LongAdder的实现的;1.7里面的concurrentHashMap的segment是依赖于ReentrantLock的,而ReentrantLock是依赖于AQS框架的,而AQS框架里面有很多知识点和Thread和Object里面的基础同步方法十分相关。所以要真正深入理解好并发容器,始终逃不开最基础的java多线程方法。
本篇文章会包含以下内容:Thread.yield,Thread.sleep,Object.wait,Object.notify,Thread.join,中断和中断异常
Thead.yield
/**
* 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时间,时间调度器怎么对待这个消息是另一回事。
这个方法一般会用在两个情况:1. 用于测试或者调试,这个可能会对复现竞争条件下的产生的bug有帮助;2. 对于设计并发控制结构有帮助(比如juc包里面的locks包)
Thread.sleep
/**
* Causes the currently executing thread to sleep (temporarily cease
* execution) for the specified number of milliseconds, 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
*
* @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 static native void sleep(long millis) throws InterruptedException;
这个其实比较简单,但是要拿来和Object.wait()来作对比,所以放上来了。注释也很简单,就是让这个线程处于睡眠状态,如果过程中被其他线程中断了,则会抛出中断异常。注意,这个方法没有synchroniz修饰,同时这个方法也不会改变当前线程的锁的状态,本身持有则继续持有,没有也没所谓。
Object.wait()
public final native void wait(long timeout) throws InterruptedException;
这个方法是比较难的方法了,在Object类中,因此每一个类都会带有这个方法,这个方法一般配合Object.notify一起使用。经典使用场景是生产者消费者模式。
官方api里面有对这个方法非常详细的注释:https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#wait--
这里基于官网的注释解释一下:
synchronized (obj)
while (<condition does not hold>)
obj.wait(timeout);//在这里一开始进入wait(),就会失去掉这个obj对象的锁!
... // Perform action appropriate to condition
这里有两个主体:线程A,对象obj(通常是一个lock)
用法是线程A调用对象obj,obj.wait(long milisecend)。那就是意思是线程A在等待对象obj,那既然是等待了,那就是这个线程进行休眠态了。还有一个前提点要注意:要调用这个对象的wait方法,前提一定要先获取了对象的锁!(monitor),用了这个对象.wait()方法的实际效果是,这个线程会加入这个对象的waiting set,并且失去这个对象的锁!(补充一下,其他对象持有的锁继续持有!)同时立马进入休眠状态,直至以下四个条件发生:
这里重点说一下,之前说过了进入wait()之后,会进入waiting set,那满足上述条件之后,那就是从waiting set里面出来,然后再和其他线程竞争锁,那如果竞争成功后,wait()方法返回了,那就是可以恢复到进入wait之前的状态了!也就是说,一旦进入wait,想出wait,要经过两重关卡:满足上述四个唤醒条件,还有就是竞争这个对象1的锁成功!
这里还有两点疑问(留待以后解决,暂时不影响我们继续看下去):
- 官网提到了一种神奇的状态叫做“ spurious wakeup”假唤醒,这个状态怎么达到,然后有什么危害?
- 如果发生了第三种情况,那这个线程会到一个什么状态,然后官网说中断异常只有在该线程恢复了调用wait前的状态才会抛出,这是为什么?
public final native void notify();
官网的介绍已经非常清晰了,这个东西说的很清晰了,和之前的wait是相辅相成的。然后要说明的一些是之前休眠态的线程是不会在争抢锁的时候有优先级的。然后就是如果一个线程要想拥有一个object的monitor的话,有以下三种方式(实际上是synchronized的三种用法):
- 执行那个object的同步实例方法;
- 执行用object来加锁的同步方法体;
- 如果这个object是一个Class(注意!),那就要执行这个class的同步静态方法。
最难的方法Thread.join
/**
* Waits at most @code millis milliseconds for this thread to
* die. A timeout of @code 0 means to wait forever.
*
* <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;
这个方法的难处就是要理解:为什么要有synchronized呢?isAlive和wait到底是调用是谁的方法呢?
理解这个问题首先要理解这个方法的含义。
假如当前运行的线程是线程A,然后当在A的方法块里面调用另一个线程B的该方法,比如B.join的话,意思就是说,A等待线程B一段间,如果是B在等待区间内消亡的话,则notifyAll会被调用,那这个线程A就可能能接受到通知从而退出等待队列去竞争锁。(如果传参是0的话,那就是join方法,意思是无限期等待B消亡)
join方法只是threadB里面的一个方法而已,但是调用方是线程A,因此是A自己触发了自己object里面的wait方法和alive方法,这个方法的修饰者是threadB当前这个对象!所以通过synchronized获得的是一个threadB的对象实例锁,这样才能去触发对threadB的等待动作!然后这个wait是需要等待唤醒的,然后这个时候threadB消亡的时候会发送notifyAll,然后达到唤醒A的目的,这样就能完成join的使命了!
这里我自身还有一个困惑:
就是如果调用了join之后线程A交出了锁,然后锁被其他线程占据了,并且迅速交出锁调用notifyAll之后把线程A唤醒了怎么办呢?这样会不会导致这个线程ThreadA依赖B结束的需求没有达到满足呢?
中断和中断异常
一直以来,很困惑我的问题是java线程里面的中断以及中断异常的问题。
当一个线程在运行的时候,你应该怎么中断他?你如果中断了他,那会怎么样?什么情况下应该抛出中断异常?为什么这么多方法喜欢抛出中断异常?我们应该怎么去处理中断异常? 中断的标志位到底在哪里?
这些中断相关的编程实践对于我来说几乎没有过,因此这些问题显得比较难以理解和回答。
这里推荐两篇blog:
知乎高赞回答中断相关的问题相当简洁! https://www.zhihu.com/question/41048032
这个是Brian Goetz 的回答,主要针对如何处理中断异常 https://www.ibm.com/developerworks/cn/java/j-jtp05236.html
当调用一个线程的interrupt方法时,实际上就是将他的中断标志位置为true,意思是一种通知机制,告诉这个线程,你应该被中断。但是对于这个通知应该怎么处理,那就是这个线程自己的事情了。对于一个线程来说:
- 如果该线程处于sleep wait join 的阻塞过程中,那么线程会立刻退出阻塞状态,并且抛出 InterruptedException,同时清空中断标志。
- 如果这个线程处于runnable状态,那么调用它的interrupt,只会将他的中断标志位置为true,仅此而已,线程将继续运行。
一个线程如果有中断的需求,那可以这么做:
- 用while循环来检测标志位,如果是发现被中断了,则停止线程;
- 在调用阻塞方法(sleep,wait之类的)的时候及时处理中断异常。
补充:Thread.interrupted()清除标志位是为了下次继续检测标志位。
以上是关于解析Thread和Object类中的多线程难点的主要内容,如果未能解决你的问题,请参考以下文章
Day276.Thread和Object类中的重要方法 -Juc