Java 多线程 (Part2: Java线程 Lock锁)
1. 乐观锁 vs 悲观锁
悲观锁: 一定会出现多线程场景,先加锁,Synchronized 和 Lock 都是悲观锁 (适合 write多)
乐观锁: 不一定出现多线程场景,先不加锁,如果数据未更新,单线程write;如果数据更新;多线程write (适合read多),CAS算法就是乐观锁, Atomic Number 中 CAS自旋是一种典型的乐观锁
/* 乐观锁与悲观锁的调用方法 */
// ------------------------- 悲观锁的调用方式 -------------------------
// synchronized
public synchronized void testMethod() {
// 操作同步资源
}
// ReentrantLock
private ReentrantLock lock = new ReentrantLock(); // 需要保证多个线程使用的是同一个锁
public void modifyPublicResources() {
lock.lock();
// 操作同步资源
lock.unlock();
}
// ------------------------- 乐观锁的调用方式 -------------------------
private AtomicInteger atomicInteger = new AtomicInteger(); // 需要保证多个线程使用的是同一个AtomicInteger
atomicInteger.incrementAndGet(); //执行自增1
2. 自旋锁 vs 适应性自旋锁
自旋锁: 让当前thread 不放弃CPU执行, 等待直到前面的thread已经释放了Lock,自旋锁的实现原理也是CAS
自旋锁常见3种Lock的形式: TicketLock,CLHlock,MCSlock
3. 无锁 vs 偏向锁 vs 轻量级锁 vs 重量级锁
这四种锁都是描述 Synchronized 关键字的状态
Synchronized 实现Thread同步的原理: 使用 Java Object Header 和 Monitor实现同步 (Monitor是使用的 OS中 Mutex Lock)
Lock状态在下方表格中,从上到下升级
- 无锁 noLock:不加锁, 多个线程不断尝试知道成功
- 偏向锁 biasedLock:当其他Thread竞争偏向锁,偏向锁才会释放Lock (在一次Thread中提高性能,依赖一次CAS)
- 轻量级锁:当偏向锁被其他Thread访问,偏向锁会升级成轻量级锁,其他线程则通过自旋获取Lock,不会 Blocked
- 重量级锁:所有Thread在等待Lock时,都会Blocked (依赖的OS的Mutex Lock实现)
偏向锁针对的是一个Thread, 轻量锁和重量锁针对的是多个Thread
4. 公平锁 vs 非公平锁
公平锁: 按照申请Lock的顺序获取Lock,Thread进入Queue 且 Queue的 Header 获取 Lock 优点: 没有Thread会被饿死,缺点: 效率低, CPU开销大
非公平锁: 直接 加Lock, 如果成功就直接执行, 如果失败就进入Queue等待
5. 可重入锁(包括同步锁, 递归锁) vs 不可重入锁
可重入锁:ReentrantLock(可重入锁),synchronized(同步锁),递归锁
不可重入锁:NonReentrantLock
/* 可重入锁的例子 doOthers()被调用时可以获得Lock */
/* 如果下面代码是 不可重入锁, 需要 doSomething()释放Lock, 会造成 DeadLock */
/* 可重入锁可以一定程度上避免死锁*/
public class Widget {
public synchronized void doSomething() {
System.out.println("方法1执行...");
doOthers();
}
public synchronized void doOthers() {
System.out.println("方法2执行...");
}
}
6. 独占锁 vs 共享锁
独占锁(排他锁):Lock只能被一个Thread占用 (比如 ReentrantLock)
共享锁: Lock可以被多个Thread占用 (比如 ReadWriteLock)
/* Write Lock 写锁的源码 */
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState(); // 取到当前锁的个数
int w = exclusiveCount(c); // 取写锁的个数w
if (c != 0) { // 如果已经有线程持有了锁(c!=0)
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread()) // 如果写线程数(w)为0(换言之存在读锁) 或者持有锁的线程不是当前线程就返回失败
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT) // 如果写入锁的数量大于最大数(65535,2的16次方-1)就抛出一个Error。
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) // 如果当且写线程数为0,并且当前线程需要阻塞那么就返回失败;或者如果通过CAS增加写线程数失败也返回失败。
return false;
setExclusiveOwnerThread(current); // 如果c=0,w=0或者c>0,w>0(重入),则设置当前线程或锁的拥有者
return true;
}
/*Read Lock 读锁的源码*/
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1; // 如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
共享锁和独占锁通过更改State的值实现ReadRead的时候是共享,其他时候是独占
非公平锁和公平锁都是独占锁
AQS中的Node类定义了 SHARED 和 EXCLUSIVE,代表了不同的锁获取模式
7. 读写锁 (ReadWriteLock)
为了提高性能,Java提出ReadWriteLock,在读的地方用 Read Lock, 在写的地方用 Write Lock
ReadWriteLock 的 关系为: 多个Read Lock 不 Mutex, Read Lock 和 Write Lock 是 Mutex 的
Read Lock --- 如果想要 read-only 并且可以多人同时读,但不能多人同时写,使用 Read Lock
Write Lock --- 如果在修改数据,并且一个人在写,同时不能读取数据,使用 Write Lock
源码可以参照 java.util.concurrent.locks.ReadWriteLock 或者 ReentrantReadWriteLock
8. 互斥锁
8.1 用 Semaphore 实现 counter=1的互斥锁
这种 counter=1的互斥锁,又称 二元信号量
// 创建一个计数阈值为 5 的信号量对象
// 只能 5 个线程同时访问
Semaphore semp = new Semaphore(5);
try { // 申请许可
semp.acquire();
try {
// 业务逻辑
} catch (Exception e) {
} finally {
// 释放许可
semp.release();
}
} catch (InterruptedException e) {
// code
}
9.1. 同步锁 (Synchronized)
同步锁Synchronized 是 独占式的悲观锁,也是可重入锁
9.1.1 Synchronized 作用范围
- 作用在method上,锁住的是 Object Instance (对象实例)
- 作用在static method上,锁住的是 Class Instance (类实例)
- 作用在Object Instance上,锁住的是 以这个Object Instance为锁的所有代码块
9.1.2 Synchronized 核心组件
- Wait Set --- 调用 wait() 被blocked 的 thread被放在这里
- Contention List(Connection List) --- 竞争队列,所有请求lock的thread都被放在contention list里面
- Entry List --- Contention List 里面可以成为候选资源的Thread被放在Entry List
- OnDeck --- 正在竞争锁资源的Thread (有且只有一个)
- Owner --- 当前已经获取到锁资源的Thread
- !Owner --- 当前释放锁的Thread
9.1.3 Synchronized 实现
- JVM每次会从Connection List的尾部拿出一个作为onDeck(锁竞争候选者),在多线程中,Connection LIst会被多线程进行CAS访问,为了降低竞争,JVM会把一部分Threads放到Entry List 作为 锁竞争候选者
- Owner 会在 Unlock的时候 将 Connection List 中的部分 Threads 移到 Entry List中,并且指定 Entry List 中的一个Thread为 onDeck
- Owner 不会把Lock给onDeck,而是把锁竞争的权利给onDeck (这是JVM的竞争切换)
- onDeck 获取到锁资源后变成了 Owner, 而没有得到锁资源的仍然在Entry List中; 如果 Owner被wait()阻塞,Owner会被转移到 waitSet中,知道被notify或者 notifyAll才会重新进入 Entry List
- 在 Connection List,Entry List 和 Wait List中的Threads全都是阻塞状态
- Synchronized 是一种非公平锁,Synchronized Threads 在 进入 Connection List时会尝试自旋获取锁,如果获取不到就会进入 Connection List. 这种逻辑对已经进入队列的Threads是不公平的
- 加锁其实就是在竞争 Monitor. 代码块加锁就是在前后加上 monitorenter 和 monitorexit实现的
- Synchronized JDK 1.6 之前是重量级锁,需要调用OS Mutex,性能有的时候是低效的
- Synchronized JDK 1.8 引入了偏向锁和轻量级锁
- 锁膨胀就是说 从偏向锁升级到轻量级锁再升级到重量级锁
- JDK 1.6 默认开启偏向锁和轻量级锁;通过 -XX: -UseBiasedLocking 来禁用偏向锁
9.2. ReentrantLock 重入锁
ReentrantLock 继承了 Lock接口,除了可以完成Synchronized可以完成的工作,还可以支持可响应中断锁,可轮询锁请求,定时锁等避免deadlock的方法
9.2.1 Lock 接口的主要方法
- void lock() --- 如果lock是空闲的,当前线程就会获取到锁
- boolean tryLock() --- 如果lock可用,则获取锁并且返回true,否则返回false
- void unlock() --- 当前线程释放持有的锁
- Condition newCondition() --- 条件对象: 获取等待通知组件
- getHoldCount() --- 查询当前线程保持此锁的次数
- getQueueLength() --- 返回正在等待此锁的线程数量
- getWaitQueueLength() --- 返回正在等待与此锁相关给定条件的线程数量
- hasWaiters(Condition condition) --- 查询在特定Condition下是否有线程等待
- hasQueuedThread(Thread thread) --- 查询给定线程是否等待获取此锁
- hasQueuedThreads --- 是否有线程等待此锁
- isFair() --- 该锁是否是公平锁
- isHeldByCurrentThread() --- 当前线程是否保持锁定
- isLock() --- 此锁是否有任意线程占用
- lockInterruptibly() --- 如果当前线程未被中断,获取锁
- tryLock() --- 尝试获取锁,仅在调用时锁未被线程占用,获得锁
- tryLock(long timeout, TimeUnit unit) --- 如果锁在给定等待时间没有被另一个线程保持,获取锁
9.2.2 ReentrantLock 和 Synchronized 对比
- ReentrantLock 通过 lock() 和 unlock() 加解锁,Synchronized会自动解锁,ReentrantLock需要手动解锁
- ReentrantLock相比Synchronized的优势是: 可中断,公平锁,多个锁
9.2.3 ReentantLock实现
public class MyService {
private Lock lock = new ReentrantLock();
//Lock lock=new ReentrantLock(true);//公平锁
//Lock lock=new ReentrantLock(false);//非公平锁
private Condition condition=lock.newCondition();//创建 Condition
public void testMethod() {
try {
lock.lock();//lock 加锁
//1:wait 方法等待:
//System.out.println("开始 wait");
condition.await();
//通过创建 Condition 对象来使线程 wait,必须先执行 lock.lock 方法获得锁
//:2:signal 方法唤醒
condition.signal();//condition 对象的 signal 方法可以唤醒 wait 线程
for (int i = 0; i < 5; i++) {
System.out.println("ThreadName=" + Thread.currentThread().getName()+ (" " + (i + 1)));
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
9.2.4 Condition类 和 Object类 Lock方法对比
- Condition 中 await 方法 == Object 中 wait 方法 (等效)
- Condition 中 signal 方法 == Object 中 notify 方法 (等效)
- Condition 中 signalAll 方法 == Object 中 notifyAll 方法 (等效)
- ReentrantLock 可以notify指定线程,Object的唤醒是随机的
9.2.5 TryLock,Lock,LockInterruptibly对比
- 如果 trylock 获得了锁,就会返回true, 否则就会返回 false; trylock可以增加时间限制,如果超过时间限制还没获得锁,返回false
- lock 如果能获得锁就返回true, 不能就返回 false
- 如果有两个线程分别执行 lock 和 lockInterruptibly, 这个时候中断这两个线程,lockInterruptibly 会throw Exception, lock 不会 throw Exception.
10. 信号量 (Semaphore)
10.1 Semaphore 和 ReentrantLock 对比
1. Semaphore 可以完成ReentrantLock所有工作,通过 acquire() 和 release() 进行获取和释放;
2. Semaphore.acquire() 和 ReentrantLock.lockInterruptibly() 作用一样,都可以被 Thread.interrupt() 中断
3. Semaphore 有 可轮询的锁请求 和 定时锁功能,用法和ReentrantLock一样,名字有变化,是tryAcquire()
4. Semaphore 提供公平锁与非公平锁机制
5. Semaphore 和 ReentrantLock一样也可以手动释放锁
11. Atomic Integer
Atomic Integer 是提供原子操作的Integer,此外还有 Atomic Boolean, Atomic Long,Atomic Reference 等
当我们处理不具有原子性,不安全的线程操作的时候,我们需要将 Integer的加减乘除变成具有原子性,此时使用 Atomic Integer.
12. 分段锁
Segment Lock 是一种锁的思想,具体实现见 ConcurrentHashMap
13. 锁优化
- 减少锁持有的时间 --- 只在有线程安全要求的程序上加锁
- 减少锁粒度 --- 将大的对象拆成小对象,增加并行度,减少锁竞争
- 锁分离 --- 读写锁的思想,读的时候加read lock, 写的时候加 write lock
- 锁粗化 --- 保证线程持有锁的时间尽量短
- 锁消除 --- 如果发现不能被共享的对象,需要消除这些对象的锁操作
相关文章
- Java 多线程(七):线程池
- Java 多线程(五):锁(三)
- Java 多线程(四):锁(二)
- Java 多线程(三):锁(一)
- Java 多线程(二):并发编程的三大特性
- Java 多线程(一):基础
- Java SE 18 新增特性
- Java SE 17 新增特性
- Java SE 16 新增特性
- Java SE 15 新增特性
- Java SE 14 新增特性
- Java SE 10 Application Class-Data Sharing 示例
- Java SE 13 新增特性
- Java SE 12 新增特性
- Java SE 11 新增特性
- Java SE 10 新增特性
- Java SE 9 模块化示例
- Java SE 9 多版本兼容 JAR 包示例
- Java SE 9 新增特性
- Java SE 8 新增特性