🔥个人主页: 中草药
🎷 一.线程
1.概念
线程(Thread)是在计算机科学中,特别是操作系统领域里的一个关键概念。它是操作系统能够进行运算调度的最小单位,被包含在一个更大的实体------进程(Process)之中。线程是进程内的一个执行单元,代表了进程内部的一个控制流,意味着一个进程中可以同时有多个线程并发执行不同的任务。
下面是一些关于线程的关键点:
-
控制流:线程是进程内单一顺序的控制流,每个线程有自己的执行栈和寄存器状态,但共享所属进程的资源,如内存空间、文件句柄和信号。
-
资源共享:在同一进程中,多个线程共享相同的虚拟地址空间、全局变量和其他进程级别的资源,这使得线程间的通信和数据共享比进程间更加高效和简单。
-
独立调度:线程是独立调度和分派的基本单位,这意味着它们可以被操作系统调度来利用CPU时间,从而实现并行或并发执行。
-
类型:线程可以分为内核线程(Kernel Thread)和用户线程(User Thread)。内核线程由操作系统直接管理和调度,而用户线程则由应用程序或库自己管理和调度,内核并不直接感知用户线程的存在。
-
轻量级:相对于进程而言,线程的创建和切换开销较小,因为它们共享同一个进程的上下文,不需要额外的系统资源分配。
-
并发性:多线程程序可以利用多核心处理器的并行计算能力,提高程序的执行效率和响应速度,即使在单核处理器上,多线程也能通过优化I/O操作和计算密集型任务的分离来提高整体性能。
-
安全性:虽然线程间共享资源可以提高效率,但也带来了同步和互斥的问题,需要通过锁定机制、信号量、管程等手段来防止数据竞争和不一致状态。
2.重要性
线程是现代操作系统和编程语言中实现并发执行的核心机制之一,广泛应用于各种软件开发场景,包括服务器应用程序、图形界面、游戏开发和数据分析等领域。
⾸先, "并发编程" 成为 "刚需".
-
并发执行:线程允许程序的多个部分同时运行,从而实现并发执行。这对于提高程序的响应能力和效率至关重要,特别是在需要处理大量数据或执行多个独立任务的情况下。
-
资源利用 :单核 CPU 的发展遇到了瓶颈. 要想提⾼算⼒, 就需要多核 CPU. ⽽并发编程能更充分利⽤多核CPU资源.在多核或超线程处理器上,线程能够充分利用硬件资源。每个核心可以同时执行一个线程,这样可以显著提高程序的执行速度和系统的整体吞吐量。
-
I/O处理:在线程的帮助下,I/O密集型操作可以在等待I/O完成的同时让出CPU,允许其他线程执行。这可以避免资源浪费,提高整体性能。
其次, 虽然多进程也能实现 并发编程, 但是线程⽐进程更轻量.
- 成本效益 :相比于进程,创建、销毁、调度线程的开销要小得多,因为它们共享进程的地址空间和其他资源。这意味着可以更经济地创建和管理大量线程。
最后, 线程虽然⽐进程轻量, 但是⼈们还不满⾜, 于是⼜有了 "线程池"(ThreadPool) 和 "协程"
(Coroutine)
关于线程池,协程的话题在本系列后续文章会加以详细阐述,在此不多加赘述.
3.线程的状态
线程在Java中具有多种状态,这些状态反映了线程在其生命周期中的不同阶段。了解这些状态对于调试和优化多线程应用至关重要。下面是Java中线程的六种主要状态:
-
NEW(新建):
- 当线程对象被创建但尚未调用
start()
方法时,线程处于 NEW 状态。 - 在此状态下,线程尚未开始执行任何操作。
- 当线程对象被创建但尚未调用
-
RUNNABLE(可运行):
- 线程一旦调用了
start()
方法,就会进入 RUNNABLE 状态。 - 处于 RUNNABLE 状态的线程可能正在运行,也可能正在等待 CPU 分配时间片以便运行。
- 当线程被唤醒、从阻塞状态返回或从等待状态返回时,也会进入 RUNNABLE 状态。
- 线程一旦调用了
-
BLOCKED(阻塞):
- 这个术语有时被用来指代线程因等待锁而无法运行的情况。
- 当线程试图获取某个对象的锁,而该锁正被其他线程持有时,线程就会进入 BLOCKED 状态。
- 这个状态在 Java 的官方文档中并不直接提及,但它通常指的是线程因同步操作(如
synchronized
)而被阻塞的情况。
-
WAITING(等待):
- 当线程调用
Object.wait()
,Thread.join()
, 或LockSupport.park()
等方法时,它将进入 WAITING 状态。 - 在这种状态下,线程将放弃所有 CPU 时间,等待其他线程发出特定信号(如
notify()
或notifyAll()
)或达到指定的时间点(如wait(long timeout)
或join(long millis)
)。 - 线程在此状态下不会消耗任何 CPU 时间。
- 当线程调用
-
TIMED_WAITING(定时等待):
- 类似于 WAITING 状态,但在 TIMED_WAITING 状态下的线程有一个额外的时间限制。
- 线程将等待直到被唤醒或超时。
- 例如,
Thread.sleep(long millis)
,Object.wait(long timeout)
, 或LockSupport.parkNanos()
和LockSupport.parkUntil()
方法会使线程进入 TIMED_WAITING 状态。
-
TERMINATED(终止):
- 当线程的
run()
方法执行完毕或由于某种原因(如抛出未捕获的异常)而提前结束时,线程进入 TERMINATED 状态。 - 终止的线程不能再被重启。
- 当线程的
我们可以形象的理解为:
4.进程和线程的区别
特征 | 进程 (Process) | 线程 (Thread) |
---|---|---|
基本单位 | 系统分配资源的最小单位 | 系统调度和执行的最小 单位 |
内存空间 | 每个进程有独立的内存空间 | 同一进程中的线程共享内存空间 |
开销 | 创建和销毁的开销大 | 创建和销毁的开销小 |
通信 | 需要使用进程间通信机制,如管道或套接字 | 可直接访问共享数据,但需同步机制 |
影响 | 一个进程的崩溃不会影响其他进程 | 一个线程的崩溃可能影响整个进程 |
调度 | 上下文切换开销大 | 上下文切换开销小 |
从属关系 | 独立运行 | 依赖于所属进程 |
资源隔离 | 每个进程有独立的资源 | 线程共享进程的资源 |
数据共享 | 数据共享复杂,需IPC | 数据共享容易,直接访问共享数据 |
优先级 | 每个进程有自己的优先级 | 线程可以有自己的优先级,受进程优先级影响 |
执行环境 | 每个进程有自己的执行环境 | 同一进程的线程共享执行环境 |
多核处理器支持 | 可以在不同核心上并行执行 | 可以在不同核心上并行执行,但受限于进程调度 |
安全性 | 更高的安全性,因为资源隔离 | 较低的安全性,因为资源共享 |
应用场景 | 需要高隔离性和安全性的多任务处理 | 需要高效并发执行和资源紧密协作的多任务处理 |
进程是包含线程的. 每个进程至少有⼀个线程存在,即主线程
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对⽤⼾层提供了⼀些 API 供⽤⼾使⽤(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进⾏了进⼀步的抽象和封装.
🪗二.Thread
1.创建线程
在Java中创建线程的五种常见方式如下:
1.继承 Thread
类:
在Java中,你可以通过继承 Thread
类来创建一个新的线程类。然后,你需要重写 run()
方法,将你希望在线程中执行的代码放在这个方法里。创建线程实例后,调用 start()
方法来启动线程。
示例:
java
1class MyThread extends Thread {
2 public void run() {
3 System.out.println("Thread running");
4 }
5}
6
7public class Main {
8 public static void main(String[] args) {
9 MyThread thread = new MyThread();
10 thread.start();
11 }
12}
2.实现 Runnable
接口:
另一种创建线程的方式是实现 Runnable
接口,然后将实现的类的实例传递给 Thread
构造函数。同样,你需要实现 run()
方法。
示例
java
class MyRunnable implements Runnable {
public void run() {
System.out.println("Runnable running");
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
3.匿名内部类
在Java中,你可以使用匿名内部类来创建线程,无论是通过实现Runnable
接口还是通过继承Thread
类。下面是如何使用这两种方式创建线程的示例:
实现 Runnable 接口
java
public class Main {
public static void main(String[] args) {
// 使用匿名内部类实现 Runnable 接口
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Running in thread using Runnable interface.");
}
});
// 启动线程
thread.start();
}
}
继承 Thread 类
java
public class Main {
public static void main(String[] args) {
// 使用匿名内部类继承 Thread 类
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("Running in thread by extending Thread class.");
}
};
// 启动线程
thread.start();
}
}
4.使用 Lambda 表达式
Java 8 引入了Lambda表达式,使得创建线程更加简洁。你可以在创建 Thread
对象时直接提供一个Lambda表达式作为 Runnable
。
java
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("Lambda running"));
thread.start();
}
}
2.控制线程
|----------------------------------------|-------------------------------------------------------------|
| 方法 | 作用 |
| start()
| 开始执行线程的 run()
方法。 |
| run()
| 定义线程要执行的操作。直接调用 run()
不会启动新线程;它仅在当前线程中运行。 |
| join()
| 等待线程终止。join()
方法可接受一个毫秒值参数,表示最多等待的时间。 |
| interrupt()
| 请求中断线程。这会导致线程的中断状态被设置为 true
。 |
| interrupted()
| 检查并清除当前线程的中断状态。 |
| sleep
(long
millis)
| 让当前线程暂停执行指定的毫秒数。 |
| yield()
: | 让当前线程放弃剩余时间片,重新排队等待下一次执行机会。 |
| setName
(String name)
| 设置线程的名字。 |
| setPriority(int priority)
| 设置线程的优先级,范围是 Thread.MIN_PRIORITY
到 Thread.MAX_PRIORITY
。 |
| Thread.currentThread() | 获取线程实例 |
注意事项
- 不要直接调用
stop()
方法:它已被废弃,因为可能会导致资源泄露和数据不一致。- 不要随意使用
ThreadDeath
: 这是一个特殊的错误,用于终止未正确处理的线程。- 处理异常 :在
run()
方法中抛出的未捕获异常将被Thread.UncaughtExceptionHandler
处理。
3.常见属性
|---------|-----------------|
| 属性 | 获取方法 |
| ID | getId() |
| 名称 | getName() |
| 状态 | getState() |
| 优先级 | getPriority() |
| 是否为后台进程 | isDaemon() |
| 是否存活 | isAilve() |
| 是否被中断 | isInterrupted() |
- 名称是各种调试工具用到
- 状态表示线程当前所处的⼀个情况,下面我们会进⼀步说明
- 优先级⾼的线程理论上来说更容易被调度到
- 关于后台线程,需要记住⼀点:JVM会在⼀个进程的所有⾮后台线程结束后,才会结束运行。
- 是否存活,即简单的理解,为 run 方法是否运行结束了
🎸三.多线程带来的的风险-线程安全 (重点)
线程安全(Thread Safety)是在多线程编程中非常关键的一个概念,它涉及到了软件设计和实现中如何确保在多线程环境下,共享资源的访问和操作能够正确无误地进行,不会导致数据的不一致、竞态条件(race conditions)、死锁(deadlocks)或其他同步错误。
1.死锁问题-synchronized
synchronized
是Java中用于实现线程同步的关键字,主要用于控制多线程环境下的并发访问,以保证数据的一致性和完整性。synchronized
可以用于方法或者代码块,其主要功能是确保同一时刻只有一个线程可以执行特定的代码段,从而避免了数据竞争和不一致的问题。
工作原理
synchronized
的关键在于锁的获取和释放。当一个线程进入synchronized
代码块或方法时,它会尝试获取与该代码块或方法相关的锁。如果锁已经被另一个线程持有,当前线程就会被阻塞,直到锁被释放。当线程退出synchronized
代码块或方法时,它会自动释放锁。
锁的信息存储在Java对象头的Mark Word中,当线程获取锁时,会将Mark Word修改为指向Thread Monitor(监视器)的指针,而当线程释放锁时,Mark Word会恢复原状或更新为新的状态。
synchronized的特性和优势
- 原子性:保证了临界区代码的原子性,即要么全部执行完成,要么全部不执行。
- 可见性 :确保了在
synchronized
块内对共享变量的修改对其他线程可见。 - 有序性:防止了指令重排序,保证了代码执行的顺序。
死锁
在Java中,死锁是一种线程间的问题,它发生在两个或多个线程相互等待对方持有的锁释放时。当这种情况发生时,所有涉及的线程都将无限期地阻塞,因为每个线程都在等待另一个线程完成某些操作,而那个操作却永远不会发生,除非外部干预。
以下是一个典型的死锁场景:
假设我们有两个对象ObjectA
和ObjectB
,以及两个线程Thread1
和Thread2
。Thread1
需要先获取ObjectA
的锁,然后获取ObjectB
的锁。同时,Thread2
需要先获取ObjectB
的锁,然后再获取ObjectA
的锁。如果Thread1
成功获取了ObjectA
的锁,但在获取ObjectB
的锁之前被中断(例如,由于时间片轮换),此时Thread2
尝试获取ObjectB
的锁并成功,接下来Thread2
试图获取ObjectA
的锁,但是ObjectA
的锁已经被Thread1
持有。这时就形成了一个死锁:
java
Thread1: 持有ObjectA的锁,等待ObjectB的锁
Thread2: 持有ObjectB的锁,等待ObjectA的锁
死锁的四个必要条件中其中:synchronized
关键字在Java中用于实现线程间的互斥和可重入,这是它核心的两个特性,我们只能从代码结构来解决问题,包括**请求与保持,循环等待,**让我们逐一解析:
互斥性(Mutual Exclusion)
互斥性是指在多线程环境下,一次只有一个线程能够访问某个资源或执行某段代码。在Java中,synchronized
关键字通过加锁机制实现了互斥性。当一个线程获得锁并开始执行synchronized
代码块或方法时,其他试图获取同一锁的线程将被阻塞,直到持有锁的线程执行完毕并释放锁。
例如,假设我们有两个线程Thread1
和Thread2
,以及一个对象obj
。如果obj
的某个方法被synchronized
修饰,那么当Thread1
正在执行这个方法时,Thread2
试图调用该方法将被阻塞,直到Thread1
执行完毕并释放锁。这就是互斥性的体现。
可重入性(Reentrancy)
可重入性意味着一个线程可以在已经持有一个锁的情况下再次获取同一个锁,而不会造成死锁。在synchronized
的上下文中,这意味着一个线程可以进入一个synchronized
代码块或方法,然后在该代码块或方法内部再次进入另一个synchronized
代码块或方法,只要这两个synchronized
区域的锁是相同的。
在Java中,synchronized
实现的锁是可重入的。当一个线程第一次获取锁时,它会增加一个"锁计数"。当该线程离开synchronized
代码块或方法时,锁计数减一。只有当锁计数降为零时,锁才被完全释放,允许其他线程获取锁。
例如:
java
public class ReentrantExample {
public synchronized void outer() {
System.out.println("Thread " + Thread.currentThread().getName() + " entered outer()");
inner();
}
public synchronized void inner() {
System.out.println("Thread " + Thread.currentThread().getName() + " entered inner()");
}
}
// 在主线程中调用
ReentrantExample reentrantExample = new ReentrantExample();
new Thread(() -> reentrantExample.outer()).start();
在这个例子中,outer()
和inner()
方法都被synchronized
修饰。当一个线程调用outer()
时,它会获取锁。然后,在outer()
内部调用inner()
,此时线程再次获取相同的锁。由于synchronized
锁的可重入性,这不会导致死锁,线程可以正常执行inner()
方法,然后在退出inner()
和outer()
时逐步释放锁。
互斥性和可重入性 是synchronized
关键字为Java多线程编程提供的关键特性,它们共同确保了线程安全和资源的正确访问。然而,过度使用synchronized
可能会引入性能瓶颈,因为频繁的锁获取和释放会增加线程的等待时间。在现代Java中,为了更高的性能和更细粒度的控制,程序员还可以选择使用java.util.concurrent
包中的高级锁机制,如ReentrantLock
和ReadWriteLock
。
synchronized的缺陷
- 性能开销 :由于
synchronized
需要进行锁的获取和释放,这可能会带来一定的性能开销,特别是在高并发的场景下。 - 死锁风险:如果使用不当,可能会导致死锁,即两个或更多的线程在等待彼此释放锁,从而无法继续执行。
使用synchronized
的注意事项
- 尽量减少
synchronized
代码块的范围,只保护必要的代码。 - 避免在
synchronized
块中执行长时间的操作,如I/O操作或网络通信。 - 使用锁时要小心,避免死锁和活锁。
- 考虑使用更细粒度的锁,如
java.util.concurrent
包下的ReentrantLock
或ReadWriteLock
,以提高并发性能。
synchronized
关键字是Java并发编程的基础,掌握其使用和原理对于开发健壮的多线程应用程序至关重要。
2.内存可见性问题-volatile
内存可见性问题在多线程编程中是一个关键概念,特别是在像Java这样的语言中,其中线程间的通信和数据共享是通过共享内存模型实现的。内存可见性问题是指当一个线程修改了一个共享变量的值,而其他线程未能立即看到这个修改,导致程序行为不符合预期的现象。
Java内存模型(JMM)
理解内存可见性问题之前,需要了解Java内存模型(JMM)。JMM定义了程序执行过程中所有线程访问共享变量的抽象行为。根据JMM,所有变量存储在主内存中,每个线程拥有自己的工作内存,其中包含了该线程使用变量的主内存的副本。线程对变量的所有操作(读、写)都在其工作内存中进行,而不是直接在主内存中进行。只有当线程结束或显式同步时,工作内存中的变量值才被写回到主内存中。
内存可见性问题的原因
内存可见性问题主要由以下几个因素引起:
-
缓存一致性:现代处理器具有多级缓存,以提高性能。当一个线程读取一个变量时,该变量的值可能被缓存在线程的本地缓存中。如果另一个线程修改了这个变量,那么第一个线程可能看不到这个更新,直到缓存一致性协议(如MESI协议)强制刷新缓存。
-
编译器优化:编译器可能重新排序代码以优化性能,这可能导致对共享变量的读写操作的顺序与源代码中指定的顺序不同。
-
操作系统调度:操作系统调度可能在不同线程之间切换,导致一个线程的写操作在另一个线程中变得不可见。
解决方案:Volatile关键字
volatile
是Java中的一个关键字,主要用于多线程环境下,用来标记那些可能被多个线程共享并可能在多个线程之间发生变化的变量。volatile
关键字有以下主要作用:
-
保证可见性* : 当一个线程修改了
volatile
变量的值,这个修改将会立即对所有其他线程可见。这是因为volatile
变量的读取和写入不会被缓存在寄存器或处理器缓存中,而是直接从主内存中读取和写回到主内存中。这就确保了所有线程看到的是最新的值。 -
禁止指令重排序* : 在编译器和处理器中,为了优化性能,通常会对指令进行重排序。但是,这种重排序可能会破坏多线程程序中的逻辑顺序。
volatile
变量禁止了包含它的语句的指令重排序,确保了指令的执行顺序按照代码的书写顺序进行,这对于维持程序的因果关系至关重要。 -
不保证原子性* : 虽然
volatile
提供了可见性和禁止指令重排序的能力,但它并不保证复合操作的原子性。例如 ,i++
这样的操作在多线程环境下并不是原子性的,即使i
被声明为volatile
。这是因为i++
涉及到读取、计算和写回三个步骤,而volatile
只能保证每次读取和写入的原子性,不能保证整个复合操作的原子性。(因此volatile与synchronized并且互不冲突) -
防止编译器优化* : 编译器通常会进行一些优化,比如如果一个变量的值在一个方法中没有改变,那么编译器可能会缓存这个值而不是每次都去读取它。然而,对于
volatile
变量,编译器不会进行这样的优化,因为volatile
变量的值可能在任何时候由其他线程改变。
使用volatile
关键字的示例:
java
public class Counter {
private volatile int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
在这个例子中,count
被声明为volatile
,这样在多线程环境中,每个线程对count
的修改对其他线程都是立即可见的。然而,increment()
方法并没有提供原子性,因为在多线程环境中,多个线程可能同时读取count
的值,进行自增操作,然后写回,这可能导致竞态条件。
因此,在处理复合操作时,如果需要原子性,应该考虑使用更高层次的并发工具,如java.util.concurrent
包中的AtomicInteger
,或者使用synchronized
关键字来确保代码块的互斥访问。
3.线程饿死问题-wait¬ify
在Java中,线程饿死(Thread Starvation)指的是一个或多个线程由于某种原因永远无法获得CPU时间片,从而无法执行其任务的情况。线程饿死通常是由于资源分配不公、线程优先级设置不当、锁竞争过于激烈或者调度策略不合理等原因造成的。
wait() 方法
wait()
方法用于暂停当前线程的执行,并使线程进入等待状态,直到被其他线程唤醒。调用 wait()
方法的对象必须处于同步上下文中,即在 synchronized
块或方法内调用,因为 wait()
方法需要在锁对象上调用。wait()
方法有三种重载形式:
void wait()
:使当前线程等待,直到其他线程调用此对象的notify()
或notifyAll()
方法。void wait(long millis)
:使当前线程等待最多millis
毫秒,之后线程将自动恢复执行。void wait(long millis, int nanos)
:类似于第二种形式,但提供了更精确的等待时间控制。
当一个线程调用 wait()
方法后,它会释放调用 wait()
方法的对象上的锁,允许其他线程获取该锁并执行同步代码块。当线程被唤醒后,它将再次尝试获取锁,如果锁可用,则线程将从 wait()
调用处继续执行;如果锁被其他线程占用,则线程将继续等待。
总而言之,wait()方法一共做了三件事:
- 使当前执行代码的线程进行等待。(把线程放在等待队列中)
- 释放当前锁
- 在满足一定条件时被唤醒,重新尝试获取这个锁
wait()结束等待的条件
- 其他线程调用该对象的notify方法
- wait等待时间超时(wait方法提供一个带有timeout参数的版本,来指定等待时间)
- 其他线程调用该等待线程的interrupted方法,导致wait抛出InterruptedException异常
举例
java
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("等待中");
object.wait();
System.out.println("等待结束");
}
}
wait 要搭配 synchronized 来使⽤. 脱离 synchronized 使⽤ wait 会直接抛出异常.
notify() 方法
notify()
方法用于唤醒正在等待此对象监视器锁的一个线程。当一个线程调用 notify()
时,它会选择正在等待此对象锁的一个线程(具体选择哪个线程由JVM决定)并将其标记为可运行状态。但是,被唤醒的线程并不会立即恢复执行,而是要等到当前线程释放锁后,被唤醒的线程才有机会重新获取锁并继续执行。
notify方法是唤醒等待的线程
- 方法notify()也要在同步方法和同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其他线程,对其发出通知notify,并使他们重新索取该对象的对象锁
- 如果有多个线程等待。则由调度器随机挑选出一个呈现wait状态的线程(并不存在 先来后到)
- 在notify()方法之后,当前线程不会马上释放该对象锁,要等待执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁
notify()
方法也有一个重载版本 notifyAll()
,它会唤醒所有正在等待此对象锁的线程。然而,即使所有等待的线程都被唤醒,也只有其中一个线程能够获取锁并继续执行,其他的线程将再次进入等待状态,直到锁被释放。一般情况 我们会选择唤醒一个,如果一下唤醒所有,这些被唤醒的线程就会无序的竞争锁
若notify调用次数超过了wait次数,也不会有副作用
wait¬ify
wait和sleep的对比
以下是 wait()
和 sleep()
方法的主要区别,以表格形式展示:
其实理论上 wait 和 sleep 完全是没有可比性的,因为⼀个是⽤于线程之间的通信的,⼀个是让线程阻塞⼀段时间,
唯⼀的相同点就是都可以让线程放弃执行⼀段时间
特性 | wait() 方法 |
sleep() 方法 |
---|---|---|
所属类 | java.lang.Object |
java.lang.Thread |
调用要求 | 必须在 synchronized 上下文中调用 |
可在任何线程中调用,无需同步上下文 |
锁的行为 | 释放对象锁 | 不释放任何锁 |
中断处理 | 抛出 InterruptedException ,并清除中断状态 |
抛出 InterruptedException ,不改变中断状态 |
用途 | 线程间通信和同步(如生产者/消费者模式) | 延迟线程执行(如定时任务、动画效果) |
调用示例 | 在同步方法或同步块内调用 | 可直接在任何线程的方法中调用 |
请注意,wait()
方法还有两个重载版本:wait(long timeout)
和 wait(long timeout, int nanos)
,允许线程等待一个特定的时间段后被唤醒。同样,sleep()
方法也有多个重载版本,包括 sleep(long millis)
和 sleep(long millis, int nanos)
,用于指定睡眠的毫秒数和纳秒数。在实际应用中,这些方法的具体选择取决于对精确性和响应中断的需求。
🎹四.总结与反思
男儿不展风云志,空负天生八尺躯。------冯梦龙
在学习Java多线程的过程中,我深刻体会到多线程编程的复杂性和挑战性。尽管Java提供了丰富的API来简化线程的管理,但正确地设计和实现多线程程序仍然是一个需要细致思考的过程。以下几点是我认为特别需要注意的地方:
-
线程安全性 :在多线程环境中,共享数据的访问必须谨慎处理,否则容易出现数据不一致或竞态条件。使用
synchronized
关键字和并发工具类是保证线程安全的有效手段。 -
性能考量:虽然多线程可以提高程序的并发能力和效率,但过多的线程创建和切换反而会增加系统的开销。合理使用线程池可以避免这种问题。
-
调试困难:多线程程序的调试往往比单线程程序更难,因为线程的执行顺序是不确定的。使用合适的日志记录和调试工具对于定位和解决问题至关重要。
-
设计模式:掌握一些多线程相关的设计模式,如生产者-消费者模式、读写锁模式等,可以帮助更高效地解决问题。
-
深入学习:虽然初步掌握了多线程的基本概念和技巧,但我意识到要成为多线程编程的专家,还需要不断深入学习,理解更深层次的原理和高级话题,如JMM(Java内存模型)、线程局部变量、原子操作等。
总之,多线程编程是一门深奥的艺术,它需要理论知识和实践经验的结合。在未来的项目中,我会更加注重线程安全和性能优化,同时也会持续探索和学习更多的高级主题,以提升我的多线程编程技能。
🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀
以上,就是本期的全部内容啦,若有错误疏忽希望各位大佬及时指出💐
制作不易,希望能对各位提供微小的帮助,可否留下你免费的赞呢🌸