什么是线程?线程概念及方法详细讲解

目录

1 多线程

1.1 并发与并⾏

1.2 线程与进程

1.3 创建线程类

2 多线程详解

2.1 多线程原理

2.2 Thread类

2.3 创建线程⽅式⼆

2.4 Thread 和 Runnable 的区别

2.5 匿名内部类⽅式实现线程的创建

3 线程安全

3.1 线程安全

3.2 线程同步

3.3 同步代码块

3.4 同步⽅法

3.5 Lock锁

4 线程状态

4.1 线程状态概述

4.2 Timed Waiting(计时等待)

4.3 BLOCKED(锁阻塞)

4.4 Waiting(⽆限等待)

4.5 练习

5 小结


1 多线程


我们在之前,学习的程序在没有跳转语句的前提下,都是由上⾄下依次执⾏,那现在想要设计⼀个程序, 边打游戏边听歌,怎么设计?
要解决上述问题,咱们得使⽤多进程或者多线程来解决。

1.1 并发与并⾏

  • 并发:指两个或多个事件在同⼀个时间段内发⽣。
  • 并⾏:指两个或多个事件在同⼀时刻发⽣(同时发⽣)。

什么是线程?线程概念及方法详细讲解_第1张图片

在操作系统中,安装了多个程序,并发指的是在⼀段时间内宏观上有多个程序同时运⾏,这在单 CPU 系统中,每⼀时刻只能有⼀道程序执⾏,即微观上这些程序是分时的交替运⾏,只不过是给⼈的感觉是同时运 ⾏,那是因为分时交替运⾏的时间是⾮常短的。
⽽在多个 CPU 系统中,则这些可以并发执⾏的程序便可以分配到多个处理器上( CPU ),实现多任务并⾏ 执⾏,即利⽤每个处理器来处理⼀个可以并发执⾏的程序,这样多个程序便可以同时执⾏。⽬前电脑市场 上说的多核 CPU ,便是多核处理器,核 越多,并⾏处理的程序越多,能⼤⼤的提⾼电脑运⾏的效率。
注意:单核处理器的计算机肯定是不能并⾏的处理多个任务的,只能是多个任务在单个 CPU 上并发运 ⾏。同理 , 线程也是⼀样的,从宏观⻆度上理解线程是并⾏运⾏的,但是从微观⻆度上分析却是串⾏ 运⾏的,即⼀个线程⼀个线程的去运⾏,当系统只有⼀个 CPU 时,线程会以某种顺序执⾏多个线程, 我们把这种情况称之为线程调度。

1.2 线程与进程

  • 进程:是指⼀个内存中运⾏的应⽤程序,每个进程都有⼀个独⽴的内存空间,⼀个应⽤程序可以同时运⾏多个进程;进程也是程序的⼀次执⾏过程,是系统运⾏程序的基本单位;系统运⾏⼀个程序即是 ⼀个进程从创建、运⾏到消亡的过程。
  • 线程:线程是进程中的⼀个执⾏单元,负责当前进程中程序的执⾏,⼀个进程中⾄少有⼀个线程。⼀个进程中是可以有多个线程的,这个应⽤程序也可以称之为多线程程序。
简⽽⾔之:⼀个程序运⾏后⾄少有⼀个进程,⼀个进程中可以包含多个线程
我们可以再电脑底部任务栏,右键 --> 打开任务管理器,可以查看当前任务的进程:
进程
什么是线程?线程概念及方法详细讲解_第2张图片

 什么是线程?线程概念及方法详细讲解_第3张图片

线程调度:
  • 分时调度

        所有线程轮流使⽤ CPU 的使⽤权,平均分配每个线程占⽤ CPU 的时间。

  • 抢占式调度

        优先让优先级⾼的线程使⽤ CPU,如果线程的优先级相同,那么会随机选择⼀个(线程随机性), Java使⽤的为抢占式调度。

  1. 设置线程的优先级

