zl程序教程

您现在的位置是:首页 >  Java

当前栏目

Java 多线程 (Part2: Java线程 Lock锁)

2023-02-19 12:17:52 时间
Java 锁的分类图

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状态在下方表格中,从上到下升级

从左往右是Lock状态升级的流程
4种Synchronized锁的 存储内容
  1. 无锁 noLock:不加锁, 多个线程不断尝试知道成功
  2. 偏向锁 biasedLock:当其他Thread竞争偏向锁,偏向锁才会释放Lock (在一次Thread中提高性能,依赖一次CAS)
  3. 轻量级锁:当偏向锁被其他Thread访问,偏向锁会升级成轻量级锁,其他线程则通过自旋获取Lock,不会 Blocked
  4. 重量级锁:所有Thread在等待Lock时,都会Blocked (依赖的OS的Mutex Lock实现)

偏向锁针对的是一个Thread, 轻量锁和重量锁针对的是多个Thread

4. 公平锁 vs 非公平锁

公平锁: 按照申请Lock的顺序获取Lock,Thread进入Queue 且 Queue的 Header 获取 Lock 优点: 没有Thread会被饿死,缺点: 效率低, CPU开销大

非公平锁: 直接 加Lock, 如果成功就直接执行, 如果失败就进入Queue等待

公平锁比非公平锁获取同步状态时多了一个 hasQueuedPredecessors

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)

在这个例子里面,读锁是共享锁,写锁是独占锁;读锁在共享时可以保证并发读非常高效
这是共享锁的state, 一共32位,读状态代表read lock的个数,写状态代表write lock的个数
/* 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 作用范围

  1. 作用在method上,锁住的是 Object Instance (对象实例)
  2. 作用在static method上,锁住的是 Class Instance (类实例)
  3. 作用在Object Instance上,锁住的是 以这个Object Instance为锁的所有代码块

9.1.2 Synchronized 核心组件

  1. Wait Set --- 调用 wait() 被blocked 的 thread被放在这里
  2. Contention List(Connection List) --- 竞争队列,所有请求lock的thread都被放在contention list里面
  3. Entry List --- Contention List 里面可以成为候选资源的Thread被放在Entry List
  4. OnDeck --- 正在竞争锁资源的Thread (有且只有一个)
  5. Owner --- 当前已经获取到锁资源的Thread
  6. !Owner --- 当前释放锁的Thread

9.1.3 Synchronized 实现

Sychronized 核心组件之间的操作流程
  1. JVM每次会从Connection List的尾部拿出一个作为onDeck(锁竞争候选者),在多线程中,Connection LIst会被多线程进行CAS访问,为了降低竞争,JVM会把一部分Threads放到Entry List 作为 锁竞争候选者
  2. Owner 会在 Unlock的时候 将 Connection List 中的部分 Threads 移到 Entry List中,并且指定 Entry List 中的一个Thread为 onDeck
  3. Owner 不会把Lock给onDeck,而是把锁竞争的权利给onDeck (这是JVM的竞争切换)
  4. onDeck 获取到锁资源后变成了 Owner, 而没有得到锁资源的仍然在Entry List中; 如果 Owner被wait()阻塞,Owner会被转移到 waitSet中,知道被notify或者 notifyAll才会重新进入 Entry List
  5. 在 Connection List,Entry List 和 Wait List中的Threads全都是阻塞状态
  6. Synchronized 是一种非公平锁,Synchronized Threads 在 进入 Connection List时会尝试自旋获取锁,如果获取不到就会进入 Connection List. 这种逻辑对已经进入队列的Threads是不公平的
  7. 加锁其实就是在竞争 Monitor. 代码块加锁就是在前后加上 monitorenter 和 monitorexit实现的
  8. Synchronized JDK 1.6 之前是重量级锁,需要调用OS Mutex,性能有的时候是低效的
  9. Synchronized JDK 1.8 引入了偏向锁和轻量级锁
  10. 锁膨胀就是说 从偏向锁升级到轻量级锁再升级到重量级锁
  11. JDK 1.6 默认开启偏向锁和轻量级锁;通过 -XX: -UseBiasedLocking 来禁用偏向锁

9.2. ReentrantLock 重入锁

ReentrantLock 继承了 Lock接口,除了可以完成Synchronized可以完成的工作,还可以支持可响应中断锁可轮询锁请求定时锁等避免deadlock的方法

9.2.1 Lock 接口的主要方法

  1. void lock() --- 如果lock是空闲的,当前线程就会获取到锁
  2. boolean tryLock() --- 如果lock可用,则获取锁并且返回true,否则返回false
  3. void unlock() --- 当前线程释放持有的锁
  4. Condition newCondition() --- 条件对象: 获取等待通知组件
  5. getHoldCount() --- 查询当前线程保持此锁的次数
  6. getQueueLength() --- 返回正在等待此锁的线程数量
  7. getWaitQueueLength() --- 返回正在等待与此锁相关给定条件的线程数量
  8. hasWaiters(Condition condition) --- 查询在特定Condition下是否有线程等待
  9. hasQueuedThread(Thread thread) --- 查询给定线程是否等待获取此锁
  10. hasQueuedThreads --- 是否有线程等待此锁
  11. isFair() --- 该锁是否是公平锁
  12. isHeldByCurrentThread() --- 当前线程是否保持锁定
  13. isLock() --- 此锁是否有任意线程占用
  14. lockInterruptibly() --- 如果当前线程未被中断,获取锁
  15. tryLock() --- 尝试获取锁,仅在调用时锁未被线程占用,获得锁
  16. tryLock(long timeout, TimeUnit unit) --- 如果锁在给定等待时间没有被另一个线程保持,获取锁

9.2.2 ReentrantLock 和 Synchronized 对比

  1. ReentrantLock 通过 lock() 和 unlock() 加解锁,Synchronized会自动解锁,ReentrantLock需要手动解锁
  2. 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方法对比

  1. Condition 中 await 方法 == Object 中 wait 方法 (等效)
  2. Condition 中 signal 方法 == Object 中 notify 方法 (等效)
  3. Condition 中 signalAll 方法 == Object 中 notifyAll 方法 (等效)
  4. ReentrantLock 可以notify指定线程,Object的唤醒是随机的

9.2.5 TryLock,Lock,LockInterruptibly对比

  1. 如果 trylock 获得了锁,就会返回true, 否则就会返回 false; trylock可以增加时间限制,如果超过时间限制还没获得锁,返回false
  2. lock 如果能获得锁就返回true, 不能就返回 false
  3. 如果有两个线程分别执行 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
  • 锁粗化 --- 保证线程持有锁的时间尽量短
  • 锁消除 --- 如果发现不能被共享的对象,需要消除这些对象的锁操作