本文主要介绍了Java多线程中的线程池、Java中的锁、synchronized锁及相关问答等知识点
Java-多线程-并发知识点02
- 线程池
- 如何创建线程池
- 使用 ThreadPoolExecutor 类创建线程池
- 使用 Executors 工厂类创建线程池
- 线程池有些什么参数?
- 线程池的使用方法
- 线程池常用的阻塞队列?
- 线程池处理任务的流程?
- 如何优化线程池的性能?
- Java中的锁
- 乐观锁与悲观锁
- 乐观锁
- 悲观锁
- 自旋锁 与 适应性自旋锁
- 自旋锁
- 适应性自旋锁
- 自旋锁和适应性自旋锁对比
- 公平锁与非公平锁
- 公平锁:
- 非公平锁:
- 公平锁与非公平锁对比
- 可重入锁 与 非可重入锁
- 可重入锁(Reentrant Lock):
- 非可重入锁:
- 可重入锁 与 非可重入锁对比
- 内置锁(synchronized)
- 内置锁的工作原理:
- 内置锁的特点和用法
- synchronized
- synchronized作用范围
- synchronized 使用场景
- synchronized 的底层实现原理
- synchronized有什么样的缺陷?
- synchronized和Lock的对比
- synchronized修饰的方法在抛出异常时,会释放锁吗?
- 多个线程等待同一个synchronized锁的时候,JVM如何选择下一个获取锁的线程?
- 什么是锁的升级和降级?
- synchronied同步锁的四种状态
- 1、无锁状态(Unlocked):
- 2、偏向锁(Biased Lock):
- 3、轻量级锁(Lightweight Lock):
- 4、重量级锁(Heavyweight Lock):
线程池
线程池就是管理一系列线程的资源池,是管理和复用线程的机制,可以有效地控制线程数量和线程的生命周期。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。使用线程池可以避免线程的创建和销毁带来的额外开销,同时还能提高系统的响应性和吞吐量。
如何创建线程池
创建线程池可以通过直接实例化 ThreadPoolExecutor 类或使用 Executors 工厂类提供的方法来实现。以下是创建线程池的方式及步骤:
使用 ThreadPoolExecutor 类创建线程池
1、创建线程池对象:通过 ThreadPoolExecutor 的构造函数创建线程池对象,并指定线程池的参数,包括核心线程数、最大线程数、线程存活时间、任务队列等。
2、提交任务:使用线程池对象的 execute() 或 submit() 方法提交任务给线程池执行。任务可以是 Runnable 或 Callable 类型的对象。
3、关闭线程池:在不再需要线程池时,调用线程池对象的 shutdown() 或 shutdownNow() 方法关闭线程池,释放资源。
示例代码
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// 步骤1:创建线程池对象
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
60, // 线程存活时间(秒)
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>() // 任务队列
);
// 步骤2:提交任务给线程池执行
for (int i = 0; i < 10; i++) {
final int taskNum = i;
executor.execute(() -> {
System.out.println("Task " + taskNum + " is running.");
});
}
// 步骤3:关闭线程池
executor.shutdown();
}
}
使用 Executors 工厂类创建线程池
1、选择线程池类型:通过 Executors 工厂类的静态方法选择合适的线程池类型,如 newFixedThreadPool()、newCachedThreadPool()、newSingleThreadExecutor()、newScheduledThreadPool() 等。
2、提交任务:使用返回的线程池对象的 execute() 或 submit() 方法提交任务给线程池执行。
3、关闭线程池:与直接实例化 ThreadPoolExecutor 类相同,在不再需要线程池时,调用线程池对象的 shutdown() 或 shutdownNow() 方法关闭线程池。
示例代码:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// 步骤1:选择线程池类型并创建线程池对象
ExecutorService executor = Executors.newFixedThreadPool(3); // 创建固定大小线程池
// 步骤2:提交任务给线程池执行
for (int i = 0; i < 10; i++) {
final int taskNum = i;
executor.execute(() -> {
System.out.println("Task " + taskNum + " is running.");
});
}
// 步骤3:关闭线程池
executor.shutdown();
}
}
线程池有些什么参数?
线程池在创建时可以设置多个参数,这些参数可以影响线程池的工作方式、性能和资源利用率。以下是常见的线程池参数:
参数名称 | 描述 |
---|---|
核心线程数(CorePoolSize) | 线程池中保持的常驻线程数量,即使线程处于空闲状态也不会被销毁。用于处理大部分任务。 |
最大线程数(MaximumPoolSize) | 线程池允许的最大线程数量,当任务数量超过核心线程数时会创建新的线程,但不会超过最大线程数。用于处理突发性任务增加时的情况。 |
线程存活时间(KeepAliveTime) | 空闲线程的存活时间,超过这个时间如果线程未被使用就会被销毁。通常与线程存活时间单位一起设定,如秒、分钟等。 |
时间单位(Time Unit) | 用于设定线程存活时间的单位,例如秒、分钟等。 |
任务队列(Task Queue) | 用于存储待执行的任务,当线程池中的线程数量已达到核心线程数,并且任务数量继续增加时,新的任务会被放入任务队列中等待执行。任务队列可以是有界队列或无界队列。 |
拒绝策略(Rejected Execution Handler) | 当任务无法被线程池执行时的处理策略。常见的拒绝策略包括丢弃任务、丢弃最旧的任务、抛出异常等。 |
线程池的使用方法
1、创建线程池对象,可以使用 Executors.newFixedThreadPool() 或 Executors.newCachedThreadPool() 等方法创建不同类型的线程池。
2、提交任务到线程池中,可以使用 execute()提交一个Runnable任务 或 submit()提交一个Callable 方法提交任务。
3、等待任务执行完成,可以使用 shutdown() 或 shutdownNow() 方法停止线程池并等待任务执行完成。
线程池的参数需要根据具体的场景进行调整,例如:
- 如果任务的数量比较少,可以使用固定大小的线程池(例如,newFixedThreadPool()),避免频繁创建和销毁线程带来的额外开销。
- 如果任务的数量比较多,可以使用可缓存线程池(例如,newCachedThreadPool()),根据需要动态创建和销毁线程。
- 如果任务的执行时间比较长,可以适当增加线程池中的最大线程数,以提高系统的并发能力。
- 如果任务的执行时间比较短,可以适当减少线程池中的最大线程数,以避免线程数量过多带来的性能问题。
线程池常用的阻塞队列?
阻塞队列在多线程编程中具有重要的作用,能够有效地解决线程间的通信、协调生产者消费者、平衡系统资源、任务调度和处理等问题,提高了系统的并发性能和可靠性。
常用的阻塞队列:
1、LinkedBlockingQueue
:这是一个无界队列,即可以存放任意数量的任务。它采用链表实现,当任务数量超过核心线程数时,新任务会被放入该队列中等待执行。适用于任务量较大且任务执行时间较短的场景。
2、ArrayBlockingQueue
:这是一个有界队列,需要在创建时指定队列的容量。当任务数量超过核心线程数时,新任务会被放入该队列中等待执行。适用于需要控制任务数量的场景。
3、SynchronousQueue
:这是一个容量为0的特殊队列,每个插入操作必须等待另一个线程的对应删除操作,反之亦然。适用于任务直接传递给工作线程的场景,可以实现更高的并发性能。
线程池处理任务的流程?
线程池处理任务的一般流程如下:
1、任务提交:外部程序或线程将任务提交给线程池。任务可以是 Runnable 类型的任务(不返回结果)或 Callable 类型的任务(可返回结果)。
2、任务接收:线程池接收到任务后,根据任务类型(Runnable 或 Callable)选择相应的线程执行任务。
3、任务队列:如果线程池中的线程数量未达到核心线程数,任务会立即分配给空闲线程执行;如果线程池中的线程数量已达到核心线程数,并且任务队列未满,则任务会被放入任务队列中等待执行。
4、线程执行:线程从任务队列中取出任务并执行,执行过程中可能会产生结果(如果是 Callable 类型的任务)。
5、任务完成:任务执行完成后,线程将结果返回给线程池,线程池根据需要将结果返回给提交任务的程序或线程。
6、线程回收:如果任务队列为空,并且线程池中的线程数量超过核心线程数,并且空闲时间超过设定的存活时间,多余的线程会被回收,以节省系统资源。
7、任务处理异常:如果任务执行过程中发生异常,线程池会处理异常情况,例如记录日志、重新执行任务或采取其他策略。
8、线程池关闭:当不再需要线程池时,可以调用线程池的关闭方法(如 shutdown())来关闭线程池,释放资源。
如何优化线程池的性能?
1、合理设置线程池参数:核心线程数、最大线程数、线程存活时间等。
2、选择合适的任务队列类型:对于任务数量不稳定且任务执行时间短的场景,可以选择适当大小的阻塞队列,如 LinkedBlockingQueue 或 ArrayBlockingQueue。对于任务直接交给工作线程处理的场景,可以选择 SynchronousQueue,避免任务在队列中等待。
3、监控和调整线程池状态:使用监控工具或框架对线程池的状态进行监控,包括线程池大小、活动线程数、任务队列长度等指标。
4、优化任务执行逻辑:对于耗时较长的任务,考虑对任务进行拆分、异步执行或并行处理,提高任务的执行效率。
Java中的锁
在Java中,锁是用于控制多个线程对共享资源的访问的机制,可以通过锁来实现对共享资源的互斥访问,从而避免并发访问导致的数据不一致或竞态条件等问题。
乐观锁与悲观锁
乐观锁和悲观锁是两种不同的锁策略,用于处理多线程环境下的并发访问问题。
乐观锁
观锁的思想是假设多个线程之间不会产生数据冲突,每个线程直接进行操作,如果操作发现数据被其他线程修改了,则认为操作失败,需要重新尝试。乐观锁通常不使用锁机制,而是通过版本号(或时间戳)等机制来实现。在Java中,StampedLock就是乐观锁的一种实现。
乐观锁的特点:
乐观锁认为数据访问时不存在竞争和冲突,因此不会立即加锁,而是直接操作数据,如果操作失败(如版本号不一致),再进行重试。
乐观锁的使用场景:
适用于读操作频繁、写操作相对较少的场景,可以减少锁的竞争和提高并发性能。例如,数据库中的乐观锁实现(如基于版本号或时间戳的乐观锁)、Java中的StampedLock类。
悲观锁
悲观锁的思想是在操作数据之前,先获取锁,这样其他线程就无法同时访问该数据,确保数据的独占性。悲观锁的代表就是传统的互斥锁,如Java中的synchronized关键字和ReentrantLock类。
悲观锁的特点:
悲观锁认为数据访问时存在竞争和冲突,因此在访问数据前先获取锁,保证独占性。
悲观锁的使用场景:
适用于写操作频繁或者数据竞争激烈的场景,例如数据库中的行级锁、Java中的synchronized关键字和ReentrantLock类。
自旋锁 与 适应性自旋锁
自旋锁和适应性自旋锁都是在多线程编程中用于控制临界区访问的锁机制。
自旋锁
自旋锁是一种忙等待的锁,在尝试获取锁时,线程会反复检查锁的状态,不断循环直到获取到锁或者超过一定的等待时间。自旋锁适用于临界区的竞争不激烈,且期望等待时间较短的情况下,可以减少线程切换的开销。
适应性自旋锁
适应性自旋锁是自旋锁的一种改进版本,在自旋等待过程中,会根据历史获取锁的情况动态调整自旋的次数或者等待时间,以适应当前环境的竞争情况。适应性自旋锁可以根据实际情况有效地控制自旋等待时间,避免长时间忙等导致的资源浪费和性能下降。
自旋锁和适应性自旋锁对比
- 自旋次数:自旋锁的自旋次数通常是固定的,无法根据实际情况动态调整。
- 等待时间:自旋锁的等待时间由程序员设定,无法自适应调整。
- 资源利用:自旋锁在临界区竞争不激烈且等待时间较短时,可以减少线程切换的开销,但在竞争激烈或等待时间较长时可能会造成资源浪费。
- 适应性:适应性自旋锁可以根据实际情况动态调整自旋等待次数或等待时间,可以更加灵活地利用CPU资源,避免长时间忙等导致的性能下降。
总的来说,适应性自旋锁相对于普通自旋锁具有更好的资源利用和性能表现,在多线程环境下可以更加有效地控制线程的等待时间,提高系统的并发性能。
公平锁与非公平锁
公平锁和非公平锁是Java中用于控制线程获取锁的两种策略,它们主要影响线程在锁竞争时的获取顺序
公平锁:
公平锁是指多个线程按照请求锁的顺序来获取锁,即先到先得的原则。当一个线程请求公平锁时,如果当前锁没有被其他线程持有,则该线程可以立即获取锁;如果当前锁已经被其他线程持有,则该线程会进入等待队列,按照先来先服务的顺序等待锁释放。
特点:
公平锁保证了线程获取锁的公平性,避免了线程饥饿(即某些线程一直无法获取锁)的情况,但可能会增加线程切换的开销。
非公平锁:
非公平锁是指多个线程获取锁的顺序是不确定的,线程可以直接尝试获取锁,不考虑等待队列中其他线程的顺序。如果当前锁没有被其他线程持有,则该线程可以立即获取锁;如果当前锁已经被其他线程持有,则该线程会进入等待队列,但不会按照先来先服务的原则等待锁释放。
特点:
非公平锁允许一些线程插队获取锁,可能会导致某些线程长时间无法获取锁,但可以减少线程切换的开销,提高系统的吞吐量。
公平锁与非公平锁对比
- 公平性:公平锁保证了线程获取锁的公平性,而非公平锁不考虑线程的顺序,可能导致部分线程长时间无法获取锁。
- 性能:非公平锁由于不需要考虑线程的顺序,可能会减少线程切换的开销,提高系统的吞吐量,但可能会降低公平性。
- 等待队列:公平锁的等待队列按照先来先服务的原则排队等待,而非公平锁的等待队列没有固定的顺序,可以允许插队。
应用场景:
- 如果对线程获取锁的顺序要求较高,可以选择公平锁;
- 如果对性能要求较高,可以选择非公平锁。
- 需要注意的是,非公平锁可能会导致某些线程长时间无法获取锁,因此需要根据实际情况权衡利弊。
可重入锁 与 非可重入锁
可重入锁(Reentrant Lock)和非可重入锁是两种不同的锁机制,主要区别在于是否允许同一个线程多次获取同一个锁。
可重入锁(Reentrant Lock):
可重入锁允许同一个线程在持有锁的情况下再次获取同一个锁,而不会发生死锁或线程阻塞的情况。可重入锁通常会记录锁的持有次数,在释放锁时对持有次数进行递减,直到持有次数为零时释放锁。
特点:
可重入锁可以简化编程模型,避免了因为同一个线程重复获取锁导致的死锁问题。
非可重入锁:
非可重入锁不允许同一个线程在持有锁的情况下再次获取同一个锁,如果尝试再次获取锁,会导致线程阻塞或死锁。非可重入锁在尝试获取锁时会检查当前线程是否已经持有锁,如果是则阻塞或者抛出异常。
特点:
非可重入锁会强制要求线程在使用锁时必须遵循一定的顺序和规则,避免了潜在的死锁问题。
可重入锁 与 非可重入锁对比
- 线程安全性:可重入锁允许同一个线程多次获取锁,不会导致死锁或线程阻塞,保证了线程安全;非可重入锁不允许同一个线程多次获取锁,如果尝试获取会导致线程阻塞或死锁,需要谨慎使用。
- 编程模型:可重入锁可以简化编程模型,允许在同一个线程内部对共享资源进行多次加锁和解锁操作;非可重入锁要求线程在使用锁时必须遵循严格的加锁和解锁顺序,可能增加编程复杂度。
- 性能:可重入锁的性能通常比非可重入锁更好,因为可重入锁允许同一个线程多次获取锁,避免了频繁的线程阻塞和解锁操作。
在实际应用中,通常情况下会使用可重入锁来实现对共享资源的线程安全访问,因为它具有较好的性能和灵活性,并且可以避免一些潜在的死锁问题。非可重入锁通常不常用,除非需要严格控制线程获取锁的顺序和规则。
内置锁(synchronized)
内置锁指的是Java中的对象监视器锁(Object Monitor Lock),它是由Java虚拟机(JVM)在对象上自动加锁和解锁的机制。内置锁是synchronized关键字实现同步的基础,也是Java中最常用的锁机制之一。
内置锁的工作原理:
1、每个Java对象都有一个与之关联的监视器锁。
2、当线程进入synchronized代码块或方法时,它会尝试获取该对象的监视器锁。
3、如果该对象的监视器锁没有被其他线程持有,则该线程获取锁并继续执行代码;如果监视器锁已经被其他线程持有,则该线程进入阻塞状态,等待锁释放。
4、当线程执行完synchronized代码块或方法时,会释放对象的监视器锁,其他线程可以继续竞争该锁。
内置锁的特点和用法
- 自动加锁和解锁:内置锁由JVM自动管理,线程进入和退出synchronized代码块时会自动加锁和解锁。
- 独占性:内置锁是一种排他锁,同一时间只允许一个线程持有对象的监视器锁。
- 对象级别的锁:每个Java对象都有自己的监视器锁,因此同一个对象的不同synchronized代码块之间是互斥的,不同对象的监视器锁互不影响。
- 实现同步:通过synchronized关键字可以实现对共享资源的线程安全访问,保证了多线程环境下的数据一致性。
synchronized
synchronized
是 Java 中用于实现线程同步的关键字,它可以用于方法和代码块,用来确保在多线程环境下对共享资源的安全访问。
synchronized作用范围
1、方法级别:使用 synchronized 修饰方法时,整个方法体被视为同步代码块,该方法在被调用时会自动获取对象的监视器锁(即内置锁),其他线程必须等待当前线程执行完该方法后才能进入。
public synchronized void synchronizedMethod() {
// 同步代码块
}
2、代码块级别:使用 synchronized 修饰代码块时,需要指定获取锁的对象,可以是 this 对象(当前实例)、类对象(ClassName.class)、任意对象等,同一时间只允许一个线程执行该代码块。
public void synchronizedBlock() {
synchronized (this) {
// 同步代码块
}
}
synchronized 使用场景
synchronized 关键字适用于需要保证线程安全性的场景,包括共享资源的访问、临界区的保护、单例模式的实现等。
1、当一个方法需要保证在同一时间只能被一个线程访问时,可以使用 synchronized 修饰方法,确保方法的线程安全性。
public synchronized void synchronizedMethod() {
// 线程安全的方法
}
2、当只有部分代码需要同步,而不是整个方法时,可以使用 synchronized 修饰代码块,指定需要同步的对象,通常是当前对象(this)或者其他共享对象。
public void synchronizedBlock() {
synchronized (this) {
// 需要同步的代码块
}
}
3、当多个线程需要同时访问共享资源时,可以使用 synchronized 来保证对共享资源的互斥访问,避免数据竞争和不一致性。
public class SharedResource {
private int count;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
4、当多个线程需要访问临界区(Critical Section)时,可以使用 synchronized 来保护临界区代码,确保同一时间只有一个线程可以执行临界区代码。
public class CriticalSection {
private int counter;
public void criticalSection() {
synchronized (this) {
// 临界区代码
counter++;
}
}
}
5、在多线程环境下,使用 synchronized 可以安全地实现单例模式,确保只有一个实例被创建。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
synchronized 的底层实现原理
synchronized 的底层实现原理涉及到 Java 虚拟机(JVM)中的对象监视器锁(Object Monitor Lock)机制和字节码指令。
1、对象头信息:
每个 Java 对象在内存中都有一个对象头(Object Header),其中包含了对象的元数据信息,其中就包括用于实现同步的锁信息。
对象头中的一部分用于存储对象的监视器锁状态,包括锁的持有者(Owner)、等待队列(Wait Queue)、锁标识(Lock Status)等。
2、字节码指令:
在 Java 编译后的字节码中,对于 synchronized 关键字修饰的方法或代码块,会生成对应的 monitorenter 和 monitorexit 字节码指令,用于对对象的监视器锁进行获取和释放操作。
monitorenter 用于获取对象的监视器锁,如果锁已被其他线程持有,则当前线程会进入锁的等待队列并阻塞。
monitorexit 用于释放对象的监视器锁,将锁的状态标记为可用,唤醒等待队列中的线程竞争锁。
3、锁的获取和释放:
当一个线程执行到 monitorenter 指令时,会尝试获取对象的监视器锁。如果锁未被其他线程持有,则获取成功;如果锁已被其他线程持有,则当前线程进入阻塞状态。
当一个线程执行完同步代码块或方法,或者出现异常时,会执行 monitorexit 指令释放对象的监视器锁,唤醒等待队列中的线程。
4、线程状态管理:
JVM 使用底层的线程调度机制来管理线程的状态转换,包括阻塞、就绪、运行等状态。
在 synchronized 中,线程在获取锁时可能会进入阻塞状态,而在释放锁后会转换为就绪状态,等待调度器分配执行时间片。
总结
:
总的来说,synchronized 的底层实现依赖于对象的监视器锁机制和字节码指令,通过 monitorenter 和 monitorexit 指令来控制对象的锁状态,实现对共享资源的同步访问。这个机制确保了多个线程对同一个对象的同步代码块或方法在同一时间只能有一个线程执行,从而保证了线程安全性和数据一致性。
synchronized有什么样的缺陷?
1、性能开销:
synchronized 在实现上会涉及到获取锁和释放锁的操作,这些操作会引入一定的性能开销,特别是在高并发环境下可能会导致线程竞争和锁争用,降低系统的性能。
2、非公平性:
synchronized 是一种非公平锁机制,即无法保证等待时间最长的线程优先获得锁。这可能导致某些线程长时间处于饥饿状态,无法获取到锁,影响系统的公平性。
3、无法中断:
使用 synchronized 获取锁的过程是不可中断的,即使线程处于等待状态,也无法通过中断线程的方式来释放锁,这可能导致一些场景下的死锁问题。
4、粒度粗糙:
synchronized 的锁粒度较粗,通常是针对整个方法或代码块进行同步,这可能会导致一些不必要的性能损失或者降低并发度。
5、只能基于对象锁:
synchronized 是基于对象监视器锁实现的,因此只能对对象进行同步,无法对基本类型或静态方法进行同步。
6、可能引发死锁:
如果在 synchronized 的同步代码块或方法中发生了嵌套锁的情况,并且线程获取锁的顺序不一致,可能会引发死锁问题,导致程序无法继续执行。
synchronized和Lock的对比
特性 | synchronized | Lock |
---|---|---|
锁类型 | 隐式锁 | 显式锁 |
获取锁方式 | 自动获取和释放 | 手动获取和释放 |
锁的粒度 | 方法级别或代码块级别 | 可以设置为更细粒度的锁,如锁的粒度可以是单个字段或对象 |
锁的公平性 | 非公平锁 | 可以选择公平锁或非公平锁 |
中断支持 | 不支持 | 支持 |
条件变量 | 不支持 | 支持 |
锁的获取超时 | 不支持 | 支持 |
可重入性 | 支持 | 支持 |
锁的多样性 | 只能基于对象锁 | 可以使用不同类型的锁,如可重入锁、读写锁等 |
性能开销 | 较低 | 通常比synchronized稍高,但可以通过更灵活的控制来优化 |
synchronized修饰的方法在抛出异常时,会释放锁吗?
当使用 synchronized 关键字修饰的方法在执行过程中抛出异常时,会释放锁。这是因为 synchronized 关键字实际上是基于对象的内置锁实现的,而在 Java 中,对象的内置锁是在进入 synchronized 代码块或方法时获取的,并且会在退出代码块或方法时释放。
多个线程等待同一个synchronized锁的时候,JVM如何选择下一个获取锁的线程?
在多个线程等待同一个 synchronized 锁的情况下,Java 虚拟机(JVM)会使用一种非公平的获取锁策略。
当多个线程竞争同一个 synchronized 锁时,JVM 不会考虑线程等待时间的长短,而是随机选择一个等待的线程来获取锁。这种策略并不保证等待时间最长的线程优先获得锁,也不保证公平性。
什么是锁的升级和降级?
锁的升级和降级是指在多线程环境下,对锁的使用进行优化的过程。
1、锁的升级:
锁的升级是指在程序运行过程中,根据线程的实际情况,从较低级别的锁升级到较高级别的锁。
例如,一个对象初始时可能使用的是非公平锁或偏向锁,当多个线程开始竞争该对象的锁时,系统可能会将锁升级为重量级锁,以确保线程之间的互斥性。
2、锁的降级:
锁的降级是指在程序运行过程中,根据线程的实际情况,从较高级别的锁降级到较低级别的锁。
例如,一个对象初始时使用的是重量级锁,但在某些情况下,系统发现只有少数线程在竞争该对象的锁,可以将锁降级为偏向锁或轻量级锁,以提高程序的并发性能。
synchronied同步锁的四种状态
synchronized同步锁有四种状态,分别是无锁状态、偏向锁、轻量级锁和重量级锁。
1、无锁状态(Unlocked):
当一个线程访问同步代码块时,如果这个同步代码块没有被其他线程锁定,那么该线程将获取到锁,并将对象头的Mark Word标记为偏向锁或无锁状态,表示该对象处于无锁状态。
2、偏向锁(Biased Lock):
当一个线程获取了对象的锁并进入同步代码块时,JVM会在对象头的Mark Word中记录锁定线程的线程ID,如果其他线程再来访问这个同步代码块,JVM会检查Mark Word,如果线程ID与当前线程相同,表示可以直接进入同步代码块,无需额外的同步操作。
3、轻量级锁(Lightweight Lock):
当有多个线程尝试获取同一个对象的锁时,会进入轻量级锁状态。JVM会在当前线程的栈帧中创建Lock Record,里面包含了指向对象头的指针,并尝试使用CAS(Compare and Swap)操作将对象头的Mark Word替换为指向Lock Record的指针,如果替换成功,表示当前线程获取了轻量级锁,可以进入同步代码块;如果替换失败,表示有其他线程竞争锁,会升级为重量级锁。
4、重量级锁(Heavyweight Lock):
当轻量级锁竞争失败时,锁会升级为重量级锁,此时锁会被升级为重量级锁并且会将等待的线程加入到阻塞队列中,被阻塞的线程会进入阻塞状态,直到获取到锁才能继续执行。重量级锁的竞争使用操作系统的互斥量实现,相比轻量级锁会增加线程的上下文切换和系统调用的开销。