锁面试


1.CAS

CAS(Compare-And-Swap,比较与交换)是一种常见的无锁(lock-free)机制,用于实现多线程环境下的原子操作。CAS操作通过硬件支持的原子指令来实现,确保在多线程竞争的情况下,数据的一致性和操作的原子性。

CAS操作涉及三个参数:

  • V:需要更新的变量的内存地址。
  • E:预期值,即当前线程认为变量的值应该是什么。
  • N:新值,即希望将变量更新为的值。

CAS的执行步骤如下:

  1. 读取变量V的当前值
  2. 比较V的当前值与预期值E:
    • 如果相等,说明没有其他线程修改过V,则将变量的值更新为N,并返回true,表示更新成功。
    • 如果不相等,说明在此期间V已经被其他线程修改,则更新失败,返回false

这个过程是由硬件指令(如x86架构中的CMPXCHG指令)保证的原子操作,避免了竞态条件(Race Condition)。

boolean isUpdated = atomicInteger.compareAndSet(expectedValue, newValue);

CAS的缺点

  • ABA问题:在CAS操作中,如果一个变量的值从A变为B,再变回A,CAS检查时可能认为值没有变化,从而导致误判。为了解决这个问题,Java引入了AtomicStampedReference,它在CAS操作中加入了版本号(或时间戳),从而检测值的变化。
  • 自旋问题:CAS操作失败时通常会不断重试(自旋),这可能导致CPU资源的浪费,尤其是在高竞争的场景下。
  • 只能保证一个变量的原子性:CAS只能保证一个共享变量的原子性,如果需要同时操作多个变量,必须使用其他机制如锁或通过组合使用AtomicReference来实现。

哪些类用到了CAS

1. Atomic系列类

这些类都是在java.util.concurrent.atomic包中,使用CAS实现了原子操作。

  • AtomicInteger: 通过CAS实现了整数值的原子更新操作,如incrementAndGet()compareAndSet()等。
  • AtomicLong: 与AtomicInteger类似,但操作的是long类型的值。
  • AtomicBoolean: 通过CAS操作boolean类型的值。

2. Concurrent集合类

这些类在其实现中使用了CAS来保证多线程环境下的安全和性能。

  • ConcurrentHashMap<K,V>: 通过CAS来实现节点的插入、更新和删除操作,确保在高并发下的线程安全性。
  • ConcurrentLinkedQueue<E>: 使用CAS来实现无锁的队列操作,提供高效的并发队列。
  • ConcurrentLinkedDeque<E>: 类似于ConcurrentLinkedQueue,但支持双端操作。
  • ConcurrentSkipListMap<K,V>ConcurrentSkipListSet<E>: 这些类通过CAS实现了跳表(Skip List)的无锁并发操作。

3. LocksSynchronizers

这些类使用CAS来实现高效的线程控制机制。

  • ReentrantLock: 使用CAS来控制锁的状态(如获取锁和释放锁)。
  • StampedLock: 结合了悲观和乐观锁的特性,使用CAS来处理不同模式下的锁定。
  • CountDownLatch: 使用CAS实现计数器的递减操作,保证线程之间的同步。
  • Semaphore: 在信号量计数的增减操作中使用CAS,确保多线程环境下的安全。
  • Exchanger<V>: 使用CAS来实现两个线程之间的数据交换操作。
  • CyclicBarrier: 使用CAS控制线程到达屏障的数量,确保线程安全。
  • Phaser: 使用CAS实现阶段的推进和参与线程的注册/注销。

4. ThreadPoolExecutor

  • ThreadPoolExecutor中,CAS用于控制线程池中的工作线程数量、任务的提交和执行状态

UNSAFE类

提供了一组直接操作内存、线程、类加载等底层操作的方法。由于它能够绕过Java虚拟机(JVM)的大多数安全检查,因此称为“unsafe”(不安全)。Unsafe类主要用于JVM和核心库的内部实现,在Java应用程序中直接使用它是非常危险的。