什么是线程?线程概念及方法详细讲解_第4张图片

        2.抢占式调度详解

        ⼤部分操作系统都⽀持多进程并发运⾏,现在的操作系统⼏乎都⽀持同时运⾏多个程序。⽐如: 现在我们上课⼀边使⽤编辑器,⼀边使⽤录屏软件,同时还开着画图板, dos 窗⼝等软件。此
时,这些程序是在同时运⾏, 感觉这些软件好像在同⼀时刻运⾏着
        实际上,CPU (中央处理器)使⽤抢占式调度模式在多个线程间进⾏着⾼速的切换。对于 CPU ⼀个核⽽⾔,某个时刻,只能执⾏⼀个线程,⽽ CPU 的在多个线程间切换速度相对我们的感觉 要快,看上去就是在同⼀时刻运⾏。
        其实,多线程程序并不能提⾼程序的运⾏速度,但能够提⾼程序运⾏效率,让 CPU 的使⽤率更 ⾼。

什么是线程?线程概念及方法详细讲解_第5张图片

1.3 创建线程类

Java 使⽤ java.lang.Thread 类代表 线程 ,所有的线程对象都必须是 Thread 类或其⼦类的实例。每个线程的作⽤是完成⼀定的任务,实际上就是执⾏⼀段程序流即⼀段顺序执⾏的代码。 Java 使⽤线程执⾏体来 代表这段程序流。 Java 中通过继承 Thread 类来 创建 启动多线程 的步骤如下:
1. 定义 Thread 类的⼦类,并重写该类的 run() ⽅法,该 run() ⽅法的⽅法体就代表了线程需要完成的任务,因此把 run() ⽅法称为线程执⾏体。
2. 创建 Thread ⼦类的实例,即创建了线程对象
3. 调⽤线程对象的 start() ⽅法来启动该线程
代码如下:
测试类:

public class Demo01 {
 public static void main(String[] args) {
 // 创建⾃定义线程对象
 MyThread mt = new MyThread("新的线程!");
 // 开启新线程
 mt.start();
 // 在主⽅法中执⾏for循环
 for (int i = 0; i < 10; i++) {
 System.out.println("main线程!" + i);
 }
 } 
}
⾃定义线程类:
public class MyThread extends Thread {
 // 定义指定线程名称的构造⽅法
 public MyThread(String name) {
 // 调⽤⽗类的String参数的构造⽅法,指定线程的名称
 super(name);
 }
 /**
 * 重写run⽅法,完成该线程执⾏的逻辑
 */
 @Override
 public void run() {
 for (int i = 0; i < 10; i++) {
 System.out.println(getName() + ":正在执⾏!" + i);
 }
 }
}

2 多线程详解


2.1 多线程原理

刚刚我们已经写过⼀版多线程的代码,很多同学对原理不是很清楚,那么现在我们先画个多线程执⾏时序 图来体现⼀下多线程程序的执⾏流程。 代码如下:
⾃定义线程类:
public class MyThread extends Thread {
 /*
 * 利⽤继承中的特点
 * 将线程名称传递 进⾏设置
 */
 public MyThread(String name) {
 super(name);
 }
 /*
 * 重写run⽅法
 * 定义线程要执⾏的代码
 */
 public void run() {
 for (int i = 0; i < 20; i++) {
 //getName()⽅法 来⾃⽗亲
 System.out.println(getName() + i);
 }
 }
}
测试类:
public class Demo {
 public static void main(String[] args) {
 System.out.println("这⾥是main线程");
 MyThread mt = new MyThread("⼩强");
 mt.start(); // 开启了⼀个新的线程
 for (int i = 0; i < 20; i++) {
 System.out.println("旺财:" + i);
 }
 } 
}
流程图:
什么是线程?线程概念及方法详细讲解_第6张图片

程序启动运⾏ main 时候, java 虚拟机启动⼀个进程,主线程 main main() 调⽤时候被创建。随着调⽤mt 的对象的 start ⽅法,另外⼀个新的线程也启动了,这样,整个应⽤就在多线程下运⾏。
通过这张图我们可以很清晰的看到多线程的执⾏流程,那么为什么可以完成并发执⾏呢?我们再来讲⼀讲原理。
多线程执⾏时,到底在内存中是如何运⾏的呢?以上个程序为例,进⾏图解说明:
多线程执⾏时,在栈内存中,其实 每⼀个执⾏线程都有⼀⽚⾃⼰所属的栈内存空间。 进⾏⽅法的压栈和弹栈。
什么是线程?线程概念及方法详细讲解_第7张图片
当执⾏线程的任务结束了,线程⾃动在栈内存中释放了。但是当 所有的执⾏线程 都结束了(“前置”线程结束了),进程才结束了。

