Java多线程基础
1. 概念
1.1 线程与进程
- 进程
- 进程是指运行中的程序,比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存空间。当我们使用迅雷,又启动了一个进程,操作系统将为迅雷分配新的内存空间。
- 一个进程中至少有一个线程。
- 进程是程序的一次执行过程,或是正在运行的一个程序。是动态过程:有它自身的产生、存在和消亡的过程
- 线程
- 线程是由进程创建的,是进程的一个实体
- 一个进程可以拥有多个线程
1.2 单线程与多线程
- 单线程:同一个时刻,只允许执行一个线程
- 多线程:同一个时刻,可以执行多个线程(比如:一个qq进程,可以同时打开多个聊天窗口,一个迅雷进程,可以同时下载多个文件)
1.3 并发与并行
-
并发:同一个时刻,多个任务交替执行,造成一种“貌似同时”的错觉,简单的说,单核cpu实现的多任务就是并发。
-
并行:同一个时刻,多个任务同时执行。多核cpu可以实现并行
在电脑里,是有可能存在并发和并行同时存在的(多核cpu的情况下)
2. 线程的创建
①继承Thread类
测试代码
/**
* @date: 2022/5/25
* @FileName: Thread01
* @author: Yan
* @Des:
*/
// 1.定义线程类Cat 继承 Thread类
class Cat extends Thread {
/**
* 示例:使用继承Thread类的方式创建一个线程,实现输出1-5
* 2.子类中重写Thread类中的run方法
*/
@Override
public void run() {
while (true){
System.out.println("芜湖~我是线程,我起飞啦 " + "主线程继续执行" + Thread.currentThread().getName());
try {
//让主线程休眠
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Thread01 {
public static void main(String[] args) throws InterruptedException {
// 创建一个cat对象,可以当做一个线程使用
// 3.创建线程对象
Cat cat = new Cat();
// 启动线程
//启动线程-> 最终会执行cat的run方法
// 4.调用线程对象的start方法启动线程
cat.start();
//说明: 当main线程启动一个子线程 Thread-0, 主线程不会阻塞, 会继续执行
//这时 主线程和子线程是交替执行..
for(int i = 0; i < 10; i++) {
System.out.println("主线程 i=" + i);
//让主线程休眠
Thread.sleep(1000);
}
}
}
测试结果
芜湖~我是线程,我起飞啦 主线程继续执行Thread-0
主线程 i=7
芜湖~我是线程,我起飞啦 主线程继续执行Thread-0
主线程 i=8
芜湖~我是线程,我起飞啦 主线程继续执行Thread-0
主线程 i=9
芜湖~我是线程,我起飞啦 主线程继续执行Thread-0
芜湖~我是线程,我起飞啦 主线程继续执行Thread-0
芜湖~我是线程,我起飞啦 主线程继续执行Thread-0
芜湖~我是线程,我起飞啦 主线程继续执行Thread-0
注意点
多线程的机制
当程序启动时会首先创建一个main线程,当main线程启动一个子线程 Thread-0, 主线程不会阻塞, 会继续执行,如下图所示
而且当主线程结束后,子线程并不会立即停止
为什么启动线程要使用
start()
而不是用run方法? 若直接使用
run()
会从主线程进入到这个所谓的“子线程”,但是这个线程名打印出来还是叫main,因为run()
方法就是一个普通的方法,主线程会执行完run后再继续执行后续代码,并不是真正意义上启动一个线程,就是串行化启动线程,主线程会阻塞在run()
这里。start()启动线程 -> 最终会执行cat的run方法:
在start()源码中有一个start0的本地方法,这次是真正实现多线程的方法
(1) public synchronized void start() { start0(); } (2) //start0() 是本地方法,是JVM调用, 底层是c/c++实现 //真正实现多线程的效果, 是start0(), 而不是 run private native void start0();
②实现Runnable接口
测试代码
/**
* @date: 2022/5/25
* @FileName: Thread02
* @author: Yan
* @Des:
*/
// 1.定义线程实现类 Dog类 实现 Runnable接口
class Dog implements Runnable { //通过实现Runnable接口,开发线程
int count = 0;
/**
* 示例:使用实现Runnable接口的方式创建一个线程,实现从1输出到5。
* 2.实现类中重写Runnable接口中的run方法
*/
@Override
public void run() { //普通方法
while (true) {
System.out.println("小狗汪汪叫..hi" + (++count) + Thread.currentThread().getName());
//休眠1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 10) {
break;
}
}
}
}
public class Thread02 {
public static void main(String[] args) {
// 3.创建Runnable接口的子类对象
Dog dog = new Dog();
//dog.start(); 这里不能调用start
// 4.通过Thread类创建线程对象
//创建了Thread对象,把 dog对象(实现Runnable),放入Thread,类似于设计模式中的代理模式
Thread thread = new Thread(dog);
// 5.调用Thread类的start方法启动线程
thread.start();
}
}
测试结果
小狗汪汪叫..hi1Thread-0
小狗汪汪叫..hi2Thread-0
小狗汪汪叫..hi3Thread-0
小狗汪汪叫..hi4Thread-0
小狗汪汪叫..hi5Thread-0
小狗汪汪叫..hi6Thread-0
区别:Thread和Runnable的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
总结: 实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
③通过Callable和Future创建线程
3. 线程状态转换图
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
4. 线程的常用操作方法
①设置和获取线程名字
获取当前线程名称的代码: Thread.currentThread().getName();
设置线程名称:可以通过Thread
类的构造方法 或者 setName
方法设置线程名称
②线程的休眠
- 使线程转到阻塞状态。
- millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
public static void sleep(long millis)
:使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
③线程的强制运行(线程的加入、插队)
等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
void join()
:等待该线程终止。
调用Thread
类中的jojn
方法可以让一个线程独占CPU资源,直到它完成线程的所有操作,CPU资源才会分配给其他线程执行。
插队的线程一旦插队成功,则肯定先执行完插入的线程所有的任务
④线程的暂停(线程的让步)
暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
(让出cpu,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功)
static void yield()
:暂停当前正在执行的线程对象,并执行其他线程。
yield
方法可以是线程暂时让出CPU,但是也有可能继续被CPU调度而接着执行。
yield方法和sleep方法的区别:
sleep
方法使当前线程暂停指定的时间yield
方法使运行状态的线程进入就绪状态
⑤守护线程(后台线程)
在Java程序中有两类线程,分别是用户线程(前台线程)、守护线程(后台线程)。
用户线程和守护线程的区别:
- 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束
- 守护线程:一般为工作线程服务的,当所有的用户线程结束,守护线程自动结束
常见的守护线程:垃圾回收机制
void setDaemon(boolean on)
:将该线程标记为守护线程或用户线程。
boolean isDaemon()
:测试该线程是否为守护线程。
/**
* @date: 2022/5/25
* @FileName: ThreadMethod03
* @author: Yan
* @Des:
*/
public class ThreadMethod03 {
public static void main(String[] args) throws InterruptedException {
MyDaemonThread myDaemonThread = new MyDaemonThread();
//如果我们希望当main线程结束后,子线程自动结束
//,只需将子线程设为守护线程即可
myDaemonThread.setDaemon(true);
myDaemonThread.start();
for( int i = 1; i <= 10; i++) {//main线程
System.out.println("主线程...");
Thread.sleep(1000);
}
}
}
class MyDaemonThread extends Thread {
@Override
public void run() {
for (; ; ) {//无限循环
try {
Thread.sleep(1000);//休眠1000毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("守护线程...");
}
}
}
注意点:
- 将线程设置为后台线程后,当所有非后台线程执行完毕时,后台线程也会停止执行。
- main线程是非后台线程。否则JVM虚拟机不会退出。
⑥线程的优先级
Java程序中有最高、中等、最低3种优先级,当所有的线程在运行前都会保持在就绪状态,会先执行优先级高的线程。
int getPriority()
:返回线程的优先级。
void setPriority(int newPriority)
:更改线程的优先级。
Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
优先级 | 描述 | 表示常量 |
---|---|---|
MIN_PRIORITY | 最低优先级 | 1 |
NORM_PRIORITY | 中等优先级,默认优先级 | 5 |
MAX_PRIORITY | 最高优先级 | 10 |
注意点:
主线程的默认优先级为Thread.NORM_PRIORITY。
线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。
⑦线程的中断
中断线程,但并没有真正的结束线程。所以一般用于中断正在休眠的线程
常用方法:
void interrupt()
:中断线程。static boolean interrupted()
:测试当前线程是否已经中断。boolean isInterrupted()
: 测试线程是否已经中断。
不要以为它是中断某个线程!它只是向线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出抛出,从而结束线程,但是如果你吃掉了这个异常,那么这个线程还是不会中断的!
线程的中断其实是为了优雅的停止线程的运行,为了不使用stop方法而设置的。因为JDK不推荐使用stop方法进行线程的停止,stop方法会释放锁并强制终止线程,会造成执行一半的线程终止,带来数据的不一致性。
测试代码
public class ThreadInterrupt {
public static void main(String[] args) throws InterruptedException {
//测试相关的方法
T t = new T();
t.setName("ThreadInterruptTest");
t.setPriority(Thread.MIN_PRIORITY);//1
t.start();//启动子线程
//主线程打印5 hi ,然后我就中断 子线程的休眠
for(int i = 0; i < 5; i++) {
Thread.sleep(1000);
System.out.println("hi " + i);
}
System.out.println(t.getName() + " 线程的优先级 =" + t.getPriority());//1
t.interrupt();//当执行到这里,就会中断 t线程的休眠.
}
}
class T extends Thread { //自定义的线程类
@Override
public void run() {
while (true) {
for (int i = 0; i < 5; i++) {
//Thread.currentThread().getName() 获取当前线程的名称
System.out.println(Thread.currentThread().getName() + " 吃包子~~~~" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
System.out.println(Thread.currentThread().getName() + " 休眠中~~~");
Thread.sleep(5000);//20秒
} catch (InterruptedException e) {
//当该线程执行到一个interrupt 方法时,就会catch 一个 异常, 可以加入自己的业务代码
//InterruptedException 是捕获到一个中断异常.
System.out.println(Thread.currentThread().getName() + "被 interrupt了");
}
}
}
}
5. 线程同步机制
在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性。
(也可以这里理解:线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作。)
有三种方式完成同步操作:
- 同步代码块。
- 同步方法。
- 锁机制(Lock锁)
互斥锁概念
- Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。
- 每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
- 关键字synchronized来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问
①同步代码块——synchronized关键字
类似于排队上厕所,锁就是厕所门,拿到锁的人就进入厕所关上门(上锁)自己操作,没有锁的就在外面排队等着厕所里面的人弄完开门(释放锁)出来
同步代码块: synchronized
关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
// 格式:
synchronized(同步锁){
// 需要同步操作的代码
}
// 格式:针对于static的方法
//1
public synchronized static void m1() {
//这个锁是加在 SellTicket03.class(也就是类对象本身)
}
//2
//如果在静态方法中,实现一个同步代码块.
public static void m2() {
synchronized (SellTicket03.class) {
System.out.println("m2");
}
}
同步锁: 对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
-
锁对象 可以是任意类型。
-
多个线程对象 要使用同一把锁。
对于非static方法,同步锁就是this(当前对象),也可以是其他对象(要求是同一个对象)
对于static方法,可以使用当前方法所在类的字节码对象,也就是当前类本身(类名.class)。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
使用多线程模拟售票——使用同步代码块解决多线程超卖问题
/**
* @date: 2022/5/26
* @FileName: ticket
* @author: Yan
* @Des:
*/
public class SellTicket {
public static void main(String[] args) {
System.out.println("===使用实现接口方式来售票=====");
// SellTicket02 sellTicket02 = new SellTicket02();
SellTicket03 sellTicket03 = new SellTicket03();
// new Thread(sellTicket02).start();//第1个线程-窗口
// new Thread(sellTicket02).start();//第2个线程-窗口
// new Thread(sellTicket02).start();//第3个线程-窗口
new Thread(sellTicket03).start();//第1个线程-窗口
new Thread(sellTicket03).start();//第2个线程-窗口
new Thread(sellTicket03).start();//第3个线程-窗口
}
}
// 使用同步方法synchronized关键字实现线程同步
class SellTicket03 implements Runnable {
private int ticketNum = 1000;//让多个线程共享 ticketNum
private boolean loop = true;//控制run方法变量
Object object = new Object();
public void sell(){
// 同步代码块
//this 是当前对象
// synchronized (this){
// 也可以是其他对象(要求是同一个对象),该object对于三个线程来说是同一个对象
synchronized (object){
if (ticketNum <= 0) {
System.out.println("售票结束...");
loop = false;
return;
}
//休眠50毫秒, 模拟
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"
+ " 剩余票数=" + (--ticketNum));//1 - 0 - -1 - -2
}
}
@Override
public void run() {
while (loop) {
sell();
}
}
}
②同步方法——synchronized关键字
同步方法:使用synchronized
修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
// 格式:加在方法上
public synchronized void method(){
// 可能会产生线程安全问题的代码
}
使用多线程模拟售票——使用同步方法解决多线程超卖问题
/**
* @date: 2022/5/26
* @FileName: ticket
* @author: Yan
* @Des:
*/
public class SellTicket {
public static void main(String[] args) {
System.out.println("===使用实现接口方式来售票=====");
// SellTicket02 sellTicket02 = new SellTicket02();
SellTicket03 sellTicket03 = new SellTicket03();
// new Thread(sellTicket02).start();//第1个线程-窗口
// new Thread(sellTicket02).start();//第2个线程-窗口
// new Thread(sellTicket02).start();//第3个线程-窗口
new Thread(sellTicket03).start();//第1个线程-窗口
new Thread(sellTicket03).start();//第2个线程-窗口
new Thread(sellTicket03).start();//第3个线程-窗口
}
}
// 使用同步方法synchronized关键字实现线程同步
class SellTicket03 implements Runnable {
private int ticketNum = 1000;//让多个线程共享 ticketNum
private boolean loop = true;//控制run方法变量
//同步方法, 在同一时刻, 只能有一个线程来执行sell方法
//1. public synchronized void sell() {} 就是一个同步方法
//2. 这时锁在 this对象
//3. 也可以在代码块上写 synchronize ,同步代码块, 互斥锁还是在this对象
public synchronized void sell(){
if (ticketNum <= 0) {
System.out.println("售票结束...");
loop = false;
return;
}
//休眠50毫秒, 模拟
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"
+ " 剩余票数=" + (--ticketNum));//1 - 0 - -1 - -2
}
@Override
public void run() {
while (loop) {
sell();
}
}
}
③锁机制(Lock锁)
java.util.concurrent.locks.Lock
机制提供了比synchronized
代码块和synchronized
方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock
都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁,加锁与释放锁方法化了,如下:
public void lock()
:加同步锁。
public void unlock()
:释放同步锁。
6. 线程死锁
多个线程都占用对方的锁资源,但是不肯想让,导致了死锁。
同一时刻,死锁的出现:
- 线程A:我有o1锁,我在等o2锁
- 线程B:我有o2锁,我在等o1锁
/**
* @date: 2022/5/26
* @FileName: DeadLock
* @author: Yan
* @Des:
*/
public class DeadLock {
public static void main(String[] args) {
//模拟死锁现象
DeadLockDemo A = new DeadLockDemo(true);
A.setName("A线程");
DeadLockDemo B = new DeadLockDemo(false);
B.setName("B线程");
A.start();
B.start();
}
}
//线程
class DeadLockDemo extends Thread {
static Object o1 = new Object();// 保证多线程,共享一个对象,这里使用static
static Object o2 = new Object();
boolean flag;
public DeadLockDemo(boolean flag) {//构造器
this.flag = flag;
}
@Override
public void run() {
//下面业务逻辑的分析
//1. 如果flag 为 T, 线程A 就会先得到/持有 o1 对象锁, 然后尝试去获取 o2 对象锁
//2. 如果线程A 得不到 o2 对象锁,就会Blocked
//3. 如果flag 为 F, 线程B 就会先得到/持有 o2 对象锁, 然后尝试去获取 o1 对象锁
//4. 如果线程B 得不到 o1 对象锁,就会Blocked
if (flag) {
synchronized (o1) {//对象互斥锁, 下面就是同步代码
System.out.println(Thread.currentThread().getName() + " 进入1");
synchronized (o2) { // 这里获得li对象的监视权
System.out.println(Thread.currentThread().getName() + " 进入2");
}
}
} else {
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + " 进入3");
synchronized (o1) { // 这里获得li对象的监视权
System.out.println(Thread.currentThread().getName() + " 进入4");
}
}
}
}
}
7. 线程之间数据的传递
在多线程的异步开发模式下,数据的传递和返回和同步开发模式有很大的区别。由于线程的运行和结束是不可预料的,因此,在传递和返回数据时就无法象函数一样通过函数参数和return语句来返回数据
数据传递的三种方法:
- 通过构造方法传递数据
- 通过变量和方法传递数据
- 通过回调函数传递数据
①通过构造方法传递数据
- 在创建线程时,必须要建立一个Thread类的或其子类的实例。因此,我们不难想到在调用start方法之前通过线程类的构造方法将数据传入线程。并将传入的数据使用类变量保存起来,以便线程使用(其实就是在run方法中使用)。
/**
* @date: 2022/5/26
* @FileName: ThreadData1
* @author: Yan
* @Des:
*/
public class ThreadData1 extends Thread{
private String name ;
public ThreadData1 (String name ){
this.name = name ;
}
@Override
public void run ( ){
System.out.println ( "hello " + name ) ;
}
public static void main ( String[ ] args ){
Thread thread = new ThreadData1 ( "world" ) ;thread .start( ) ;
}
}
②通过变量和方法传递数据
- 向对象中传入数据一般有两次机会,第一次机会是在建立对象时通过构造方法将数据传入,另外一次机会就是在类中定义一系列的public的方法或变量(也可称之为字段)。然后在建立完对象后,通过对象实例逐个赋值。
/**
* @date: 2022/5/26
* @FileName: ThreadData2
* @author: Yan
* @Des:
*/
public class ThreadData2 implements Runnable{
private String name;
public void setName (String name){
this.name = name ;
}
@Override
public void run(){
System.out.println ( "hello " +name ) ;
}
public static void main (String[ ] args){
ThreadData2 myThread = new ThreadData2( );
myThread .setName ( "world " ) ;
Thread thread = new Thread ( myThread ) ;
thread.start( ) ;
}
}
③通过回到函数传递数据
- 上面讨论的两种向线程中传递数据的方法是最常用的。但这两种方法都是main方法中主动将数据传入线程类的。这对于线程来说,是被动接收这些数据的。
- 然而,在有些应用中需要在线程运行的过程中动态地获取数据,如在下面代码的run方法中产生了3个随机数,然后通过Work类的process方法求这三个随机数的和,并通过Data类的value将结果返回。从这个例子可以看出,在返回value之前,必须要得到三个随机数。
- 也就是说,这个 value是无法事先就传入线程类的。
/**
* @date: 2022/5/26
* @FileName: ThreadData3
* @author: Yan
* @Des:
*/
class Data{
public int value = 0;
}
class Work{
// 定义对应要使用的回调函数
public void process(Data data, Integer... numbers){
for (int n : numbers){
data.value += n ;
}
}
}
public class ThreadData3 extends Thread {
private Work work;
public ThreadData3 (Work work){
this.work = work;
}
@Override
public void run() {
java.util.Random random = new java.util.Random() ;
Data data = new Data ();
int n1 = random.nextInt ( 1000 ) ;
int n2 = random.nextInt (2000 ) ;
int n3 = random.nextInt ( 3000 ) ;
work.process(data, n1, n2, n3) ; //使用回调函数
System.out.println(String.valueOf(n1) +"+" + String.valueOf(n2)+"+"+String.valueOf(n3) +"=" +data.value ) ;
}
public static void main (String[ ] args){
Thread thread = new ThreadData3(new Work());
thread.start( ) ;
}
}
8. 线程的等待环境机制
8.1 线程间通信
为什么要处理线程间通信:
多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当需要多个线程来共同完成一件任务,并且希望多个线程有规律的执行, 那么多线程之间需要一些协调通信,以此来达到多线程共同操作一份数据。
如何保证线程间通信有效利用资源:
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。就是多个线程在操作同一份数据时,避免对同一共享变量的争夺。也就是需要通过一定的手段使各个线程能有效的利用资源。而这种手段即 —— 等待唤醒机制(等待唤醒机制就是用于解决线程间通信的问题的)
8.2 等待唤醒机制
什么是等待唤醒机制?
**这是多个线程间的一种协作机制。**谈到线程便会经常想到的是线程间的竞争(
race
),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就是在一个线程进行了规定操作后,就进入等待状态(wait()
), 等待其他线程执行完指定代码过后再将其唤醒(notify()
);在有多个线程进行等待时, 如果需要,可以使用
notifyAll()
来唤醒所有的等待线程。wait/notify
就是线程间的一种协作机制。
8.3 等待唤醒中的方法
使用到的3个方法的含义如下:
wait
:**线程不再活动,不再参与调度,进入wait set
中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是WAITING
。**它还要等着别的线程执行一个特别的动作,也即是“通知(notify
)”在这个对象上等待的线程从wait set
中释放出来,重新进入到调度队列(ready queue
)中notify
:则选取所通知对象的wait set
中的一个线程释放;notifyAll
:则释放所通知对象的wait set
上的全部线程。
注意:哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以它需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调 用 wait 方法之后的地方恢复执行。
8.4 总结
- 如果能获取锁,线程就从
WAITING
状态变成RUNNABLE
状态; - 否则,从
wait set
出来,又进入entry set
,线程就从WAITING
状态又变成BLOCKED
状态
调用
wait
和notify
方法需要注意的细节
- **
wait
方法与notify
方法必须要由同一个锁对象调用。**因为:对应的锁对象可以通过notify
唤醒使用同一个锁对象调用的wait
方法后的线程。- **
wait
方法与notify
方法是属于Object
类的方法的。**因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object
类的。- **
wait
方法与notify
方法必须要在同步代码块或者是同步函数中使用。**因为:必须要通过锁对象调用这2个方法。
9. 线程池
9.1 线程池概念
线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
好处:
- **降低资源消耗。**减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- **提高响应速度。**当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- **提高线程的可管理性。**可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
9.2 线程池的使用
Java里面线程池的顶级接口是 java.util.concurrent.Executor
,但是严格意义上讲 Executor
并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService
。
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在 java.util.concurrent.Executors
线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors
工程类来创建线程池对象。
public static ExecutorService newFixedThreadPool(int nThreads)
:返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)
public Future<?> submit(Runnable task)
:获取线程池中的某一个线程对象,并执行
Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
9.3 使用步骤
使用线程池中线程对象的步骤:
- 创建线程池对象。
- 创建Runnable接口子类对象。
- 提交Runnable接口子类对象。
- 关闭线程池(一般不做)。
资料来源