在编程工作中,我们经常会用到或听到多线程三个字,多线程编程的好处就是可以让多个任务进行并发,从而更加充分利用CPU,减少CPU的无效等待时间。
多线程的执行流程图如下:

接下来我们会映照上图介绍多线程执行过程中经历的五种状态:
1. 新建状态:
新建状态就是我们通过new关键字实例化出一个线程类的对象时的状态。
public class IsAThread extends Thread{ System.out.println("这是一个线程类");
public static void main(String[] args) { IsAThread isAThread = new IsAThread();
此时,我们就说 isAThread 这个线程对象进入了新建状态。
2. 可运行状态:
当我们调用了新建状态下的线程对象的 start() 方法来启动这个线程,并且线程对象已经准备好了除CPU时间片段之外的所有资源后,该线程对象会被放入“可运行线程池”中等待CPU分配时间片段给自身。在自身获得CPU的时间片段之后便会执行自身 run() 方法中定义的逻辑,示例中的线程对象的 run() 方法是打印了 “这是一个线程类” 这么一句话到控制台。
public static void main(String[] args) { IsAThread isAThread = new IsAThread();
3. 运行状态:
运行状态的线程在分配到CPU的时间片段之后,便会真正开始执行线程对象 run() 方法中定义的逻辑代码了,示例中的线程对象的 run() 方法是打印了 “这是一个线程类” 这么一句话到控制台。
1)但是生产环境中的线程对象的 run() 方法一般不会这么简单,可能业务代码逻辑复杂,造成CPU的时间片段所规定的时长已经用完之后,业务代码还没执行完;
2)或者是当前线程主动调用了Thread.yield()方法来让出自身的CPU时间片段。
public class IsAThread extends Thread{ // 主动让出自身获取到的CPU时间片段给其他线程使用 System.out.println("这是一个线程类");
此时,运行状态会转回可运行状态,等待下一次分配到CPU时间片段之后继续执行未完成的操作。
4. 阻塞状态:
阻塞状态指的是运行状态中的线程因为某种原因主动放弃了自己的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 { CountDownLatch countDownLatch; StringBufferThread(StringBuffer sb, CountDownLatch countDownLatch) { this.countDownLatch = countDownLatch; // StringBufferThread这个类作为锁 synchronized (StringBufferThread.class) { sb.append("This is StringBufferThread1\n"); countDownLatch.countDown();
StringBufferThread2:
import java.util.concurrent.CountDownLatch; public class StringBufferThread2 implements Runnable{ CountDownLatch countDownLatch; StringBufferThread2(StringBuffer sb, CountDownLatch countDownLatch) { this.countDownLatch = countDownLatch; // 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执行,我们让thread1执行后主线程睡眠5秒钟再执行thread2, 如果不进行睡眠的话我们无法控制CPU分配时间片段,有可能直接就先分配给thread2线程了, // 调用countDownLatch.await()保证子线程全部执行完后主线程才继续执行 System.out.println(tipStr.toString());
那么我们先来看一下这种没使用wait()和notify()的情形下,先后执行这两个线程对象时的结果:

跟逻辑一样,先执行了stringBufferThread然后执行了stringBufferThread2。
接下来,修改StringBufferThread类:
import java.util.concurrent.CountDownLatch; public class StringBufferThread implements Runnable { CountDownLatch countDownLatch; StringBufferThread(StringBuffer sb, CountDownLatch countDownLatch) { this.countDownLatch = countDownLatch; // StringBufferThread这个类作为锁 synchronized (StringBufferThread.class) { 在将字符串追加到StringBuffer前,调用锁对象StringBufferThread这个类的wait(), StringBufferThread.class.wait(); } catch (InterruptedException e) { sb.append("This is StringBufferThread1\n"); countDownLatch.countDown();
为StringBufferThread类中的run方法中,将字符串"This is StringBufferThread1\n"加入StringBuffer对象之前,加入wait()方法来进行等待,注意,wait()方法会立刻释放掉自身的锁后,也就是其他争取到锁的线程可以运行被这个synchronized保护的代码块了。
随后,我们修改StringBufferThread2:
import java.util.concurrent.CountDownLatch; public class StringBufferThread2 implements Runnable{ CountDownLatch countDownLatch; StringBufferThread2(StringBuffer sb, CountDownLatch countDownLatch) { this.countDownLatch = countDownLatch; // StringBufferThread这个类作为锁 synchronized (StringBufferThread.class) { sb.append("This is StringBufferThread2\n"); 在将字符串追加到StringBuffer后,调用锁对象StringBufferThread这个类的notify(), 来唤醒本这个锁对象的wait()方法等待的子线程,本例中就是main方法中的stringBufferThread这个子线程 StringBufferThread.class.notify(); countDownLatch.countDown();
也就是在字符串"This is StringBufferThread2\n"追加到StringBuffer之后调用了 notify() 方法来唤醒被 StringBufferThread.class 这个锁等待的线程,本例中就是main方法中的stringBufferThread这个子线程,本唤醒的子线程会进入等锁池,等待重新争取到锁之后,会继续执行代码。
main方法不变,我们来看看执行结果:

与我们预想的一样,因为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 { CountDownLatch countDownLatch; StringBufferThread(StringBuffer sb, CountDownLatch countDownLatch) { this.countDownLatch = countDownLatch; // StringBufferThread这个类作为锁 synchronized (StringBufferThread.class) { // 睡眠10秒,因为主线程在调用本线程5秒后就会调用第二个子线程,多睡眠5秒,就能看出效果 } catch (InterruptedException e) { sb.append("This is StringBufferThread1\n"); countDownLatch.countDown();
StringBufferThread2:
import java.util.concurrent.CountDownLatch; public class StringBufferThread2 implements Runnable{ CountDownLatch countDownLatch; StringBufferThread2(StringBuffer sb, CountDownLatch countDownLatch) { this.countDownLatch = countDownLatch; // 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执行,我们让thread1执行后主线程睡眠5秒钟再执行thread2, 如果不进行睡眠的话我们无法控制CPU分配时间片段,有可能直接就先分配给thread2线程了, // 调用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 { CountDownLatch countDownLatch; StringBufferThread(StringBuffer sb, CountDownLatch countDownLatch, Thread thread2) { this.countDownLatch = countDownLatch; // 这里阻塞住,等待thread2执行完毕才会继续向下执行 } catch (InterruptedException e) { sb.append("This is StringBufferThread1\n"); countDownLatch.countDown();
StringBufferThread2:
import java.util.concurrent.CountDownLatch; public class StringBufferThread2 implements Runnable { CountDownLatch countDownLatch; StringBufferThread2(StringBuffer sb, CountDownLatch countDownLatch) { this.countDownLatch = countDownLatch; thread2睡眠3秒,就能看出效果,如果join()失效, 那么StringBuffer中一定是"This is StringBufferThread1\n"开头的 } catch (InterruptedException e) { 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); // 调用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 { 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);
执行效果如下:

线程会阻塞在这里等待我们从控制台输入内容。

输入内容后,线程继续运行。
|