在Java中,锁是实现多线程并发控制的一种重要机制。它可以保证多个线程之间安全地访问共享资源,防止数据的不一致性。锁有两种类型:内部锁和外部锁。内部锁是通过synchronized实现的,它可以解决方法或代码块在多线程环境下的同步问题。外部锁则是通过ReentrantLock等类实现的,除了能解决同步问题外,还提供了更多高级功能,如公平锁、非公平锁、条件等待/通知等。
锁优化是为了提高系统的并发性能,包括减少锁的竞争、减小锁的粒度、减少锁的持有时间等,以减少线程等待锁的时间,提高系统的吞吐量。
锁升级是指,当锁的竞争情况变得激烈时,JVM会将锁的状态由轻量级锁升级为重量级锁,以保证线程安全。同样,当锁的竞争情况减轻时,JVM也会将锁的状态由重量级锁降级为轻量级锁,以提高系统的并发性能。
锁优化和锁升级的重要性在于,它们可以在保证线程安全的前提下,提高系统的并发性能,从而提高系统的吞吐量。在高并发的系统中,锁优化和锁升级是不可或缺的。
偏向锁是Java 6引入的一种新的锁优化策略。它的主要思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时记录下这个线程的信息,当这个线程再次请求锁时,无需再做任何同步操作。这样做的目的是为了消除无竞争的同步原语,进一步提高程序的运行性能。如果有其他线程尝试获取这个锁,那么偏向模式就会被关闭。偏向锁适用于只有一个线程访问同步块的场景。
偏向锁的核心思想是,在无竞争的情况下,把整个同步消除掉。也就是说,如果一个锁只被一个线程锁定,而没有其他线程来竞争这个锁,那么这个锁就会偏向于这个线程,从而消除这个锁的同步操作。
偏向锁的底层实现,主要是通过在对象头中的Mark Word里存储偏向的线程ID来实现的。当线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID。如果下次该线程再次尝试获取这个锁,由于检查到锁对象已经偏向于自己,所以无需再做任何同步操作。只有当其他线程尝试获取这个锁时,应用程序才需要做出真正的同步操作,例如撤销偏向锁,升级为轻量级锁等。
轻量级锁是用来提高线程并发性的一种锁优化策略,主要针对锁的竞争不激烈的场合。轻量级锁相比于重量级锁(即操作系统层面的锁),其等待是通过自旋实现的,不会将线程状态置为阻塞,从而减少了不必要的线程上下文切换。
当一个线程尝试获取某个轻量级锁时,它首先会检查这个锁是否处于偏向状态,如果不是,它会在自己的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,然后尝试使用CAS操作将对象头中的Mark Word替换为指向锁记录的指针。如果成功,那么这个线程就持有了这个轻量级锁。如果失败,那么它会检查对象头中的Mark Word是否指向自己的锁记录,如果是,那么它就成功地获取到了轻量级锁;否则,它会自旋等待或者升级为重量级锁。
轻量级锁的底层实现主要依赖于CAS操作和自旋技术。CAS操作是用于实现非阻塞性同步的基础,它可以在无锁的情况下保证共享数据的同步;自旋则是为了避免线程在获取不到锁时,立即进入阻塞状态,而是进行几个轮次的循环尝试,看是否能够获取到锁,从而减少线程上下文切换的开销。
重量级锁是Java中最传统的synchronized锁,是一种互斥锁,它是依赖于JVM和操作系统的线程调度机制。当一个线程获得一个对象的重量级锁后,其他任何线程都无法再获得该对象的锁,如果还有线程尝试获取该对象的锁,就会被阻塞住,直到锁的所有者线程释放该锁。
当一个线程尝试获取一个对象的重量级锁时,如果该对象的锁已经被其他线程持有,那么该线程就会被阻塞,即不会继续执行,进入阻塞状态。直到持有锁的线程释放了锁,JVM才会从被阻塞的线程中选择一个,将锁分配给它,其他的线程仍然保持阻塞状态。
重量级锁的底层主要依赖于操作系统的Mutex Lock(互斥锁)来实现,当一个线程获取不到锁时,它会进入阻塞状态,等待锁的释放。在这个过程中,涉及到操作系统用户模式和内核模式的转换,以及线程的调度和切换,这些都是需要消耗大量系统资源的操作,因此称为“重量级锁”。
适用场景:当锁的竞争非常激烈,即锁保护的代码经常会被多个线程同时执行时,重量级锁的开销反而相对较小。因为这种情况下线程如果不进入阻塞状态,而是采用自旋等待锁的释放,不仅会占用CPU资源,而且由于锁的竞争激烈,线程获取锁的成功率很低,因此重量级锁更加适合。
锁的升级主要有以下几种触发条件:
偏向锁升级为轻量级锁:当一个线程获取了偏向锁,而另一个线程也试图获取这个锁的时候,持有偏向锁的线程会被挂起,JVM会撤销偏向锁,然后把锁升级为轻量级锁。
轻量级锁升级为重量级锁:当轻量级锁竞争失败时,如果自旋等待也不能获取到锁,这个时候就会把轻量级锁升级为重量级锁。此时,没有获取到锁的线程会进入阻塞状态,等待锁释放。
例如有两个线程A和B,它们要竞争同一个锁。开始时,该锁是偏向锁状态,线程A首先获取到了这个偏向锁,然后线程B也试图获取这个锁,这时偏向锁就会升级为轻量级锁,线程A会被挂起。然后线程B开始自旋等待,试图获取轻量级锁,如果自旋等待成功,那么线程B就获取到了轻量级锁;如果自旋等待失败,那么轻量级锁就会升级为重量级锁,线程B会进入阻塞状态,等待锁释放。
Java代码示例0
这段代码的运行先后顺序能够模拟出锁的升级过程。首先,线程1获取到偏向锁;然后,线程2尝试获取锁,这会导致偏向锁升级为轻量级锁;最后,线程3尝试获取锁,这会导致轻量级锁升级为重量级锁。
偏向锁和轻量级锁的升级过程是在Java HotSpot VM中的实现,不同的虚拟机实现可能会有所不同。代码中的sleep方法只是为了模拟锁的升级过程,真实的锁升级过程是在JVM内部进行的。
public class LockUpgradeDemo {
private static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
// 线程1获取偏向锁
new Thread(() -> {
synchronized (lock) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 确保线程1获取偏向锁
Thread.sleep(100);
// 线程2尝试获取锁会导致偏向锁升级为轻量级锁
new Thread(() -> {
synchronized (lock) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 确保线程2开始运行
Thread.sleep(100);
// 线程3尝试获取锁会导致轻量级锁升级为重量级锁
new Thread(() -> {
synchronized (lock) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
减少锁的持有时间:只在必要的时候持有锁,尽量缩短锁的持有时间,从而减少线程阻塞的可能性。
减少锁的粒度:使用更细粒度的锁,例如,如果只有一个线程会访问一个对象的某个字段,那么就不需要对整个对象加锁。
锁分离:如果一个类有多个独立的操作,那么可以为每个操作使用不同的锁,这样就可以避免不必要的同步。
锁粗化:如果一个线程在一段时间内会多次获取和释放同一个锁,那么JVM可能会尝试将这些操作合并为一次,这就是锁粗化。
锁消除:如果JVM检测到一段代码中的锁操作是不必要的,那么可能会消除这个锁操作。
使用无锁数据结构:例如,Java的java.util.concurrent包中提供了许多无锁数据结构,如ConcurrentHashMap、CopyOnWriteArrayList等。
使用读写锁:如果一个数据结构的读操作比写操作更频繁,那么使用读写锁可以提高性能。
使用偏向锁和轻量级锁:JVM在1.6之后引入了偏向锁和轻量级锁,用于优化无竞争的同步代码,可以有效减少无必要的重量级锁操作。
我们本章节主要了解核心的两种锁消除和锁粗化
锁消除是Java中的一种锁优化技术。优化后的代码可以避免因为竞争锁而导致的线程阻塞,从而提高系统的运行性能。
锁消除的主要原理是在编译阶段,通过一种叫做逃逸分析的技术,分析对象的作用域,发现一些在多线程环境下不可能存在共享资源竞争的情况,从而消除不必要的同步措施。
举个例子,如果某个对象只在一个线程的作用域内使用,那么它就不可能被其他线程访问到,因此,这个对象上的synchronized关键字就没有任何实际的意义,可以被安全地删除。
public class LockElimination {
public void append(String str1, String str2){
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
System.out.println(sb.toString());
}
}
在这个例子中,StringBuffer是线程安全的,内部的append方法是加了synchronized关键字的。但是在append方法中,sb这个对象只会在当前线程中使用,其他线程无法访问到这个对象,因此sb对象上的synchronized关键字实际上是没有必要的。在运行时,JVM会自动消除这个锁,提高程序的性能。
锁粗化是Java中的另一种锁优化策略,主要用于减少线程获取和释放锁的次数。
锁粗化的基本思想是将多个连续的加锁、解锁操作合并为一次,将加锁的同步范围扩大到其外部。这样,可以减少同步的次数,提高性能。当然,锁粗化的前提是,必须保证扩大后的同步代码块的执行不会影响到程序的并行度。
我们举个栗子
public class LockCoarsening {
private StringBuffer sb = new StringBuffer();
public void append(String str){
for (int i = 0; i < 10000; i++) {
sb.append(str);
}
}
}
在这个例子中,每次调用append方法时,都会连续进行10000次加锁和解锁操作。这种情况下,JVM会自动进行锁粗化,将这10000次加锁操作合并为一次,将锁的范围扩大到整个append方法。这样做可以显著减少锁的竞争,提高程序的性能。
《深入理解Java虚拟机:JVM高级特性与最佳实践》 - 周志明
https://www.baeldung.com/java-synchronized