目录
线程释放资源的三种方法
系统内部开销:yield < sleep < wait
Object.wait()
wait()方法是一个成员方法,用于主动放弃当前线程获得的对象锁,进入阻塞态,并等待锁对象上的notify()/notifyAll()方法调用,以唤起当前阻塞的线程。
当前线程被唤起时,需要再次尝试获取同步对象的监听器(锁),以能够进入继续执行同步代码块,因此wait()方法被在同步代码块中被调用。
时间开销:线程状态切换时间 + 线程等待时间 + 线程调度时间 + 对象锁获取时间
java
/**
* Causes the current thread to wait until another thread invokes the
* {@link java.lang.Object#notify()} method or the
* {@link java.lang.Object#notifyAll()} method for this object.
* In other words, this method behaves exactly as if it simply
* performs the call {@code wait(0)}.
* <p>
* 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.
* <p>
* As in the one argument version, interrupts and spurious wakeups are
* possible, and this method should always be used in a loop:
* <pre>
* synchronized (obj) {
* while (<condition does not hold>)
* obj.wait();
* ... // Perform action appropriate to condition
* }
* </pre>
* This method should only be called by a thread that is the owner
* of this object's monitor. See the {@code notify} method for a
* description of the ways in which a thread can become the owner of
* a monitor.
*
* @throws IllegalMonitorStateException if the current thread is not
* the owner of the object's monitor.
* @throws InterruptedException if any thread interrupted the
* current thread before or while the current thread
* was waiting for a notification. The <i>interrupted
* status</i> of the current thread is cleared when
* this exception is thrown.
* @see java.lang.Object#notify()
* @see java.lang.Object#notifyAll()
*/
public final void wait() throws InterruptedException {
wait(0);
}
Thread.sleep()
sleep()方法是一个静态成员方法,调用时主动挂起当前线程,线程不会赶往阻塞态,也不会释放对象监听器,不过在调用此方法时需要指定睡眠时间,以使用线程调度器能够在指定的时间再次唤起当前线程,继续从挂载点执行。
时间开销:线程等待时间 + 线程调度时间
java
/**
* 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;
Thread.yield()
yield()方法是一个静态成员方法,会向调度器发送一个请求,表示愿意主动释放自己占用的处理器,避免自己过度占用,导致其它线程饥饿,但这并不意味着当前线程一定会释放资源,这取决于调度器的运行时调度策略,因此此方法并非用于资源同步目的,而是为了提升多线程场景下的处理性能。
调用此方法后,当前线程只会主动释放占用的CPU,而会使当前线程从运行态(RUNNING)转换到就绪态(READY)且不会释放对象锁,同时也不会保证一定能够释放处理器。
时间开销: 线程调度时间
java
/**
* 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();
Thread.yield()的特性
- 相比于sleep/wail方法,此方法的副作用最小,只会影响CPU时间片的分配;
- 适用于计算密集型的工作场景,当资源充足时,收益并不明显或没有;
- 不是用于线程同步目的,而是为了协调多线程的工作进程,减缓当前线程对于CPU的长期占用。
- 程序设计时,通常结合CAS方法使用,避免线程阻塞或等待;
- 调用方法,并不意味着当前线程一定会释放处理器,这取决于调度器。
使用Thread.yield()测试高负载场景下的多线程协作
测试代码的设计思想:
- 获取当前系统可用的处理器数量,这里为10;
- 创建10个Looping线程,尽最大努力占用处理器时间片,不过其中一个线程,t1需要调用yield()方法,以测试方法调用前后的效果。
- 额外创建一个工作线程t2,以验证yield()方法调用时,这个线程确实受到了影响。
为了能够方便观测t2线程确实受到了影响,即yield()方法确实提升了多线程的相对工作进度,这里在t1线程looping时进行计数,以查看t2线程执行时的数字大小,从而判定影响。
scala
public statick void main() {
var sync = 0
var t2Run = false
var quit = false
// 10 processors
var processors = Runtime.getRuntime.availableProcessors()
println("Available processors: " + processors)
// make 1 thread to loop and to yield after counted 8,000,000
val t1 = new Thread {
override def run(): Unit = {
while (!quit) {
if (sync > 8000000) {
println(s"${Thread.currentThread().getName} - hopefully t2 to work $sync")
t2Run = true
// Key point
Thread.`yield`()
}
sync += 1
}
}
}
t1.start()
// make 8 threads to looping
while (processors > 1) {
val t = new Thread {
override def run(): Unit = {
while(true) {
// busy cpu
}
}
}
t.start()
processors -= 1
}
Thread.sleep(2000)
val t2 = new Thread {
override def run(): Unit = {
while (!t2Run) {
// looping to wait breaking
}
println(s"${Thread.currentThread().getName} - working with $t2Run")
quit = true
println(s"${Thread.currentThread().getName} - working done")
System.exit(0)
}
}
t2.start()
while (true) {
// main thread looping
}
}
测试结果
重点观测计数器的显示值,以判定yield()方法调用前后的差别。
不调用yield()方法
shell
实验一:
Thread-14 - hopefully t2 working 8496165
Thread-14 - hopefully t2 working 8496166
Thread-14 - hopefully t2 working 8496167
Thread-24 - working with true
Thread-24 - working done
Thread-14 - hopefully t2 working 8496168
实验二:
Thread-14 - hopefully t2 working 8535080
Thread-14 - hopefully t2 working 8535081
Thread-14 - hopefully t2 working 8535082
Thread-24 - working with true
Thread-14 - hopefully t2 working 8535083
Thread-24 - working done
实验三:
Thread-14 - hopefully t2 working 8633271
Thread-14 - hopefully t2 working 8633272
Thread-14 - hopefully t2 working 8633273
Thread-24 - working with true
Thread-24 - working done
Thread-14 - hopefully t2 working 8633274
调用yield()方法
shell
实验一:
Thread-14 - hopefully t2 working 8092040
Thread-14 - hopefully t2 working 8092041
Thread-14 - hopefully t2 working 8092042
Thread-24 - working with true
Thread-24 - working done
实验二:
Thread-14 - hopefully t2 working 8127893
Thread-14 - hopefully t2 working 8127894
Thread-14 - hopefully t2 working 8127895
Thread-24 - working with true
Thread-24 - working done
实验三:
Thread-14 - hopefully t2 working 8004998
Thread-14 - hopefully t2 working 8004999
Thread-14 - hopefully t2 working 8004999
Thread-24 - working with true
Thread-24 - working done
测试结论
经过多轮实验,大部分情况下,调用yield()
方法时,t2线程的执行时间提前了(计数器最大8128893
),也意味着整个JVM进程会提前退出;而不调用
时,t2线程的执行触发时间被延迟了,计数器最大为8633271
。
由此看,yield()
方法确实能够达到主动释放处理器/cpu的目的,但释放时机是不确定的,同时是否一定会释放取决于调度器的实时决策。
yield
方法能够达到协调多线程的相对处理进度,通过线程主动释放正在占用的CPU时间片,从一定上提升了系统的整体性能,但具体是能够提升多少需要根据实际场景进行充分的测试。
Thread.yield()在ForkJoinPool中的应用
ForkJoinPool是在JDK 1.7版本引入的,采用分治方法,将一个大任务拆分成多个子任务并行计算再聚合的工作模式,适用于计算密集型的工作场景,它的主要特性如下:
- ForkJoinPool是一个使用分治策略递归执行任务的线程池。
- 它被诸如Kotlin和Akka之类的JVM语言用来构建消息驱动的应用程序。
- ForkJoinPool并行执行任务,从而实现计算机资源的高效使用。
- 工作窃取算法通过允许空闲线程从繁忙线程中窃取任务来优化资源利用率。
- 任务存储在一个双端队列中,LIFO 策略用于存储,FIFO用于窃取。
- ForkJoinPool框架中的主要类包括ForkJoinPool、RecursiveAction和RecursiveTask:
- RecursiveAction用于计算递归操作,不返回任何值。
- RecursiveTask类似,但返回一个值。
- compute()方法在两个类中都被重写以实现自定义逻辑。
- fork()方法调用compute()方法并将任务分解为更小的子任务。
- join()方法等待子任务完成并合并它们的结果。
- ForkJoinPool 通常与并行流和CompletableFuture一起使用。
awaitQuiescence(...)
方法旨在使线程池尽快沉寂,即利用work-stealing
机制尽快完成任务,因此内部会使用while循环
不断地查检队列中是否还有空闲线程能够帮忙处理任务
,因此为了避免在无可用资源的情况下,一直消耗CPU资源同时避免线程阻塞,在方法实现时调用了Thread.yield()
方法,试图在每一次检查时都主动释放CPU时间片,以尝试加快其它线程 的进度 。
java
public class ForkJoinPool extends AbstractExecutorService {
/**
* If called by a ForkJoinTask operating in this pool, equivalent
* in effect to {@link ForkJoinTask#helpQuiesce}. Otherwise,
* waits and/or attempts to assist performing tasks until this
* pool {@link #isQuiescent} or the indicated timeout elapses.
*
* @param timeout the maximum time to wait
* @param unit the time unit of the timeout argument
* @return {@code true} if quiescent; {@code false} if the
* timeout elapsed.
*/
public boolean awaitQuiescence(long timeout, TimeUnit unit) {
long nanos = unit.toNanos(timeout);
ForkJoinWorkerThread wt;
Thread thread = Thread.currentThread();
if ((thread instanceof ForkJoinWorkerThread) &&
(wt = (ForkJoinWorkerThread)thread).pool == this) {
// 如果当前线程是worker线程,则消费自己队列中的任务
helpQuiescePool(wt.workQueue);
return true;
}
// 非pool中的内部线程时,就尝试切分任务,即stealing任务
long startTime = System.nanoTime();
WorkQueue[] ws;
int r = 0, m;
boolean found = true;
while (!isQuiescent() && (ws = workQueues) != null &&
(m = ws.length - 1) >= 0) {
if (!found) {
if ((System.nanoTime() - startTime) > nanos)
return false;
// 这里是重点,当没有可用资源时,申请释放占用的CPU时间片,
// 避免一直循环判断,浪费宝贵的时间片
Thread.yield(); // cannot block
}
found = false;
for (int j = (m + 1) << 2; j >= 0; --j) {
ForkJoinTask<?> t; WorkQueue q; int b, k;
if ((k = r++ & m) <= m && k >= 0 && (q = ws[k]) != null &&
(b = q.base) - q.top < 0) {
found = true;
// 执行子任务
if ((t = q.pollAt(b)) != null)
t.doExec();
break;
}
}
}
return true;
}
}
ForkJoinPool的工作原理
参考文档:
java线程池(四):ForkJoinPool的使用及基本原理
Java中使用ForkJoinPool的实现示例