2.2 Thread

在上⼀章内容中我们已经可以完成最基本的线程开启,那么在我们完成操作过程中⽤到了 java.lang.Thread 类, API 中该类中定义了有关线程的⼀些⽅法,具体如下:
构造⽅法:
  • public Thread() :分配⼀个新的线程对象。
  • public Thread(String name) :分配⼀个指定名字的新的线程对象。
  • public Thread(Runnable target) :分配⼀个带有指定⽬标新的线程对象。
  • public Thread(Runnable target, String name) :分配⼀个带有指定⽬标新的线程对象并指定名字。
常⽤⽅法:
  • public String getName() :获取当前线程名称。
  • public void start() :导致此线程开始执⾏;Java虚拟机调⽤此线程的run⽅法。
  • public void run() :此线程要执⾏的任务在此处定义代码。
  • public static void sleep(long millis) :使当前正在执⾏的线程以指定的毫秒数暂停(暂时停⽌执⾏)。
  • public static Thread currentThread() :返回对当前正在执⾏的线程对象的引⽤。
翻阅 API 后得知创建线程的⽅式总共有两种,⼀种是继承 Thread 类⽅式,⼀种是实现 Runnable 接⼝⽅式,⽅式⼀我们上⼀章已经完成,接下来讲解⽅式⼆实现的⽅式。

2.3 创建线程⽅式⼆

采⽤ java.lang.Runnable 也是⾮常常⻅的⼀种,我们只需要重写 run ⽅法即可。
步骤如下:
1. 定义 Runnable 接⼝的实现类,并重写该接⼝的 run() ⽅法,该 run() ⽅法的⽅法体同样是该线程的线程执⾏体。
2. 创建 Runnable 实现类的实例,并以此实例作为 Thread target 来创建 Thread 对象,该 Thread 对象才是 真正的线程对象。
3. 调⽤线程对象的 start() ⽅法来启动线程。
代码如下:
public class MyRunnable implements Runnable {
 @Override
 public void run() {
 for (int i = 0; i < 20; i++) {
 System.out.println(Thread.currentThread().getName() + " " + i);
 }
 } 
}
public class Demo {
 public static void main(String[] args) {
 // 创建⾃定义类对象 线程任务对象
 MyRunnable mr = new MyRunnable();
 // 创建线程对象
 Thread t = new Thread(mr, "⼩强");
 t.start();
 for (int i = 0; i < 20; i++) {
 System.out.println("旺财 " + i);
 }
 }
}
通过实现 Runnable 接⼝,使得该类有了多线程类的特征。 run() ⽅法是多线程程序的⼀个执⾏⽬标。所有的多线程代码都在 run ⽅法⾥⾯。 Thread 类实际上也是实现了 Runnable 接⼝的类。
在启动的多线程的时候,需要先通过 Thread 类的构造⽅法 Thread(Runnable target) 构造出对象,然后调⽤Thread 对象的 start() ⽅法来运⾏多线程代码。
实际上所有的多线程代码都是通过运⾏ Thread start() ⽅法来运⾏的。因此,不管是继承 Thread 类还是实现 Runnable 接⼝来实现多线程,最终还是通过 Thread 的对象的 API 来控制线程的,熟悉 Thread 类的 API 是进⾏多线程编程的基础。
Tips Runnable 对象仅仅作为 Thread 对象的 target Runnable 实现类⾥包含的 run() ⽅法仅作为线程 执⾏体。⽽实际的线程对象依然是 Thread 实例,只是该 Thread 线程负责执⾏其 target run() ⽅法。

2.4 Thread Runnable 的区别