虽然Unsafe类是内部使用的类,但它提供了底层的CAS操作API,许多并发类在其内部实现中都直接使用了Unsafe类的CAS方法,如compareAndSwapIntcompareAndSwapLongcompareAndSwapObject

底层操作能力Unsafe类提供了访问和操作底层内存、对象以及线程的能力,包括直接操作内存、CAS(Compare-And-Swap)操作、对象字段偏移量获取、类实例化等。

Unsafe类的主要功能

  • 内存操作
    • allocateMemory(long bytes):分配一块指定大小的内存,类似于C语言中的malloc
    • freeMemory(long address):释放通过allocateMemory分配的内存。
    • putLong(long address, long value)getLong(long address):直接在指定内存地址上读写数据。
  • CAS操作
    • compareAndSwapInt(Object obj, long offset, int expect, int update):对指定对象的字段进行CAS操作。
    • compareAndSwapLong(Object obj, long offset, long expect, long update):对long类型字段进行CAS操作。
    • compareAndSwapObject(Object obj, long offset, Object expect, Object update):对引用类型字段进行CAS操作。
  • 对象操作
    • getObject(Object obj, long offset)putObject(Object obj, long offset, Object value):直接读取和写入对象的字段。
    • getInt(Object obj, long offset)putInt(Object obj, long offset, int value):读取和写入整数字段。
    • objectFieldOffset(Field field):获取对象字段在内存中的偏移量,用于直接访问对象字段。
  • 线程操作
    • park(boolean isAbsolute, long time):挂起当前线程,类似于LockSupport.park
    • unpark(Thread thread):唤醒指定线程,类似于LockSupport.unpark
  • 类操作
    • defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain domain):在运行时动态定义类。
    • ensureClassInitialized(Class<?> c):确保指定类已初始化。

3. Unsafe类的使用场景

尽管Unsafe类不推荐在应用程序中直接使用,但它在以下场景中非常有用:

  • JVM内部实现:许多JVM内部操作依赖Unsafe类实现,例如直接内存分配、对象实例化等。
  • 高性能并发库java.util.concurrent包中的许多类(如AtomicInteger, ConcurrentHashMap)使用Unsafe类来实现无锁的并发操作,特别是CAS操作。
  • 性能优化:在极少数情况下,某些库或框架可能使用Unsafe类来进行性能优化,例如绕过对象创建时的初始化步骤。

Java的锁有哪些

内置锁(Synchronized)

Java内置的锁是最简单和最常用的锁机制,它是语言级的锁,使用synchronized关键字来实现。

在Java中,synchronized关键字用于同步代码块或方法,以确保在多线程环境中对共享资源的访问是线程安全的。根据synchronized关键字的使用位置,它的作用范围会有所不同,具体表现为加在方法上和加在类上的区别如下:

1. synchronized加在实例方法上

javaCopy codepublic synchronized void instanceMethod() {
    // 同步代码
}
  • 作用范围:当synchronized加在实例方法上时,锁定的是当前实例对象(即this)。
  • 线程独占性:只有获取了当前实例对象锁的线程才能执行该方法。其他线程如果调用同一个实例的这个synchronized方法或其他synchronized实例方法,都需要等待锁的释放。
  • 适用场景:适用于同步控制与特定实例相关的操作,多个实例之间不会互相影响。

2. synchronized加在类方法(静态方法)上

javaCopy codepublic static synchronized void staticMethod() {
    // 同步代码
}
  • 作用范围:当synchronized加在静态方法上时,锁定的是当前类的Class对象(即ClassName.class)。
  • 线程独占性:只有获取了当前类Class对象锁的线程才能执行该静态方法。其他线程如果调用同一个类的这个synchronized静态方法或其他synchronized静态方法,都需要等待锁的释放。
  • 适用场景:适用于同步控制与类本身相关的操作(如访问或修改静态变量),无论创建了多少实例,静态方法的锁都是类级别的,所有线程共享。

3. synchronized加在代码块上

