1、wait()函数
当一个线程调用共享变量的wait()方法时,该线程就会被阻塞挂起,直到发生下面几件事之一才会返回:1、其他线程调用该共享对象的的notify()或者notifyAll()方法;2、其他线程调用该线程的interrupt()方法,该线程抛出InterruptedException()异常返回。
需要注意的是一个线程如果要调用wait()方法,则该线程必须事先获得对象的监视器锁,如果没有获得监视器锁,则在调用wait()方法时会抛出IllegalMonitorException()异常。
获得监视器锁的方法有以下几种:
1)、使用synchorized同步代码块时,使用该共享变量作为参数
2)、使用该共享变量的方法,在该共享变量的方法前加入synchorized修饰。
需要注意的是一个线程在其他线程没有调用notify()或者notifyAll()的进行通知,或者其他线程调用该线程的interrupt()使该线程中断的情况下或者等待超时,该线程也会从被挂起转为运行状态,这种情况被称为虚假唤醒。
另外注意的是当前线程在调用共享变量的wait()方法后只会释放当前共享变量上的锁,如果当前线程中还含有其他共享变量的锁,则这些锁不会释放。例如以下代码:
package com.yfq.demo;
/**
* @author YuanFengQiqo
* @date 2019/3/22 21:30
*/
public class Wait {
private static volatile Object obj1=new Object();//共享变量1
private static volatile Object obj2=new Object();//共享变量2
public static void main(String[] args) {
//线程锁1
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
//获取obj1锁
synchronized (obj1){
System.out.println("thread1 获得锁obj1");
//获取obj2锁
synchronized (obj2){
System.out.println("thread1 获得锁obj2");
//线程thread1阻塞,并释放掉锁obj1
try {
obj1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
//线程锁2
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj1){
System.out.println("thread2 获得锁obj1");
System.out.println("thread2 试图获得锁obj2");
synchronized (obj2){
System.out.println("thread2 获得锁obj2");
try {
System.out.println("thread1阻塞,并释放掉obj1锁");
obj1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
//启动thread1和thread2锁
thread1.start();
thread2.start();
//让main线程睡眠2秒,以达到让thread1和thread2先获取cpu的执行权
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//等待两个线程结束了,再执行main线程
System.out.println("main线程执行完毕");
}
}
|
注意:当前线程使用共享变量的wait()方法使当前线程阻塞挂起。如果其他线程中断了该线程,该线程会抛出一个InterruptedException异常并返回。
2、wait(long timeout)函数
当一个线程调用共享变量的wait(long time )函数阻塞挂起,在规定的超时时间内没有期它线程来调用该共享变量的notify()或者notifyAll(),则该线程会自动返回。
3、notify()函数和notifyAll()函数
当一个线程调用共享变量的notify()函数后,会唤醒另一个因调用该共享变量的wait()函数而阻塞挂起的线程。一个共享变量上可能有多个线程调用wait()被阻塞挂起,具体叫醒哪个线程是随机唤醒的。此外被唤醒的线程不能马上从wait方法返回并继续执行,这是因为它必须获取共享变量的监视器锁才能执行,也就是说唤醒它的那个线程需要释放掉共享变量的监视器锁,但是被唤醒的线程也不一定会获得监视器锁,因为它需要和其他线程竞争竞争该锁。只有被唤醒的线程获得了监视器锁才会继续执行。
4、join()函数
wait()、notify()、notifyAll()方法都是Object类的,而join()方法是Thread类的,join()是一个无参数无返回值得函数,比如多个线程一起下载资源,然后需要等多个线程都下载完毕后在汇总处理,join()就可以完成这样的事情。示例代码如下:
package com.yfq.demo;
/**
* @author YuanFengQiqo
* @date 2019/3/22 22:34
*/
public class Join {
public static void main(String[] args) throws InterruptedException {
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程1执行");
}
});
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程2执行");
}
});
//启动线程两个子线程
thread1.start();
thread2.start();
System.out.println("等待子线程执行完毕");
//等待子线程执行完毕后返回
thread1.join();
thread1.join();
System.out.println("子线程执行完毕,开始执行main线程");
}
}
|
从上面的代码中看出,启动两个线程,然后调用两个线程join()方法,当主线程执行到thread1.join()方法时会被阻塞,然后等待线程thread1执行完毕,thread1.join()执行完毕后就会返回。然后主线程执行到thread2.join()后,再次被阻塞,等待thread2线程执行完毕后返回。
注意:线程A调用线程B的join()会导致线程A阻塞,线程C调用线程A的interrupt()方法时,线程A会抛出InterruptedException异常而返回。实例代码如下:
package com.yfq.demo;
/**
* @author YuanFengQiqo
* @date 2019/3/22 22:51
*/
public class JoinAndInterrupt {
public static void main(String[] args) {
//创建线程一
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
//在线程一中死循环
for (;;){
}
}
});
//获取当前执行的main线程
Thread currentThread = Thread.currentThread();
//创建线程2
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//在线程2中调用主线程的intterupt()方法使主线程中断返回而不是一直处于阻塞状态
currentThread.interrupt();
}
});
//启动线程一
thread1.start();
thread2.start();
try {
thread1.join();
} catch (InterruptedException e) {
System.out.println("main线程因阻断异常,从阻塞中返回出来");
e.printStackTrace();
}
}
}
|
5、yield()函数
yield()函数是Thread的一个静态方法,当一个线程使用yield()方法时,实际上就是在告诉线程调度器该线程让出cpu的执行权,但是线程调度器可以无条件的忽略请求。我们知道操作系统为每一个线程创建一个时间片来占有cpu,正常情况下只有当一个线程的时间片用完了,线程调度器才会进行下一轮的线程调度。而当一个线程调用yield()函数时,是在告诉线程调度器自己的时间片没有用完,但是自己不想用了,你现在可以进行下一轮的时间调度了。
当一个线程调用yield()函数时,该线程会让出cpu执行权,然后处于就绪状态,然后线程调度器会从线程就绪队列中获取一个线程优先级最高的线程来获取cpu的执行权,当然也可能让刚才让出cpu执行权的那个线程获得cpu的执行权。
代码如下
package com.yfq.demo2;
/**
* @author YuanFengQiqo
* @date 2019/3/25 22:42
*/
public class Test implements Runnable{
@Override
public void run() {
for(int i=0;i<5;i++){
if(i%5==0) {
System.out.println(Thread.currentThread().getName() + "线程即将让出线程权");
Thread.yield();
}
}
System.out.println(Thread.currentThread().getName() + "线程出让线程权成功");
}
}
class TestYield{
public static void main(String[] args) {
Thread t1=new Thread(new Test());
Thread t2=new Thread(new Test());
Thread t3=new Thread(new Test());
t1.start();
t2.start();
t3.start();
}
}
|
yield和sleep的区别:当线程调用sleep()方法时,当前线程在指定的时间内被阻塞,在这个时间内,该线程不在被线程调度器调用,但是在该时间睡眠的时间内,该线程不会让出监视器锁,即该线程抱着资源睡觉,当睡眠时间结束后,该线程会处于就绪状态,当获得到cpu资源后就会继续执行。
当线程调用yield()时,该线程会让出cpu的执行权,处于就绪状态,不会被挂起阻塞,可能在线程调度器下一轮时间调度的过程中该线程会重新获得cpu的执行权。
6、守护线程和用户线程
java中的线程分为两类:守护线程和用户线程。例如,JVM启动时会调用main()函数,main函数所在的线程就是用户线程,而垃圾回收线程是守护线程。守护线程与用户线程的区别在于:当最后一个非守护线程结束时,JVM也就正常退出了,而不管当前是否有守护线程,换句话说守护线程是否结束,只要用户线程结束了,JVM就会正常退出。简单的说只要有一个用户线程没有结束,JVM就不能停止。创建守护线程,只需调用线程的setDaemo(true)即可。如果你希望在主线程运行完毕后,JVM立即停止,只需要在创建线程的时候,将线程设为守护线程即可。
7、ThreadLocal
ThreadLocal是JDK包提供的,当你创建一个ThreadLocal变量时,访问这个变量的线程都会有这变量的本地副本,当多个线程操作这个变量时,实际上是操作各自线程的本地副本,从而避免了,线程安全的问题。