如果⼀个类继承 Thread ,则不适合资源共享。但是如果实现了 Runable 接⼝的话,则很容易的实现资源共享。
总结:
实现 Runnable 接⼝⽐继承 Thread 类所具有的优势:
1. 适合多个相同的程序代码的线程去共享同⼀个资源。
2. 可以避免 java 中的单继承的局限性。
3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独⽴。
4. 线程池只能放⼊实现 Runable Callable 类线程,不能直接放⼊继承 Thread 的类。
扩充:在 java 中,每次程序运⾏⾄少启动 2 个线程。⼀个是 main 线程,⼀个是垃圾收集(GC)线程。因为 每当使⽤ java 命令执⾏⼀个类的时候,实际上都会启动⼀个 JVM ,每⼀个 JVM 其实在就是在操作系 统中启动了⼀个进程。

2.5 匿名内部类⽅式实现线程的创建

使⽤线程的匿名内部类⽅式,可以⽅便的实现每个线程执⾏不同的线程任务操作。
使⽤匿名内部类的⽅式实现 Runnable 接⼝,重写 Runnable 接⼝中的 run ⽅法:
public class NoNameInnerClassThread {
 public static void main(String[] args) {
// new Runnable() {
// public void run() {
// for (int i = 0; i < 20; i++) {
// System.out.println("张宇:" + i);
// }
// }
// }; //---这个整体 相当于new MyRunnable()
 Runnable r = new Runnable() {
 public void run() {
 for (int i = 0; i < 20; i++) {
 System.out.println("张宇:" + i);
 }
 }
 };
 
 new Thread(r).start();
 for (int i = 0; i < 20; i++) {
 System.out.println("费⽟清:" + i);
 }
 }
}

3 线程安全


3.1 线程安全

如果有多个线程在同时运⾏,⽽这些线程可能会同时运⾏这段代码。程序每次运⾏结果和单线程运⾏的结果是⼀样的,⽽且其他的变量的值也和预期的是⼀样的,就是线程安全的。
我们通过⼀个案例,演示线程的安全问题:
电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 葫芦娃⼤战奥特曼 ,本次电影的座位 100 个(本场电影只能卖 100 张票)。
我们来模拟电影院的售票窗⼝,实现多个窗⼝同时卖 葫芦娃⼤战奥特曼 这场电影票(多个窗⼝⼀起卖这100 张票)
需要窗⼝,采⽤线程对象来模拟;需要票, Runnable 接⼝⼦类来模拟
模拟票:
public class Ticket implements Runnable {
 private int ticket = 100;
 /*
 * 执⾏卖票操作
 */
 @Override
 public void run() {
 // 每个窗⼝卖票的操作
 // 窗⼝ 永远开启
 while (true) {
 if (ticket > 0) {// 有票 可以卖
 // 出票操作
 // 使⽤sleep模拟⼀下出票时间
 try {
 Thread.sleep(100);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 // 获取当前线程对象的名字
 String name = Thread.currentThread().getName();
 System.out.println(name + "正在卖:" + ticket--);
 }
 }
 }
}
测试类:

public class Demo {
 public static void main(String[] args) {
 // 创建线程任务对象
 Ticket ticket = new Ticket();
 // 创建三个窗⼝对象
 Thread t1 = new Thread(ticket, "窗⼝1");
 Thread t2 = new Thread(ticket, "窗⼝2");
 Thread t3 = new Thread(ticket, "窗⼝3");
 
 // 同时卖票
 t1.start();
 t2.start();
 t3.start();
 }
}

