Java多线程

Java多线程

线程

线程是计算机程序中执行的最小单位,是进程内的一个执行路径。简单来说,一个进程可以包含多个线程,它们共享进程的资源,比如内存和文件句柄,但每个线程有自己独立的栈和程序计数器。

线程的特点:

  1. 轻量级:线程的创建和销毁比进程更快,资源占用更少。
  2. 共享资源:同一进程中的线程可以共享数据和资源,方便数据传递。
  3. 并发执行:多个线程可以并发执行,充分利用多核处理器,提高程序的效率。
  4. 独立性:虽然线程共享资源,但一个线程的崩溃不会直接影响到其他线程。

编写程序的两种方式

通过继承Thread类

  1. 构造方法
  • Thread(): 创建一个新线程。
  • Thread(Runnable target): 创建一个新线程并指定要执行的目标。
  • Thread(String name): 创建一个新线程并指定线程名称。
  • Thread(Runnable target, String name): 创建一个新线程,指定目标和线程名称。
  1. 启动与运行
  • void start(): 启动线程,调用 run() 方法。
  • void run(): 线程执行的代码,通常由 Runnable 接口实现。
  1. 线程状态管理
  • void join(): 等待线程结束。
  • void join(long millis): 等待指定时间或直到线程结束。
  • void interrupt(): 中断线程。
  • boolean isAlive(): 检查线程是否仍在运行。
  1. 线程信息
  • String getName(): 获取线程名称。
  • int getPriority(): 获取线程优先级。
  • void setPriority(int newPriority): 设置线程优先级。
  • Thread.State getState(): 获取线程当前状态。
  1. 线程调度
  • static void sleep(long millis): 使当前线程睡眠指定时间。
  • static void yield(): 暂停当前线程,让其他线程有机会执行。

​ 6. 线程组

  • ThreadGroup getThreadGroup(): 获取线程所属的线程组。

使用实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class thread_test extends Thread{
public thread_test(String name){ //构造函数
super(name); //调用Tread的构造方法,把name传递给父类
System.out.println(name+"创建成功");
}

public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName());
Thread.yield();
}
}

public static void main(String[] args) {
thread_test t1 = new thread_test("1");
thread_test t2 = new thread_test("2");
t1.start();
t2.start();
}
}

第一次运行:

1
2
3
4
5
6
7
8
9
10
1创建成功
2创建成功
1
1
1
2
2
2

Process finished with exit code 0

第二次运行:

1
2
3
4
5
6
7
8
9
10
1创建成功
2创建成功
1
2
2
2
1
1

Process finished with exit code 0

可以看到两次运行的结果是不同的,这是因为两个线程是同时进行的,不分快慢。

通过Runnable接口方法

如果一个类已经继承了其他类,由于Java是单继承的,所以不能再继承Thread类,需要通过实现Runnable接口来建立线程类。

定义线程类载体,并编写run()方法
1
2
3
4
5
class test implements Runnable {
public void run(){
//具体内容
}
}
建立线程载体对象
1
test obj = new test();
利用线程载体对象建立线程
1
Thread t = new Thread();
启动线程
1
t.start();
实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class thread_test implements Runnable{
String name;
public thread_test(String name){
this.name = name;
System.out.println(name+"创建成功");
}
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(name); //这个name是这个类里的name
Thread.yield();
}
}

public static void main(String[] args) {
thread_test r1 = new thread_test("1");
thread_test r2 = new thread_test("2");
Thread t1 = new Thread(r1); //需要再使用Thread类来启动线程
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
}

Thread构造方法

  1. 默认构造方法
1
Thread thread = new Thread();

创建一个新的线程实例,但没有指定任务。

  1. 指定Runnable对象
1
Thread thread = new Thread(Runnable target);

通过实现Runnable接口的对象创建线程。

示例:

1
2
Runnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
  1. 指定Runnable对象和线程名称
1
Thread thread = new Thread(Runnable target, String name);

创建一个线程,并为它指定一个名称。

示例:

1
Thread thread = new Thread(myRunnable, "MyThread");
  1. 指定线程名称和线程组
1
Thread thread = new Thread(ThreadGroup group, Runnable target, String name);

创建一个线程,并将其加入到指定的线程组。

  1. 指定线程组、Runnable对象和线程名称
1
Thread thread = new Thread(ThreadGroup group, Runnable target, String name, long stackSize);