除了方法级的锁定,synchronized还可以加在代码块上,以锁定特定对象或类:

  • 锁定实例对象

    javaCopy codepublic void method() {
        synchronized(this) {
            // 同步代码
        }
    }

    作用同synchronized实例方法,锁定当前实例对象。

  • 锁定类对象

    javaCopy codepublic void method() {
        synchronized(ClassName.class) {
            // 同步代码
        }
    }

    作用同synchronized静态方法,锁定当前类对象。

区别总结

  • 同步范围synchronized加在实例方法上时,锁定范围是实例对象级别,针对单个实例进行同步控制;加在类方法上时,锁定范围是类对象级别,针对整个类进行同步控制。
  • 影响范围:实例方法的synchronized只影响同一个实例的其他synchronized实例方法;而类方法的synchronized会影响该类的所有实例对象和该类的其他synchronized静态方法。
  • 使用场景:如果需要保护与实例相关的数据,应使用synchronized实例方法;如果需要保护与类相关的静态数据或资源,应使用synchronized静态方法。

锁优化:

JVM对synchronized的优化

  1. 偏向锁(Biased Locking)
    • 原理:当一个线程首次获取锁时,JVM会偏向于该线程,并将锁标记为偏向锁。之后,如果同一线程再次请求这个锁,JVM不需要执行锁竞争操作,而是直接授予锁。偏向锁在无竞争场景下极大地减少了加锁的开销。
    • 适用场景:适用于锁竞争少、同一线程多次进入同步代码块的场景。
  2. 轻量级锁(Lightweight Locking)
    • 原理:在锁没有被其他线程竞争的情况下,JVM会将锁从偏向锁升级为轻量级锁。此时,线程通过CAS(Compare-And-Swap)操作来获取锁,而不是传统的阻塞方式。
    • 适用场景:适用于短时间内可能出现锁竞争,但竞争不激烈的场景。
  3. 自旋锁(Spin Locking)
    • 原理:自旋锁避免了线程在锁竞争失败后立即阻塞,而是让线程短时间内不断地尝试获取锁。这种方式适用于锁竞争时间短的场景,因为线程不会立即进入阻塞态,而是不断尝试获取锁,从而避免了上下文切换的开销。
    • 适用场景:适用于锁竞争频繁但每次竞争时间较短的场景。
  4. 锁消除(Lock Elimination)
    • 原理:在编译期间,JVM可以通过逃逸分析(Escape Analysis)检测到某些锁只在单线程环境中使用,或者锁对象没有逃逸到其他线程,那么JVM会自动消除这些锁。
    • 适用场景:适用于单线程场景或锁对象未共享的场景。
  5. 锁粗化(Lock Coarsening)
    • 原理:如果JVM检测到在一个方法或代码块中多次使用相同的锁,而这些加锁操作是紧密相连的,那么JVM会将这些锁操作合并为一个更大的锁块,从而减少加锁和解锁的频率。
    • 适用场景:适用于频繁的、短时间的锁操作。
  6. 适应性自旋(Adaptive Spinning)
    • 原理:在自旋锁的基础上,JVM会根据前一次自旋的结果动态调整自旋的次数。如果自旋能够快速获取锁,则增加自旋次数;反之,则减少自旋次数或直接阻塞线程。
    • 适用场景:适用于锁竞争时间波动较大的场景。

开发者对synchronized的优化

  1. 减少锁的持有时间
    • 做法:尽量将synchronized代码块的范围缩小到最小,确保只在确实需要同步的地方加锁,减少锁的持有时间。
    • 效果:减小锁的持有时间可以减少线程阻塞的时间,从而提高系统的并发性。
  2. 减少锁的粒度
    • 做法:将大锁拆分为小锁,针对不同的数据或资源使用不同的锁,以减少锁的竞争。
    • 效果:减小锁的粒度可以减少锁的争用,提高系统的并发能力。
  3. 使用更高效的并发工具
    • 做法:在某些场景下,可以用Java的其他并发工具类替代synchronized,如ReentrantLockReadWriteLockStampedLock等。
    • 效果:这些工具类提供了更多的功能和优化,如可中断的锁获取、超时等待、读写分离等,能够在特定场景下提供更高的性能。
  4. 避免不必要的同步
    • 做法:通过仔细分析代码逻辑,避免在不需要同步的地方使用synchronized,例如仅在读操作时不需要同步。
    • 效果:减少不必要的同步可以显著提高性能,特别是在读多写少的场景中。

