并发线程基础第九篇

目录

并发工具-J.U.C

AQS原理

概述

 实现不可重入锁

自定义同步器

自定义锁

ReentrantLock原理

非公平锁的加锁实现原理

1.先从构造器来看,默认为非公平锁实现

2.NonfairSync 继承自 AQS

3.没有竞争时,if语句块直接成立

 4.有竞争时,进入acquire(1)

非公平锁的解锁实现原理

锁重入原理 

公平锁的实现原理

读写锁ReentrantReadWriteLock

读写锁StampedLock

Semaphore(信号量)

CountDownLatch(倒计时门闩)

CyclicBarrier(循环栅栏)


并发工具-J.U.C

AQS原理

概述

AQS(AbstractQueuedSynchronizer)是java中用于构建锁和同步器的抽象基类。它提供了一种基于FIFO等待队列的锁和同步器的实现框架,是并发编程中实现锁和同步器的重要基础。

AQS提供了两种基本的同步原语:独占锁和共享锁。独占锁是指同一时刻只能有一个线程持有的锁,典型的代表是ReentrantLock。共享锁是指同一时刻可以被多个线程持有的锁,典型的代表是 CountDownLatch 和 Semaphore。

特点:

        1用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取 锁和释放锁

  •         getState - 获取 state 状态
  •         setState - 设置 state 状态
  •         compareAndSetState - cas 机制设置 state 状态
  •         独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源

子类主要实现这样一些方法

  • tryAcquire
  • tryRelease
  • tryAcquireShared
  • tryReleaseShared
  • isHeldExclusively

 实现不可重入锁