创建一个线程,指定线程组、Runnable任务、名称以及栈大小。

  1. 指定优先级

虽然构造方法中不直接设置优先级,但可以在创建线程后使用setPriority(int priority)方法:

1
thread.setPriority(Thread.MAX_PRIORITY);

生命周期

线程创建后并不会执行,需要调用start方法才能启动线程,启动了之后也不一定马上运行。线程从创建到结束是有一个过程的,这个过程就称为线程的生命周期。

线程生命周期

这里我们可以看到和进程的生命周期类似,但是

主要区别

  • 资源:进程有独立的内存空间,线程共享同一进程的资源。
  • 管理:进程的创建和管理开销相对较大,线程则相对轻量。
  • 调度:线程调度通常比进程调度更频繁。

优先级

优先级是线程获得CPU调度的优先度。优先级高的线程排在线程队列的前端,优先获得处理机的控制权,可以在短时间内进入运行状态。在Java中,线程的优先级是一个整型值,用于表示线程的相对重要性。线程优先级的设置可以影响线程调度的顺序,但并不保证。优先级的范围通常是从1到10,Java提供了以下常量来表示优先级:

  • Thread.MIN_PRIORITY(1)
  • Thread.NORM_PRIORITY(5)
  • Thread.MAX_PRIORITY(10)

设置线程优先级

你可以通过 setPriority(int newPriority) 方法来设置线程的优先级,例如:

1
2
Thread thread = new Thread();
thread.setPriority(Thread.MAX_PRIORITY);

获取线程优先级

使用 getPriority() 方法可以获取线程的优先级:

1
int priority = thread.getPriority();

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class PriporityDemo extends Thread{
public void run(){
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+" 第 "+(i+1)+"次运行");
Thread.yield(); //让出cpu控制权
}
}
public static void main(String[] args) {
PriporityDemo t1 = new PriporityDemo();
PriporityDemo t2 = new PriporityDemo();
PriporityDemo t3 = new PriporityDemo();
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.NORM_PRIORITY);
t3.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.start();
t3.start();
}
}

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Thread-01次运行
Thread-02次运行
Thread-03次运行
Thread-04次运行
Thread-11次运行
Thread-12次运行
Thread-13次运行
Thread-14次运行
Thread-21次运行
Thread-05次运行
Thread-15次运行
Thread-22次运行
Thread-23次运行
Thread-24次运行
Thread-25次运行

可以看到,虽然并不是严格的按等级来运行排序,但是大致可以看出,优先级越高的获得CPU的次数越多。

线程的调度

线程调度是操作系统或Java虚拟机(JVM)负责管理和安排线程执行的过程。它决定了哪些线程可以运行以及它们运行的顺序。通常我们的计算机只有一个CPU,线程只有得到CPU时间片才可以执行命令。调度模式其实有两种:分时调度模式和抢占调度模式。而Java的线程调度机制是基于抢占式调度的,下面是一些关键概念:

  1. 调度算法

Java的线程调度依赖于底层操作系统的调度算法,常见的调度算法包括:

  • 时间片轮转:每个线程被分配一个时间片,时间片用完后,操作系统会切换到下一个线程。
  • 优先级调度:根据线程的优先级来决定调度顺序,优先级高的线程有更高的机会获得CPU时间。
  • 公平调度:确保所有线程都有机会运行,避免某些线程长时间等待。
  1. 线程状态

线程的状态影响调度的方式,主要状态包括:

  • 新建状态(New):线程被创建,但尚未启动。
  • 就绪状态(Runnable):线程已准备好运行,等待操作系统分配CPU。
  • 运行状态(Running):线程正在执行。
  • 阻塞状态(Blocked):线程因等待某种资源而暂停。
  • 等待状态(Waiting):线程等待其他线程的通知或特定条件。
  • 死亡状态(Terminated):线程已完成执行。
  1. 优先级的影响

如前所述,线程的优先级可能会影响调度,但具体效果依赖于JVM和操作系统的实现。在许多系统中,高优先级线程会在就绪队列中获得优先权,但并不保证一定先执行。

  1. Thread.sleep() 和 yield()
  • Thread.sleep(milliseconds):使当前线程暂停指定时间,允许其他线程运行。
  • Thread.yield():提示调度器当前线程愿意让出CPU,允许其他同优先级的线程执行。
  1. 使用线程池

