1.CAS
CAS(Compare-And-Swap,比较与交换)是一种常见的无锁(lock-free)机制,用于实现多线程环境下的原子操作。CAS操作通过硬件支持的原子指令来实现,确保在多线程竞争的情况下,数据的一致性和操作的原子性。
CAS操作涉及三个参数:
- V:需要更新的变量的内存地址。
- E:预期值,即当前线程认为变量的值应该是什么。
- N:新值,即希望将变量更新为的值。
CAS的执行步骤如下:
- 读取变量V的当前值。
- 比较V的当前值与预期值E:
- 如果相等,说明没有其他线程修改过V,则将变量的值更新为N,并返回
true
,表示更新成功。 - 如果不相等,说明在此期间V已经被其他线程修改,则更新失败,返回
false
。
- 如果相等,说明没有其他线程修改过V,则将变量的值更新为N,并返回
这个过程是由硬件指令(如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.
Locks
和Synchronizers
类
这些类使用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方法,如compareAndSwapInt
、compareAndSwapLong
和compareAndSwapObject
。
底层操作能力: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
的优化
- 偏向锁(Biased Locking)
- 原理:当一个线程首次获取锁时,JVM会偏向于该线程,并将锁标记为偏向锁。之后,如果同一线程再次请求这个锁,JVM不需要执行锁竞争操作,而是直接授予锁。偏向锁在无竞争场景下极大地减少了加锁的开销。
- 适用场景:适用于锁竞争少、同一线程多次进入同步代码块的场景。
- 轻量级锁(Lightweight Locking)
- 原理:在锁没有被其他线程竞争的情况下,JVM会将锁从偏向锁升级为轻量级锁。此时,线程通过CAS(Compare-And-Swap)操作来获取锁,而不是传统的阻塞方式。
- 适用场景:适用于短时间内可能出现锁竞争,但竞争不激烈的场景。
- 自旋锁(Spin Locking)
- 原理:自旋锁避免了线程在锁竞争失败后立即阻塞,而是让线程短时间内不断地尝试获取锁。这种方式适用于锁竞争时间短的场景,因为线程不会立即进入阻塞态,而是不断尝试获取锁,从而避免了上下文切换的开销。
- 适用场景:适用于锁竞争频繁但每次竞争时间较短的场景。
- 锁消除(Lock Elimination)
- 原理:在编译期间,JVM可以通过逃逸分析(Escape Analysis)检测到某些锁只在单线程环境中使用,或者锁对象没有逃逸到其他线程,那么JVM会自动消除这些锁。
- 适用场景:适用于单线程场景或锁对象未共享的场景。
- 锁粗化(Lock Coarsening)
- 原理:如果JVM检测到在一个方法或代码块中多次使用相同的锁,而这些加锁操作是紧密相连的,那么JVM会将这些锁操作合并为一个更大的锁块,从而减少加锁和解锁的频率。
- 适用场景:适用于频繁的、短时间的锁操作。
- 适应性自旋(Adaptive Spinning)
- 原理:在自旋锁的基础上,JVM会根据前一次自旋的结果动态调整自旋的次数。如果自旋能够快速获取锁,则增加自旋次数;反之,则减少自旋次数或直接阻塞线程。
- 适用场景:适用于锁竞争时间波动较大的场景。
开发者对synchronized
的优化
- 减少锁的持有时间
- 做法:尽量将
synchronized
代码块的范围缩小到最小,确保只在确实需要同步的地方加锁,减少锁的持有时间。 - 效果:减小锁的持有时间可以减少线程阻塞的时间,从而提高系统的并发性。
- 做法:尽量将
- 减少锁的粒度
- 做法:将大锁拆分为小锁,针对不同的数据或资源使用不同的锁,以减少锁的竞争。
- 效果:减小锁的粒度可以减少锁的争用,提高系统的并发能力。
- 使用更高效的并发工具
- 做法:在某些场景下,可以用Java的其他并发工具类替代
synchronized
,如ReentrantLock
、ReadWriteLock
、StampedLock
等。 - 效果:这些工具类提供了更多的功能和优化,如可中断的锁获取、超时等待、读写分离等,能够在特定场景下提供更高的性能。
- 做法:在某些场景下,可以用Java的其他并发工具类替代
- 避免不必要的同步
- 做法:通过仔细分析代码逻辑,避免在不需要同步的地方使用
synchronized
,例如仅在读操作时不需要同步。 - 效果:减少不必要的同步可以显著提高性能,特别是在读多写少的场景中。
- 做法:通过仔细分析代码逻辑,避免在不需要同步的地方使用
显式锁
java.util.concurrent.locks
包提供了一些显式的锁类,如ReentrantLock
,它们相比内置锁提供了更多的功能和灵活性。
- ReentrantLock:一个可重入锁,提供了比
synchronized
更高级的功能,如公平锁、公平锁模式选择、尝试加锁(tryLock
)、超时等待加锁等。 - ReentrantReadWriteLock:一个读写锁,实现了读写分离,读操作共享锁,写操作独占锁。它提高了读多写少场景下的并发性。
自旋锁
CountDownLatch
是 Java 中的一个同步辅助工具,属于
java.util.concurrent
包中的一部分。它用于让一个或多个线程等待直到其他线程完成一组操作。CountDownLatch
主要用于在并发环境中协调多个线程的执行,尤其是当某些线程需要等待其他线程完成某些工作后才能继续执行时。
CountDownLatch
使用一个计数器来表示线程需要等待的操作次数。每当一个操作完成时,计数器减一。线程可以在计数器为零之前被阻塞,直到所有操作完成,计数器降到零。
CountDownLatch
CountDownLatch
是一种同步辅助工具,允许一个或多个线程等待,直到其他线程完成一组操作。
主要方法
await()
:使当前线程等待直到计数器的值变为零。线程会在这里被阻塞,直到其他线程调用countDown()
方法使计数器减到零。java Copy code latch.await();
await(long timeout, TimeUnit unit)
:使当前线程等待直到计数器的值变为零或等待时间超时。该方法允许设置一个超时时间,超时后线程将继续执行,即使计数器值未变为零。java Copy code latch.await(10, TimeUnit.SECONDS);
countDown()
:减少计数器的值。当一个操作完成时,调用countDown()
方法将计数器的值减一。如果计数器的值减到零,所有在await()
方法中等待的线程将被唤醒。java Copy code latch.countDown();
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 实例来实现分布式锁,以提高锁的可靠性。其基本步骤如下:
- 部署多个 Redis 实例:
- 部署 N 个独立的 Redis 实例(通常建议 N 为奇数,如 5 个实例)。
- 请求锁:
- 客户端请求锁时,尝试在所有 Redis 实例上设置锁。
- 使用
SET key value NX PX <expire>
命令,在每个 Redis 实例上设置一个带有超时的锁键。
- 判断锁的获取成功:
- 锁获取成功的条件是:在至少 N/2 + 1 个 Redis 实例上成功设置了锁。
- 如果成功设置的实例数量达到大于 N/2 的数量,则认为锁成功获取。
- 释放锁:
- 客户端在使用完共享资源后,需要释放锁。
- 释放锁时,客户端需要在所有 Redis 实例上删除锁键。
- 处理锁的超时和过期:
- 锁的超时时间(
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的避免内存泄漏