 结果中有⼀部分这样现象:

什么是线程?线程概念及方法详细讲解_第8张图片

发现程序出现了两个问题:
1. 相同的票数,⽐如 2 这张票被卖了两回。
2. 不存在的票,⽐如 0 票与 -1 票,是不存在的。
这种问题,⼏个窗⼝(线程)票数不同步了,这种问题称为线程安全性问题。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,⽽⽆写操作,⼀般来说,这个全局变量是线程安全的;若有多个线程同时执⾏写操作,⼀般都需要考 虑线程同步,否则的话就可能影响线程安全。

3.2 线程同步

当我们使⽤多个线程访问同⼀资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。
要解决上述多线程并发访问⼀个资源的安全性问题:也就是解决重复票与不存在票问题, Java 中提供了同 步机制 synchronized 来解决。
根据案例简述:
窗⼝1线程进⼊操作的时候,窗⼝2和窗⼝3线程只能在外等着,窗⼝1操作结束,窗⼝1和窗⼝2和窗⼝3才有机会进⼊代码去执⾏。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
为了保证每个线程都能正常执⾏原⼦操作, Java 引⼊了线程同步机制。
那么怎么去使⽤呢?有三种⽅式完成同步操作:
1. 同步代码块。
2. 同步⽅法。
3. 锁机制。

3.3 同步代码块

  • 同步代码块: synchronized 关键字可以⽤于⽅法中的某个区块中,表示只对这个区块的资源实⾏互斥访问。
格式:
synchronized(同步锁) {
 需要同步操作的代码
}
同步锁:
对象的同步锁只是⼀个概念,可以想象为在对象上标记了⼀个锁。
1. 锁对象,可以是任意类型。
2. 多个线程对象,要使⽤同⼀把锁。
注意:在任何时候,最多允许⼀个线程拥有同步锁,谁拿到锁就进⼊代码块,其他的线程只能在外等 着( BLOCKED )。
使⽤同步代码块解决代码:
public class Ticket implements Runnable {
 private int ticket = 100;
 Object lock = new Object();
 /*
 * 执⾏卖票操作
 */
 @Override
 public void run() {
 // 每个窗⼝卖票的操作
 // 窗⼝ 永远开启
 while (true) {
 synchronized (lock) {
 if (ticket > 0) { // 有票 可以卖
 // 出票操作
 // 使⽤sleep模拟⼀下出票时间
 try {
 Thread.sleep(50);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 // 获取当前线程对象的名字
 String name = Thread.currentThread().getName();
 System.out.println(name + "正在卖: " + ticket--);
 }
 }
 }
 }
}
当使⽤了同步代码块后,上述的线程的安全问题,解决了。

3.4 同步⽅法

  • 同步⽅法:使⽤ synchronized 修饰的⽅法,就叫做同步⽅法,保证A线程执⾏该⽅法的时候,其他线程只能在⽅法外等着。
格式:
public synchronized void method() {
 可能会产⽣线程安全问题的代码
}
同步锁是谁?
对于⾮ static ⽅法,同步锁就是 this
对于 static ⽅法,我们使⽤当前⽅法所在类的字节码对象(类名 .class )。

使⽤同步⽅法代码如下:

public class Ticket implements Runnable {
 private int ticket = 100;
 /*
 * 执⾏卖票操作
 */
 @Override
 public void run() {
 // 每个窗⼝卖票的操作
 // 窗⼝ 永远开启
 while (true) {
 sellTicket();
 }
 }
 /*
 * 锁对象 是 谁调⽤这个⽅法 就是谁
 * 隐含 锁对象 就是 this
 */
 public synchronized void sellTicket() {
 if (ticket > 0) { // 有票 可以卖
 // 出票操作
 // 使⽤sleep模拟⼀下出票时间
try {
 Thread.sleep(100);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 // 获取当前线程对象的名字
 String name = Thread.currentThread().getName();
 System.out.println(name + "正在卖:" + ticket--);
 }
 }
}

3.5 Lock

java.util.concurrent.locks.Lock 机制提供了⽐ synchronized 代码块和 synchronized ⽅法更⼴泛的
锁定操作,同步代码块 / 同步⽅法具有的功能 Lock 都有,除此之外更强⼤,更体现⾯向对象。
Lock 锁也称同步锁,创建对象 Lock lock = new ReentrantLock() ,加锁与释放锁⽅法如下:
  • public void lock() :加同步锁。
  • public void unlock() :释放同步锁。
使⽤如下:
public class Ticket implements Runnable {
 private int ticket = 100;
 
