什么是线程
官方解释:线程是操作系统能够进行运算调度的最小单位,包含于进程之中,是进程中的实际运作单位。也就是说线程是代码运行的载体,我们所编写的代码都是在线程上跑的,以一个最简单的 hellowWorld 为例:
什么是多线程
顾名思义就是多个线程同时运行,提高程序执行速度。单个线程一次只能做一件事,想要提高执行效率有两种途径:
- 异步。因为大多数时候线程都不是时刻在进行计算,都是在等待 io 操作,那么就可以将等待时间利用起来以提高线程的利用率。
- 多线程。一个线程一次只能做一件事,那么多个线程就能同时做多件事了,通过增大线程数来提高执行速度。
如何创建线程-创建线程有两种方法
继承 Thread 类
不推荐本方式来创建线程,原因显而易见:java 不支持多继承,如果继承了 Thread 类就不能再继承其他类了。
使用继承方式创建线程代码如下:
public class CustomThreadExtendThread extends Thread{
@Override
public void run() {
String threadName = Thread.currentThread().getName();
long threadId = Thread.currentThread().getId();
System.out.println("创建线程名为:"+threadName+",id为:"+threadId);
}
public static void main(String[] args){
Thread thread1 = new CustomThreadExtendThread();
Thread thread2 = new CustomThreadExtendThread();
thread1.start();
thread2.start();
}
}
实现 runnable 接口
实现接口来创建线程是目前推荐的一种方式,原因也很简单:一个类可以实现多个接口。实现 Runnable 接口并不影响实现类再去实现其他接口。
使用实现接口方式创建线程代码如下:
public class CustomThreadImplementInterface implements Runnable {
@Override
public void run() {
Thread.currentThread().setName(((Double) Math.random()).toString());
String threadName = Thread.currentThread().getName();
long threadId = Thread.currentThread().getId();
System.out.println("创建线程名为:" + threadName + ",id为:" + threadId);
}
public static void main(String[] args) {
Thread thread1 = new Thread(new CustomThreadImplementInterface());
Thread thread2 = new Thread(new CustomThreadExtendThread());
thread1.start();
thread2.start();
//使用lambda表达式,让创建线程更简单
new Thread(() -> {
System.out.println("创建了一个新线程");
}).start();
}
}
通过查看 Thread 源码可以看到 Thread 类也是 Runnable 接口的一个实现类。
PS:后续代码全部使用 runnable 创建线程
线程状态
上面只是演示了线程的创建,现在来详细了解线程的状态。在 java 规范中,线程可以有以下 6 种状态:
- New(新创建)
- Runnable(可运行)
- Blocked(阻塞)
- Waiting(等待)
- Timed waiting(计时等待)
-
Terminated(被终止)
1. 新建状态:
新建状态就是我们通过new关键字实例化出一个线程类的对象时的状态。
public class IsAThread extends Thread{
@Override
public void run() {
System.out.println("这是一个线程类");
}
}
public static void main(String[] args) {
// 线程进入新建状态
IsAThread isAThread = new IsAThread();
}
此时,我们就说 isAThread 这个线程对象进入了新建状态。
2. 可运行状态:
一旦调用 Thread 类的 start 方法,线程就处于可运行状态,并且线程对象已经准备好了除CPU时间片段之外的所有资源后,该线程对象会被放入“可运行线程池”中等待CPU分配时间片段给自身。在自身获得CPU的时间片段之后便会执行自身 run() 方法中定义的逻辑,示例中的线程对象的 run() 方法是打印了 “这是一个线程类” 这么一句话到控制台。
public static void main(String[] args) {
// 线程进入新建状态
IsAThread isAThread = new IsAThread();
// 线程进入可运行状态
isAThread.start();
}
3. 运行状态:
运行状态的线程在分配到CPU的时间片段之后,便会真正开始执行线程对象 run() 方法中定义的逻辑代码了,示例中的线程对象的 run() 方法是打印了 “这是一个线程类” 这么一句话到控制台。
- 1)但是生产环境中的线程对象的 run() 方法一般不会这么简单,可能业务代码逻辑复杂,造成CPU的时间片段所规定的时长已经用完之后,业务代码还没执行完;
- 2)或者是当前线程主动调用了Thread.yield()方法来让出自身的CPU时间片段。
public class IsAThread extends Thread{
@Override
public void run() {
// 主动让出自身获取到的CPU时间片段给其他线程使用
Thread.yield();
System.out.println("这是一个线程类");
}
}
此时,运行状态会转回可运行状态,等待下一次分配到CPU时间片段之后继续执行未完成的操作。
4. 阻塞状态:
当线程处于阻塞或等待状态时,不运行任何代码且消耗最少的资源。直到重新运行。有如下几种途径让线程进入阻塞或等待状态:
- 当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有
- 当线程等待另一个线程通知调度器一个条件时,进入等待状态。比如调用 Object.wait 或 Thread.join 方法,或等待 java.util.concurrent 库中的 Lock 或 Condition 时。
- 当调用计时等待方法时。比如 Thread.sleep,Object.wait,Thread.join,Lock.tryLock 以及 Condition.await
阻塞状态指的是运行状态中的线程因为某种原因主动放弃了自己的CPU时间片段来让给其他线程使用,可能的阻塞类型及原因有:
4.1 等待阻塞:
线程被调用了 Object.wait() 方法后会立刻释放掉自身获取到的锁并进入“等待池”进行等待,等待池中的线程被其他线程调用了 Object.notify() 或 Object.notifyAll() 方法后会被唤醒从而从“等待池”进入到“等锁池”,“等锁池”中的线程在重新获取到锁之后会转为可运行状态。
值得注意的是:wait()和notify()/notifyAll()只能用在被synchronized包含的代码块中,而说明中的Object.wait和Object.notify的这个Object实际上是指作为synchronized锁的对象。
例如:
我们创建两个线程类,StringBufferThread和StringBufferThread2,这两个类唯一的不同就是run()方法的实现。
- StringBufferThread:
import java.util.concurrent.CountDownLatch;
public class StringBufferThread implements Runnable {
StringBuffer sb;
CountDownLatch countDownLatch;
StringBufferThread(StringBuffer sb, CountDownLatch countDownLatch) {
this.sb = sb;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
// StringBufferThread这个类作为锁
synchronized (StringBufferThread.class) {
sb.append("This is StringBufferThread1\n");
countDownLatch.countDown();
}
}
}
- StringBufferThread2:
import java.util.concurrent.CountDownLatch;
public class StringBufferThread2 implements Runnable{
StringBuffer sb;
CountDownLatch countDownLatch;
StringBufferThread2(StringBuffer sb, CountDownLatch countDownLatch) {
this.sb = sb;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
// StringBufferThread这个类作为锁
synchronized (StringBufferThread.class) {
sb.append("This is StringBufferThread2\n");
countDownLatch.countDown();
}
}
}
- main:
public static void main(String[] args) throws InterruptedException {
StringBuffer tipStr = new StringBuffer();
// 使用CountDownLatch保证子线程全部执行完成后主线程才打印结果
CountDownLatch countDownLatch = new CountDownLatch(2);
StringBufferThread stringBufferThread = new StringBufferThread(tipStr, countDownLatch);
StringBufferThread2 stringBufferThread2 = new StringBufferThread2(tipStr, countDownLatch);
Thread thread1 = new Thread(stringBufferThread);
Thread thread2 = new Thread(stringBufferThread2);
thread1.start();
/*
为了保证先让thread1执行,我们让thread1执行后主线程睡眠5秒钟再执行thread2,
如果不进行睡眠的话我们无法控制CPU分配时间片段,有可能直接就先分配给thread2线程了,
这样就会造成thread2先于thread1执行
*/
Thread.sleep(5000);
thread2.start();
// 调用countDownLatch.await()保证子线程全部执行完后主线程才继续执行
countDownLatch.await();
System.out.println(tipStr.toString());
}
那么我们先来看一下这种没使用wait()和notify()的情形下,先后执行这两个线程对象时的结果:
与我们预想的一样,因为thread1在追加字符串到StringBuffer对象之前调用了锁对象的wait(),就立即释放掉了自身获取到的锁并进入等待池中了,这时thread2获取了锁,将字符串"This is StringBufferThread2\n"首先追加到了StringBuffer对象的开头,然后调用锁对象的notify()方法唤醒了thread1,被唤醒的thread1重新获取锁之后,才将自身的字符串"This is StringBufferThread1\n"追加到了StringBuffer对象的末尾。
4.2 同步阻塞:
线程执行到了被 synchronized 关键字保护的同步代码时,如果此时锁已经被其他线程取走,则该线程会进入到“等锁池”,直到持有锁的那个线程释放掉锁并且自身获取到锁之后,自身会转为可运行状态。
例子如下:
- StringBufferThread:
import java.util.concurrent.CountDownLatch;
public class StringBufferThread implements Runnable {
StringBuffer sb;
CountDownLatch countDownLatch;
StringBufferThread(StringBuffer sb, CountDownLatch countDownLatch) {
this.sb = sb;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
// StringBufferThread这个类作为锁
synchronized (StringBufferThread.class) {
try {
// 睡眠10秒,因为主线程在调用本线程5秒后就会调用第二个子线程,多睡眠5秒,就能看出效果
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sb.append("This is StringBufferThread1\n");
countDownLatch.countDown();
}
}
}
- StringBufferThread2:
import java.util.concurrent.CountDownLatch;
public class StringBufferThread2 implements Runnable{
StringBuffer sb;
CountDownLatch countDownLatch;
StringBufferThread2(StringBuffer sb, CountDownLatch countDownLatch) {
this.sb = sb;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
// StringBufferThread这个类作为锁
synchronized (StringBufferThread.class) {
sb.append("This is StringBufferThread2\n");
countDownLatch.countDown();
}
}
}
main方法不变:
public static void main(String[] args) throws InterruptedException {
StringBuffer tipStr = new StringBuffer();
// 使用CountDownLatch保证子线程全部执行完成后主线程才打印结果
CountDownLatch countDownLatch = new CountDownLatch(2);
StringBufferThread stringBufferThread = new StringBufferThread(tipStr, countDownLatch);
StringBufferThread2 stringBufferThread2 = new StringBufferThread2(tipStr, countDownLatch);
Thread thread1 = new Thread(stringBufferThread);
Thread thread2 = new Thread(stringBufferThread2);
thread1.start();
/*
为了保证先让thread1执行,我们让thread1执行后主线程睡眠5秒钟再执行thread2,
如果不进行睡眠的话我们无法控制CPU分配时间片段,有可能直接就先分配给thread2线程了,
这样就会造成thread2先于thread1执行
*/
Thread.sleep(5000);
thread2.start();
// 调用countDownLatch.await()保证子线程全部执行完后主线程才继续执行
countDownLatch.await();
System.out.println(tipStr.toString());
}
执行结果如下:
由此可见,主线程调用thread1后的5秒后调用了thread2,thread1在执行时首先拿走了锁对象并睡眠了10秒,在这10秒钟,thread2有5秒的时间(10秒减去主线程等待的5秒)去执行run方法中的字符串追加操作,但是因为锁已经被thread1拿走了,所以thread2在这漫长的5秒钟之内什么都做不了,只能等待thread1将字符串"This is StringBufferThread1\n"先追加到StringBuffer的开头,然后才能把自己的字符串"This is StringBufferThread2\n"追加到StringBuffer的末尾。
4.3 其他阻塞:
1)线程中执行了 Thread.sleep(xx) 方法进行休眠会进入阻塞状态,直到Thread.sleep(xx)方法休眠的时间超过参数设定的时间而超时后线程会转为可运行状态。Thread.sleep(xx)方法的使用在本文很多例子都体现了,就不演示了。
2)线程ThreadA中调用了ThreadB.join()方法来等待ThreadB线程执行完毕,从而ThreadA进入阻塞状态,直到ThreadB线程执行完毕后ThreadA会转为可运行状态。
例子如下:
- StringBufferThread:
import java.util.concurrent.CountDownLatch;
public class StringBufferThread implements Runnable {
StringBuffer sb;
CountDownLatch countDownLatch;
Thread thread2;
StringBufferThread(StringBuffer sb, CountDownLatch countDownLatch, Thread thread2) {
this.sb = sb;
this.countDownLatch = countDownLatch;
this.thread2 = thread2;
}
@Override
public void run() {
try {
// 这里阻塞住,等待thread2执行完毕才会继续向下执行
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
sb.append("This is StringBufferThread1\n");
countDownLatch.countDown();
}
}
- StringBufferThread2:
import java.util.concurrent.CountDownLatch;
public class StringBufferThread2 implements Runnable {
StringBuffer sb;
CountDownLatch countDownLatch;
StringBufferThread2(StringBuffer sb, CountDownLatch countDownLatch) {
this.sb = sb;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
/*
thread2睡眠3秒,就能看出效果,如果join()失效,
那么StringBuffer中一定是"This is StringBufferThread1\n"开头的
*/
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
sb.append("This is StringBufferThread2\n");
countDownLatch.countDown();
}
}
随后,修改main方法:
public static void main(String[] args) throws InterruptedException {
StringBuffer tipStr = new StringBuffer();
// 使用CountDownLatch保证子线程全部执行完成后主线程才打印结果
CountDownLatch countDownLatch = new CountDownLatch(2);
StringBufferThread2 stringBufferThread2 = new StringBufferThread2(tipStr, countDownLatch);
Thread thread2 = new Thread(stringBufferThread2);
StringBufferThread stringBufferThread = new StringBufferThread(tipStr, countDownLatch, thread2);
Thread thread1 = new Thread(stringBufferThread);
thread1.start();
thread2.start();
// 调用countDownLatch.await()保证子线程全部执行完后主线程才继续执行
countDownLatch.await();
System.out.println(tipStr.toString());
}
执行结果如下:
由此可见,虽然thread1先于thread2执行,但是因为在将字符串追加到StringBuffer对象前调用了thread2.join(),便被阻塞住了,此时thread2睡眠三秒后,将字符串"This is StringBufferThread2\n"追加到了StringBuffer对象的开头,thread2执行完毕;随后因为thread1等待的thread2已经执行完毕了,thread1便由阻塞状态转为可运行状态,在分配到CPU的时间片段后,便将字符串"This is StringBufferThread1\n"追加到了StringBuffer对象的结尾。
3)线程中进行了I/O操作,I/O操作在输入输出行为执行完毕之前都不会返回给调用者任何结果,直到I/O操作执行完毕之后线程会转为可运行状态。
例如:
我们编写ThreadTest类:
import java.util.Scanner;
public class ThreadTest implements Runnable {
@Override
public void run() {
System.out.println("This is StringBufferThread1 Begin\n");
Scanner scanner = new Scanner(System.in);
System.out.println("请输入内容:");
// 线程会阻塞在这,等待用户在控制台输入数据后继续执行
String content = scanner.nextLine();
System.out.println("您输入的内容是:" + content + "\n");
System.out.println("This is StringBufferThread1 end\n");
}
}
执行main方法:
public static void main(String[] args) throws InterruptedException {
ThreadTest threadTest = new ThreadTest();
Thread thread1 = new Thread(threadTest);
thread1.start();
}
执行效果如下:
线程会阻塞在这里等待我们从控制台输入内容。
输入内容后,线程继续运行。
线程属性
线程有各种属性:优先级,守护线程,线程组以及处理未捕获异常处理器。
线程优先级
java 中,每个线程都有一个优先级。默认情况下,线程继承父线程优先级。也可以调用setPriority方法指定优先级。优先级范围:1(MIN_PRIORITY)-10(MAX_PRIORITY).NORM_PRIORITY 为 5,这些常量定义在 Thread 类中.
注意: 线程优先级时高度依赖于系统的,因此当 java 线程优先级映射到宿主机平台的优先级时,优先级个数可能会变少或者变成 0.比如,Windows 中有 7 个优先级,java 线程映射时部分优先级将会映射到相同的操作系统优先级上。Oracle 为 Linux 编写的 java 虚拟机中,忽略了线程的优先级,所有 java 线程都有相同的优先级。不要编写依赖优先级的代码。
守护线程
通过调用Thread.setDaemon(true)将一个线程转换为守护线程。守护线程唯一的用户是为其他线程提供服务,比如计时线程,定时发送计时信号给其他线程。因此当虚拟机中只有守护线程时,虚拟机就会关闭退出。不要在守护线程中访问任何资源,处理任何业务逻辑
未捕获异常处理器
线程的 run 方法不能抛出任何受查异常,非受查异常会导致线程终止,除了 try/catch 捕获异常外,还可以通过未捕获异常处理器来处理异常。异常处理器需要实现Thread.UncaughtExceptionHandler接口。
可以使用线程示例的setUncaughtExceptionHandler()方法为某个线程设置处理器,也可使用Thread.setDefaultUncaughtExceptionHandler()为所有线程设置默认处理器,代码如下:
public class CustomExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("捕获到线程"+t.getName()+",异常:" + e.getMessage());
e.printStackTrace();
}
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(new CustomExceptionHandler());
new Thread(() -> {
throw new RuntimeException("test");
}).start();
}
}
如果不设置默认处理器且不为独立的线程设置处理器,那么该线程的处理器就为该线程的线程组对象--ThreadGroup(因为线程组对象实现了Thread.UncaughtExceptionHandler接口)。