1. 线程使用
线程的创建,分为以下三种方式:
- 继承 Thread 类,重写 run 方法
- 实现 Runnable 接口,实现 run 方法
- 实现 Callable 接口,实现 call 方法
1)继承 Thread 类
Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。
class ThreadTest {
public static void main(String[] args) throws Exception {
MyThread thread = new MyThread();
thread.start();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread");
}
}
以上程序执行结果如下:
Thread
2)实现 Runnable 接口
如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个Runnable 接口。
请参考以下代码:
class ThreadTest {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
new Thread(runnable).start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable");
}
}
以上程序执行结果如下:
Runnable
3)实现 Callable 接口
有返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口。执行Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程了。
请参考以下代码:
class ThreadTest {
public static void main(String[] args) throws Exception {
MyCallable callable = new MyCallable();
// 定义返回结果
FutureTask<String> result = new FutureTask(callable);
// 执行程序
new Thread(result).start();
// 输出返回结果
System.out.println(result.get());
}
}
class MyCallable implements Callable {
@Override
public String call() {
System.out.println("Callable");
return "Success";
}
}
以上程序执行结果如下:
Callable Success
可以看出,Callable 的调用是可以有返回值的,它弥补了之前调用线程没有返回值的情况,它是随着 JDK 1.5 一起发布的。
4)JDK 8 创建线程
JDK 8 之后可以使用 Lambda 表达式很方便地创建线程,请参考以下代码:
new Thread(() -> System.out.println("Lambda Of Thread.")).start();
2.线程生命周期
线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直”霸占”着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换
2.1 新建状态(NEW )
当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值
2.2 就绪状态(RUNNABLE )
当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。
2.3 运行状态(RUNNING )
如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。
2.4 阻塞状态(BLOCKED )
阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
2.4.1 等待阻塞 ( o.wait-> 等待对列 ) :
运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)中。
2.4.2 同步阻塞 (lock-> 锁池 )
运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。
2.4.3 其他阻塞 (sleep/join)
运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。
2.5 线程死亡(DEAD )
2.5.1 正常结束
- run()或 call()方法执行完成,线程正常结束。
2.5.2 异常结束
- 线程抛出一个未捕获的 Exception 或 Error。
2.5.3 调用 stop
- 直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。
3. 终止线程4种方式
3.1 正常运行结束
程序运行结束,线程自动结束。3.2 使用退出标志退出线程
一般 run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:最直接的方法就是设一个boolean 类型的标志,并通过设置这个标志为 true或 false 来控制 while循环是否退出,代码示例:
public class ThreadSafe extends Thread {
public volatile boolean exit = false;
public void run() {
while (!exit){
//do something
}
}
}
定义了一个退出标志 exit,当 exit 为 true 时,while 循环退出,exit 的默认值为 false.在定义 exit时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。
3.3 Interrupt 方法结束线程
使用 interrupt()方法来中断线程有两种情况:
1. 线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。
2. 线程未处于阻塞状态:使用 isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。
public class ThreadSafe extends Thread {
public void run() {
while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
try{
Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
}catch(InterruptedException e){
e.printStackTrace();
break;//捕获到异常之后,执行 break 跳出循环
}
}
}
}
3.4 stop 方法终止线程 (线程不安全)
程序中可以直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用 stop 方法来终止线程。
4. 线程高级用法
4.1 线程等待(wait)
调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。使用 wait() 方法实现线程等待,代码如下:
System.out.println(LocalDateTime.now());
Object lock = new Object();
Thread thread = new Thread(() -> {
synchronized (lock){
try {
// 1 秒钟之后自动唤醒
lock.wait(1000);
System.out.println(LocalDateTime.now());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
以上程序执行结果如下:
2019-06-22T20:53:08.776 2019-06-22T20:53:09.788
注意:使用 wait() 方法时,必须先持有当前对象的锁,否则会抛出异常 java.lang.IllegalMonitorStateException。
4.2 线程唤醒(notify/notifyAll)
Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。
使用 notify()/notifyAll() 方法唤醒线程。
- notify() 方法随机唤醒对象的等待池中的一个线程;
- notifyAll() 唤醒对象的等待池中的所有线程。
使用如下:
Object lock = new Object();
lock.wait();
lock.notify();
// lock.notifyAll();
4.3 线程休眠(sleep)
sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态
// 休眠 1 秒
Thread.sleep(1000);
4.4 sleep 与 与 wait 区别
- 对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于Object 类中的。
- sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态
- 在调用 sleep()方法的过程中,线程不会释放对象锁。
当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
4.5 start 与 与 run 区别
start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码。
- 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于**就绪状态**, 并没有运行。
- 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。
4.6 等待其他线程终止(join)
Join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。
Thread joinThread = new Thread(() -> {
try {
System.out.println("执行前");
Thread.sleep(1000);
System.out.println("执行后");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
joinThread.start();
joinThread.join();
System.out.println("主程序");
以上程序执行结果:
执行前 执行后 主程序
为什么要用 join() 方法 ?
很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要在子线程结束后再结束,这时候就要用到 join() 方法。
4.7 线程让步(yield)
yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。
new Thread(){
@Override
public void run() {
for (int i = 1; i < 10; i++) {
if (i == 5) {
// 让同优先级的线程有执行的机会
this.yield();
}
}
}
}.start();
注意:yield 方法是让同优先级的线程有执行的机会,但不能保证自己会从正在运行的状态迅速转换到可运行的状态。
4.8 线程中断(interrupt )
中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。
1. 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
2. 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出InterruptedException,从而使线程提前结束 TIMED-WATING 状态。
3. 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。
4. 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程thread的时候,可以调用thread.interrupt()方法,在线程的run方法内部可以根据 thread.isInterrupted()的值来优雅的终止线程。
使用 System.exit(0)
可以让整个程序退出;要中断单个线程,可配合 interrupt()
对线程进行“中断”。使用代码如下:
Thread interruptThread = new Thread() {
@Override
public void run() {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
System.out.println("i:" + i);
if (this.isInterrupted()) {
break;
}
}
}
};
interruptThread.start();
Thread.sleep(10);
interruptThread.interrupt();
4.9 线程优先级(setPriority)
在 Java 语言中,每一个线程有一个优先级,默认情况下,一个线程继承它父类的优先级。可以使用 setPriority 方法设置(1-10)优先级,默认的优先级是 5,数字越大表示优先级越高,优先级越高的线程可能优先被执行的概率就越大。
设置优先级的代码如下:
Thread thread = new Thread(() -> System.out.println("Java"));
thread.setPriority(10);
thread.start();
4.10 其他方法
- sleep():强迫一个线程睡眠N毫秒。
2. isAlive(): 判断一个线程是否存活。
3. join(): 等待线程终止。
4. activeCount(): 程序中活跃的线程数。
5. enumerate(): 枚举程序中的线程。
6. currentThread(): 得到当前线程。
7. isDaemon(): 一个线程是否为守护线程。
8. setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
9. setName(): 为线程设置一个名称。
10. wait(): 强迫一个线程等待。
11. notify(): 通知一个线程继续运行。
12. setPriority(): 设置一个线程的优先级。
13. getPriority()::获得一个线程的优先级