Java多线程
Java多线程
线程
线程是计算机程序中执行的最小单位,是进程内的一个执行路径。简单来说,一个进程可以包含多个线程,它们共享进程的资源,比如内存和文件句柄,但每个线程有自己独立的栈和程序计数器。
线程的特点:
- 轻量级:线程的创建和销毁比进程更快,资源占用更少。
- 共享资源:同一进程中的线程可以共享数据和资源,方便数据传递。
- 并发执行:多个线程可以并发执行,充分利用多核处理器,提高程序的效率。
- 独立性:虽然线程共享资源,但一个线程的崩溃不会直接影响到其他线程。
编写程序的两种方式
通过继承Thread类
- 构造方法
Thread()
: 创建一个新线程。Thread(Runnable target)
: 创建一个新线程并指定要执行的目标。Thread(String name)
: 创建一个新线程并指定线程名称。Thread(Runnable target, String name)
: 创建一个新线程,指定目标和线程名称。
- 启动与运行
void start()
: 启动线程,调用run()
方法。void run()
: 线程执行的代码,通常由Runnable
接口实现。
- 线程状态管理
void join()
: 等待线程结束。void join(long millis)
: 等待指定时间或直到线程结束。void interrupt()
: 中断线程。boolean isAlive()
: 检查线程是否仍在运行。
- 线程信息
String getName()
: 获取线程名称。int getPriority()
: 获取线程优先级。void setPriority(int newPriority)
: 设置线程优先级。Thread.State getState()
: 获取线程当前状态。
- 线程调度
static void sleep(long millis)
: 使当前线程睡眠指定时间。static void yield()
: 暂停当前线程,让其他线程有机会执行。
6. 线程组
ThreadGroup getThreadGroup()
: 获取线程所属的线程组。
使用实例:
1 |
|
第一次运行:
1 |
|
第二次运行:
1 |
|
可以看到两次运行的结果是不同的,这是因为两个线程是同时进行的,不分快慢。
通过Runnable接口方法
如果一个类已经继承了其他类,由于Java是单继承的,所以不能再继承Thread类,需要通过实现Runnable接口来建立线程类。
定义线程类载体,并编写run()方法
1 |
|
建立线程载体对象
1 |
|
利用线程载体对象建立线程
1 |
|
启动线程
1 |
|
实例
1 |
|
Thread构造方法
- 默认构造方法
1 |
|
创建一个新的线程实例,但没有指定任务。
- 指定Runnable对象
1 |
|
通过实现Runnable
接口的对象创建线程。
示例:
1 |
|
- 指定Runnable对象和线程名称
1 |
|
创建一个线程,并为它指定一个名称。
示例:
1 |
|
- 指定线程名称和线程组
1 |
|
创建一个线程,并将其加入到指定的线程组。
- 指定线程组、Runnable对象和线程名称
1 |
|
创建一个线程,指定线程组、Runnable任务、名称以及栈大小。
- 指定优先级
虽然构造方法中不直接设置优先级,但可以在创建线程后使用setPriority(int priority)
方法:
1 |
|
生命周期
线程创建后并不会执行,需要调用start方法才能启动线程,启动了之后也不一定马上运行。线程从创建到结束是有一个过程的,这个过程就称为线程的生命周期。
这里我们可以看到和进程的生命周期类似,但是
主要区别
- 资源:进程有独立的内存空间,线程共享同一进程的资源。
- 管理:进程的创建和管理开销相对较大,线程则相对轻量。
- 调度:线程调度通常比进程调度更频繁。
优先级
优先级是线程获得CPU调度的优先度。优先级高的线程排在线程队列的前端,优先获得处理机的控制权,可以在短时间内进入运行状态。在Java中,线程的优先级是一个整型值,用于表示线程的相对重要性。线程优先级的设置可以影响线程调度的顺序,但并不保证。优先级的范围通常是从1到10,Java提供了以下常量来表示优先级:
Thread.MIN_PRIORITY
(1)Thread.NORM_PRIORITY
(5)Thread.MAX_PRIORITY
(10)
设置线程优先级
你可以通过 setPriority(int newPriority)
方法来设置线程的优先级,例如:
1 |
|
获取线程优先级
使用 getPriority()
方法可以获取线程的优先级:
1 |
|
实例
1 |
|
输出
1 |
|
可以看到,虽然并不是严格的按等级来运行排序,但是大致可以看出,优先级越高的获得CPU的次数越多。
线程的调度
线程调度是操作系统或Java虚拟机(JVM)负责管理和安排线程执行的过程。它决定了哪些线程可以运行以及它们运行的顺序。通常我们的计算机只有一个CPU,线程只有得到CPU时间片才可以执行命令。调度模式其实有两种:分时调度模式和抢占调度模式。而Java的线程调度机制是基于抢占式调度的,下面是一些关键概念:
- 调度算法
Java的线程调度依赖于底层操作系统的调度算法,常见的调度算法包括:
- 时间片轮转:每个线程被分配一个时间片,时间片用完后,操作系统会切换到下一个线程。
- 优先级调度:根据线程的优先级来决定调度顺序,优先级高的线程有更高的机会获得CPU时间。
- 公平调度:确保所有线程都有机会运行,避免某些线程长时间等待。
- 线程状态
线程的状态影响调度的方式,主要状态包括:
- 新建状态(New):线程被创建,但尚未启动。
- 就绪状态(Runnable):线程已准备好运行,等待操作系统分配CPU。
- 运行状态(Running):线程正在执行。
- 阻塞状态(Blocked):线程因等待某种资源而暂停。
- 等待状态(Waiting):线程等待其他线程的通知或特定条件。
- 死亡状态(Terminated):线程已完成执行。
- 优先级的影响
如前所述,线程的优先级可能会影响调度,但具体效果依赖于JVM和操作系统的实现。在许多系统中,高优先级线程会在就绪队列中获得优先权,但并不保证一定先执行。
- Thread.sleep() 和 yield()
- Thread.sleep(milliseconds):使当前线程暂停指定时间,允许其他线程运行。
- Thread.yield():提示调度器当前线程愿意让出CPU,允许其他同优先级的线程执行。
- 使用线程池
在实际应用中,使用线程池(如 ExecutorService
)可以更有效地管理线程调度,减少线程创建和销毁的开销。
加锁及死锁
加锁
线程加锁是用于控制对共享资源的访问,以防止线程间的竞争条件和数据不一致。Java提供了多种方式来实现加锁,最常见的是使用 synchronized
关键字和 Lock
接口。以下是主要概念:
1. synchronized (同步)关键字
实例方法加锁:锁定对象的实例,确保同一时间只有一个线程能执行该方法。
1
2
3public synchronized void method() {
// 线程安全的代码
}静态方法加锁:锁定类的对象,确保同一时间只有一个线程能执行该方法。
1
2
3public static synchronized void staticMethod() {
// 线程安全的代码
}代码块加锁:可以更灵活地锁定特定的对象。
1
2
3
4
5public void method() {
synchronized (this) {
// 线程安全的代码
}
}
2. Lock (上锁)接口
Lock
接口提供了比 synchronized
更灵活的锁机制。常用的实现是 ReentrantLock
。
获取锁:
1
2
3
4
5
6
7Lock lock = new ReentrantLock();
lock.lock(); // 获取锁
try {
// 线程安全的代码
} finally {
lock.unlock(); // 确保释放锁
}公平与非公平锁:可以创建公平锁,确保按请求顺序获取锁,或非公平锁,可能会导致某些线程饿死。
线程死锁是指两个或多个线程在执行过程中,因为争夺资源而造成一种互相等待的状态,导致它们无法继续执行。死锁是一种严重的并发问题,可能导致程序停滞不前。
死锁
发生死锁通常需要满足以下四个条件:
- 互斥条件:至少有一个资源必须处于非共享状态,即一个资源只能被一个线程占用。
- 保持与等待:一个线程至少持有一个资源,并等待获取其他资源。
- 不剥夺条件:已经获得的资源在使用完之前不能被强行剥夺。
- 环路等待:存在一组线程,每个线程持有至少一个资源并等待另一个线程持有的资源,从而形成环路。
示例
以下是一个简单的死锁示例:
1 |
|
输出
1 |
|
可以看到Thread1获得了lock1的锁,休眠了100ms。此时Thread2获得了lock2的锁,然后休眠100ms。Thread1休眠结束后需要lock2的锁而Thread2休眠结束后需要lock1的锁,但是各自需要的锁都被对方锁把持着,这就陷入了僵局。
死锁检测与预防
- 避免死锁:
- 资源有序分配:对资源进行排序,确保线程按固定顺序获取资源。
- 使用尝试锁:使用
tryLock()
方法尝试获取锁,如果失败,则可以选择不等待。 - 减少持锁时间:尽量减少持有锁的时间,避免长时间占用资源。
- 检测死锁:
- Java提供的
ThreadMXBean
可以用于检测死锁,可以查看当前线程状态并检测死锁情况。
- Java提供的
1 |
|