 Lock lock = new ReentrantLock();
 /*
 * 执⾏卖票操作
 */
 @Override
 public void run() {
 // 每个窗⼝卖票的操作
 // 窗⼝ 永远开启
 while (true) {
 lock.lock();
 if (ticket > 0) { // 有票 可以卖
 // 出票操作
 // 使⽤sleep模拟⼀下出票时间
 try {
 Thread.sleep(50);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 // 获取当前线程对象的名字
 String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
 }
 lock.unlock();
 }
 }
}

4 线程状态


4.1 线程状态概述

当线程被创建并启动以后,它既不是⼀启动就进⼊了执⾏状态,也不是⼀直处于执⾏状态。在线程的⽣命 周期中,有⼏种状态呢?在 API java.lang.Thread.State 这个枚举中给出了六种线程状态:
这⾥先列出各个线程状态发⽣的条件,下⾯将会对每种状态进⾏详细解析
什么是线程?线程概念及方法详细讲解_第9张图片

我们不需要去研究这⼏种状态的实现原理,我们只需知道在做线程操作中存在这样的状态。那我们怎么去理解这⼏个状态呢,新建与被终⽌还是很容易理解的,我们就研究⼀下线程从 Runnable (可运⾏)状态与 ⾮运⾏状态之间的转换问题。

4.2 Timed Waiting(计时等待)

Timed Waiting API 中的描述为:⼀个正在限时等待另⼀个线程执⾏⼀个(唤醒)动作的线程处于这⼀状态。单独的去理解这句话,真是⽞之⼜⽞,其实我们在之前的操作中已经接触过这个状态了,在哪⾥呢?
在我们写卖票的案例中,为了减少线程执⾏太快,现象不明显等问题,我们在 run ⽅法中添加了 sleep 语句,这样就强制当前正在执⾏的线程休眠 (暂停执⾏) ,以 减慢线程
其实当我们调⽤了 sleep ⽅法之后,当前执⾏的线程就进⼊到 休眠状态 ,其实就是所谓的 Timed Waiting(计时等待),那么我们通过⼀个案例加深对该状态的⼀个理解。
实现⼀个计数器,计数到 100 ,在每个数字之间暂停 1 秒,每隔 10 个数字输出⼀个字符串
代码:
public class MyThread extends Thread {
 public void run() {
 for (int i = 0; i < 100; i++) {
 if ((i) % 10 == 0) {
 System.out.println("-------" + i);
 }
 System.out.print(i);
 try {
 Thread.sleep(1000);
 System.out.print(" 线程睡眠1秒!\n");
 } catch (InterruptedException e) {
 e.printStackTrace(); }
 }
 }
 public static void main(String[] args) {
 new MyThread().start();
 }
}
通过案例可以发现, sleep ⽅法的使⽤还是很简单的。我们需要记住下⾯⼏点:
1. 进⼊ TIMED_WAITING 状态的⼀种常⻅情形是调⽤的 sleep ⽅法,单独的线程也可以调⽤,不⼀定⾮要有协作关系。
2. 为了让其他线程有机会执⾏,可以将 Thread.sleep() 的调⽤ 放线程 run() 之内 。这样才能保证该线程执⾏过程中会睡眠。
3. sleep 与锁⽆关,线程睡眠到期⾃动苏醒,并返回到 Runnable (可运⾏)状态。
⼩提示: sleep() 中指定的时间是线程不会运⾏的最短时间。因此, sleep() ⽅法不能保证该线程睡眠到期后就开始⽴刻执⾏。

 Timed Waiting 线程状态图:

什么是线程?线程概念及方法详细讲解_第10张图片

4.3 BLOCKED(锁阻塞)

Blocked 状态在 API 中的介绍为:⼀个正在阻塞等待⼀个监视器锁(锁对象)的线程处于这⼀状态。
我们已经学完同步机制,那么这个状态是⾮常好理解的了。⽐如,线程 A 与线程 B 代码中使⽤同⼀锁,如果 线程 A 获取到锁,线程 A 进⼊到 Runnable 状态,那么线程 B 就进⼊到 Blocked 锁阻塞状态。
这是由 Runnable 状态进⼊ Blocked 状态。除此 Waiting 以及 Time Waiting 状态也会在某种情况下进⼊阻塞状态,⽽这部分内容作为扩充知识点带领⼤家了解⼀下。
Blocked 线程状态图:
什么是线程?线程概念及方法详细讲解_第11张图片

4.4 Waiting(⽆限等待)

Wating 状态在 API 中介绍为:⼀个正在⽆限期等待另⼀个线程执⾏⼀个特别的(唤醒)动作的线程处于这⼀状态。
那么我们之前遇到过这种状态吗?答案是并没有,但并不妨碍我们进⾏⼀个简单深⼊的了解。我们通过⼀段代码来学习⼀下:
public class WaitingTest {
 public static Object obj = new Object();
 
