三、锁

1、自旋锁

自旋锁是一种锁机制,常用于多线程编程中。当多个线程需要访问同一段共享内存时,为了保证数据的一致性和正确性,需要对共享内存进行同步访问。自旋锁通过让线程在访问共享内存时循环等待的方式,避免了线程的阻塞和切换,从而提高了并发程序的执行效率。

在使用自旋锁时,当一个线程想要访问共享内存时,它首先会尝试获取锁。如果锁当前没有被其他线程占用,则该线程会立即获得锁,并且可以访问共享内存。如果锁已经被其他线程占用,则该线程会在一个循环中不断地进行测试,直到获取到锁为止。这个过程中,线程并不会被阻塞,而是一直处于运行状态,不停地尝试获取锁。当一个线程访问完共享内存后,它会释放锁,以便其他线程可以继续访问共享内存。

自旋锁的实现通常是基于硬件的原子操作,例如比较交换指令(CMPXCHG)等。自旋锁的优点是可以避免线程阻塞和切换的开销,从而提高并发程序的执行效率。但是,如果多个线程同时竞争同一个自旋锁,那么它们可能会在循环等待的过程中占用过多的 CPU 时间,从而降低程序的性能。因此,在使用自旋锁时,需要根据具体情况进行权衡和选择。

2、适应性自旋(Adaptive Spinning)

适应性自旋是一种改进自旋锁的方法,可以更好地平衡线程的竞争和等待时间。在传统的自旋锁中,线程在等待获取锁的过程中会一直循环忙等,这样会导致线程的性能浪费和能耗增加。而适应性自旋则会根据当前线程等待锁的时间长度和锁的拥有者情况等因素,来决定线程何时开始忙等,何时进入睡眠状态等。

具体来说,适应性自旋的实现通常是基于自旋计数器和自旋时间限制。当一个线程想要获取锁时,它会先检查自旋计数器是否达到了阈值。如果自旋计数器达到了阈值,则该线程会开始忙等。如果自旋计数器没有达到阈值,则该线程会先等待一段时间,然后再次检查锁的状态。如果锁已经被释放,则该线程可以立即获取锁;如果锁还未被释放,则该线程会再次进入等待状态,直到自旋时间限制达到或锁被释放为止。

适应性自旋可以根据具体情况动态地调整自旋计数器和自旋时间限制,从而更好地平衡线程的竞争和等待时间。适应性自旋可以减少线程的性能浪费和能耗增加,从而提高并发程序的性能。

3、偏向锁(Biased Locking)

轻量级锁是一种用于提高多线程程序并发性能的锁机制。它主要是为了在一些短时间内只有一个线程访问共享数据的场景下,减少线程阻塞和唤醒的开销,从而提高并发性能。在这种情况下,传统的互斥锁机制会导致线程频繁地阻塞和唤醒,严重影响程序的性能。而轻量级锁则可以在不阻塞线程的情况下,实现对共享数据的同步访问。

轻量级锁的实现主要是基于CAS(Compare And Swap)指令和对象头的标记位。当一个线程想要访问共享数据时,它首先会尝试获取轻量级锁。如果当前没有其他线程持有该锁,则该线程可以直接将对象头中的标记位设置为锁标记,并将锁的拥有者设置为自己。这个过程是通过CAS指令实现的,保证了线程间的原子性操作。

如果另一个线程也尝试获取该锁,则它会发现该锁已经被占用,此时它就会尝试自旋获取锁。自旋的时间非常短暂,只有几个循环迭代,如果在短时间内另一个线程没有释放锁,那么当前线程就会放弃自旋,升级为重量级锁,阻塞等待锁的释放。

轻量级锁的优点在于,它不需要阻塞线程,可以有效地减少线程的上下文切换和调度开销,从而提高程序的性能。但是,轻量级锁适用于竞争不激烈的场景,如果竞争激烈,就会频繁地发生自旋,降低程序的性能。因此,在使用轻量级锁时,需要根据实际情况进行权衡和选择。


Java中的偏向锁是一种针对单线程访问同步块的优化技术。当一个线程首次进入同步块时,如果同步块没有被其他线程占用,那么该线程就会获得偏向锁,此后该线程再次进入同步块时就不需要进行锁的竞争和申请,直接进入同步块即可。这种方式可以避免多线程竞争锁的开销,提高程序的性能和效率。

偏向锁的使用需要考虑到以下几点:

  1. 同步块的访问方式:偏向锁只适用于单线程访问同步块的场景。如果同步块存在多线程竞争的情况,那么偏向锁的效果会被削弱甚至失效。

  2. 延迟加载:偏向锁的申请需要一定的时间开销,因此JVM采用了延迟加载的策略,只有当同步块被多次访问时,才会触发偏向锁的申请。这样可以避免在同步块一开始就进行偏向锁的申请,从而降低性能开销。

  3. 锁撤销:当有其他线程竞争同步块时,持有偏向锁的线程会将偏向锁升级为轻量级锁或重量级锁,此时会涉及到锁的撤销。偏向锁的撤销会带来一定的性能开销,因此在设计程序时需要考虑到偏向锁的撤销情况,避免出现性能问题。

总的来说,偏向锁是一种针对单线程访问同步块的优化技术,可以避免多线程竞争锁的开销,提高程序的性能和效率。在使用偏向锁时需要考虑同步块的访问方式、延迟加载和锁撤销等因素,避免出现性能问题。

4、轻量级锁(Lightweight Locking)

