本文内容提要:wait()、notify()、join()、sleep()、yield()、interrupt()、ThreadLocal、InheritThreadLocal、TransmittableThreadLocal。
Thread的生命周期
Thread的生命周期分为初始化,就绪,运行,阻塞,终止,其中只有运行状态的线程拥有CPU资源的时间片。
Object-线程的wait()和notify()
线程的等待和通知方法放在Object类里而非Thread类,对于wait()方法来说,必须在调用之前获取对应实例的监视器锁,否则会抛出IllegalMonitorStateException。而通常,锁资源可以是任意对象,把wait()、notify()、notifyAll()方法放在Obejct方法里,符合Java把所有类都会使用的方法定义在Object类的思想。
注意:正如前文所提:调用wait()之前,必须在调用之前获取对应实例的监视器锁。
private static void interruptTest() throws InterruptedException {
Integer obj = 1;
Thread a = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("this is begining");
try {
synchronized (obj) {
obj.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("this is ending");
}
});
a.start();
a.join();
}
当线程调用wait方法后,会释放锁资源,并进入阻塞状态。等待其它线程调用notify()方法、或者notifyAll()方法唤醒,或者interrupt中断和wait(time)调用后等待超时的虚假唤醒。当调用notify()函数,且对于锁对象obj,存在多个线程处于阻塞状态,会随机选一个进行唤醒。而notifyAll()则会唤醒obj下所有阻塞的对象。注意:唤醒并不代表立刻执行,而是竞争锁,竞争到锁后才会到就绪状态,只有等到竞争到CPU资源也就是时间片后才变成运行状态继续执行。
上述的运行->阻塞->就绪->执行的状态转换涉及到一个细节,就是线程如何知道再次执行时从哪里开始继续往下执行,因此会在阻塞时,或者说进行时间片切换时,记录当前执行地址,这里用到的是线程私有的程序计数器。
Thread里的方法
等待线程终止的join()方法
有时候存在这样的需求,主线程开启n个子线程,并希望在所有子线程结束后在进行一些逻辑操作。这时候就需要用到join()方法。
public static void main(String[] args) throws InterruptedException {
final Thread mainThread = Thread.currentThread();
Thread a = new Thread(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("subThread is over");
});
a.start();
a.join();
System.out.println("main is over");
}
输出结果为:
subThread is over
main is over
主线程在调用了a线程后进入阻塞状态,这时可以通过interrupt()方法中断阻塞状态。
public static void main(String[] args) throws InterruptedException {
final Thread mainThread = Thread.currentThread();
Thread a = new Thread(() -> {
while (true) {
}
});
Thread b = new Thread(() -> {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
mainThread.interrupt();
});
b.start();
a.start();
try {
a.join();
} catch (InterruptedException e) {
System.out.println("main is interrupted");
}
System.out.println("main is over");
}
输出结果为:
main is interrupted
main is over
让线程睡眠的sleep()方法
sleep()方法会让当前线程进入阻塞状态,但不会释放锁资源。
public static void main(String[] args) throws InterruptedException {
final Thread mainThread = Thread.currentThread();
Integer lock1 = 1, lock2 = 1;
Thread a = new Thread(() -> {
synchronized (lock1) {
System.out.println("a get lock1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("a get lock2");
}
}
});
Thread b = new Thread(() -> {
synchronized (lock1) {
System.out.println("b get lock1");
synchronized (lock2) {
System.out.println("b get lock2");
}
}
});
a.start();
b.start();
a.join();b.join();
System.out.println("main is over");
}
在上面的例子中,由于a线程先获取到锁资源lock1,即使a调用sleep()方法进入阻塞状态,b线程仍然无法获取到锁资源lock1(即lock1在a线程sleep()之后并没有被释放)。注意:sleep()入参不能为负数,会抛出异常。
让出CPU时间片的yield()方法
yield()方法调用后会暗示线程调度器希望让出当前线程所占的时间片,但是线程调度器可以无条件忽略这个暗示。如果yield()方法成功让出CPU时间片,就会进入就绪状态,等待重新竞争到时间片继续执行。所以,存在这样的情况线程A在调用yield()方法后,通过竞争在下一轮线程调度中再次获取到了时间片。同样的,yield()方法不会让出锁资源,下面的demo可以证明即使a线程yield(),b线程获取到时间片开始执行,仍然无法获取到lock1资源,所以输出结果仍然是先执行完a线程。
public static void main(String[] args) throws InterruptedException {
final Thread mainThread = Thread.currentThread();
Integer lock1 = 1, lock2 = 1;
Thread a = new Thread(() -> {
synchronized (lock1) {
System.out.println("a get lock1");
Thread.yield();
synchronized (lock2) {
System.out.println("a get lock2");
}
}
});
Thread b = new Thread(() -> {
System.out.println("b get cpu!");
synchronized (lock1) {
System.out.println("b get lock1");
synchronized (lock2) {
System.out.println("b get lock2");
}
}
});
a.start();
b.start();
a.join();
b.join();
System.out.println("main is over");
}
输出结果为:
a get lock1
b get cpu!
a get lock2
b get lock1
b get lock2
main is over
如果将yield()替换为wait(),a线程进入阻塞状态后,释放资源,b线程成功获取到lock1锁资源,输出结果证明他们的差异。
public static void main(String[] args) throws InterruptedException {
final Thread mainThread = Thread.currentThread();
Integer lock1 = 1, lock2 = 1;
Thread a = new Thread(() -> {
synchronized (lock1) {
System.out.println("a get lock1");
try {
lock1.wait();
} catch (InterruptedException e) {
System.out.println("a is interrupted");
}
synchronized (lock2) {
System.out.println("a get lock2");
}
}
});
Thread b = new Thread(() -> {
System.out.println("b get cpu!");
synchronized (lock1) {
System.out.println("b get lock1");
synchronized (lock2) {
System.out.println("b get lock2");
}
a.interrupt();
}
});
a.start();
b.start();
a.join();
b.join();
System.out.println("main is over");
}
输出结果:
a get lock1
b get cpu!
b get lock1
b get lock2
a is interrupted
a get lock2
main is over
设置中断标志的interrupt()方法
前文的最佳配角interrupt()方法,并非暴力地直接中断对应的线程,而是对对应的线程设置中断标志。
// 检测当前实例线程是否被中断,中断true,否则false
private native boolean isInterrupted(boolean ClearInterrupted);
// 检测当前线程是否被中断,如果发现线程被中断,会清除中断标志,返回true。否则返回false
private native boolean interrupted(){
return currentThread().isInterrupted(true);
}
// 设置中断标志位true
public void interrupt();
interrupted()检测的是当前线程
这里要注意的是interrupted()检测的是当前线程,跟句柄无关。如下面的demo:
public static void main(String[] args) throws InterruptedException {
final Thread mainThread = Thread.currentThread();
Thread a = new Thread(() -> {
while (true) {
}
});
a.start();
a.interrupt();
System.out.println();
System.out.println("is interrupted :" + a.isInterrupted()); // 1
System.out.println("is interrupted :" + a.interrupted()); // 2
System.out.println("is interrupted :" + a.isInterrupted()); // 3
}
输出结果:
true
false
true
2处虽然句柄为a线程,但是正如前文所述,在interrupted()方法中会调用Thread.getCurrentThread()方法获取当前线程,获取到线程为主线程,而主线程并未被中断,所以输出false。
interrupt() 只是设置中断标志,并非直接中断
public static void main(String[] args) throws InterruptedException {
final Thread mainThread = Thread.currentThread();
Thread a = new Thread(() -> {
while (true) {
System.out.println("a is working");
}
});
a.start();
a.interrupt();
a.join();
}
输出结果:
a is working
a is working
a is working
a is working
a is working
...
可以发现如果a在内部没有调用wait、sleep等方法进入阻塞状态,就不会被中断。
ThreadLocal — 你不得不知道的坑
ThreadLocal只能在保证当前线程可以获取到对应的变量。
考虑到存在这样的情况,主线程在ThreadLocal中放了参数,并启用了多个子线程进行工作,同时子线程需要用到前面主线程在ThreadLocal中放置的参数。这时候考虑到用InheritThreadLocal,在Thread.init()方法源码中可以看到,当线程初始化时,InheritThreadLocal中存放的参数会被复制到子线程的InheritThreadLocal中。
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
...
...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
那么是否InheritThreadLocal就已经能解决多线程问题了呢?答案是并不能。因为我们知道在线程池的使用中,为了减少线程初始化和销毁的性能消耗,提出了线程复用的概念。对于核心线程来说,一旦被初始化后,就不会被销毁。对于InheritThreadLocal而言,其变量的传递主要依赖于Thread.init()方法中进行参数复制传递。
所以当使用线程池时,会发现每个线程的InheritThreadLocal中的参数,一旦被赋值后就不会再更新,也就失去了它的正确性,可以理解为是非线程安全的。这时候可以考虑使用TransmittableThreadLocal来解决,具体可见TTL项目的官网说明(如传递链路id等)。