 public static void main(String[] args) {
 // 演示waiting
 new Thread(new Runnable() {
 @Override
 public void run() {
 while (true) {
 synchronized (obj) {
 try {
 System.out.println(Thread.currentThread().getName() + "=== 获取到锁对
象,调⽤wait⽅法,进⼊waiting状态,释放锁对象");
 obj.wait(); // ⽆限等待
 // obj.wait(5000); // 计时等待, 5秒 时间到,⾃动醒来
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 System.out.println(Thread.currentThread().getName() + "=== 从waiting状态
醒来,获取到锁对象,继续执⾏了");
 }
 }
 }
 }, "等待线程").start();
 
 new Thread(new Runnable() {
 @Override
 public void run() {
// while (true) { // 每隔3秒 唤醒⼀次
 try {
 System.out.println(Thread.currentThread().getName() + "----- 等待3秒 钟");
 Thread.sleep(3000);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 synchronized (obj) {
 System.out.println(Thread.currentThread().getName() + "----- 获取到锁对
象,调⽤notify⽅法,释放锁对象");
 obj.notify();
 }
 }
// }
 }, "唤醒线程").start();
 }
}
通过上述案例我们会发现,⼀个调⽤了某个对象的 Object.wait ⽅法的线程会等待另⼀个线程调⽤此对象的Object.notify() ⽅法 或 Object.notifyAll() ⽅法。
其实 waiting 状态并不是⼀个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间⼜存在协作关系。就好⽐在公司⾥你和你的同事们,你们可能 存在晋升时的竞争,但更多时候你们更多是⼀起合作以完成某些任务。
当多个线程协作时,⽐如 A B 线程,如果 A 线程在 Runnable (可运⾏)状态中调⽤了 wait() ⽅法那么 A 线程 就进⼊了 Waiting (⽆限等待)状态,同时失去了同步锁。假如这个时候 B 线程获取到了同步锁,在运⾏状 态中调⽤了 notify() ⽅法,那么就会将⽆限等待的 A 线程唤醒。注意是唤醒,如果获取到锁对象,那么 A 线 程唤醒后就进⼊ Runnable (可运⾏)状态;如果没有获取锁对象,那么就进⼊到 Blocked (锁阻塞状 态)。
Waiting 线程状态图:
什么是线程?线程概念及方法详细讲解_第12张图片

⼀条有意思的 Tips
我们在翻阅 API 的时候会发现 Timed Waiting (计时等待)与 Waiting (⽆限等待)状态联系还是很紧密的,⽐如 Waiting (⽆限等待)状态中 wait ⽅法是空参的,⽽ Timed Waiting (计时等待)中 wait 法是带参的。这种带参的⽅法,其实是⼀种倒计时操作,相当于我们⽣活中的⼩闹钟,我们设定好时 间,到时通知,可是如果提前得到(唤醒)通知,那么设定好时间再通知也就显得多此⼀举了,那么 这种设计⽅案其实是⼀举两得。如果没有得到(唤醒)通知,那么线程就处于 Timed Waiting 状态, 直到倒计时完毕⾃动醒来;如果在倒计时期间得到(唤醒)通知,那么线程从 Timed Waiting 状态⽴
刻唤醒。

4.5 练习

线程通信:练习1

需求:两个线程

线程1:图片的加载  1%~100%

线程2:图片的显示  显示

同时开启线程

线程2 等待 线程1 结束后在执行,

在线程2中使用 线程1.join() -> 线程2 进入阻塞状态

采用匿名内部类的方式

public class ThreadDemo3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(){
          public void run(){
              System.out.println("开始加载...");
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }

              for (int i = 1; i <= 100; i++) {
                  System.out.println("已加载" + i + "%");
                  try {
                      Thread.sleep((long) (Math.random() * 1000));
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
              System.out.println("结束加载");
          }
        };

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    t1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("等待加载中。。。");
                System.out.println("显示图片");

            }
        });

        t1.start();
        t2.start();

    }
}

