Java 线程的基本使用
线程的2种实现方式
1、继承Thread类,重写run()方法
public class Thread1 extends Thread{ @Override public void run() { //要执行的代码 while (true){ System.out.println("thread1 is running...."); } } }
Thread1 thread1 = new Thread1(); thread1.start();
2、实现Runnable接口
public class MyRunnable implements Runnable{ @Override public void run() { //要执行的代码 while (true) { System.out.println("thread2 is running...."); } } }
MyRunnable myRunnable = new MyRunnable(); Thread thread2 = new Thread(myRunnable); thread2.start();
相比而言,第一种要简单些。
我们测试下:
public class Test { public static void main(String[] args) { Thread1 thread1 = new Thread1(); thread1.start(); MyRunnable myRunnable = new MyRunnable(); Thread thread2 = new Thread(myRunnable); thread2.start(); } }
运行,看到thread1、thread2交替执行
线程的start()方法是初始化一个线程、分配一条线程所需的资源,把这条线程放到调度队列中,让线程处于ready状态。获取到cpu的使用权时自动调用线程的run()方法执行代码。
如果是直接调用run()方法,那就只是在当前线程中调用一个类的一个方法,并没有使用多线程。
线程的6种状态
Thread类在内部枚举类中定义了6种状态:
public enum State { NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED; }
1、New 新建
new Thread()已经创建线程,但尚未调用start()启动线程
2、RUNNABLE 可运行
Java把就绪(ready)和运行(running)两种状态合并为一种状态:可运行(runnable)。
调用了start方法,线程就处于可运行状态(就绪)。此线程获取到时间片后,开始执行run()中的代码,处于运行(running)状态。
3、BLOCKED 阻塞
处于阻塞状态的线程并不会占用CPU资源。以下情况会让线程进入阻塞状态:
①等待获取锁
等待获取一个锁,而该锁被其它线程持有,则该线程进入阻塞状态。当其它线程释放了该锁,并且线程调度器允许该线程持有该锁时,该线程退出阻塞状态。
②IO阻塞
线程发起了一个阻塞式IO后也会进入阻塞状态,比如等待用户输入内容然后继续执行。
IO是很广的概念,包括磁盘IO、网络IO等。
4、WAITING 无限期等待
处于此种状态的线程不会被分配 CPU 时间片,需要被其它线程显式地唤醒,才会进入就绪状态。
以下3个方法会让线程进入无限等待状态
- Object.wait() 结束:Object.notify() / Object.notifyAll()
- Thread.join() 结束:被调用的线程执行完毕
- LockSupport.park() 结束:LockSupport.unpark(currentThread)
5、TIMED_WAITING 限时等待
处于这种状态的线程也不会被分配CPU 时间片,但在指定时间之后会被系统自动唤醒,进入就绪状态。
以下3个方法会让线程进入限时等待状态:
- Thread.sleep(time) 方法 结束:sleep时间结束
- Object.wait(time) 方法 结束:wait时间结束,或者调用Object.notify() / notifyAll()
- LockSupport.parkNanos(time)/parkUntil(time) 方法 结束:park时间结束,或者调用LockSupport.unpark(currentThread)
处于阻塞状态、无限期等待、限时等待的线程都会让出cpu的使用权,阻塞结束、显式唤醒后进入就绪状态,需要重新获取时间片才能接着运行。
6、TERMINATED 死亡
线程结束任务之后自动消亡,或者线程执行时发生了异常而结束。
线程的生命周期
常用方法
1、获取当前线程
Thread thread = Thread.currentThread(); //获取当前线程
2、线程的基本信息
Thread thread = new Thread1(); //只要是Thread类的对象即可 System.out.println(thread.getId()); //线程id。 System.out.println(thread.getName()); //线程名,不是线程对象名。主线程为main,其它为Thread-n,n从0开始 System.out.println(thread.getPriority()); //优先级,未指定时默认为5 System.out.println(thread.getState()); //线程状态。6种状态中的某一种 System.out.println(thread.isDaemon()); //是否是守护线程,默认是false
Thread thread = new Thread1(); //只要是Thread类的对象即可 thread.setName("my_thread"); //设置线程名,默认主线程是main,其它线程以Thread-n的方式命名 thread.setPriority(5); //设置线程优先级,[1,10]上的整数,默认为5。优先级高的线程优先执行 thread.setPriority(Thread.MIN_PRIORITY); //1 thread.setPriority(Thread.NORM_PRIORITY); //5 thread.setPriority(Thread.MAX_PRIORITY); //10 thread.setDaemon(true); //设置是否为守护线程,默认false thread.start(); //设置线程的基本信息要在start()启动线程之前
3、线程的优先级
不同的操作系统, 对线程优先级的支持不同,一般情况下优先级高的线程优先分配时间片,优先被执行,但并一定是如此。
写代码而定时候, 不要过度依赖线程优先级, 如果程序运行是否正确取决于是否按所设置的优先级运行, 那可能会出问题。最好加同步锁。
线程的优先级具有传递性:
public class Test { public static void main(String[] args) throws InterruptedException { Thread.currentThread().setPriority(10); Thread thread = new Thread1(); thread.start(); //在一个线程中start()开启另一个线程,开启的线程会自动继承开启它的线程的优先级 System.out.println(thread.getPriority()); //10 } }
4、守护线程
线程分为2类:用户线程、守护线程。线程默认为用户线程(main线程默认也是用户线程)。
如果一个程序|应用中只有一个线程在运行,且这个线程是守护线程,则jvm直接退出,程序终止运行。
public class Test { public static void main(String[] args) throws InterruptedException { Thread1 thread1 = new Thread1(); thread1.setDaemon(true); //守护线程 thread1.start(); System.out.println("main"); //至此main线程(用户线程)执行完毕,只有一个线程,且是守护线程,jvm退出,守护线程不再继续执行,程序终止 } }
start()开启线程后,这句代码就算执行完了,继续执行后面的代码,并不是说要等开启的线程执行完毕、这句代码才算执行完。
public class Test { public static void main(String[] args) throws InterruptedException { Thread1 thread1 = new Thread1(); thread1.setDaemon(true); //守护线程 thread1.start(); Thread2 thread2 = new Thread2(); thread2.start(); //用户线程 System.out.println("main"); //至此main线程执行完毕 } }
thread2、main线程执行完毕后,jvm退出,thread1不再继续执行,程序终止。
如果没有用户线程,但是有2个及以上的守护线程在运行,则jvm不会退出,程序(守护线程)继续执行。
5、join() Thread的实例方法,在当前线程中加入另一条线程
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread1 thread1 = new Thread1();
thread1.start();
thread1.join(); //在当前线程(此处是主线程)中加入线程thread1(把thread1加入当前线程中)。join()要放在start()之后
//....
}
}
如果不使用join(),2条线程会交替执行,使用join()之后,会先把加入的线程执行完,再执行当前线程。
6、yield() 线程让步,Thread类的静态方法
当前线程让出cpu的使用权,进入ready状态。
这个方法是不可靠的,不一定会真的让出来,视系统当前的运行状况而定。
7、sleep() Thread类的静态方法,使当前线程休眠指定时间
public class Thread1 extends Thread{ @Override public void run() { System.out.println("thread1 is running..."); try { Thread.sleep(500); //使当前线程休眠500ms,会让出cpu的使用权,500ms后自动苏醒,进入ready状态
Thread.sleep(500,100); //精确到纳秒。参数:毫秒、纳秒 Thread.sleep(0); //触发操作系统立刻重新进行一次CPU竞争。 // Thread1继承自Thead,也可以使用Thread1来操作 } catch (InterruptedException e) { //如果在线程休眠时,此线程被中断,会抛出线程中断异常,并立即唤醒此线程,进入ready状态。因为线程的中断只能由线程本身来控制 e.printStackTrace(); } System.out.println("thread1 is running again..."); } }
8、wait()、notify()、notifyAll()
这三个方法需要控制对对象的控制权(monitor),所以属于Object类,均为Object的实例方法。
这三个方法能够被调用的前提是已经获取了相应的互斥锁,所以这三个方法都只能在同步代码块中使用。
wait(),把持有该对象线程的对象控制权交出去(释放锁),然后处于等待状态。
notify(),会通知某个正在等待这个对象的控制权的线程可以继续运行(获取锁)。
nofifyAll(),会通知所有等待这个对象控制权的线程继续运行,如果有多个正在等待该对象控制权时,具体唤醒哪个线程,就由操作系统进行调度。
使用示例:
public class Thread1 extends Thread { public static Object lock = new Object(); //锁。可以把要使用的公共资源直接作为锁,比如大家都要使用的文件,也可以把一个Object对象作为锁,获取到锁后,才可以操作公共资源 @Override public void run() { //.... //同步代码块 synchronized (lock){ //获取锁 System.out.println("已获取到锁,开始使用资源"); //..... try { lock.wait(); //让出cpu使用权,释放锁,进入WAITING状态(无限期等待),如果要继续运行,需要在其它线程中显式唤醒
//wait(1000); //进入TIMED-WAITING(限时等待状态),指定时间后会自动唤醒,进入ready状态
//wait(1000,100); //精确到纳秒 } catch (InterruptedException e) { e.printStackTrace(); } //..... //如果同步代码块中、wait()后面还有代码,被其它线程唤醒后进入ready状态,获取到时间片后需要重新获取锁后才能继续执行后面的代码 } //..... //如果同步代码块中、wait()后面没有代码,则被其它线程唤醒后进入ready状态,获取到时间片后就继续执行,无需获取锁 } }
public class Thread2 extends Thread { public static Object lock = Thread1.lock; //锁,要是同一个对象 @Override public void run() { //.... //同步代码块 synchronized (lock){ lock.notify(); // lock.notifyAll(); System.out.println("已获取到锁,开始使用资源"); //..... } //..... } }
wait()让出锁后,这把锁分配给等待获取这个锁的哪个线程?
如果等待这个锁的某个线程在同步代码块中使用了notify(),则唤醒该线程,直接把锁分配给这个线程。
如果等待这个锁的那些线程都使用notifyAll(),则唤醒所有等待这把锁的线程,由操作系统调度,由操作系统决定分配给哪个线程。
public class Test { public static void main(String[] args) { Thread1 thread1 = new Thread1(); thread1.start(); Thread2 thread2 = new Thread2(); thread2.start(); } }
需要先启动wait()方法所在的线程,再启动notify()方法所在的线程
锁怎么写?
- 写成public static的成员变量,其它线程直接获取
- 写成private,设置一个构造方法传入锁,锁对象在父线程(比如上面的主线程)中创建
怎么写都行,只要是同一个锁对象(地址相同)。
9、线程中断
如果要在sleep()、wait()指定的时间之前就唤醒线程,或者wait()未指定等待时间、要唤醒线程,都可以使用interrupt()来唤醒线程。
sleep()、wait()都是放在try中的,如果在休眠、等待期间,在其它线程中使用interrupt()中断正在休眠、等待的线程,增在休眠|等待的线程会抛出中断异常,并立即被唤醒,进入ready状态。
其实如果wait()未指定等待时间,等其它等待这个锁的线程都用完了这个锁(没线程使用这个锁的时候),也会被自动唤醒。
- interrupt() Thread类的实例方法,中断线程。比如thread1.interrupt();中断thread1。
只是设置一个中断标志(发送一个中断信号),由该线程自己决定怎么做(在合适的时间、代码处停下来),并不是说该线程一定会马上被中断(其实往往会继续运行)
除了中断正在运行的线程,还可以唤醒沉睡|等待的线程。
- isInterrupted() Thread类的实例方法,判断线程是否真的被中断了
- interrupted() Thread类的静态方法,判断当前线程是否真的被中断了,并清除之前设置的中断标志
10、wait()和sleep()的区别
①sleep()是Thread类的静态方法,wait()是Object类的实例方法。
②sleep()不会释放锁,当前线程仍持有锁;wait()会释放锁,被唤醒后进入ready状态,获取时间片后需要重新获取锁。
③sleep()必须捕获异常,而wait()、notify()、notifyAll()不需要捕获异常。
④wait()、notify()、notifyAll()必须在同步方法或同步代码块中调用(总之要先获取到锁),而sleep没有这方面的限制。
11、park()、unpark()
均为LockSupport类的静态方法。
LockSupport.park()将当前线程挂起,让出cpu的使用权。park()、parkNanos()、parkUntil(),均有多个重载方法,可以设置挂起时间、锁对象。
注意:只是将当前线程挂起,并不会释放锁。
LockSupport.unpark(Thread thread)将指定线程立即恢复运行(ready)状态。等park()指定的时间到了,或者等待这个锁的所有线程都执行完毕,也会重新回到运行状态。
thread.interrupt()线程中断具有unpark()同样的功能。
LockSupport比Object的wait()、notify()有两大优势:
①LockSupport不需要写在同步代码块里 ,所以线程间也不需要维护一个共享的同步对象了(锁),实现了线程间的解耦。
②unpark()方法可以先于park()方法调用,无需担心线程间执行的先后顺序。