Java 多线程
进程和线程
- 进程(Process)是计算机中的程序关于某数据集合上的一次运动活动
- 是系统进行资源分配的基本单位
- 简单理解:程序的执行过程
- 独立性:每一个进程都有自己的空间,在没有经过进程本身允许的情况下,一个进程不可以直接访问其他的进程空间
- 动态性:进程是动态产生的,动态消亡的
- 并发性:任何进程都可以同其他进程一起并发执行
并行和并发
- 并行:在同一时刻,有多个指令在CPU上**同时**执行
- 并发:在同一时刻,有多个指令在CPU上交替执行
多进程同时工作
对于一个CPU而言,它是在多个线程间轮换执行的
线程
线程(Thread):进程可以同时执行多个任务,每个任务就是线程
多线程的意义
- 随着处理器上的核心数量越来越多,现在大多数计算机都比以往更加擅长并行计算
- 一个线程,在一个时刻,只能运行在一个处理器核心上
- 提高执行效率
- 同时处理多个任务


Java开启线程方式
- Java默认是多线程,当运行时默认会存在main线程和垃圾回收线程
开启线程,有以下三种方式:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
继承Thread类
步骤如下:
- 编写一个类继承Thread类
- 重写run方法
- 将线程任务代码写在run方法中
- 创建线程对象
- 调用start方法开启线程(调用start方法后,会自动调用run方法)
示例:
package thread;
public class ThreadDemo01 {
public static void main(String[] args) {
// 4. 创建线程对象
MyThread01 mt = new MyThread01();
// 5. 调用start方法开启线程
mt.start();
// 区分线程
for (int i = 0; i < 500; i++) {
System.out.println("main线程执行了" + i);// main线程和对象线程交替执行
}
}
}
// 1. 编写一个类继承Thread类
class MyThread01 extends Thread {
// 2. 重写run方法
@Override
public void run() {
// 3. 将线程任务代码写在run方法中
for (int i = 0; i < 500; i++) {
System.out.println("线程任务执行了" + i);
}
}
}
实现Runnable接口
- 编写一个类实现Runnable接口
- 重写run方法
- 将线程任务代码写在run方法中
- 创建Runnable接口的实现类对象
- 创建线程对象,并将Runnable接口的实现类对象传入
- 使用线程对象调用start方法开启线程
示例:
package thread;
public class ThreadDemo02 {
public static void main(String[] args) {
// 4. 创建Runnable接口的实现类对象
MyRunnable01 mr = new MyRunnable01();
// 5. 创建线程对象,并将Runnable接口的实现类对象传入
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
// 6. 使用线程对象调用start方法开启线程
t1.start();
t2.start();
// 区分线程
for (int i = 0; i < 500; i++) {
System.out.println("main线程执行了:" + i);
}
}
}
// 1. 编写一个类实现Runnable接口
class MyRunnable01 implements Runnable {
// 2. 重写run方法
@Override
public void run() {
// 3. 将线程任务代码写在run方法中
for (int i = 0; i < 500; i++) {
System.out.println("自己的线程任务:" + i);
}
}
}
实现Callable接口
- 编写一个类实现Callable接口(需要指定泛型,根据call方法返回的类型确定类型)
- 重写call方法(此方法存在返回值)
- 将线程任务代码写在call方法中
- 创建线程资源对象
- 创建线程任务对象,封装线程资源
- 创建线程对象,传入线程任务
- 使用线程对象调用start开启线程
示例:
package thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadDemo03 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 4. 创建线程资源对象
MyCallable01 mc = new MyCallable01();
// 5. 创建线程任务对象,封装线程资源
FutureTask<Integer> task1 = new FutureTask<>(mc);
// 6. 创建线程对象,传入线程任务
Thread t1 = new Thread(task1);
// 7. 使用线程对象调用start开启线程
t1.start();
for (int i = 1; i <= 100; i++) {
System.out.println("main线程执行中:" + i);
}
// 8. 获取返回结果
Integer res = task1.get();
System.out.println("线程执行结果:" + res);
}
}
// 1. 编写一个类实现Callable接口
class MyCallable01 implements Callable<Integer> {
// 2. 重写call方法
@Override
public Integer call() throws Exception {
// 3. 将线程任务代码写在call方法中
int count = 0;
for (int i = 1; i <= 100; i++) {
count += i;
System.out.println("线程执行中:" + i);
}
return count;
}
}
FutureTask类实现了RunnableFuture接口,这个接口继承了Runnable接口
调用get()方法获取线程的执行结果,要注意必须在调用了start()方法之后(不然线程都没运行就直接要求拿到结果,就会卡住)
三种方式的选择
第一种,继承Thread类:实现简单,代码少,但是由于Java只支持单继承,所以当继承了Thread类后,就不能再继承其他类了
第二种,实现Runnable接口:实现起来虽然较复杂,但是比继承更加灵活,且实际开发中用的较多
第三种,实现Callable接口:当需要线程执行完后返回结果,可以使用这种方式,因为call方法有返回值
综上所述:第二种和第三种的优势更符合开发项目,实际开发中也使用的更多
线程相关方法
| 方法名称 | 说明 |
|---|---|
| String getName() | 返回此线程的名称 |
| void setName(String name) | 设置线程的名字(构造方法也可以设置名字) |
| static Thread currentThread() | 获取当前线程的对象 |
| static void sleep(long time) | 让线程休眠指定的时间,单位为毫秒 |
| setPriority(int newPriority) | 设置线程的优先级 |
| final int getPriority() | 获取线程的优先级 |
| final void setDaemon(boolean on) | 设置为守护线程 |
获取线程名称
示例:
package thread;
public class ThreadDemo04 {
public static void main(String[] args) {
MyThread02 mt01 = new MyThread02();
MyThread02 mt02 = new MyThread02();
mt01.start();
mt02.start();
}
}
class MyThread02 extends Thread {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println(super.getName() + "线程执行中:" + i);
}
}
}
线程如果不手动设置名称,那么就为默认名称:Thread-数字
比如:第一个线程名称为Thread-01,第二个线程名称为Thread-02
设置线程名称
线程的名称是可以重复的
利用setName设置线程名称:
package thread;
public class ThreadDemo04 {
public static void main(String[] args) {
MyThread02 mt01 = new MyThread02();
MyThread02 mt02 = new MyThread02();
mt01.setName("线程A:");
mt02.setName("线程B:");
mt01.start();
mt02.start();
}
}
class MyThread02 extends Thread {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println(super.getName() + "线程执行中:" + i);
}
}
}
利用构造方法设置线程名称:
package thread;
public class ThreadDemo04 {
public static void main(String[] args) {
MyThread02 mt01 = new MyThread02();
MyThread02 mt02 = new MyThread02("线程A:");
MyThread02 mt03 = new MyThread02("线程B:");
mt01.start();
mt02.start();
mt03.start();
}
}
class MyThread02 extends Thread {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println(super.getName() + "线程执行中:" + i);
}
}
public MyThread02() {
super();
}
public MyThread02(String name) {
super(name);
}
}
以上两种方式都继承了Thread类,所以可以直接调用**super.getName()**方法获取线程名称
但是当类实现Runnable或Callable接口时,不能调用super.getName(),因为这个类的父类没有继承Thread类
类实现接口时,获取线程名称:
package thread;
public class ThreadDemo05 {
public static void main(String[] args) {
MyRunnable02 mr01 = new MyRunnable02();
Thread t1 = new Thread(mr01,"线程A:");
MyRunnable02 mr02 = new MyRunnable02();
Thread t2 = new Thread(mr02);
t2.setName("线程B:");
t1.start();
t2.start();
}
}
class MyRunnable02 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println(Thread.currentThread().getName() + "线程执行中:" + i);
}
}
}
创建线程对象时,可以接收两个参数,第一个参数是Runnable实现类对象,第二参数是线程名称
获取当前线程对象
示例:
package thread;
public class ThreadDemo06 {
public static void main(String[] args) {
System.out.println(Thread.currentThread()); // 获取main的线程对象
System.out.println(Thread.currentThread().getName()); // 获取main的线程对象的名称
}
}
让线程休眠指定时间
示例:
package thread;
public class ThreadDemo07 {
public static void main(String[] args) throws InterruptedException {
for (int i = 5; i != 0; i--) {
Thread.sleep(1000); // 休眠1000毫米
System.out.println("倒计时:" + i);
}
}
}
设置线程优先级
线程调度方式
- 抢占式调度
- Java中默认采用的就是这种方式
- 哪个线程抢到就执行哪个线程
- 非抢占式调度
- 每个线程依次执行
| 方法名称 | 说明 |
|---|---|
| setPriority(int newPriority) | 设置线程的优先级 |
| final int getPriority() | 获取线程的优先级 |
优先级:1 ~ 10,默认为5,优先级越高,抢到CPU执行的概率越高
示例:
package thread;
public class ThreadDemo08 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println(Thread.currentThread().getName() + "---执行执行中---" + i);
}
}
},"线程A:");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println(Thread.currentThread().getName() + "---执行执行中---" + i);
}
}
}, "线程B:");
System.out.println(t1.getPriority());// 5
System.out.println(t2.getPriority());// 5
t1.setPriority(1);
t2.setPriority(10);
System.out.println(t1.getPriority()); // 1
System.out.println(t2.getPriority()); // 10
t1.start();
t2.start();// 线程B先执行完毕的概率更高
}
}
设置优先级只是提高抢到CPU执行的概率,而不是哪个线程一定会先拿到CPU执行
守护线程
- 当一条线程被关闭,那么另一条线程也要随着这条线程的关闭而关闭,这就是守护线程
示例:
package thread;
public class ThreadDemo09 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "--- 线程执行中 ---" + i);
}
}
},"线程A:");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println(Thread.currentThread().getName() + "--- 线程执行中 ---" + i);
}
}
},"线程B:");
t2.setDaemon(true);
t1.start();
t2.start();
}
}
设置守护线程不需要将守护线程与被守护线程显式关联
整个执行流程如下:
- main线程启动t1和t2
- t1(用户线程)执行20次循环
- t2(守护线程)执行循环(可能执行不完)
- 当t1执行完毕后,JVM发现没有其他用户线程在运行
- JVM退出,强制终止t2(即使t2还没执行完200次)
线程安全和同步
线程的安全问题
安全问题出现条件:
- 多线程环境
- 存在共享数据
- 有多条语句操作共享数据
示例:
package thread;
public class ThreadDemo10 {
/*
需求:
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口买票
请设计一个程序模拟电影院买票
*/
public static void main(String[] args) {
TicketTask.tickets = 100;
TicketTask ticketTask_01 = new TicketTask();
TicketTask ticketTask_02 = new TicketTask();
TicketTask ticketTask_03 = new TicketTask();
Thread t1 = new Thread(ticketTask_01,"一号窗口:");
Thread t2 = new Thread(ticketTask_02,"二号窗口:");
Thread t3 = new Thread(ticketTask_03,"三号窗口:");
t1.start();
t2.start();
t3.start();
}
}
class TicketTask implements Runnable {
static int tickets;
@Override
public void run() {
while (tickets != 0) {
tickets--;
System.out.println(Thread.currentThread().getName() + "卖出了第" + (100 - tickets) + "张票");
}
}
}
问题如下:

假设线程1在执行过程中,CPU执行权被线程二抢到了,那么就会进入到线程二的处理逻辑中。此时数据还未被线程1修改,所以线程1和线程2在操作同一份数据,这就会出问题
线程同步
- 同步技术:将多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程可以执行
- 同步代码块
- 同步方法
- Lock锁
同步代码块
关键字:synchronized [ˈsɪŋkrənaɪzd]
格式:
synchronized(锁对象) {
多条语句操作共享数据代码
}
锁对象可以是任意对象,但是需要保证多条线程的锁对象,是同一把锁
同步可以解决多线程的数据安全问题,但是也会降低程序的运行效率
将线程的安全问题这一章的代码进行优化:
package thread;
import java.util.Objects;
public class ThreadDemo11 {
/*
需求:
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口买票
请设计一个程序模拟电影院买票
*/
public static void main(String[] args) {
TicketTaskAfterFix.tickets = 100;
TicketTaskAfterFix ticketTask_01 = new TicketTaskAfterFix();
Thread t1 = new Thread(ticketTask_01,"一号窗口:");// 创建第一个线程
Thread t2 = new Thread(ticketTask_01,"二号窗口:");// 创建第二个线程
Thread t3 = new Thread(ticketTask_01,"三号窗口:");// 创建第三个线程
t1.start();
t2.start();
t3.start();
}
}
class TicketTaskAfterFix implements Runnable {
static int tickets;
Object lock = new Object();
@Override
public void run() {
while (tickets != 0) {
synchronized (lock) {
tickets--;
System.out.println(Thread.currentThread().getName() + "卖出了第" + (100 - tickets) + "张票");
}
}
}
}
同步代码块要在while内部
表示线程A进入while内部后,先上锁,执行完ticket–操作后,打印控制台,最后释放锁
A线程 => 进入while内部 => 上锁 => 开始操作逻辑 => 执行完成,释放锁
当同步代码块在while外部
表示线程A先上锁,然后执行while逻辑代码,直到所有票数为0,才释放锁,其他线程没有机会参与进去
A线程 => 先上锁 => 执行while逻辑代码 => 票数减到0 => 释放锁
注意:这里一定只能创建一个Runnable实现类对象,否则会导致生成的锁不是一个锁。一定要保证锁的一致性
为了保证锁的一致性,更加推荐直接使用当前的类对象作为锁对象,即XXX.class:
@Override
public void run() {
while (tickets != 0) {
synchronized (TicketTaskAfterFix.class) {
tickets--;
System.out.println(Thread.currentThread().getName() + "卖出了第" + (100 - tickets) + "张票");
}
}
}
同步方法
在方法的返回值类型前面加入synchronized关键字
方法分为静态和非静态
静态方法的锁对象是字节码对象,非静态方法的锁对象是this
示例:
package thread;
import java.util.Objects;
public class ThreadDemo11 {
/*
需求:
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口买票
请设计一个程序模拟电影院买票
*/
public static void main(String[] args) {
TicketTaskAfterFix.tickets = 100;
TicketTaskAfterFix ticketTask_01 = new TicketTaskAfterFix();
Thread t1 = new Thread(ticketTask_01,"一号窗口:");
Thread t2 = new Thread(ticketTask_01,"二号窗口:");
Thread t3 = new Thread(ticketTask_01,"三号窗口:");
t1.start();
t2.start();
t3.start();
}
}
class TicketTaskAfterFix implements Runnable {
static int tickets;
@Override
public void run() {
while (true) {
if (!sellTickets()) {
return;
}
}
}
private synchronized boolean sellTickets() {
if (tickets == 0) return false;
tickets--;
System.out.println(Thread.currentThread().getName() + "卖出了第" + (100 - tickets) + "张票");
return true;
}
}
这里只创建了一个Runnable实现类对象,所以三个线程的锁对象都是当前对象,保证了锁的一致性
但如果创建了多个Runnable实现类对象,那么这几个对象的this并不是同一个对象,无法保证锁的一致性,此时只能将方法转换为静态方法
| 创建方式 | 同步方法类型 | 锁对象 | 是否有效 |
|---|---|---|---|
| 多个实例 | 实例方法 | 各实例自身 | ❌ 无效 |
| 多个实例 | 静态方法 | 类对象 | ✅ 有效 |
| 单个实例 | 实例方法 | 该实例 | ✅ 有效 |
| 单个实例 | 静态方法 | 类对象 | ✅ 有效 |
Lock锁
- 使用Lock锁,可以更清晰的看到哪里加了锁,哪里释放了锁
- 先前的同步代码块
synchronized (锁对象) { ... }、同步方法public synchornized void methd() {},无法直观的看到上锁、释放锁的过程 - Lock锁可以解决以上问题:
lock.lock();
...
lock.unlock();
- Lock是接口,无法直接创建对象
| 构造方法 | 说明 |
|---|---|
| public ReentrantLock() | 创建一个ReentrantLock的实力 |
| 成员方法 | 说明 |
|---|---|
| void lock() | 加锁 |
| void unlock() | 释放锁 |
示例:
package thread;
import java.util.Objects;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadDemo11 {
/*
需求:
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口买票
请设计一个程序模拟电影院买票
*/
public static void main(String[] args) {
TicketTaskAfterFix.tickets = 100;
TicketTaskAfterFix ticketTask_01 = new TicketTaskAfterFix();
Thread t1 = new Thread(ticketTask_01,"一号窗口:");
Thread t2 = new Thread(ticketTask_01,"二号窗口:");
Thread t3 = new Thread(ticketTask_01,"三号窗口:");
t1.start();
t2.start();
t3.start();
}
}
class TicketTaskAfterFix implements Runnable {
static int tickets;
ReentrantLock rtl = new ReentrantLock();
@Override
public void run() {
while (true) {
rtl.lock();
if (tickets == 0) break;
tickets--;
System.out.println(Thread.currentThread().getName() + "卖出了第" + (100 - tickets) + "张票");
rtl.unlock();
}
rtl.unlock(); // 最后一次的锁也需要释放,否则程序会卡主
}
}
线程池
- 将线程对象交给线程池维护,可以降低系统成本,从而提升程序的性能
系统创建一个线程的成本是比较高的,因为它涉及到与操作系统交互
当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程,就会严重浪费系统资源
【强制】 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
【强制】 线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽风险
说明:Executor返回的线程池对象的弊端如下:
- FixedThreadPool和SingleThreadPool: - 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM(OutOfMemory)
- CachedThreadPool: - 允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM
使用JDK提供的线程池
Executors中提供静态方法来创建线程池
| 方法 | 介绍 |
|---|---|
| static ExecutorService newCachedThreadPool() | 创建一个默认的线程池 |
| static newFixedThreadPool(int nThreads) | 创建一个指定最大线程数量的线程池 |
示例:
package pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDemo01 {
public static void main(String[] args) {
// 获取指定线程数量的线程池对象
ExecutorService pool2 = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
// 多次提交线程任务,会复用线程池中的线程对象,且线程名称的数字不会超过10
pool2.submit(new Runnable() {
@Override
public void run() {
// 程序运行完成,JVM不会停止,除非关闭线程池
System.out.println(Thread.currentThread().getName() + "提交了线程任务");
}
});
}
pool2.shutdown();
}
public static void getPool() {
// 获取线程池对象
ExecutorService pool1 = Executors.newCachedThreadPool();
// 提交线程任务到线程池
for (int i = 0; i < 100; i++) {
// 多次提交线程任务,会复用线程池中的线程对象
pool1.submit(new Runnable() {
@Override
public void run() {
// 程序运行完成,JVM不会停止,除非关闭线程池
System.out.println(Thread.currentThread().getName() + "提交了线程任务");
}
});
}
// 关闭线程池
pool1.shutdown();
}
}
自定义线程池
创建ThreadPoolExecutor类对象,用于自定义线程池
| 构造方法 | 说明 |
|---|---|
| ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue |
corePoolSize:核心线程数量(正式员工) maximumPoolSize:最大线程数量(正式员工 + 临时员工) keepAliveTime:空闲时间 unit:时间单位 workQueue:任务队列(指定排队人数) threadFactory:线程对象工厂 handler:拒绝策略 |
注意事项:
- 核心数量不能为0
- 最大线程数量不能为0,且数量必须>=核心线程数量
- 空闲时间不能小于0(不能为负数)
- 时间单位必须使用TimeUnit枚举类型
- 任务队列不能为null
- 线程对象工厂不能为null
- 拒绝策略不能为null
示例:
package pool;
import java.util.concurrent.*;
public class ThreadPoolDemo02 {
public static void main(String[] args) {
// 任务队列分为有界队列和无界队列
// 有界队列:new ArrayBlockingQueue<>(10),参数表示队伍最大长度
// 无界队列:new LinkedBlockingQueue<>()
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2,
5,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
Executors.defaultThreadFactory(),// 基本上是固定写法
new ThreadPoolExecutor.AbortPolicy());
// 当超过了核心 + 队列长度,就会尝试创建临时线程
for (int i = 1; i <= 13; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "提交了任务");
}
});
}
// 触发拒绝策略,控制台会警告
for (int i = 1; i <= 16; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "提交了任务");
}
});
}
}
}
提交的任务线程数大于了核心线程数+任务队列长度,就会创建临时线程(线程任务数 > 核心线程数 + 任务队列的数量)
当线程任务数大于了最大线程数+任务队列数,就会启动拒绝策略(线程任务数 > 最大线程数 + 任务队列的数量)
线程拒绝策略
| 策略选项 | 说明 |
|---|---|
| ThreadPoolExecutor.AbortPolicy | 丢弃任务并抛出RejectedExecutionException异常(默认且推荐) |
| ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但是不抛出异常(不推荐) |
| ThreadPoolExecutor.DiscardOldestPolicy | 抛弃队列中等待最久的任务,然后把当前任务加入队列中 |
| ThreadPoolExecutor.CallerRunsPolicy | 调用任务的run方法,绕过线程池直接执行 |