Java多线程是一种允许我们并行执行多个任务的技术,它是Java语言的一个重要特性,有助于提高应用程序的性能和响应速度。线程是程序执行路径的最小单位,它是轻量级的进程,可以在操作系统的管理下独立运行。
1.创建线程
Thread
在Java中,继承Thread类是创建多线程的一种传统方式。
我们需要创建一个类,继承Thread类,在子类中必须重写Thread类的run()方法。用线程对象的start()方法来启动线程。调用start()方法后,JVM会调用run()方法,从而开始执行线程,如下图所示:
对于简单的任务,继承Thread类并重写run()方法是一种快速创建线程的方式。
Runnable
Runnable接口是一个用于实现多线程编程的基本接口。它定义了一个方法run(),该方法包含了线程要执行的任务代码。
要使用Runnable接口,你需要创建一个实现了Runnable接口的类,并实现run()方法。
创建并运行Runnable线程:Runnable接口的类需要通过Thread类来启动线程,如下:
run()方法内部的代码是由线程调度器来调用的,不是直接调用run()方法。run()方法没有返回值,也不能抛出检查型异常。
Thread和Runnable的主要区别
继承与实现:
Thread是类,继承它会限制类的继承能力。
Runnable是接口,实现它可以保持类的继承能力。
线程创建和管理:
使用Thread类创建线程时,每个线程都有一个独立的实例。
使用Runnable接口,可以创建多个线程共享同一个Runnable实例。
资源共享:
由于Runnable可以被多个线程共享,因此在处理共享资源时更方便。
继承Thread类通常意味着每个线程有自己的资源副本。
功能:
Thread类提供了更多的线程控制功能。
Runnable接口只定义了run方法,不提供线程控制功能。
在选择使用Thread还是Runnable时,一般推荐使用Runnable,因为它提供了更好的灵活性,特别是在需要继承其他类或者实现资源共享时。另外,使用Runnable也便于将任务传递给线程池进行管理。然而,如果不需要这些特性,或者是在简单的场景中,直接使用Thread类也是可行的。
2.线程的生命周期
Java线程的生命周期主要分为以下五个状态:
1. 新建(New):
当使用new关键字创建一个线程对象时,该线程就处于新建状态。
在这个阶段,线程并没有开始执行,只是创建了一个线程的实例。
2. 就绪(Runnable):
调用线程的start()方法后,线程进入就绪状态。
此时,线程已经准备好被线程调度器选中并执行,但是具体何时执行由线程调度器决定。
就绪状态的线程可能由于系统资源或其他线程的竞争而暂时得不到CPU时间。
3. 运行(Running):
当线程调度器选中处于就绪状态的线程时,该线程将进入运行状态。
在这个状态中,线程占有CPU并执行其run()方法中的代码。
注意,运行状态是就绪状态的一个子集,所以线程在运行状态时也可以被看作是处于就绪状态。
4. 阻塞(Blocked):
线程因为某些原因放弃CPU,暂时停止执行,进入阻塞状态。
阻塞状态的原因有多种,包括等待某些资源、等待锁、等待其他线程的通知(如调用wait()方法)等。
当线程等待的事件发生时,线程将重新进入就绪状态。
5. 终止(Terminated):
线程的run()方法执行完成后,或者线程因为一个未捕获的异常而终止,线程进入终止状态。
一旦线程进入终止状态,它就不能再次被调度执行。
以下是线程生命周期状态的转换图:
理解线程的生命周期对于编写高效且正确的多线程程序是非常重要的。
3.线程调度
优先级
在多线程编程中,线程优先级是一个重要的概念,它影响线程调度器如何分配CPU时间给各个线程。线程优先级通常用于指示调度器哪个线程更重要或者需要更多的CPU时间。
在Java中,线程优先级被定义为从1到10的整数,其中:
Thread.MIN_PRIORITY = 1(最低优先级)
Thread.NORM_PRIORITY = 5(默认优先级)
Thread.MAX_PRIORITY = 10(最高优先级)
设置和获取线程优先级:
在Java中,可以使用以下方法来设置和获取线程的优先级:
设置线程的优先级
public final void setPriority(int newPriority)
返回线程的优先级。
public final int getPriority()
示例:
public class ThreadPriorityExample {
public static void main(String[] args) {
// 创建一个线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 线程执行的代码
System.out.println("线程正在运行...");
}
});
// 获取线程的默认优先级
int defaultPriority = thread.getPriority();
System.out.println("线程默认优先级: " + defaultPriority);
// 设置线程的优先级为最高
thread.setPriority(Thread.MAX_PRIORITY);
System.out.println("线程优先级已设置为: " + thread.getPriority());
// 启动线程
thread.start();
}
}
过度使用线程优先级可能会导致性能开销,因为调度器需要更多的工作来决定哪个线程应该执行。线程优先级是多线程编程中的一个有用工具,但应该谨慎使用,以避免引入不必要的复杂性和潜在的问题。
线程休眠
线程休眠是一种使当前正在执行的线程暂停执行给定时间的操作。这可以通过Thread类的sleep(long millis)静态方法来实现,其中millis参数指定线程暂停执行的毫秒数。
在某些情况下,线程可能不需要持续运行,休眠可以减少CPU的使用。
在多线程环境中,休眠可以用来协调不同线程之间的操作,比如避免某个线程过早地处理另一个线程还未准备好的数据。
如何使用线程休眠?
try {
// 休眠线程1000毫秒(1秒)
Thread.sleep(1000);
} catch (InterruptedException e) {
// 线程休眠被中断时会抛出InterruptedException异常
// 这里可以处理中断异常,例如重新设置中断状态
Thread.currentThread().interrupt();
// 或者执行其他清理工作
}
注意:当线程在休眠期间被其他线程中断时,sleep方法会抛出InterruptedException。因此,通常需要在调用sleep的地方包含一个try-catch块来处理这个异常。
当执行完sleep()后,线程进入就绪状态。
线程让步
线程让步(Thread Yielding)是一种线程调度机制,它允许当前正在执行的线程暂停其执行,并允许线程调度器选择其他线程来执行。这是线程主动放弃CPU时间片的一种方式,但并不保证它一定能够让出CPU给其他线程。
在Java中,线程让步可以通过Thread类的yield()方法来实现。以下是关于线程让步的一些详细信息:
yield()是一个静态方法,当当前线程调用它时,它告诉线程调度器可以允许其他线程运行,这是一种暗示,但不是命令,线程调度器可能会忽略这个暗示。
使用方法:
Thread.yield();
多数情况下,线程让步对于程序的正确性不是必需的,它更多的是一种优化手段。
线程插队
线程插队(Thread Preemption)是指在一个多线程环境中,一个线程强制中断另一个线程的执行,以便自己能够获得CPU时间片并开始执行。这种机制通常由操作系统的线程调度器控制,但在Java中,也可以通过一些方法来影响线程的执行顺序。
方法:
Thread.join():当一个线程调用另一个线程的join()方法时,它将等待被调用线程结束。一旦被调用线程结束,等待的线程会重新获得CPU时间片并继续执行。
Thread.interrupt():当一个线程被中断时,如果它正在等待或者执行一个可以抛出InterruptedException`的操作,它会立即抛出这个异常,从而结束当前的操作,这可以看作是一种插队行为。
使用Thread.join()进行线程插队示例:
public class ThreadJoinExample {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
public void run() {
System.out.println("1");
}
});
Thread thread2 = new Thread(new Runnable() {
public void run() {
System.out.println("正在运行");
try {
thread1.join(); // 线程2将等待线程1完成
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("2");
}
});
thread2.start();
thread1.start();
}
}
线程2在开始执行后调用thread1.join(),这会导致线程2等待线程1完成。一旦线程1完成,线程2将继续执行,这可以看作是线程1插队到线程2前面。
线程插队是线程调度的一部分,它有助于确保高优先级任务能够及时执行,或者协调多个线程的执行顺序。
4.多线程同步
多线程同步是确保当多个线程同时访问共享资源时,不会发生数据不一致或竞态条件的一种机制。
*竞态条件:竞态条件是指程序的执行结果依赖于事件或线程的顺序时序,这种情况下,多个线程试图同时访问和修改变量,可能导致不可预测的结果。
Java提供了以下几种机制来同步线程,以避免竞态条件:
同步方法
通过在方法声明中使用synchronized关键字,可以确保同一时间只有一个线程可以执行该方法。
书写同步方法:
public synchronized void synchronizedMethod() {
// 方法体
}
使用同步方法可以确保在多线程环境中对共享资源的线程安全访问,如下例:
public class Counter {
private int count = 0;
// 同步方法来递增计数器
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
public static void main(String[] args) {
Counter counter = new Counter();
// 创建两个线程,每个线程都会递增计数器1000次
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
}, "Thread-1");
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
}, "Thread-2");
// 启动线程
thread1.start();
thread2.start();
try {
// 等待线程结束
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印最终的计数器值
System.out.println(counter.getCount());
}
}
thread1和thread2都会调用increment方法1000次。由于increment方法是同步的,即使两个线程同时运行,它们也不会同时执行这个方法,因此count的值将正确地递增,而不会出现线程安全问题。
同步代码块
同步代码块(Synchronized Block)是Java提供的一种线程同步机制,它允许程序员指定某个代码块在执行时必须获得某个对象的锁。与同步方法不同,同步代码块更加灵活,因为它可以在任何方法内部对任何代码块进行同步,而不仅仅是整个方法。
使用语法:
synchronized (object) {
// 需要同步的代码块
}
*这里的object被称为锁对象。当一个线程进入同步代码块时,它会尝试获取object对象的锁。如果锁被其他线程持有,则当前线程将等待,直到锁被释放。
同步代码块的一个示例:
public class SynchronizedBlockExample {
public void performTask() {
// 创建一个锁对象
final Object lock = new Object();
// 第一个线程的工作
Thread thread1 = new Thread(() -> {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
System.out.println("Thread 1: " + i);
}
}
});
// 第二个线程的工作
Thread thread2 = new Thread(() -> {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
System.out.println("Thread 2: " + i);
}
}
});
// 启动线程
thread1.start();
thread2.start();
}
public static void main(String[] args) {
SynchronizedBlockExample example = new SynchronizedBlockExample();
example.performTask();
}
}
我们定义了一个名为performTask的方法,它创建了两个线程。每个线程都有一个同步代码块,它们都使用同一个锁对象lock。这意味着在任何给定时间,只有一个线程可以执行其同步代码块中的代码。
当thread1启动并执行其同步代码块时,它将持有lock对象的锁。在thread1完成其同步代码块的执行之前,thread2将无法进入其同步代码块,因为thread2也需要相同的锁。这确保了两个线程不会同时执行它们的同步代码块,从而避免了潜在的线程安全问题。
同步代码块是处理并发编程中共享资源同步问题的有效方式,但过度使用同步可能导致性能问题,因为它会引入线程之间的等待时间。因此,在实现同步代码块时,应尽量减少同步代码块中的代码量,并确保只在必要时同步。
死锁
死锁(Deadlock)是并发控制中的一种特殊场景,它发生在两个或多个线程永久性地阻塞,每个线程都在等待其他线程释放锁。由于每个线程都在等待其他线程释放锁,而没有一个线程愿意放弃它所持有的锁,因此线程无法继续执行,导致系统无法继续运行。
以下是死锁产生的四个必要条件:
1. 互斥条件:资源不能被多个线程共同使用,只能由一个线程独占。
2. 持有和等待条件:线程至少持有一个资源,并且正在等待获取额外的资源,而该资源又被其他线程持有。
3. 非抢占条件:线程所获得的资源在未使用完毕前不能被其他线程强行抢占。
4. 循环等待条件:存在一种线程资源的循环等待链,链中的每一个线程都在等待下一个线程所持有资源
死锁例子:
如图t1锁定了resource1而t2锁定了resource2,然后两个线程都试图锁定对方持有的资源,就会发生死锁。
遇到死锁后,可以改变程序设计,以避免至少一个死锁的必要条件。例如,可以通过确保所有线程以相同的顺序请求资源来避免循环等待条件,也可以检测死锁,并采取措施解除死锁。
持续更新中