对于多线程的情况下,怎样理解Object和Thread中有关线程的方法
1、先来了解下线程
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与 进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
出自:github高star JavaGuide学习引导
用一个例子了解学习引导
public static void main(String[] args) {
Thread t = new Thread(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("1");
});
t.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main");
}
这个例子展示了线程的join方法的使用方法。
在t线程执行结束之前,main线程一直阻塞。
下图为输出结果
这里就非常奇怪了,为什么join方法可以导致main线程阻塞,基于什么原理?
接着往下看。
2、基于Thread对象的分析
2.1 join方法
join()
本质是调用join(long millis)
public final void join() throws InterruptedException {
join(0);
}
join(long millis)
/**
* Waits at most {@code millis} milliseconds for this thread to
* die. A timeout of {@code 0} means to wait forever.
* 0表示永远等待
* <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.
*/
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) {//永远等待的判断,毫秒是否等于0
while (isAlive()) {//线程存活
wait(0);//本质是调用Object对象的wait方法,永远等待
}
} else {
while (isAlive()) {//指定时间的超时通用代码模式
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);//等待指定时间,然后继续执行
now = System.currentTimeMillis() - base;
}
}
}
join(long millis,int nanos)
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
join(millis);
1s = 10^3 millis 1秒等于1000毫秒
1millis=10^3 micros 1毫秒等于1000微秒
1 micros=10^3 nanos 1微秒等于1000纳秒
本质还是调用 join(millis);就看这个nanos是否满足条件,则算作一毫秒
2.2 sleep静态native方法
睡眠指定毫秒值后醒来,线程依然拥有对象监视器的所有权,而wait方法则是释放了对象监视器的所有权。
public static native void sleep(long millis) throws InterruptedException;
2.3 yield方法,
出让CPU时间片,重新回到就绪状态,yield方法使用的较为稀有,源码中给出的是
- It may be useful for debugging or testing purposes, where it may help
to reproduce bugs due to race conditions.
可以用于测试并发竞争的bug - 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();
JDK源码举例,在FutureTask阻塞时调用。
阻塞等待的时候,由于多线程执行的原因导致状态值变更由 COMPLETING->NORMAL的时候使用awaitDone(boolean timed, long nanos)
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
final long deadline = timed ? System.nanoTime() + nanos : 0L;
WaitNode q = null;
boolean queued = false;
for (;;) {
if (Thread.interrupted()) {
removeWaiter(q);
throw new InterruptedException();
}
int s = state;
if (s > COMPLETING) {
if (q != null)
q.thread = null;
return s;
}
/*
* A线程在改变状态(任务线程),B线程在for(;;)循环中(主线程)
* 当循环的时候,其他线程正在改变future对象的状态,
* 即将从COMPLETING转变为NORMAL之前的时候,当前线程在判断状态,
* 那么就出让CPU时间片,暂时不竞争,下次获取CPU时间片的时候,再来继续循环判断。
* 这就是yield方法的用法之一
*/
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
else if (q == null)
q = new WaitNode();
else if (!queued)
queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
q.next = waiters, q);
else if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
removeWaiter(q);
return state;
}
LockSupport.parkNanos(this, nanos);
}
else
LockSupport.park(this);
}
}
3、基于Object对象的分析
3.1 wait
wait()方法本质调用wait(0),forever wait until notify or notifyAll
wait的使用条件:当前线程必须拥有对象的监视器,wait时,当前线程释放监视器所有权,并进入等待状态直到被其他线程唤醒等待在该对象上等待获取对象监视器的所有权的其他线程(同一时间只有一个对象可以拥有对象监视器的所有权),使用该对象的notify方法或notifyAll方法可以唤醒,并且当前线程就可以等待重新获取对象的监视器所有权并回复执行。
/**
* The current thread must own this object's monitor. The thread releases ownership
* of this monitor and waits until another thread notifies threads waiting on this
* object's monitor to wake up either through a call to the {@code notify} method or
* the {@code notifyAll} method. The thread then waits until it can re-obtain
* ownership of the monitor and resumes execution.
*/
public final void wait() throws InterruptedException {
wait(0);
}
值得注意的是:该方法必须使用在同步锁中,//且尽量使用while循环,不使用if,防止虚假唤醒(spurious weak),虚假唤醒就是由于多线程消费的时候,在生产者notfiyAll时,其他等待的都被唤醒了,但是只有一个可以成功消费,那么其他的依然只能在while循环中继续wait,如果使用if,会导致所有被唤醒的线程继续向下执行,造成error
synchronized (obj) {
while (<condition does not hold>)
obj.wait();
// Perform action appropriate to condition
}
wait(long timeout)native方法
public final native void wait(long timeout) throws InterruptedException;
wait(long timeout,int nanos)
if (nanos > 0) {
timeout++;//只是+1了而已
}
wait(timeout);
3.2 notify
notify() native方法
该方法必须使用在同步锁中 、唤醒所有等待在此对象上等待获得对象监视器的所有权的线程,在当前线程放弃锁之前,唤醒线程将无法继续,也就是说同一时间只有一个线程能获取锁。
使用条件:
- 对象方法上的同步锁
- 对象方法里的同步块
- 对象类对象的静态方法
public final native void notify();
notifyAll() native方法
唤醒所有等待在在此对象上等待获取对象监视器的所有权的线程。
public final native void notifyAll();
个人认为:这里获取对象监视器所有权就是能否获取对象的锁,只有有锁,才能执行。
4、再回过头来看第一章的例子
为什么join方法可以导致main线程阻塞,基于什么原理?
继续看代码,写的注释。
public static void main(String[] args) {
Thread t = new Thread(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("1");
});
t.start();
try {
/*
* t.join表示的主线程main调用了,获得了t对象的锁,内部调用了wait(0)
* 那么调用wait的对象势必是Thread,也就是我们new的对象t,
* 在join方法内部的那对这句话的解释就是这样的,
* while (isAlive()) {
* wait(0);
* }
* isAlive()可以判断线程是否存活也就是t这个线程是不是还没有死亡,
* wait(0)表示的是一直等待,wait()方法本质也是调用wait(0)。
*
* 所以整个.join方法的理解是这样的:
* 1、main线程获得了t对象的锁(join方法上的synchronized关键字),获取的对象监视器的所有权,
* 然后在while循环的位置判断了线程存活,并wait等待,释放了t对象锁,释放对象监视器所有权,然后main线程等待被其他线程唤醒。
* 2、这个时候由于t线程在执行,接着t线程执行结束,由于线程t执行结束的时候,
* 会调用自己的notifyAll方法(非java实现,底层实现),然后等待获得此对象t的监视器的所有线程的等待都会被唤醒。
* 3、main线程就可以从wait状态被唤醒,然后接着判断isAlive(),
* 发现t线程没了(这里,是因为在Thread内部的isAlive方法,且是t.join方法,
* 虽然是main线程,但是是判断t线程是否存在),所有main线程可以继续走下去了。
*/
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main");
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyMMdd")));
}
这下是不是理解了这几个方法了?
5 总结
- join方法,使线程由异步执行,转为join至主线程一同执行。本质调用wait(0)进行阻塞的,而且当线程死亡时,线程会底层调用notifyAll方法,进行唤醒。
- yield方法出让线程cpu时间片
- sleep方法,使线程进入睡眠状态,但仍持有锁。
- wait方法,使线程进入等待状态,需在同步锁中执行,会释放锁,且配合着notify或者notifyAll方法执行,且需使用while循环而非if,防止虚假唤醒,
- notify方法,需在同步锁中执行使用需获得锁,且配合着wait方法使用。