线程通知与等待
wait()/wait(long timeout)
当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:
(1)其他线程调用了该共享对象的notify()或者notifyAll()方法;
(2)其他线程调用了该线程的interrupt()方法,该线程抛出 InterruptedException
异常返回。
// 生产者线程
synchronized(queue) {
// 消费队列满,则等待队列空闲
while(queue.size() == MAX_SIZE) {
try{
// 挂起当前线程,并释放同步块获取的queue上的锁,让消费者线程可以获取该锁
// 然后获取队列里面的元素
queue.wait();
}catch(Exception ex){
ex.printStackTrace();
}
}
// 空闲则生成元素,并通知消费者线程
queue.add(ele);
queue.notifyAll();
}
// 消费者线程
synchronized(queue) {
// 消费队列为空
while(queue.size() == 0) {
try{
// 挂起当前线程,并释放通过同步块获取的queue上的锁,让生产者线程可以获取锁
// 将生产元素放入队列
queue.wait();
}catch(Exception ex) {
ex.printStackTrace();
}
// 消费元素,并通知唤醒生产者线程
queue.take();
queue.notifyAll();
}
}
notify()/notifyAll()
一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的,被唤醒的线程不能马上从wait方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行
notifyAll()方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程
private static volatile Object resouceA = new Object();
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(()->{
synchronized (resouceA){
System.out.println("threadA get resourceA lock");
try {
System.out.println("threadA begin wait");
resouceA.wait();
System.out.println("threadA end wait");
}catch (InterruptedException e){
e.printStackTrace();
}
}
});
Thread threadB = new Thread(()->{
synchronized (resouceA){
System.out.println("threadB get resourceA lock");
try {
System.out.println("threadB begin wait");
resouceA.wait();
System.out.println("threadA end wait");
}catch (InterruptedException e){
e.printStackTrace();
}
}
});
Thread threadC = new Thread(()->{
synchronized (resouceA){
System.out.println("threadC begin notify");
// 调用notify 线程A,线程B,只有一个线程唤醒
resouceA.notify();
// 调用notifyAll 线程A和线程B 都唤醒
// resouceA.notifyAll()
}
});
threadA.start();
threadB.start();
Thread.sleep(1000);
threadC.start();
threadA.join();
threadB.join();
threadC.join();
}
只有当前线程获取到了
共享变量的监视器锁
后,才可以调用共享变量的notify()方法,否则会抛出IllegalMonitorStateException
异常
join()
java.lang.Thread#join()throws InterruptedException
等待线程执行终止join,可以响应中断
public static void main(String[] args) {
Thread threadA = new Thread(()->{
System.out.println("threadA begin run ");
for(;;){}
});
final Thread mainThread = Thread.currentThread();
Thread threadB = new Thread(()->{
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
// 中断主线程
mainThread.interrupt();
});
threadA.start();
threadB.start();
try {
// 等待threadA执行结束
threadA.join();
}catch (InterruptedException e) {
System.out.println("Main thread"+e);
}
System.out.println("main end");
}
threadA begin run
Main threadjava.lang.InterruptedException
main end
sleep()
Thread.sleep(1000)
让线程睡眠,可以响应中断
当一个执行中的线程调用了Thread的sleep方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与CPU的调度,获取到CPU资源后就可以继续运行了。如果在睡眠期间其他线程调用了该线程的interrupt()方法中断了该线程,则该线程会在调用sleep方法的地方抛出InterruptedException异常而返回
yelid
Thread类中有一个静态的yield方法,当一个线程调用yield方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗示。
当一个线程调用yield方法时,当前线程会让出CPU使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出CPU的那个线程来获取CPU执行权
sleep与yield方法的区别在于,当线程调用sleep方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。而调用yield方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。
线程上下文切换
在多线程编程中,线程个数一般都大于CPU个数,而每个CPU同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的,CPU资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,这就是上下文切换,从当前线程的上下文切换到了其他线程。
那么就有一个问题,让出CPU的线程等下次轮到自己占有CPU时如何知道自己之前运行到哪里了?
所以在切换线程上下文时需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场。
线程上下文切换时机有:当前线程的CPU时间片使用完处于就绪状态时,当前线程被其他线程中断时
线程中断
-
void interrupt()
中断线程,例如,当线程A运行时,线程B可以调用线程A的interrupt()方法来设置线程A的中断标志为true并立即返回。设置标志仅仅是设置标志,线程A实际并没有被中断,它会继续往下执行。如果线程A因为调用了wait系列函数、join方法或者sleep方法而被阻塞挂起,这时候若线程B调用线程A的interrupt()方法,线程A会在调用这些方法的地方抛出InterruptedException异常而返回
线程通过检查此时是否被中断来进行相应,可以通过isInterrupted()来判断是否被中断。 -
boolean isInterrupted()
检测当前线程是否被中断,如果是返回true,否则返回false -
Thread.interrupted()
检测当前线程是否被中断,如果是返回true,否则返回false。与isInterrupted不同的是,该方法如果发现当前线程被中断,则会清除中断标志.
使用Interrupted优雅退
public void run {
try{
// 线程推出条件
while(!Thread.currentThread().isInterrupted()&& other){
// do more work
}
}catch(InterruptedException e) {
}
}
守护线程和用户线程
Java中的线程分为两类,分别为daemon线程(守护线程)和user线程(用户线程)。在JVM启动时会调用main函数,main函数所在的线程就是一个用户线程,其实在JVM内部同时还启动了好多守护线程,比如垃圾回收线程
t.setDaemon(true); //设置守护线程
区别: 主线线程结束后,用户子线程继续运行,守护线程立即结束。
线程死锁
死锁的产生必须具备以下四个条件
● 互斥条件:指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
● 请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
● 不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
● 环路等待条件:指在发生死锁时,必然存在一个线程—资源的环形链,即线程集合{T0,T1, T2, …, Tn}中的T0正在等待一个T1占用的资源,T1正在等待T2占用的资源,……Tn正在等待已被T0占用的资源。
可以通过线程堆栈查看死锁情况