Java基础——多线程
本文系统介绍了Java多线程编程的核心概念与实现方法。主要内容包括:1)线程与进程的区别,以及并发与并行的概念;2)四种线程创建方式(继承Thread类、实现Runnable/Callable接口、线程池);3)线程安全问题的解决方案(同步代码块、同步方法、ReentrantLock等);4)死锁产生原因及预防措施;5)线程通信机制(wait/notify)。文章通过售票系统等实例,详细演示了多线
思考:我们在之前,学习的程序在没有跳转语句的前提下,都是由上至下依次执行,那现在想要设计一个程序,边打游戏边听歌,怎么设计?
要解决上述问题,需要使用多进程或者多线程来解决.
目录
一、什么是多线程
1、线程与进程
| 程序 | 为了完成某个任务和功能,选择一种编程语言编写的一组指令的集合。 |
| 软件 | 一个或多个程序(可执行代码) + 相关的资源文件、配置文件、文档等,构成一个软件系统或软件产品。 |
| 进程 | 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。 |
| 线程 | 线程是进程中的一个执行单元,是CPU调度的最小单位,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。 |
简而言之:一个软件中至少有一个应用程序,应用程序的一次运行就是一个进程,一个进程中至少有一个线程。进程是资源的容器,线程是执行的单元。

2、并发与并行
| 并发 | 指两个或多个事件在同一时刻发生(同时发生)。指在同一时刻,有多条指令在多个处理器上同时执行。 |
| 并行 | 指两个或多个事件在同一个时间段内发生。指在同一个时刻只能有一条指令执行,但多个进程的指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。 |

3、线程调度
| 分时调度 | 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间 |
| 抢占式调度 | 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度 |
二、如何创建&&启动线程
当运行Java程序时,其实已经有一个线程了,那就是main线程。
那么如何创建和启动main线程以外的线程呢?
1、继承Thread类
Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。
Java中通过继承Thread类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
代码示例:
public class MyThread extends Thread{
@Override
public void run() {//线程的业务逻辑
for (int i = 0; i<10; i++){
System.out.println("mythread线程正在执行:"+new Date().getTime());
}
}
}
class ThreadTest {
public static void main(String[] args) {
//1.自定义线程打印
new MyThread1().start();
//2.主线程循环打印
for (int i=0; i<10; i++){
System.out.println("main主线程正在执行:"+new Date().getTime());
}
}
}
执行效果如下:

2、实现Runnable接口
Java有单继承的限制,当我们无法继承Thread类时,那么该如何做呢?在核心类库中提供了Runnable接口,我们可以实现Runnable接口,重写run()方法,然后再通过Thread类的对象代理启动和执行我们的线程体run()方法
步骤如下:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动线程。代码如下:
public class MyThread2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("mythread2线程正在执行:"+ new Date().getTime());
}
}
}
class ThreadTest {
public static void main(String[] args) {
//1.自定义线程打印
new Thread(new MyThread2()).start();
//2.主线程循环打印
for (int i=0; i<10; i++){
System.out.println("main主线程正在执行:"+new Date().getTime());
}
}
}
执行效果如下:

在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的
tips:Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。
3、实现Callable接口
我们发现Runnable没有返回值,不支持泛型的返回值不可以抛出异常,我们可以通过 Callable 接口创建线程。在核心类库中提供了 java.util.concurrent.Callable接口,我们可以实现 Callable 接口,重写 call()方法,然后通过 FutureTask类包装,再由 Thread类的对象代理启动和执行我们的线程体 call()方法,并且能够获取线程执行的结果。
步骤如下:
- 定义 Callable 接口的实现类,并重写该接口的
call()方法,该call()方法的方法体同样是该线程的线程执行体,且该方法可以有返回值并可以抛出异常。 - 创建 Callable 实现类的实例,并使用
FutureTask类包装该实例(FutureTask实现了RunnableFuture接口,而RunnableFuture继承了Runnable接口)。 - 以
FutureTask实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。 - 调用线程对象的start()方法来启动线程。代码如下:
public class MyThread3 implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("mythread3线程正在执行:"+ new Date().getTime());
}
return "MyThread3的call()方法运行结束";
}
}
class ThreadTest {
public static void main(String[] args) {
//1.自定义线程打印
MyThread3 myThread3 = new MyThread3();
//FutureTask是个帮助类,实现了Runable
FutureTask<String> futureTask = new FutureTask<>(myThread3);
new Thread(futureTask).start();
//2.主线程循环打印
for (int i=0; i<10; i++){
System.out.println("main主线程正在执行:"+new Date().getTime());
}
}
}
代码效果如下:

4、线程池-ThreadPoolExecutor
Java 有创建线程开销较大的问题,当我们需要频繁创建线程执行任务时,可以使用线程池来管理和复用线程。在核心类库中提供了 java.util.concurrent.ThreadPoolExecutor类,我们可以创建线程池实例,将任务(Runnable 或 Callable)提交给线程池,由线程池统一管理和调度线程的执行,提高资源利用率和系统响应速度。
步骤如下:
- 定义线程池的参数,包括核心线程数、最大线程数、空闲线程存活时间、任务队列等,并使用这些参数创建 ThreadPoolExecutor 实例。
- 创建任务(可以是 Runnable 或 Callable 接口的实现类),定义线程执行体。
- 将任务提交给线程池执行,线程池会自动分配空闲线程或创建新线程来执行任务。
- 对于 Callable 任务,可以通过返回的 Future 对象获取任务执行结果。
- 任务执行完成后,根据实际情况关闭线程池,释放资源。
4.1、线程池的核心参数及作用
| corePoolSize(核心线程数) |
|
| maximumPoolSize(最大线程数) |
|
| keepAliveTime + unit(空闲存活时间 + 时间单位) |
|
| workQueue(任务队列) |
|
| threadFactory(线程工厂) |
|
| handler(拒绝策略) |
|
代码如下:
import java.util.concurrent.*;
public class ThreadPoolExecutorTest {
public static void main(String[] args) {
// 1. 定义线程池参数
int corePoolSize = 2; // 核心线程数(固定员工数)
int maximumPoolSize = 4; // 最大线程数(最多员工数)
long keepAliveTime = 30; // 非核心线程空闲30秒后销毁
TimeUnit unit = TimeUnit.SECONDS;
// 任务队列:容量为2的有界队列(最多排2个订单)
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);
// 线程工厂:自定义线程名称(方便排查问题)
ThreadFactory threadFactory = new ThreadFactory() {
private int count = 1;
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("外卖骑手-" + count++);
return thread;
}
};
// 拒绝策略:提交任务的线程自己执行(老板亲自送)
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
// 2. 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
// 3. 提交7个任务(模拟7个订单)
for (int i = 1; i <= 7; i++) {
final int taskId = i;
executor.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(
Thread.currentThread().getName() + " 正在送订单" + taskId);
Thread.sleep(1000); // 模拟送单耗时1秒
System.out.println(
Thread.currentThread().getName() + " 完成订单" + taskId);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
// 4. 关闭线程池(等待所有任务完成)
executor.shutdown();
效果如下:

5、Executors 工具类
Executors是 JDK 提供的一个线程池工具类(位于java.util.concurrent包下),它封装了ThreadPoolExecutor的创建细节,提供了一系列静态工厂方法,让你可以快速创建各种类型的线程池,无需手动设置corePoolSize、workQueue等复杂参数。代码如下:
public class ThreadTest {
public static void main(String[] args) {
// 创建固定3个线程的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 提交5个任务
for (int i = 1; i <= 5; i++) {
int taskNum = i;
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(
Thread.currentThread().getName() + " 执行任务" + taskNum);
try {
Thread.sleep(1000); // 模拟任务耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
fixedThreadPool.shutdown(); // 关闭线程池
}
}
运行效果如下:

三、Thread类常用方法
|
方法 |
用途 |
|
start() |
启动线程,并执行对象的run()方法 |
|
run() |
线程在被调度时执行的操作 |
|
getName() |
返回线程的名称 |
|
setName(String name) |
设置该线程名称 |
|
static Thread currentThread() |
返回当前线程。在Thread子类中就是this,通常用于主线程和 Runnable实现类 |
|
static void yield() |
释放当前cpu的执行权 |
|
join() |
在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后 |
|
stop |
已过时。当执行此方法时,强制结束当前线程。 |
|
static void sleep(long millis) |
令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队 |
|
isAlive() |
判断当前线程是否存活 |
四、线程安全问题
1、共享资源的冲突
案例一:
有几个人围着桌子吃蛋糕,当你举起叉子,准备叉起最后一块蛋糕,正当叉子碰到蛋糕的时候才发现它已经被另一个叉子叉起。这类现象就是共享资源(蛋糕)中出现的冲突,而两个人同时用叉子叉同一块蛋糕的过程我们就称为并发
案例二:

2、问题演示
2.1、创建售票线程运行结果如下:

程序出现了两个问题:
- 相同的票数,比如5这张票被卖了两回。
- 不存在的票,比如0票与-1票,是不存在的。
3、问题分析
线程安全问题都是由全局变量及静态变量引起的。
若每个线程对全局变量、静态变量只读,不写,一般来说,这个变量是线程安全的;
若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

综上所述,线程安根本原因:
多个线程在操作共享的数据;
4、问题解决-线程同步
专业:即线程操作共享数据时其他线程不可以参与执行,直到该线程完成操作;民间:同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说;
为了保证每个线程都能正常执行共享资源操作,Java引入了7种线程同步机制:
|
1)同步代码块(synchronized) 2)同步方法(synchronized) 3)同步锁(ReenreantLock) 4)特殊域变量(volatile) 5)局部变量(ThreadLocal) 6)阻塞队列(LinkedBlockingQueue) 7)原子变量(Atomic) |
4.1、同步代码块(synchronized)
同步代码块 :
|
synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。 |
语法:
|
synchronized(钥匙){ 需要同步操作的代码 } |
钥匙:
|
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
使用同步代码块代码如下:
public class Ticket implements Runnable {
private int ticktNum = 10;
//定义锁对象
Object obj = new Object();
public void run() {
while (true) {
//同步代码块
synchronized (obj) {
if (ticktNum > 0) {
//1.模拟出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.打印进程号和票号,票数减1
String name = Thread.currentThread().getName();
System.out.println("线程" + name + "售票:" + ticktNum--);
}
}
}
}
}
执行结果如下:

4.2、同步方法(synchronized)
同步方法:
|
使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。 |
格式:
|
public synchronized void method(){ 可能会产生线程安全问题的代码 } |
同步锁是谁?
|
对于非static方法,同步锁就是this。 对于static方法,同步锁是当前方法所在类的字节码对象(类名.class)。 |
使用同步方法代码如下:
public class Ticket implements Runnable {
private int ticktNum = 10;
//定义锁对象
Object obj = new Object();
public void run() {
while (true) {
sellTicket();
}
}
//同步方法
private synchronized void sellTicket() {
if (ticktNum > 0) {
try {
//1.模拟出票时间
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.打印进程号和票号,票数减1
String name = Thread.currentThread().getName();
System.out.println("线程" + name + "售票:" + ticktNum--);
}
}
}
执行结果如下:

4.3、同步锁(ReentrantLock):
|
java.util.concurrent.locks.Lock 提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。 |
同步锁方法:
|
public void lock() :加同步锁。 public void unlock() :释放同步锁。 |
使用同步锁代码如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Ticket implements Runnable {
private int ticktNum = 10;
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock();
try {
if (ticktNum > 0) {
//1.模拟出票时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.打印进程号和票号,票数减1
String name = Thread.currentThread().getName();
System.out.println("线程" + name + "售票:" + ticktNum--);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
//放锁
lock.unlock();
}
}
}
}
执行效果如下:

五、死锁
多线程改善了系统资源的利用率并提高了系统的处理能力。然而,并发执行也带来了新的问题--死锁。
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续

1、死锁演示
public class DeadLockTest {
private static Object r1 = new Object();//定义成静态变量,使线程可以共享实例
private static Object r2 = new Object();//定义成静态变量,使线程可以共享实例
public static void main(String[] args) {
Thread p1 = new Thread() {
@Override
public void run() {
synchronized (r1) {
System.out.println("Thread1 get r1");
try {
// 休眠1秒,确保线程2有时间获取LOCK_B,放大死锁概率
Thread.sleep(1000);
} catch (InterruptedException e) {
}
synchronized (r2) {
System.out.println("Thread2 get t2");
}
}
}
};
Thread p2 = new Thread() {
@Override
public void run() {
synchronized (r2) {
System.out.println("Thread2 get p2");
try {
// 休眠1秒,确保线程1有时间获取LOCK_A
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (r1) {
System.out.println("Thread2 get t1");
}
}
}
};
p1.start();
p2.start();
}
}
执行效果如下:

2、产生死锁的原因
线程 1 持有r1,等待获取r2;
线程 2 持有r2,等待获取r1;
两个线程都不会释放已持有的锁,且都在等待对方的锁,形成循环等待,最终导致死锁。
3、如何避免死锁
避免一个线程同时获取多个锁;
六、线程通讯
1、为什么要线程通信
多个线程并发执行时,在默认情况下CPU是随机切换线程的,有时我们希望CPU按我们的规律执行线程,此时就需要线程之间协调通信。
2、线程通讯方式
线程间通信常用常用方法:
| wait() | 一旦执行此方法,当前线程就进入阻塞状态,并释放cpu资源。 |
| notify() |
一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。 |
| notifyAll() | 一旦执行此方法,就会唤醒所有被wait的线程。 |
线程通信的例子:使用两个线程打印 1-100。线程1, 线程2 交替打印
class ThreadCommunication implements Runnable {
private int i = 1;
public void run() {
while (true) {
synchronized (this) {
this.notify();//唤醒等待此钥匙的线程
if (i <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + i++);
} else {
break;
}
try {
this.wait();//拿到此钥匙的线程进入阻塞状态,并释放cpu资源
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
总结
Java多线程编程是实现并发执行的核心技术。程序是完成特定任务的指令集合,而软件则是程序、资源文件和文档的综合体。进程是运行中的应用程序,拥有独立内存空间;线程是进程内的执行单元,是CPU调度的最小单位。
线程创建四大方式
-
继承Thread类:重写run()方法,直接创建子类实例调用start()
-
实现Runnable接口:解决单继承限制,将实现类实例作为Thread的target
-
实现Callable接口:支持返回值、泛型和异常抛出,需用FutureTask包装
-
线程池(ThreadPoolExecutor):最推荐方式,通过核心参数(corePoolSize、maximumPoolSize等)管理线程复用
线程安全与同步
多线程共享资源时会出现线程安全问题,如售票系统的重复售票。解决方案包括:
-
同步代码块(synchronized块)
-
同步方法(synchronized方法)
-
同步锁(ReentrantLock)
-
volatile变量、ThreadLocal等
死锁与线程通信
死锁发生在多个线程互相等待对方释放资源时,可通过避免嵌套锁、使用定时锁等方式预防。线程通信通过wait()、notify()、notifyAll()实现协调,典型应用于生产者-消费者模式。
核心要点
-
Java采用抢占式线程调度
-
Thread类提供了线程控制的基本API
-
线程池显著提升性能,避免频繁创建销毁开销
-
合理使用同步机制确保数据一致性
-
避免死锁需注意锁的获取顺序
掌握多线程技术能显著提升程序性能,但需谨慎处理同步和通信问题,确保程序稳定可靠。实际开发中推荐使用线程池配合Callable/Runnable,结合合适的同步策略。
更多推荐


所有评论(0)