显式锁

java.util.concurrent.locks包提供了一些显式的锁类,如ReentrantLock,它们相比内置锁提供了更多的功能和灵活性。

  • ReentrantLock:一个可重入锁,提供了比synchronized更高级的功能,如公平锁、公平锁模式选择、尝试加锁(tryLock)、超时等待加锁等。
  • ReentrantReadWriteLock:一个读写锁,实现了读写分离,读操作共享锁,写操作独占锁。它提高了读多写少场景下的并发性。

自旋锁

CountDownLatch 是 Java 中的一个同步辅助工具,属于 java.util.concurrent 包中的一部分。它用于让一个或多个线程等待直到其他线程完成一组操作。CountDownLatch 主要用于在并发环境中协调多个线程的执行,尤其是当某些线程需要等待其他线程完成某些工作后才能继续执行时。

CountDownLatch 使用一个计数器来表示线程需要等待的操作次数。每当一个操作完成时,计数器减一。线程可以在计数器为零之前被阻塞,直到所有操作完成,计数器降到零。

CountDownLatch

CountDownLatch是一种同步辅助工具,允许一个或多个线程等待,直到其他线程完成一组操作。

主要方法

  1. await():使当前线程等待直到计数器的值变为零。线程会在这里被阻塞,直到其他线程调用 countDown() 方法使计数器减到零。

    java
    Copy code
    latch.await();
  2. await(long timeout, TimeUnit unit):使当前线程等待直到计数器的值变为零或等待时间超时。该方法允许设置一个超时时间,超时后线程将继续执行,即使计数器值未变为零。

    java
    Copy code
    latch.await(10, TimeUnit.SECONDS);
  3. countDown():减少计数器的值。当一个操作完成时,调用 countDown() 方法将计数器的值减一。如果计数器的值减到零,所有在 await() 方法中等待的线程将被唤醒。

    java
    Copy code
    latch.countDown();
  4. getCount():返回当前计数器的值。

    java
    Copy code
    long count = latch.getCount();
CountDownLatch latch = new CountDownLatch(3);

new Thread(() -> {
    // 执行一些操作
    latch.countDown();
}).start();

latch.await(); // 等待所有任务完成

CountDownLatch 是一个强大的同步工具,用于协调多个线程的执行。当线程需要等待其他线程完成某些操作时,CountDownLatch 提供了一个简单有效的机制来实现这种协调。通过控制计数器的值和调用 await()countDown() 方法,开发者可以轻松地管理线程的依赖和同步。

REDLOCK

在分布式系统中,当多个节点需要访问共享资源时,需要确保对该资源的访问是互斥的,即同一时间只有一个节点可以访问该资源。分布式锁就是用来实现这一互斥控制的。传统的 Redis 分布式锁(如基于 SETNX 命令的实现)可能面临单点故障、锁丢失等问题,因此需要一种更为可靠的分布式锁机制。

Redlock 算法

Redlock 的基本思想是使用多个 Redis 实例来实现分布式锁,以提高锁的可靠性。其基本步骤如下:

  1. 部署多个 Redis 实例
    • 部署 N 个独立的 Redis 实例(通常建议 N 为奇数,如 5 个实例)。
  2. 请求锁
    • 客户端请求锁时,尝试在所有 Redis 实例上设置锁。
    • 使用 SET key value NX PX <expire> 命令,在每个 Redis 实例上设置一个带有超时的锁键。
  3. 判断锁的获取成功
    • 锁获取成功的条件是:在至少 N/2 + 1 个 Redis 实例上成功设置了锁。
    • 如果成功设置的实例数量达到大于 N/2 的数量,则认为锁成功获取。
  4. 释放锁
    • 客户端在使用完共享资源后,需要释放锁。
    • 释放锁时,客户端需要在所有 Redis 实例上删除锁键。
  5. 处理锁的超时和过期
    • 锁的超时时间(PX)应设置为足够长,以避免在正常情况下锁的超时释放,但也要避免锁在客户端崩溃时不被释放的问题。
    • 客户端需要处理锁的自动续期和超时问题,以保证锁的有效性。

