10 多线程
约 13775 字大约 46 分钟
2025-08-22
相关概念
程序、进程、线程
程序(Program
):为完成特定任务,用某种语言编写的一组指令的集合,是一段静态的代码。
进程(Process
):程序的一次执行过程,或是正在内存中运行的应用程序。
- 每个进程都有一个独立的内存空间,系统运行一个程序即是一个进程从创建、运行到消亡的过程。(生命周期)
- 程序是静态的,进程是动态的
- 进程作为操作系统调度和分配的最小单位(或者说是系统运行程序的基本单位),系统在运行时会为每个进程分配不同的内存区域
线程(Thread
):进程可以进一步细化为线程,是程序内部的一条执行路径,一个进程中至少有一个线程
一个进程同一时间若并行执行多个线程,就是支持多线程的
线程作为
CPU
调度和执行的最小单位一个进程中的多个线程共享相同的内存单元,它们从同一个堆中分配对象,可以访问相同的变量和对象,这使线程间通信更简便、高效,但多个线程操作共享的系统资源可能会带来安全隐患。
下图中,红框的蓝色区域为线程独享,黄色区域为线程共享。(
JVM
运行时数据区)
不同进程之间是不共享内存的。
线程调度
分时调度:所有线程轮流使用 CPU
,并且平均分配每个线程占用 CPU
的时间。
抢占式调度:优先级高的线程以较大的概率优先使用 CPU
。如果线程的优先级相同,那么会随机选择一个(线程随机性),Java 使用的为抢占式调度。
多线程程序的优点
提高应用程序的响应速度,对图形化界面更有意义,可增强用户体验。
提高计算机系统
CPU
的利用率。改善程序结构,将长而复杂的进程分为多个线程,独立运行,利于理解和修改。
并行和并发
并行(parallel
): 指两个或多个事件在同一时刻发生(同时发生)。在同一时刻,有多条指令在多个CPU
上同时执行,如:多个人同时做不同的事。
并发(concurrency
): 指两个或多个事件在同一个时间段内发生。在一段时间内,有多条指令在单个CPU
上快速轮换、交替执行,使得在宏观上具有多个进程同时执行的效果。
创建和启动线程
概述
Java 语言的 JVM 允许程序运行多个线程,使用java.lang.Thread
类代表线程,所有的线程对象都必须是Thread
类或其子类的实例。
Thread
类的特性:
- 线程都是通过特定
Thread
对象的run()
方法来完成操作的,因此把run()
方法体称为线程执行体 - 通过该
Thread
对象的start()
方法来启动这个线程,而非直接调用run()
- 要想实现多线程,必须在主线程中创建新的线程对象
方式一:继承Thread
类
Java 通过继承Thread
类来创建并启动多线程的步骤如下:
定义
Thread
类的子类,并重写该类的run()
方法,该run()
方法的方法体就代表了线程需要完成的任务创建
Thread
子类的实例,即创建了线程对象调用线程对象的
start()
方法来启动该线程
样例:
// 自定义线程类
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);
}
}
}
public class TestMyThread {
public static void main(String[] args) {
// 创建自定义线程对象1
MyThread mt1 = new MyThread("子线程1");
// 开启子线程1
mt1.start();
//创建自定义线程对象2
MyThread mt2 = new MyThread("子线程2");
// 开启子线程2
mt2.start();
// 在主方法中执行for循环
for (int i = 0; i < 10; i++) {
System.out.println("main线程!"+i);
}
}
}
注意:
如果自己手动调用run()
方法,那么就只是普通方法,没有启动多线程模式。
run()
方法由 JVM 调用,什么时候调用、执行的过程控制都由操作系统的CPU
调度决定。
想要启动多线程,必须调用start()
方法。
一个线程对象只能调用一次start()
方法,重复调用则将抛出异常IllegalThreadStateException
。
方式二:实现Runnable
接口
Java 有单继承的限制,当我们无法继承Thread
类时,那么该如何做呢?
在核心类库中提供了Runnable
接口,我们可以实现Runnable
接口,重写run()
方法,然后再通过Thread
类的对象代理启动和执行我们的线程体run()
方法。
定义
Runnable
接口的实现类,并重写该接口的run()
方法,run()
方法的方法体是线程的执行体。创建
Runnable
实现类的实例,并以此实例作为Thread
的target
参数来创建Thread
对象,该Thread
对象才是真正的线程对象。调用线程对象的
start()
方法,启动线程(会调用Runnable
接口实现类的run
方法)。
样例:
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 TestMyRunnable {
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);
}
}
}
实际上,所有的多线程代码都是通过运行Thread
的start()
方法来运行的。因此,不管是继承Thread
类还是实现Runnable
接口来实现多线程,最终还是通过Thread
的对象的API
来控制线程的。
Runnable
对象仅仅作为Thread
对象的target
,Runnable
实现类里包含的run()
方法仅作为线程执行体。 而Thread
Thread
target
run()
。
变形写法
使用匿名内部类对象来实现线程的创建和启动:
// 方式一的匿名写法
new Thread("新的线程!"){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}.start();
// 方式二的匿名写法
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":" + i);
}
}
}).start();
两种方式的对比
联系:
Thread
类实际上也是实现了Runnable
接口的类。
即: public class Thread implements Runnable
区别:
继承Thread
:线程代码在Thread
子类的run
方法中。
实现Runnable
:线程代码在接口的实现类的run
方法中。
实现Runnable
接口比继承Thread
类所具有的优势:
增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
Thread
类的常用结构
构造器
public Thread()
分配一个新的线程对象。
public Thread(String name)
分配一个指定名字的新的线程对象
public Thread(Runnable target)
指定创建线程的目标对象,它实现了Runnable
接口中的run
方法。
public Thread(Runnable target,String name)
分配一个带有指定目标新的线程对象并指定名字。
常用方法系列 1
public void run()
此线程要执行的任务在此处定义代码。
public void start()
让此线程开始执行,Java 虚拟机调用此线程的run
方法。
public String getName()
获取当前线程名称。
public void setName(String name)
设置该线程名称。
public static Thread currentThread()
返回对当前正在执行的线程对象的引用。
在Thread
子类中就是this
,通常用于主线程和Runnable
实现类。
public static void sleep(long millis)
使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
public static void yield()
yield
只是让当前线程暂停一下,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会。
但是这个不能保证,完全有可能的情况是:当某个线程调用了yield
方法暂停之后,线程调度器又将其调度出来重新执行。
常用方法系列 2
public final boolean isAlive()
测试线程是否处于活动状态。
如果线程已经启动且尚未终止,则为活动状态。
void join()
等待该线程终止。
void join(long millis)
等待该线程终止的时间最长为millis
毫秒,如果millis
时间到,将不再等待。
void join(long millis, int nanos)
等待该线程终止时间最长为millis
毫秒+nanos
纳秒。
public final void stop()
已过时,不建议使用。强行结束一个线程的执行,直接进入死亡状态。
run()
即刻停止,可能会导致一些清理性的工作得不到完成,如:文件,数据库等的关闭。同时,
void suspend() / void resume()
已过时,不建议使用。这两个操作就好比播放器的暂停和恢复,二者必须成对出现,否则非常容易发生死锁。
suspend()
调用会导致线程暂停,但不会释放任何锁资源,导致其它线程都无法访问被它占用的锁,直到调用resume()
。
常用方法系列 3
每个线程都有一定的优先级,同优先级线程组成先进先出队列(先到先服务),使用分时调度策略。优先级高的线程采用抢占式策略,获得较多的执行机会。
每个线程默认的优先级都与创建它的父线程具有相同的优先级。
Thread
类的三个优先级常量:
MAX_PRIORITY
:最高优先级10
MIN_PRIORITY
:最低优先级1
NORM_PRIORITY
:普通优先级5
,默认情况下main
线程具有普通优先级。
public final int getPriority()
返回线程优先级。
public final void setPriority(int newPriority)
改变线程的优先级,范围[1,10]
之间。
练习 1:
声明一个匿名内部类继承Thread
类,重写run
方法,在run
方法中获取线程名称和优先级、设置该线程优先级为最高优先级并启动该线程。
public static void main(String[] args) {
Thread t = new Thread(){
public void run(){
System.out.println(getName() + "的优先级:" + getPriority());
}
};
t.setPriority(Thread.MAX_PRIORITY);
t.start();
System.out.println(Thread.currentThread().getName() +"的优先级:" + Thread.currentThread().getPriority());
}
练习 2:
声明一个匿名内部类继承
Thread
类,重写run
方法,实现打印[1,100]
之间的偶数,要求每隔1
秒打印1
个偶数。声明一个匿名内部类继承
Thread
类,重写run
方法,实现打印[1,100]
之间的奇数,当打印到
5
时,让奇数线程暂停一下,再继续。当打印到
5
时,让奇数线程停下来,让偶数线程执行完再打印。
package com.atguigu.api;
public class TestThreadStateChange {
public static void main(String[] args) {
Thread te = new Thread() {
@Override
public void run() {
for (int i = 2; i <= 100; i += 2) {
System.out.println("偶数线程:" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
te.start();
Thread to = new Thread() {
@Override
public void run() {
for (int i = 1; i <= 100; i += 2) {
System.out.println("奇数线程:" + i);
if (i == 5) {
// Thread.yield();
try {
te.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
};
to.start();
}
}
守护线程
守护线程有个特点,就是如果所有非守护线程都死亡,那么守护线程自动死亡。
setDaemon(true)
可将指定线程设置为守护线程。
isDaemon()
可以判断线程是否是守护线程。
setDaemon
必须在线程启动之前设置,否则会报IllegalThreadStateException
异常。
public class TestThread {
public static void main(String[] args) {
MyDaemon m = new MyDaemon();
m.setDaemon(true);
m.start();
for (int i = 1; i <= 100; i++) {
System.out.println("main:" + i);
}
}
}
class MyDaemon extends Thread {
public void run() {
while (true) {
System.out.println("我一直守护者你...");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
多线程的生命周期
JDK1.5 之前:5
种状态
线程的生命周期有五种状态:新建New
、就绪Runnable
、运行Running
、阻塞Blocked
、死亡Dead
。CPU
需要在多个线程之间切换,于是线程状态会多次在运行、阻塞、就绪之间切换。
新建
new
Thread
类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。此时它和其他 Java 对象一样,。此时的线程对象并没有任何线程的动态特征,程序也不会执行它的线程体
run()
。就绪
Runnable
当线程对象调用了
至于什么时候被调度,取决于 JVM 里线程调度器的调度。start()
方法之后,线程就从新建状态转为就绪状态。注意:程序只能对新建状态的线程调用
start()
,并且只能调用一次,如果对非新建状态的线程,如已启动的线程或已死亡的线程调用start()
都会报错IllegalThreadStateException
异常。运行
Running
如果处于就绪状态的线程获得了
CPU
资源时,开始执行run()
方法的线程体代码,则该线程处于运行状态。如果计算机只有一个
CPU
核心,在任何时刻只有一个线程处于运行状态,如果计算机有多个核心,将会有多个线程并行Parallel
执行。对于抢占式策略的系统而言,。此时其他线程将获得执行机会,而在选择下一个线程时,系统会适当考虑线程的优先级。
阻塞
Blocked
当在运行过程中的线程遇到如下情况时,会让出 CPU 并临时中止自己的执行,进入阻塞状态:
- 线程调用了
sleep()
方法,主动放弃所占用的 CPU 资源 - 线程试图获取一个同步监视器,但该同步监视器正被其他线程持有
- 线程执行过程中,同步监视器调用了
wait()
,让它等待某个通知notify
- 线程执行过程中,同步监视器调用了
wait(time)
- 线程执行过程中,遇到了其他线程对象的加塞
join()
- 线程被调用
suspend
方法挂起()
当前正在执行的线程被阻塞后,其他线程就有机会执行了。针对上述情况,当发生如下情况时会解除阻塞,让该线程重新进入就绪状态, 等待线程调度器再次调度它:
- 线程的
sleep()
时间到 - 线程成功获得了同步监视器
- 线程等到了通知
notify()
- 线程
wait()
的时间到了 - 加塞
join()
的线程结束了 - 被挂起的线程又被调用了
resume()
恢复方法()
- 线程调用了
死亡
Dead
线程会以以下三种方式之一结束,结束后的线程就处于死亡状态:
run()
方法执行完成,线程正常结束- 线程执行过程中抛出了一个未捕获的异常
Exception
或错误Error
- 直接调用该线程的
stop()
来结束该线程()
JDK1.5 及之后:6
种状态
在java.lang.Thread.State
的枚举类中的定义:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED
}
当从WAITING
或TIMED_WAITING
恢复到Runnable
状态时,如果发现当前线程没有得到监视器锁,那么会立刻转入BLOCKED
状态。
根据Thread.State
的定义,阻塞状态分为三种:BLOCKED
、WAITING
、TIMED_WAITING
。
NEW
新建线程刚被创建,但是并未启动,还没调用
start
方法。RUNNABLE
可运行这里没有区分就绪和运行状态。因为对于 Java 对象来说,只能标记为可运行,至于什么时候运行,不是 JVM 来控制的,是 OS 来进行调度的,而且时间非常短暂,因此对于 Java 对象的状态来说,无法区分。
TERMINATED
被终止表明此线程已经结束生命周期,终止运行。
BLOCKED
锁阻塞正在阻塞、等待一个监视器锁(锁对象)的线程处于这一状态。只有获得锁对象的线程才有执行机会。
如:线程
A
与线程B
代码中使用同一锁,如果线程A
获取到锁,线程A
进入到RUNNABLE
状态,那么线程B
就进入到BLOCKED
锁阻塞状态。TIMED_WAITING
计时等待正在限时等待另一个线程执行一个唤醒动作的线程处于这一状态。
当前线程执行过程中遇到
Thread
类的sleep
或join
,Object
类的wait
,LockSupport
类的park
方法,并且在调用这些方法时,设置了时间,那么当前线程会进入TIMED_WAITING
,直到时间到或被中断。WAITING
无限等待正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。
当前线程执行过程中遇到
Object
类的wait
,Thread
类的join
,LockSupport
类的park
方法,并且在调用这些方法时,没有指定时间,那么当前线程会进入WAITING
状态,直到被唤醒。- 通过
Object
类的wait
进入WAITING
状态的要由Object
的notify/notifyAll
唤醒 - 通过
Condition
的await
进入WAITING
状态的要由Condition
的signal
方法唤醒 - 通过
LockSupport
类的park
方法进入WAITING
状态的要由LockSupport
类的unpark
方法唤醒 - 通过
Thread
类的join
进入WAITING
状态,只有调用join
方法的线程对象结束才能让线程恢复
- 通过
样例:
public class ThreadStateTest {
public static void main(String[] args) throws InterruptedException {
SubThread subThread = new SubThread();
System.out.println(subThread.getName() + "初始线程状态 " + subThread.getState());
subThread.start();
while(Thread.State.TERMINATED != subThread.getState()) {
System.out.println(subThread.getName() + "当前线程状态 " + subThread.getState());
Thread.sleep(500);
}
System.out.println(subThread.getName() + "最终线程状态 " + subThread.getState());
}
}
class SubThread extends Thread {
AtomicInteger atomicInteger = new AtomicInteger(3);
@Override
@SuppressWarnings("all")
public void run() {
while(atomicInteger.get() > 0) {
// for (int i = 0; i < 10; i++) {
System.out.println("打印:" + atomicInteger.decrementAndGet());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// }
}
}
}
线程安全问题及解决方案
当我们使用多个线程访问同一资源(同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题。
但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。
多线程共享同一个资源的问题(线程安全问题)
案例(模拟火车站的卖票过程):
火车站要卖票,模拟火车站的卖票过程。
列车的座位共100
个(只能出售100
张火车票)。
模拟车站售票窗口,实现多个窗口同时售票的过程。
注意:不能出现错票、重票。
不同的实现方案以及出现的问题:
局部变量不能共享
问题:
结果:发现卖出
300
张票。package com.atguigu.unsafe; class Window extends Thread { public void run() { int ticket = 100; while (ticket > 0) { System.out.println(getName() + "卖出一张票,票号:" + ticket); ticket--; } } } public class SaleTicketDemo1 { public static void main(String[] args) { Window w1 = new Window(); Window w2 = new Window(); Window w3 = new Window(); w1.setName("窗口1"); w2.setName("窗口2"); w3.setName("窗口3"); w1.start(); w2.start(); w3.start(); } }
不同对象的实例变量不共享
问题:
结果:发现卖出
300
张票。package com.atguigu.unsafe; class TicketWindow extends Thread { private int ticket = 100; public void run() { while (ticket > 0) { System.out.println(getName() + "卖出一张票,票号:" + ticket); ticket--; } } } public class SaleTicketDemo2 { public static void main(String[] args) { TicketWindow w1 = new TicketWindow(); TicketWindow w2 = new TicketWindow(); TicketWindow w3 = new TicketWindow(); w1.setName("窗口1"); w2.setName("窗口2"); w3.setName("窗口3"); w1.start(); w2.start(); w3.start(); } }
静态变量是共享的
问题 1:有重复票或跳数票问题。
结果:发现卖出近
100
张票。原因:线程安全问题
问题 2:如果要考虑有两场电影,各卖
100
张票等原因:
TicketSaleThread
类的静态变量,是所有TicketSaleThread
类的对象共享package com.atguigu.unsafe; class TicketSaleThread extends Thread { private static int ticket = 100; public void run() { while (ticket > 0) { try { Thread.sleep(10);//加入这个,使得问题暴露的更明显 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName() + "卖出一张票,票号:" + ticket); ticket--; } } } public class SaleTicketDemo3 { public static void main(String[] args) { TicketSaleThread t1 = new TicketSaleThread(); TicketSaleThread t2 = new TicketSaleThread(); TicketSaleThread t3 = new TicketSaleThread(); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } }
同一个对象的实例变量共享
示例代码:多个
Thread
线程使用同一个Runnable
对象。结果:发现卖出近
100
张票。问题:但是有重复票或跳数票问题。
原因:线程安全问题
package com.atguigu.safe; class TicketSaleRunnable implements Runnable { private int ticket = 100; public void run() { while (ticket > 0) { try { Thread.sleep(10);//加入这个,使得问题暴露的更明显 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket); ticket--; } } } public class SaleTicketDemo4 { public static void main(String[] args) { TicketSaleRunnable tr = new TicketSaleRunnable(); Thread t1 = new Thread(tr, "窗口一"); Thread t2 = new Thread(tr, "窗口二"); Thread t3 = new Thread(tr, "窗口三"); t1.start(); t2.start(); t3.start(); } }
抽取资源类,共享同一个资源对象
结果:发现卖出近
100
张票。问题:但是有重复票或跳数票问题。
原因:线程安全问题
package com.atguigu.unsafe; //1、编写资源类 class Ticket { private int ticket = 100; public void sale() { if (ticket > 0) { try { Thread.sleep(10);//加入这个,使得问题暴露的更明显 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket); ticket--; } else { throw new RuntimeException("没有票了"); } } } public class SaleTicketDemo5 { public static void main(String[] args) { //2、创建资源对象 Ticket ticket = new Ticket(); //3、启动多个线程操作资源类的对象 Thread t1 = new Thread("窗口一") { public void run() { while (true) { ticket.sale(); } } }; Thread t2 = new Thread("窗口二") { public void run() { while (true) { ticket.sale(); } } }; Thread t3 = new Thread(new Runnable() { public void run() { ticket.sale(); } }, "窗口三"); t1.start(); t2.start(); t3.start(); } }
同步机制解决线程安全问题
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java 中提供了同步机制 synchronized
来解决。
根据上图案例,卖车票的案例可以改为如下:
窗口1
线程进入操作的时候,窗口2
和窗口3
线程只能在外等着,窗口1
操作结束,窗口1
和窗口2
和窗口3
才有机会进入代码去执行。
也就是说在
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着BLOCKED
。
同步机制解决线程安全问题的原理:
同步机制的原理,其实就相当于给某段代码加“锁”,任何线程想要执行这段代码,都要先获得“锁”,我们称它为同步锁。因为 Java 对象在堆中的数据分为对象头、实例变量、空白的填充。
对象头中包含:
Mark Word
:记录了和当前对象有关的GC
、锁标记等信息- 指向类的指针:每一个对象需要记录它是由哪个类创建出来的
- 数组长度(只有数组对象才有)
哪个线程获得了“同步锁”对象之后,“同步锁”对象就会记录这个线程的ID
,这样其他线程就只能等待了,除非这个线程“释放”了锁对象,其他线程才能重新获得或占用“同步锁”对象。
同步代码块和同步方法:
- 同步代码块:
synchronized
关键字可以用于某个区块前面,表示只对这个区块的资源实行互斥访问。
synchronized(同步锁) {
// 需要同步操作的代码
}
- 同步方法:
synchronized
关键字直接修饰方法,表示同一时刻只有一个线程能进入方法,其他线程在外面等着。
public synchronized void method() {
// 可能会产生线程安全问题的代码
}
同步锁机制:
对于并发工作,你需要某种方式来防止两个任务访问相同的资源,即:共享资源竞争。防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。
第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。
synchronized
的锁是什么:
对于同步代码块来说,同步锁对象是由程序员手动指定的,很多时候也是指定为this
或类名.class
。
同步锁对象可以是任意类型,但必须保证竞争同一个共享资源的多个线程必须使用同一同步锁对象
静态方法:当前类的
Class
对象(类名.class
)非静态方法:
this
同步操作的思考顺序:
如何找问题,即代码是否存在线程安全?
- 明确哪些代码是多线程运行的代码
- 明确多个线程是否有共享数据
- 明确多线程运行代码中是否有多条语句操作共享数据
如何解决呢?
- 对多条操作共享数据的语句,只能让一个线程都执行完
- 在执行过程中,其他线程不可以参与执行,即:所有操作共享数据的这些语句都要放在同步范围中
注意:
同步操作范围太小:不能解决安全问题。
同步操作范围太大:因为一旦某个线程抢到锁,其他线程就只能等待,所以范围太大,效率会降低,不能合理利用 CPU 资源。
代码演示
静态方法加锁:
package com.atguigu.safe;
class TicketSaleThread extends Thread{
private static int ticket = 100;
public void run(){
//直接锁这里,肯定不行,会导致,只有一个窗口卖票
while (ticket > 0) {
saleOneTicket();
}
}
public synchronized static void saleOneTicket(){
//锁对象是TicketSaleThread类的Class对象,而一个类的Class对象在内存中肯定只有一个
if(ticket > 0) {
//不加条件,相当于条件判断没有进入锁管控,线程安全问题就没有解决
/*
不加判断可能出现的情况:
线程 A 进入 saleOneTicket() 方法,检查 ticket > 0,条件满足,执行售票操作,ticket 变为 0。
线程 B 在 saleOneTicket() 方法外部等待锁。
线程 A 执行完毕,释放锁。
线程 B 获得锁,进入 saleOneTicket() 方法。
线程 B 没有再次检查 ticket > 0,直接执行售票操作,ticket 变为 -1。
*/
System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);
ticket--;
}
}
}
public class SaleTicketDemo3 {
public static void main(String[] args) {
TicketSaleThread t1 = new TicketSaleThread();
TicketSaleThread t2 = new TicketSaleThread();
TicketSaleThread t3 = new TicketSaleThread();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
非静态方法加锁:
package com.atguigu.safe;
public class SaleTicketDemo4 {
public static void main(String[] args) {
TicketSaleRunnable tr = new TicketSaleRunnable();
Thread t1 = new Thread(tr, "窗口一");
Thread t2 = new Thread(tr, "窗口二");
Thread t3 = new Thread(tr, "窗口三");
t1.start();
t2.start();
t3.start();
}
}
class TicketSaleRunnable implements Runnable {
private int ticket = 100;
public void run() {
//直接锁这里,肯定不行,会导致,只有一个窗口卖票
while (ticket > 0) {
saleOneTicket();
}
}
public synchronized void saleOneTicket() {
//锁对象是this,这里就是TicketSaleRunnable对象,
//因为上面3个线程使用同一个TicketSaleRunnable对象,所以可以
if (ticket > 0) {
//不加条件,相当于条件判断没有进入锁管控,线程安全问题就没有解决
System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);
ticket--;
}
}
}
同步代码块:
package com.atguigu.safe;
public class SaleTicketDemo5 {
public static void main(String[] args) {
//2、创建资源对象
Ticket ticket = new Ticket();
//3、启动多个线程操作资源类的对象
Thread t1 = new Thread("窗口一") {
public void run() {
//不能给run()直接加锁,因为t1,t2,t3的三个run方法分别属于三个Thread类对象,
// run方法是非静态方法,那么锁对象默认选this,那么锁对象根本不是同一个
while (true) {
synchronized (ticket) {
ticket.sale();
}
}
}
};
Thread t2 = new Thread("窗口二") {
public void run() {
while (true) {
synchronized (ticket) {
ticket.sale();
}
}
}
};
Thread t3 = new Thread(new Runnable() {
public void run() {
while (true) {
synchronized (ticket) {
ticket.sale();
}
}
}
}, "窗口三");
t1.start();
t2.start();
t3.start();
}
}
//1、编写资源类
class Ticket {
private int ticket = 1000;
public void sale() {
//也可以直接给这个方法加锁,锁对象是this,这里就是Ticket对象
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);
ticket--;
} else {
throw new RuntimeException("没有票了");
}
}
}
再谈同步
单例设计模式的线程安全问题
饿汉式没有线程安全问题:
在类初始化时就直接创建单例对象,而类初始化过程是没有线程安全问题的。
package com.atguigu.single.hungry;
class HungrySingle {
// 对象是否声明为final 都可以
private static HungrySingle INSTANCE = new HungrySingle();
private HungrySingle(){}
public static HungrySingle getInstance(){
return INSTANCE;
}
}
public class HungrySingleTest {
static HungrySingle hs1 = null;
static HungrySingle hs2 = null;
// 演示是否存在线程安全问题
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run() {
hs1 = HungrySingle.getInstance();
}
};
Thread t2 = new Thread() {
@Override
public void run() {
hs2 = HungrySingle.getInstance();
}
};
t1.start();
t2.start();
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(hs1);
System.out.println(hs2);
System.out.println(hs1 == hs2);//true
}
}
懒汉式线程安全问题:
延迟创建对象,第一次调用getInstance
方法再创建对象。
使用同步方法或同步代码块:
package com.atguigu.single.lazy;
public class LazyOne {
private static LazyOne instance;
private LazyOne(){}
//方式0:获取的时候再创建对象 不加任何同步相关的措施 不安全
public static LazyOne getInstance() {
if (instance == null) {
instance = new LazyOne();
}
return instance;
}
//方式1:同步方法
public static synchronized LazyOne getInstance1(){
if(instance == null){
instance = new LazyOne();
}
return instance;
}
//方式2:同步代码块 【先加锁再判断】
public static LazyOne getInstance2(){
synchronized(LazyOne.class) {
if (instance == null) {
instance = new LazyOne();
}
return instance;
}
}
//方式3:同步代码块 【先判断再加锁】 【有指令重排问题】
public static LazyOne getInstance3(){
if(instance == null){
synchronized (LazyOne.class) {
try {
Thread.sleep(10);//加这个代码,暴露问题
} catch (InterruptedException e) {
e.printStackTrace();
}
if(instance == null){
instance = new LazyOne();
}
}
}
return instance;
}
}
上述样例代码中的方式 3 问题的补充:
对象创建过程的详细描述
在 Java 中,对象的创建过程可以分为以下几个步骤:
- 分配内存空间:
- 当使用
new
关键字创建对象时,JVM 首先会在堆内存中为对象分配足够的内存空间。- 分配内存空间后,对象的状态是未初始化的,所有字段都具有默认值(例如,
int
类型为0
,引用类型为null
)。- 引用赋值:
- 分配内存空间后,JVM 会将对象的引用赋值给变量。例如,在
instance = new LazyOne();
语句中,instance
变量会被赋值为新创建对象的引用。- 此时,
instance
变量已经指向了堆内存中的对象,但对象还未完成初始化。- 初始化对象:
- 对象的初始化包括调用构造函数(构造器),以及执行构造函数中的代码。构造函数会初始化对象的字段,并执行任何必要的初始化逻辑。
- 初始化完成后,对象的状态才是完全有效的。
指令重排序问题
由于 Java 编译器和 CPU 可能会对指令进行重排序以优化性能,可能会导致步骤 2 在步骤 3 之前执行。这会导致其他线程在对象还未完全初始化时获取到
instance
引用。关键点:
- 分配内存空间:JVM 在堆内存中为对象分配足够的内存空间。
- 引用赋值:将对象的引用赋值给变量,此时对象还未完成初始化。
- 初始化对象:调用构造函数,完成对象的初始化。
总结
对象的创建过程包括分配内存空间、引用赋值和初始化对象三个步骤。由于指令重排序问题,可能会导致其他线程在对象还未完全初始化时获取到
instance
引用。使用volatile
关键字可以避免这个问题,确保线程安全。
使用内部类:
package com.atguigu.single.lazy;
public class LazySingle {
private LazySingle(){}
public static LazySingle getInstance(){
return Inner.INSTANCE;
}
private static class Inner{
static final LazySingle INSTANCE = new LazySingle();
}
}
内部类只有在外部类被调用才加载,产生INSTANCE
实例;又不用加锁。此模式具有之前两个模式的优点,同时屏蔽了它们的缺点,是最好的单例模式。
测试类:
package com.atguigu.single.lazy;
import org.junit.Test;
public class TestLazy {
@Test
public void test01(){
LazyOne s1 = LazyOne.getInstance();
LazyOne s2 = LazyOne.getInstance();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
}
//把s1和s2声明在外面,是想要在线程的匿名内部类中为s1和s2赋值
LazyOne s1;
LazyOne s2;
@Test
public void test02(){
Thread t1 = new Thread(){
public void run(){
s1 = LazyOne.getInstance();
}
};
Thread t2 = new Thread(){
public void run(){
s2 = LazyOne.getInstance();
}
};
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
}
LazySingle obj1;
LazySingle obj2;
@Test
public void test03(){
Thread t1 = new Thread(){
public void run(){
obj1 = LazySingle.getInstance();
}
};
Thread t2 = new Thread(){
public void run(){
obj2 = LazySingle.getInstance();
}
};
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(obj1);
System.out.println(obj2);
System.out.println(obj1 == obj2);
}
}
死锁与解决方案
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
示例:
public class DeadLockTest {
public static void main(String[] args) {
StringBuilder s1 = new StringBuilder();
StringBuilder s2 = new StringBuilder();
new Thread() {
public void run() {
// 持有s1 申请s2
synchronized (s1) {
s1.append("a");
s2.append("1");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2) {
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread() {
public void run() {
// 持有s2 申请s1
synchronized (s2) {
s1.append("c");
s2.append("3");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1) {
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
}
}
诱发死锁的原因:
- 互斥条件/(资源不能被多个线程同时访问,同一时间只能有一个线程占用资源。)
resource1
和resource2
是互斥资源,同一时间只能被一个线程占用。thread1
占用了resource1
,并等待resource2
。thread2
占用了resource2
,并等待resource1
。- 由于互斥条件,两个线程都无法继续执行,陷入死锁。
- 占用且等待/(一个线程已经占用了至少一个资源,并等待获取其他线程占用的资源。)
thread1
占用了resource1
,并等待resource2
。thread2
占用了resource2
,并等待resource1
。- 由于占用且等待条件,两个线程都无法继续执行,陷入死锁。
- 不可抢夺/(一个线程占用的资源不能被其他线程强制夺走,只能由该线程主动释放。)
thread1
占用了resource1
,并等待resource2
。thread2
占用了resource2
,并等待resource1
。- 由于不可抢夺条件,两个线程都无法强制夺取对方占用的资源,陷入死锁。
- 循环等待()
thread1
占用了resource1
,并等待resource2
。thread2
占用了resource2
,并等待resource1
。- 由于循环等待条件,两个线程形成了一个循环链,陷入死锁。
以上 4 个条件,同时出现就会触发死锁。
解决死锁:
死锁一旦出现,基本很难人为干预,。
可以考虑打破上面的诱发条件。
针对条件 1()
针对条件 2()
可以,这样就不存在等待的问题。
针对条件 3()
占用部分资源的线程在进一步申请其他资源时,如果申请不到,就。
针对条件 4()
可以将资源改为线性顺序。
申请资源时,先申请序号较小的,这样避免循环等待问题。
补充:线性顺序的实现
假设有两个资源
resource1
和resource2
,我们可以为它们分配一个固定的顺序:resource1
的顺序小于resource2
。所有线程在申请资源时,必须先申请顺序较小的资源,再申请顺序较大的资源。
JDK5.0 新特性:Lock
(锁)
JDK5.0 的新增功能,保证线程的安全。
与采用synchronized
相比,Lock
可提供多种锁方案,更灵活、更强大。
Lock
通过显式定义同步锁对象来实现同步,同步锁使用Lock
对象充当。
java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的,每次只能有一个线程对Lock
对象加锁,线程开始访问共享资源之前应先获得Lock
对象。
在实现线程安全的控制中,比较常用的是ReentrantLock
,可以显式加锁、释放锁。ReentrantLock
类实现了 Lock
接口,它拥有与 synchronized
相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。
Lock
锁也称,加锁与释放锁方法,如下:
public void lock()
:加同步锁。public void unlock()
:释放同步锁。
class A{
//1. 创建Lock的实例,必须确保多个线程共享同一个Lock实例
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
//2. 调动lock(),实现需共享的代码的锁定
lock.lock();
try{
//保证线程安全的代码;
}
finally{
//3. 调用unlock(),释放共享代码的锁定
lock.unlock();
}
}
}
注意: 如果同步代码有异常,要将unlock
()写入finally
语句块。
举例:
import java.util.concurrent.locks.ReentrantLock;
class Window implements Runnable{
int ticket = 100;
//1. 创建Lock的实例,必须确保多个线程共享同一个Lock实例
private final ReentrantLock lock = new ReentrantLock();
public void run(){
while(true){
try{
//2. 调动lock(),实现需共享的代码的锁定
lock.lock();
if(ticket > 0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticket--);
}else{
break;
}
}finally{
//3. 调用unlock(),释放共享代码的锁定
lock.unlock();
}
}
}
}
public class ThreadLock {
public static void main(String[] args) {
Window t = new Window();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
t2.start();
}
}
synchronized
与Lock
的对比
Lock
是显式锁(),synchronized
是隐式锁,出了作用域、遇到异常等自动解锁。Lock
,synchronized
有代码块锁和方法锁。使用
Lock
锁,。并且具有更好的扩展性(提供更多的子类),更体现面向对象。(了解)
Lock
锁可以对读不加锁,对写加锁,synchronized
不可以。(了解)
Lock
锁可以有多种获取锁的方式,可从sleep
的线程中抢到锁,synchronized
不可以。
说明: 开发建议中处理线程安全问题优先使用顺序为: Lock ---->
同步代码块---->
同步方法。
线程的通信
线程间通信
为什么要处理线程间通信?
当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些通信机制,可以协调它们的工作,以此实现多线程共同操作一份数据。
比如:线程 A 用来生产包子的,线程 B 用来吃包子的,包子可以理解为同一资源,线程 A 与线程 B 处理的动作,一个是生产,一个是消费,此时 B 线程必须等到 A 线程完成后才能执行,那么线程 A 与线程 B 之间就需要线程通信,即:等待唤醒机制。
等待唤醒机制
在一个线程满足某个条件时,就进入等待状态(wait() / wait(time)
),等待其他线程执行完他们的指定代码过后再将其唤醒(notify()
),或可以指定 wait 的时间,等时间到了自动唤醒。
在有多个线程进行等待时,如果需要,可以使用 notifyAll()
来唤醒所有的等待线程。wait/notify
就是线程间的一种协作机制。
wait
线程不再活动,不再参与调度,进入WaitSet
中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态是WAITING
或TIMED_WAITING
。 它还要等着别的线程执行一个特别的动作,即"通知(notify
)"或者等待时间到
,在这个对象上等待的线程从WaitSet
中释放出来,重新进入到调度队列(ready queue
)中。
notify
选取所通知对象的WaitSet
中的一个线程释放。
notifyAll
释放所通知对象的WaitSet
上的全部线程。
注意:
wait
如果能获取锁,线程就从WAITING
状态变成RUNNABLE
(可运行)状态。
否则,线程就从WAITING
状态又变成BLOCKED
(等待锁)状态。
举例
使用两个线程打印1-100
。线程1
、线程2
交替打印
public class ThreadSynchronousDemo {
public static void main(String[] args) {
TsCommunication tsCommunication = new TsCommunication();
Thread thread1 = new Thread(tsCommunication, "\033[36m 线程1 \033[0m");
Thread thread2 = new Thread(tsCommunication, "\033[35m 线程2 \033[0m");
thread1.start();
thread2.start();
}
}
/**
* 打印数据线程体
* */
class TsCommunication implements Runnable {
private int i = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
// 先唤醒其他的阻塞线程
notify();
if (i <= 100) {
System.out.println(Thread.currentThread().getName() + ": " + i++);
} else {
System.exit(0);
}
try {
// 进入等待
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
调用wait
和notify
需注意的细节
wait
方法与notify
方法必须要由同一个锁对象调用
因为:对应的锁对象可以通过notify
唤醒使用同一个锁对象调用wait
方法后的线程
wait
方法与notify
方法是属于Object
类的
因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object
类的
wait
notify
因为:必须要通过锁对象调用这 2 个方法。否则会报java.lang.IllegalMonitorStateException
异常
生产者与消费者问题
等待唤醒机制可以解决经典的“”问题。
生产者与消费者问题(Producer-consumer problem
),也称有限缓冲问题(Bounded-buffer problem
),是一个多线程同步问题的经典案例。
生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也从缓冲区中消耗这些数据。
举例: 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
生产者与消费者问题中其实隐含了两个问题:
线程安全问题:因为生产者与消费者共享数据缓冲区,产生安全问题。这个问题可以使用同步解决。
线程的协调工作问题:要解决该问题,就必须让生产者线程在缓冲区满时等待(
wait
),暂停进入阻塞状态,等到下次消费者消耗了缓冲区中的数据的时候,再通知(notify
)正在等待的线程恢复到就绪状态,重新开始往缓冲区添加数据。同时,也要让消费者线程在缓冲区空时进入等待(wait
),暂停进入阻塞状态,等到生产者往缓冲区添加数据之后,再通知(notify
)正在等待的线程恢复到就绪状态。通过这样的通信机制来解决此类问题。
代码:
public class ConsumerProducerTest {
public static void main(String[] args) {
// 资源 (缓冲区)
Resources resources = new Resources();
// 生产者
Producer producer = new Producer(resources);
producer.setName("生产者1");
// 消费者
Consumer consumer1 = new Consumer(resources);
consumer1.setName("消费者1");
Consumer consumer2 = new Consumer(resources);
consumer2.setName("消费者2");
producer.start();
consumer1.start();
consumer2.start();
}
}
/**
* 资源类
*/
class Resources {
// 产品数量
private int productsNumber = 0;
// 最大数量
public static final int MAX_PRODUCT_NUMBER = 20;
// 最少数量
public static final int MIN_PRODUCT_NUMBER = 1;
// 添加产品
public synchronized void addProduct() {
if (productsNumber < MAX_PRODUCT_NUMBER) {
productsNumber++;
System.out.println(Thread.currentThread().getName() + "\033[35m 生产了第" + productsNumber + "个产品...\033[0m");
// 唤醒阻塞的消费者
notifyAll();
} else {
// 进入阻塞状态等待产品被消费
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 减少产品
public synchronized void minusProduct() {
if (productsNumber > MIN_PRODUCT_NUMBER) {
System.out.println(Thread.currentThread().getName() + "\033[36m 消费了第" + productsNumber + "个产品...\033[0m");
productsNumber--;
// 唤醒阻塞的生产者
notifyAll();
} else {
// 进入阻塞状态等待产品被生产
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 生产者
*/
class Producer extends Thread {
// 资源
private Resources resources;
public Producer(Resources resources) {
this.resources = resources;
}
@Override
public void run() {
System.out.println("-------------------生产者开始生产-------------------");
while (true) {
resources.addProduct();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 消费者
*/
class Consumer extends Thread {
// 资源
private Resources resources;
public Consumer(Resources resources) {
this.resources = resources;
}
@Override
public void run() {
System.out.println("-------------------消费者开始消费-------------------");
while (true) {
resources.minusProduct();
try {
Thread.sleep(2500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
面试题:区分sleep()
和wait()
相同点:一旦执行,都会使得当前线程结束执行状态,进入阻塞状态。
不同点:
方法属类:
sleep()
在Thread
中定义,wait()
在Object
中定义。使用范围:
sleep()
可以在,wait()
必须使用在同步代码块或同步方法中。都在同步结构中使用的时候,
sleep()
wait()
结束等待的方式不同:
sleep()
指定时间一到就结束阻塞,wait()
可以指定时间也可以无限等待直到notify
或notifyAll
。
是否释放锁的操作
任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?
释放锁的操作:
- 当前线程的同步方法、同步代码块执行结束。
- 当前线程在同步代码块、同步方法中遇到
break、return
终止了该代码块、该方法的继续执行。 - 当前线程在同步代码块、同步方法中出现了未处理的
Error
或Exception
,导致当前线程异常结束。 - 当前线程在同步代码块、同步方法中执行了锁对象的
wait()
方法,当前线程被挂起,并释放锁。
不会释放锁的操作:
线程执行同步代码块或同步方法时,调用
Thread.sleep()
、Thread.yield()
方法暂停当前线程的执行。线程执行同步代码块时,其他线程调用了该线程的
suspend()
方法将该线程挂起,该线程不会释放锁(同步监视器)。
suspend()
resume()
8. JDK5.0 新增线程创建方式
新增方式一:实现Callable
接口
与使用Runnable
相比,Callable
功能更强大些:
- 相比
run()
方法,可以有返回值 - 方法可以抛出异常
- 支持泛型的返回值(需要借助
FutureTask
类,获取返回结果)
Future
接口(了解):
- 可以对具体
Runnable
、Callable
任务的执行结果进行取消、查询是否完成、获取结果等 FutureTask
是Future
接口的唯一的实现类。FutureTask
同时实现了Runnable
,Future
接口。它既可以作为Runnable
被线程执行,又可以作为Future
得到Callable
的返回值
代码:
public class CallableTest {
public static void main(String[] args) {
// 创建Callable实现类的对象
NumThread numThread = new NumThread();
// 通过上述对象构造FutureTask对象
FutureTask futureTask = new FutureTask<>(numThread);
// 利用FutureTask对象创建Thread
Thread thread = new Thread(futureTask);
// 启动线程
thread.start();
// 获取Callable中call方法的返回值
// get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
try {
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
/**
* 创建Callable实现类
*/
class NumThread implements Callable {
/**
* 实现call方法完成功能
*/
@Override
public Object call() {
int sum = 0;
for (int i = 1; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
sum += i;
}
}
return sum;
}
}
新增方式二:使用线程池
现有问题:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
有没有一种办法使得线程可以复用,即执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁创建销毁、实现重复利用,类似生活中的公共交通工具。
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
corePoolSize
:核心池的大小maximumPoolSize
:最大线程数keepAliveTime
:线程没有任务时最多保持多长时间后会终止
线程池相关 API:
JDK5.0 之前,我们必须手动自定义线程池。
。在java.util.concurrent
包下提供了线程池相关 API:ExecutorService
和 Executors
。
ExecutorService
真正的线程池接口。
常见子类ThreadPoolExecutor
:
void execute(Runnable command)
:执行任务/命令,没返回值,一般用来执行Runnable
。<T> Future<T> submit(Callable<T> task)
:执行任务,有返回值,一般执行Callable
。void shutdown()
:关闭连接池
Executors
一个线程池的工厂类,通过此类的静态工厂方法可以创建多种类型的线程池对象。
Executors.newCachedThreadPool()
:创建一个可根据需要创建新线程的线程池。Executors.newFixedThreadPool(int nThreads)
:创建一个可重用固定线程数的线程池。Executors.newSingleThreadExecutor()
:创建一个只有一个线程的线程池。Executors.newScheduledThreadPool(int corePoolSize)
:创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
代码:
public class ThreadPoolTest {
public static void main(String[] args) {
// 创建固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
// 转换为ExecutorService的具体实现类
ThreadPoolExecutor executorService1 = (ThreadPoolExecutor) executorService;
// 设置线程池属性
// 设置线程池的线程数量上限
executorService1.setMaximumPoolSize(50);
// 执行指定的线程操作 提供Runnable接口的实现类对象 或 Callable接口的实现类对象
// execute适用于Runnable
executorService1.execute(new NumberThread());
executorService1.execute(new NumberThread1());
// submit适用于Callable
try {
Future<Object> future = executorService1.submit(new NumberThread2());
// 获取执行结果
Object sum = future.get();
System.out.println("求和结果为:" + sum);
} catch (Exception e) {
e.printStackTrace();
}
// 关闭连接池
executorService.shutdown();
}
}
/**
* 线程打印奇数
*/
class NumberThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
/**
* 线程打印偶数
*/
class NumberThread1 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
/**
* 线程求和
*/
class NumberThread2 implements Callable {
@Override
public Object call() {
int evenSum = 0;
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
evenSum += i;
}
}
return evenSum;
}
}