 线程通信:练习2

 需求: 两个线程
       线程1: 图片的加载   1% ~ 100% (等待图片显示)  图片的下载 1%~100%
       线程2: 图片的显示
       同时开启线程: 显示之前 需要等待 加载完成
                   显示后 才能开始下载

wait(long): 等待指定的时间毫秒值
wait(): 无限等待, 可以被唤醒 notify()  notifyAll()

public class TreadDemo4 {
    public static void main(String[] args) {
        Object obj = new Object();//这是用来做加锁的工具的, 此案例中什么意义都没有
        LoadThread load = new LoadThread(obj);
        ShowThread showT = new ShowThread(obj);
        Thread show = new Thread(showT);

        load.start();
        show.start();

    }
}

class LoadThread extends Thread {
    private Object obj;

    public LoadThread(Object obj) {
        this.obj = obj;
    }

    public void run() {
        System.out.println("开始加载...");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        for (int i = 1; i <= 100; i++) {
            System.out.println("已加载" + i + "%");
            try {
                Thread.sleep((long) (Math.random() * 300));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("结束加载");
        synchronized (obj){
            obj.notify();
        }

        synchronized (obj){
            try {
                obj.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("开始下载...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        for (int i = 1; i <= 100; i++) {
            System.out.println("已下载" + i + "%");
            try {
                Thread.sleep((long) (Math.random() * 300));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("结束下载");
    }
}

class ShowThread implements Runnable {
    private Object obj;

    public ShowThread(Object obj) {
        this.obj = obj;
    }

    @Override
    public void run() {
//                try {
//                    t1.join();
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
        System.out.println("等待加载中。。。");
        synchronized (obj) {
            try {
                obj.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("显示图片");

        synchronized (obj){
            obj.notify();
        }

    }
}

5 小结

到此为⽌我们已经对线程状态有了基本的认识,想要有更多的了解,详情可以⻅下图:

什么是线程?线程概念及方法详细讲解_第13张图片

什么是线程?线程概念及方法详细讲解_第14张图片

多线程: 父类 Thread
  程序: 软件, 工程
  进程: 正在运行的程序
  线程: 进程中的任务单位

  CPU 可以"同时"处理多个线程
  并行: 同一时刻, 同时运行, 通常需要多核处理器
  并发: 多线程, 交替执行(交替速度足够快, 看起来是同时)

实现多线程:

  main -> 一个线程
  执行多线程: 随机性

  方式一: 只能继承一个类, 功能性单一
    1.自定义类, 继承Thread
    2.重写run方法
    3.在主程序中创建线程对象
    4.开启线程 start()

  方式二: 实现接口
    1.自定义类, 实现Runnable接口
    2.实现run方法
    3.创建线程对象  ※  使用Runnable对象来构造
    4.开启线程 start

  方式三: 匿名内部类

Thread 基础的API:
  String getName(): Thread 属性 name
  static Thread currentThread(): 获得当前线程对象
  static void sleep(long time): 当前线程的阻塞时间

线程的状态:
    见图

线程安全: 多个线程共享资源
解决安全: 实现线程同步
         加锁: 同步锁  synchronized, 需要借助一个对象
              Lock锁  接口  实现类 ReentrantLock()
                 上锁 lock()  解锁 unlock()

线程其他属性和方法:
    setPriority(1-10越来越大): 设置优先级, 提升了这个线程的执行概率
    setDaemon(true): 设置守护线程, 所有的"前置"线程结束, 守护线程也将自动结束
                     GC -> 垃圾回收(守护线程)
                     System.gc() -> 手动清理

你可能感兴趣的:(Java,java)