偏向锁是一种用于提高多线程程序并发性能的锁机制。它主要是为了解决只有一个线程访问共享数据的场景下,传统的互斥锁机制导致的性能问题。在这种情况下,传统的互斥锁机制会产生大量的竞争和上下文切换的开销,严重影响程序的性能。而偏向锁则可以在不阻塞线程的情况下,实现对共享数据的同步访问,并且可以避免不必要的竞争和上下文切换。

偏向锁的实现主要是基于对象头的标记位和线程的ID。当一个线程第一次访问共享数据时,它会尝试获取偏向锁。如果当前没有其他线程访问该对象,则该线程可以直接将对象头中的标记位设置为偏向锁标记,并将锁的拥有者设置为自己,并且在对象头中记录该线程的ID。这个过程不需要使用任何同步机制,因此非常快速和高效。

当另一个线程尝试获取该对象的锁时,它会发现该对象已经被设置了偏向锁,并且锁的拥有者是另一个线程。此时,它就会检查该线程的ID是否和当前线程的ID相同。如果相同,说明当前线程已经持有了该对象的锁,可以直接进入同步块进行访问。如果不同,则说明发生了竞争,当前线程会放弃偏向锁,升级为轻量级锁,并且参与后续的竞争。

偏向锁的优点在于,它可以在多线程环境下,避免不必要的竞争和上下文切换的开销,提高程序的性能。但是,偏向锁适用于竞争不激烈的场景,如果竞争激烈,就会频繁地失效,降低程序的性能。因此,在使用偏向锁时,需要根据实际情况进行权衡和选择。

5、重量级锁

重量级锁是一种传统的同步机制,用于实现多线程程序中的互斥访问。当多个线程尝试同时访问共享资源时,重量级锁会阻塞其中的一部分线程,使其等待其他线程完成对共享资源的访问后再进行访问。这样可以保证共享资源的正确性和一致性,但同时也会带来性能上的开销和延迟。

重量级锁的实现需要依赖操作系统的底层同步机制,如互斥锁、信号量等,需要进行用户态到内核态的切换,涉及到线程的挂起和恢复,需要保存和恢复线程的上下文等操作,因此效率较低。

在Java中,synchronized关键字就是一种基于重量级锁的同步机制。当多个线程尝试访问共享资源时,其中的一部分线程会进入阻塞状态,等待其他线程完成对共享资源的访问后再进行访问。由于synchronized关键字是在Java层面实现的,因此需要依赖JVM和操作系统提供的底层同步机制来实现重量级锁的功能。

尽管重量级锁的实现效率较低,但在一些高并发和需要保证数据正确性的场景下,重量级锁仍然是必不可少的同步机制。为了提高程序的性能和效率,可以尽可能减少同步块的范围和持续时间,避免频繁的线程挂起和恢复。此外,还可以考虑使用更加轻量级的锁机制,如偏向锁和轻量级锁,来避免不必要的开销和延迟。

6、锁消除(Lock Elimination)

锁消除是一种编译器优化技术,用于消除不必要的同步操作,提高程序的性能和效率。在程序中,有些同步块可能永远不会被多个线程同时访问,这时候就可以采用锁消除的技术,将同步块中的锁消除掉,从而避免了不必要的开销和延迟。

锁消除的原理是利用编译器的静态分析能力,在编译时判断某个同步块是否需要加锁,如果发现该同步块永远不会被多个线程同时访问,那么就可以将其标记为不需要加锁,从而消除掉锁操作。

举个例子,假设有如下的代码:

public void test() {
  StringBuffer sb = new StringBuffer();
  for(int i = 0; i < 100000; i++) {
    sb.append(i);
  }
  System.out.println(sb.toString());
}

在该代码中,没有任何同步操作,因此也没有锁的开销。但是如果将sb对象定义为共享对象,就需要在操作sb对象的时候加锁,从而带来性能上的开销。然而,由于sb对象只被单个线程使用,因此可以进行锁消除,将同步块中的锁消除掉,从而避免了不必要的开销和延迟。

锁消除技术的使用需要谨慎,因为如果判断不准确,可能会导致线程安全问题。因此,在使用锁消除技术时,需要考虑到程序的实际情况,避免出现潜在的线程安全问题。

7、锁粗化(Lock Coarsening)

锁粗化是一种优化技术,用于减少线程在不同的同步块之间反复申请和释放锁的开销。在程序中,有些同步块之间的执行时间非常短,如果每个同步块都申请和释放锁,就会带来很大的开销。因此,锁粗化的技术就是将多个独立的同步块合并成一个大的同步块,从而减少线程在同步块之间的切换和锁申请释放的开销,提高程序的性能和效率。

举个例子,假设有如下的代码:

public void test() {
  synchronized(obj1) {
    // do something
  }
  synchronized(obj2) {
    // do something
  }
  synchronized(obj3) {
    // do something
  }
}

在该代码中,有三个独立的同步块,每个同步块之间都需要申请和释放锁。如果这些同步块的执行时间很短,那么每个同步块之间的锁申请和释放就会成为性能瓶颈。为了解决这个问题,可以采用锁粗化的技术,将这些同步块合并成一个大的同步块,从而减少了锁申请和释放的次数,提高了程序的性能和效率。

锁粗化技术的使用需要谨慎,因为如果同步块之间存在数据依赖关系,合并同步块可能会带来线程安全问题。因此,在使用锁粗化技术时,需要考虑到程序的实际情况,避免出现潜在的线程安全问题。

Last updated