在实际应用中,使用线程池(如 ExecutorService)可以更有效地管理线程调度,减少线程创建和销毁的开销。

加锁及死锁

加锁

线程加锁是用于控制对共享资源的访问,以防止线程间的竞争条件和数据不一致。Java提供了多种方式来实现加锁,最常见的是使用 synchronized 关键字和 Lock 接口。以下是主要概念:

1. synchronized (同步)关键字
  • 实例方法加锁:锁定对象的实例,确保同一时间只有一个线程能执行该方法。

    1
    2
    3
    public synchronized void method() {
    // 线程安全的代码
    }
  • 静态方法加锁:锁定类的对象,确保同一时间只有一个线程能执行该方法。

    1
    2
    3
    public static synchronized void staticMethod() {
    // 线程安全的代码
    }
  • 代码块加锁:可以更灵活地锁定特定的对象。

    1
    2
    3
    4
    5
    public void method() {
    synchronized (this) {
    // 线程安全的代码
    }
    }
2. Lock (上锁)接口

Lock 接口提供了比 synchronized 更灵活的锁机制。常用的实现是 ReentrantLock

  • 获取锁

    1
    2
    3
    4
    5
    6
    7
    Lock lock = new ReentrantLock();
    lock.lock(); // 获取锁
    try {
    // 线程安全的代码
    } finally {
    lock.unlock(); // 确保释放锁
    }
  • 公平与非公平锁:可以创建公平锁,确保按请求顺序获取锁,或非公平锁,可能会导致某些线程饿死。

线程死锁是指两个或多个线程在执行过程中,因为争夺资源而造成一种互相等待的状态,导致它们无法继续执行。死锁是一种严重的并发问题,可能导致程序停滞不前。

死锁

发生死锁通常需要满足以下四个条件:

  1. 互斥条件:至少有一个资源必须处于非共享状态,即一个资源只能被一个线程占用。
  2. 保持与等待:一个线程至少持有一个资源,并等待获取其他资源。
  3. 不剥夺条件:已经获得的资源在使用完之前不能被强行剥夺。
  4. 环路等待:存在一组线程,每个线程持有至少一个资源并等待另一个线程持有的资源,从而形成环路。
示例

以下是一个简单的死锁示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();

public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock 1...");
try {
Thread.sleep(100); // 模拟处理
} catch (InterruptedException e) {}
System.out.println("Thread 1: Waiting for lock 2...");
synchronized (lock2) {
System.out.println("Thread 1: Acquired lock 2!");
}
}
});

Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock 2...");
try {
Thread.sleep(100); // 模拟处理
} catch (InterruptedException e) {}
System.out.println("Thread 2: Waiting for lock 1...");
synchronized (lock1) {
System.out.println("Thread 2: Acquired lock 1!");
}
}
});

thread1.start();
thread2.start();
}
}
输出
1
2
3
4
Thread 1: Holding lock 1...
Thread 2: Holding lock 2...
Thread 2: Waiting for lock 1...
Thread 1: Waiting for lock 2...

可以看到Thread1获得了lock1的锁,休眠了100ms。此时Thread2获得了lock2的锁,然后休眠100ms。Thread1休眠结束后需要lock2的锁而Thread2休眠结束后需要lock1的锁,但是各自需要的锁都被对方锁把持着,这就陷入了僵局。

死锁检测与预防
  1. 避免死锁
    • 资源有序分配:对资源进行排序,确保线程按固定顺序获取资源。
    • 使用尝试锁:使用 tryLock() 方法尝试获取锁,如果失败,则可以选择不等待。
    • 减少持锁时间:尽量减少持有锁的时间,避免长时间占用资源。
  2. 检测死锁
    • Java提供的 ThreadMXBean 可以用于检测死锁,可以查看当前线程状态并检测死锁情况。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

public class DeadlockDetector {
public static void main(String[] args) {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();

if (deadlockedThreads != null) {
for (long threadId : deadlockedThreads) {
ThreadInfo threadInfo = threadMXBean.getThreadInfo(threadId);
System.out.println("Deadlocked thread: " + threadInfo.getThreadName());
}
} else {
System.out.println("No deadlocks detected.");
}
}
}

Java多线程
https://bayeeaa.github.io/2024/10/17/Java多线程/
Author
Ye
Posted on
October 17, 2024
Licensed under