自定义同步器
final class MySync extends AbstractQueuedSynchronizer{
    @Override
    protected boolean tryAcquire(int arg) {
        if(arg == 1){
            if (compareAndSetState(0,1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
        }
        return false;
    }

    @Override
    protected boolean tryRelease(int arg) {
        if(arg == 1){
            if(getState()==0){
                throw new IllegalArgumentException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
        return false;
    }

    protected Condition newCondition(){
        return new ConditionObject();
    }
    @Override
    protected boolean isHeldExclusively() {
        return getState() == 1;
    }
}
自定义锁
class MyLock implements Lock {
    static MySync sync = new MySync();

    @Override
    //尝试获得锁,不成功,进入等待队列
    public void lock() {
        sync.acquire(1);
    }

    @Override
    //尝试,不成功,进入等待队列,可打断
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override
    //尝试一次获得锁,不成功直接返回,不进入等待队列
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override
    //尝试获得锁,不成功进入等待队列,有时限
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }

    @Override
    //释放锁
    public void unlock() {
        sync.release(1);
    }

    @Override
    //生成条件变量
    public Condition newCondition() {
        return sync.newCondition();
    }
}

测试

public class Test3 {
    public static void main(String[] args) {
        //测试
        MyLock lock = new MyLock();
        new Thread(()->{
            lock.lock();
            try{
                log.debug("获得锁");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
                log.debug("释放锁");
            }
        },"t1").start();
        new Thread(()->{
            lock.lock();
            try{
                log.debug("获得锁");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
                log.debug("释放锁");
            }
        },"t2").start();
    }
}
09:05:06.448 [t1] DEBUG com.example.com.yunlong.test3.Test3 - 获得锁
09:05:07.470 [t1] DEBUG com.example.com.yunlong.test3.Test3 - 释放锁
09:05:07.470 [t2] DEBUG com.example.com.yunlong.test3.Test3 - 获得锁
09:05:08.476 [t2] DEBUG com.example.com.yunlong.test3.Test3 - 释放锁

ReentrantLock原理

非公平锁的加锁实现原理

1.先从构造器来看,默认为非公平锁实现
public ReentrantLock() {
        sync = new NonfairSync();
    }
2.NonfairSync 继承自 AQS
public void lock() {
        sync.lock();
    }
final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
3.没有竞争时,if语句块直接成立

 4.有竞争时,进入acquire(1)

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

Thread-1 执行了

  • CAS 尝试将 state 由 0 改为 1,结果失败 
  • 进入tryAcquire逻辑,这时state已为1,结果失败
  • 接下来进入addWaiter逻辑,构造Node队列
  1. 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态
  2. Node 的创建是懒惰的
  3. 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程

 当前线程进入 acquireQueued 逻辑

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
  1. acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞
  2. 如果自己是紧邻着 head(排第二位),那么再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
  3. 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,这次返回 false
  4.  shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
  5.  当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回 true
  6. 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示)

再次有多个线程经历上述过程竞争失败,变成这个样子

非公平锁的解锁实现原理

public void unlock() {
        sync.release(1);
    }
public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

Thread-0 释放锁,进入 tryRelease 流程,如果成功

  • 设置 exclusiveOwnerThread 为 null
  • state = 0

当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程

private void unparkSuccessor(Node node) {
       
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

       
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1 回到 Thread-1 的 acquireQueued 流程

如果加锁成功(没有竞争),会设置

  • exclusiveOwnerThread 为 Thread-1,state = 1
  • head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread
  • 原本的 head 因为从链表断开,而可被垃圾回收  

如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了

 如果不巧又被 Thread-4 占了先

  • Thread-4 被设置为 exclusiveOwnerThread,state = 1
  • Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞

锁重入原理 


static final class NonfairSync extends Sync {
 // ...
 
     // Sync 继承过来的方法, 方便阅读, 放在此处
     final boolean nonfairTryAcquire(int acquires) {
         final Thread current = Thread.currentThread();
         int c = getState();
         if (c == 0) {
             if (compareAndSetState(0, acquires)) {
                 setExclusiveOwnerThread(current);
                 return true;
     }
 }
 // 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入
         else if (current == getExclusiveOwnerThread()) {
             // state++
             int nextc = c + acquires;
             if (nextc < 0) // overflow
                 throw new Error("Maximum lock count exceeded");
             setState(nextc);
             return true;
     }
 return false;
 }

// Sync 继承过来的方法, 方便阅读, 放在此处
     protected final boolean tryRelease(int releases) {
         // state-- 
         int c = getState() - releases;
         if (Thread.currentThread() != getExclusiveOwnerThread())
             throw new IllegalMonitorStateException();
         boolean free = false;
         // 支持锁重入, 只有 state 减为 0, 才释放成功
         if (c == 0) {
             free = true;
         setExclusiveOwnerThread(null);
 }
        setState(c);
         return free;
 }
}

公平锁的实现原理

protected final boolean tryAcquire(int acquires){
    final Thread current = Thread currentThread();
    int c = getState();
    if (c == 0){
        //先检查AQS队列中是否有前驱节点,没有才去竞争
        if(!hasQueuedPrecessors()) && compareAndSetState(0,acquires){
            setExclusiveOwnThread(current);
            return true;
        }
    }
    else ...
}

读写锁ReentrantReadWriteLock

当读操作远远高于写操作时,这时候使用 读写锁 让 读-读 可以并发,提高性能。

提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法

public class Test4 {
    private static ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
    public static void main(String[] args) {
        new Thread(()->{
            log.debug("获得读锁");
            rw.readLock().lock();
            try{
                Thread.sleep(1000);
                log.debug("开始读取");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                rw.readLock().unlock();
            }
        },"t1").start();

        new Thread(()->{
            log.debug("获得读锁");
            rw.readLock().lock();
            try{
                Thread.sleep(1000);
                log.debug("开始读取");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                rw.readLock().unlock();
            }
        },"t2").start();

    }
}
17:07:02.318 [t1] DEBUG com.example.com.yunlong.test3.Test4 - 获得读锁
17:07:02.318 [t2] DEBUG com.example.com.yunlong.test3.Test4 - 获得读锁
17:07:03.334 [t1] DEBUG com.example.com.yunlong.test3.Test4 - 开始读取
17:07:03.334 [t2] DEBUG com.example.com.yunlong.test3.Test4 - 开始读取

 结论:

  •         读锁-读锁可以并发
  •         读锁-写锁相互阻塞
  •         写锁-写锁相互阻塞

注意事项

  • 读锁不支持条件变量
  • 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待
  • 重入时降级支持:即持有写锁的情况下去获取读锁  

读写锁StampedLock

该类自JDK8加入之后,是为了进一步优化读性能,它的特点是在使用读锁,写锁时,都必须配合使用。

示例

提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法

class DataContainerStamped{
    private int data;
    private final StampedLock lock = new StampedLock();
    public DataContainerStamped(int data){
        this.data = data;
    }
    public int read(int readTime) throws InterruptedException {
        long stamp = lock.tryOptimisticRead();
        log.debug("optimistic read locking{}",stamp);
        Thread.sleep(readTime);
        if(lock.validate(stamp)){
            log.debug("read finish...{},data...{}",stamp,data);
            return data;
        }
        //锁升级
        log.debug("update to read lock...{}",stamp);
        try{
            stamp = lock.readLock();
            log.debug("read lock{}",stamp);
            Thread.sleep(readTime);
            log.debug("read finish...{},data...{}",stamp,data);
            return data;
        }finally {
            log.debug("read unlock...{}",stamp);
            lock.unlockRead(stamp);
        }
    }
    public void write(int newData){
        long stamp = lock.writeLock();
        log.debug("write lock...{}",stamp);
        try{
            Thread.sleep(2000);
            this.data = newData;
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            log.debug("write lock...{}",stamp);
            lock.unlockWrite(stamp);
        }
    }
}

测试读-读可以优化

public class Test5 {
    public static void main(String[] args) throws InterruptedException {
        DataContainerStamped dataContainerStamped = new DataContainerStamped(100);
        new Thread(()->{
            try {
                dataContainerStamped.read(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"t1").start();
        Thread.sleep(500);
        new Thread(()->{
            try {
                dataContainerStamped.read(0);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"t2").start();
    }

}
17:38:49.607 [t1] DEBUG com.example.com.yunlong.test3.DataContainerStamped - optimistic read locking256
17:38:50.119 [t2] DEBUG com.example.com.yunlong.test3.DataContainerStamped - optimistic read locking256
17:38:50.119 [t2] DEBUG com.example.com.yunlong.test3.DataContainerStamped - read finish...256,data...100
17:38:50.622 [t1] DEBUG com.example.com.yunlong.test3.DataContainerStamped - read finish...256,data...100

测试 读-写 时优化读补加读锁   

public class Test5 {
    public static void main(String[] args) throws InterruptedException {
        DataContainerStamped dataContainerStamped = new DataContainerStamped(100);
        new Thread(()->{
            try {
                dataContainerStamped.read(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"t1").start();
        Thread.sleep(500);
        new Thread(()->{
            dataContainerStamped.write(110);
        },"t2").start();
    }

}
17:41:11.624 [t1] DEBUG com.example.com.yunlong.test3.DataContainerStamped - optimistic read locking256
17:41:12.132 [t2] DEBUG com.example.com.yunlong.test3.DataContainerStamped - write lock...384
17:41:12.650 [t1] DEBUG com.example.com.yunlong.test3.DataContainerStamped - update to read lock...256
17:41:14.134 [t2] DEBUG com.example.com.yunlong.test3.DataContainerStamped - write lock...384
17:41:14.134 [t1] DEBUG com.example.com.yunlong.test3.DataContainerStamped - read lock513
17:41:15.149 [t1] DEBUG com.example.com.yunlong.test3.DataContainerStamped - read finish...513,data...110
17:41:15.149 [t1] DEBUG com.example.com.yunlong.test3.DataContainerStamped - read unlock...513

注意 

  • StampedLock 不支持条件变量
  • StampedLock 不支持可重入

 根据不同的需求选择适应的读写锁

Semaphore(信号量)

Semaphore(信号量)是一个经典的同步工具,用于控制同时访问某个特定资源的线程数量。它维护了一个许可证(permit)的计数器,线程在访问资源之前必须先获取许可证,访问完成后必须释放许可证。Semaphore可以用来实现线程的互斥访问,也可以用来实现资源的控制和限流。

Semaphore提供了两种主要的操作:

  1. acquire(): 获取一个许可证,如果没有许可证可用,则线程将阻塞直到有许可证可用。
  2. release(): 释放一个许可证,归还给信号量。
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    private static final int MAX_AVAILABLE = 5; // 最大可用许可证数量
    private static Semaphore semaphore = new Semaphore(MAX_AVAILABLE, true); // 创建信号量,第二个参数设置为true表示公平性

    public static void main(String[] args) {
        // 创建多个线程并启动
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire(); // 获取许可证
                    System.out.println(Thread.currentThread().getName() + " is accessing the resource.");
                    // 模拟访问资源的耗时操作
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release(); // 释放许可证
                }
            }, "Thread-" + i).start();
        }
    }
}

在这个示例中,Semaphore被用来控制同时访问资源的线程数量。Semaphore的初始许可证数量为5,意味着最多同时允许5个线程访问资源。当线程尝试获取许可证时,如果当前已经达到了许可证的上限,则线程会被阻塞,直到有其他线程释放许可证为止。通过Semaphore,可以有效地控制并发访问的数量,实现资源的限流 

CountDownLatch(倒计时门闩)

CountDownLatch(倒计时门闩)是Java中的一个同步工具,它可以用来实现线程之间的等待机制。它内部维护了一个计数器,初始值为一个正整数,表示需要等待的线程数量。每个线程在完成自己的任务后,可以调用countDown()方法将计数器减一,当计数器的值减至零时,所有等待的线程都会被唤醒

CountDownLatch 提供了两个主要的方法:

  1. countDown(): 将计数器减一,表示一个线程已经完成了任务。
  2. await(): 等待计数器的值减至零,阻塞当前线程直到计数器的值为零。

示例 

import java.util.concurrent.CountDownLatch;

public class CountdownLatchExample {
    private static final int THREAD_COUNT = 5;
    private static final CountDownLatch latch = new CountDownLatch(THREAD_COUNT);

    public static void main(String[] args) throws InterruptedException {
        // 创建多个线程并启动
        for (int i = 0; i < THREAD_COUNT; i++) {
            new Thread(() -> {
                // 模拟线程执行任务的耗时操作
                try {
                    Thread.sleep((long) (Math.random() * 1000));
                    System.out.println(Thread.currentThread().getName() + " has finished its task.");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown(); // 每个线程执行完成后将计数器减一
                }
            }, "Thread-" + i).start();
        }

        // 等待所有线程执行完成
        latch.await();
        System.out.println("All threads have finished their tasks.");
    }
}

在这个示例中,CountDownLatch的初始计数器值为5,代表了需要等待的线程数量。每个线程在执行完任务后,都会调用countDown()方法将计数器减一。主线程调用await()方法进入等待状态,直到所有线程执行完毕,计数器减至零,主线程被唤醒。通过CountDownLatch,可以实现线程之间的协作,等待所有线程执行完特定任务后再继续执行后续操作。

应用:可以配合线程池使用。相比于join(),属于高级工具。

  •         同步等待多线程准备完毕
  •         同步等待多个远程调用结束

CyclicBarrier(循环栅栏)

CyclicBarrier(循环栅栏)是Java中的一个同步工具,它可以让一组线程互相等待,直到所有线程都到达了某个同步点之后,才继续执行。与CountDownLatch不同的是,CyclicBarrier的同步点是可重复利用的,一旦所有线程都到达了同步点,栅栏就会打开,所有线程都会被释放,并且栅栏重新初始化,可以再次使用。

CyclicBarrier提供了以下主要的方法:

  1. await(): 让当前线程等待,直到所有线程都到达了同步点。
  2. await(long timeout, TimeUnit unit): 让当前线程等待一段时间,如果超时则继续执行。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    private static final int THREAD_COUNT = 5;
    private static final CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT, () -> {
        System.out.println("All threads have reached the barrier.");
    });

    public static void main(String[] args) {
        // 创建多个线程并启动
        for (int i = 0; i < THREAD_COUNT; i++) {
            new Thread(() -> {
                // 模拟线程执行任务的耗时操作
                try {
                    Thread.sleep((long) (Math.random() * 1000));
                    System.out.println(Thread.currentThread().getName() + " has reached the barrier.");
                    barrier.await(); // 等待其他线程到达栅栏
                    System.out.println(Thread.currentThread().getName() + " continues its work.");
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, "Thread-" + i).start();
        }
    }
}

 在这个示例中,CyclicBarrier的初始参与线程数量为5,当所有线程都调用了await()方法到达了栅栏时,栅栏就会打开,所有线程都会被释放,并且会执行给定的回调函数。在回调函数中,可以执行一些额外的逻辑,比如输出一条提示信息。通过CyclicBarrier,可以实现多个线程之间的同步,并在特定点集合执行任务。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值