优点

  • 容错性:通过多个 Redis 实例分散风险,即使部分实例发生故障,只要大多数实例正常工作,锁仍然有效。
  • 高可用性:即使某个 Redis 实例宕机,其他实例仍然可以继续提供锁服务。
  • 一致性:通过多数投票机制(N/2 + 1)确保锁的一致性,减少了锁丢失的概率。

局限性

  • 复杂性:与单一 Redis 实例相比,使用多个 Redis 实例增加了系统的复杂性和运维成本。
  • 网络延迟:由于需要与多个 Redis 实例进行交互,可能会增加网络延迟。
  • 锁的获取时间:在高负载情况下,获取锁可能需要较长时间,特别是当多个实例的网络延迟较大时。

使用示例

使用 Redlock 时,通常需要使用 Redis 客户端库来实现,例如 Redisson 提供了对 Redlock 算法的支持。以下是使用 Redisson 实现 Redlock 的一个示例代码片段:

javaCopy codeimport org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class RedlockExample {
    public static void main(String[] args) {
        // 配置 Redisson 客户端
        Config config = new Config();
        config.useClusterServers()
              .addNodeAddress("redis://127.0.0.1:6379")
              .addNodeAddress("redis://127.0.0.1:6380")
              .addNodeAddress("redis://127.0.0.1:6381");
        
        RedissonClient redisson = Redisson.create(config);

        // 获取分布式锁
        RLock lock = redisson.getLock("myLock");
        try {
            // 尝试获取锁
            lock.lock();
            // 执行受保护的操作
            System.out.println("Lock acquired, performing protected operation.");
        } finally {
            // 释放锁
            lock.unlock();
        }
        
        // 关闭 Redisson 客户端
        redisson.shutdown();
    }
}

总结

Redlock 是一种通过多个 Redis 实例来实现高可靠性的分布式锁算法,适用于需要高可用性和容错性的分布式系统。通过在多个 Redis 实例上设置锁,并采用多数投票机制来保证锁的一致性,Redlock 提供了一种在分布式环境中实现互斥控制的可靠方案。

ThreadLocl

ThreadLocal 类是用来提供线程内部的局部变量,即线程本地变量。这种变量在多线程环境下访问(通过get和set方法访问)时能够保证各个线程的变量相对独立于其他线程内的变量,不同线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。

线程隔离机制共享变量的监控

目的就是在多线程环境中,无需加锁,也能保证数据的安全性。

高并发中会存在多个线程同时修改一个共享变量的场景,这就可能会出现线性安全问题。

为了解决线性安全问题,可以通过加锁来实现,例如使用synchronized 或者Lock。但是加锁的方式可能会导致系统变慢。

因此可以使用ThreadLocal类访问共享变量,这样会在每个线程的本地,都保存一份共享变量的拷贝副本。这是一种“空间换时间”的方案,虽然会让内存占用大很多,但是由于不需要同步也就减少了线程可能存在的阻塞等待,从而提高时间效率。

即 存储 key在ThreadLocalMap中然后使用 set getEntry remove操作

从上面的分析我们已经知道,数据其实都放在了 ThreadLocalMap 中,ThreadLocal 的 get、set 和 remove 方法实际上都是通过 ThreadLocalMap 的 getEntry、set 和 remove 方法实现的。如果想真正全方位的弄懂 ThreadLocal,势必得再对 ThreadLocalMap 做一番理解。

数据库连接的隔离,比如说spring的jdbc的数据库

客户端的请求会话的一些隔离

可以用于hashmap中的线性探测寻址,解决hashmap中的冲突问题

弱引用key的避免内存泄漏


文章作者: 索冀峰
文章链接: http://suojifeng.xyz
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 索冀峰 !
  目录