并发编程9 - AQS

一. AQS 定义

AQS:AbstractQuenedSynchronizer 抽象的队列式同步器,是除 synchronized 之外的锁机制。ReentrantLock 即是基于AQS实现的独占锁

AQS基于 CLH 队列,用 volatile 修饰共享变量 state,线程通过 CAS 去改变 state 的值,成功则获取锁成功,失败则进入等待队列,等待被唤醒。

利用 AQS 实现自定义锁:

class MyAQSLock implements Lock {

    //内部类,利用AQS实现自定义锁
    class AQSLock extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
        	// CAS 修改状态
            if (compareAndSetState(0, 1)) {
            	//设置锁的所有者为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        //这个方法无需加锁,因为每次只有一个线程会访问该方法
        @Override
        protected boolean tryRelease(int arg) {
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        //是否锁被独占
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
    }

    private AQSLock lock = new AQSLock();

    @Override
    public void lock() {
        lock.acquire(1);
    }
    
	//可打断
    @Override
    public void lockInterruptibly() throws InterruptedException {
        lock.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return lock.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return lock.tryAcquireNanos(1, unit.toNanos(time));
    }

    @Override
    public void unlock() {
        lock.release(1);
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

二. 读写锁

1. ReentrantReadWriteLock

当读操作的频率远远高于写时,可以考虑使用读写锁 ReentrantReadWriteLock,读锁在 “读-读” 操作时可以实现并发,在 “读-写” 操作时会互斥。即读锁与写锁互斥。
示例:

class Data {
    private static final Logger logger = LoggerFactory.getLogger(Data.class);
    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock rLock = rwLock.readLock();
    private ReentrantReadWriteLock.WriteLock wLock = rwLock.writeLock();

    public void getData() throws InterruptedException {
        logger.info("获取读锁");
        rLock.lock();
        try {
            logger.info("获取读锁成功");
            Thread.sleep(1000);
        } finally {
            logger.info("释放读锁");
            rLock.unlock();
        }
    }

    public void putData() throws InterruptedException {
        logger.info("获取写锁");
        wLock.lock();
        try {
            logger.info("获取写锁成功");
            Thread.sleep(1000);
        } finally {
            logger.info("释放写锁");
            wLock.unlock();
        }
    }
}

测试:

public static void main(String[] args) {
 	Data data = new Data();
    new Thread(() -> {
        try {
            data.getData();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();

    new Thread(() -> {
        try {
            data.putData();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}

读写互斥:
在这里插入图片描述
读读并发:
在这里插入图片描述
注意:

  • 读锁不支持条件变量;
  • 重入时升级不支持,即同一个线程获取了读锁后再去获取写锁会导致获取写锁永久等待;但重入时支持降级,即获取了写锁后可以再获取读锁。

应用:缓存读取、缓存更新

2. StampedLock

由于 ReentrantReadWriteLock 在实现读读并发时还是需要通过CAS的方式不断修改锁的标识位,其性能肯定不如不加锁。StampedLock 在读的过程中采用了乐观读的方式,即配合一个 “” 使用。首先获取戳,读完之后验戳,只有验戳失败以后才加读锁重新读取,最大性地提升读的性能

class MyStampedLock {
    private static final Logger logger = LoggerFactory.getLogger(MyStampedLock.class);
    private StampedLock stampedLock = new StampedLock();
    private int data;

    public MyStampedLock(int data) {
        this.data = data;
    }

    public int getData() {
    	//乐观读,没有加锁,只是获取了戳	
        long stamp = stampedLock.tryOptimisticRead();
        logger.info("乐观读:" + stamp);
        try {
            Thread.sleep(1000); //模拟读的过程
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //验戳失败,加读锁
        if (stampedLock.validate(stamp)) {
            logger.info(stamp + "验证通过");
            return data;
        }
        stamp = stampedLock.readLock();
        logger.info("获取读锁:" + stamp);
        try {
            Thread.sleep(1000); //模拟读的过程,重新读取
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            logger.info("释放读锁:" + stamp);
            stampedLock.unlockRead(stamp);
        }
        return data;
    }

    public void setData(int data) {
    	//写的时候直接加写锁
        long stamp = stampedLock.writeLock();
        logger.info("获取写锁:" + stamp);
        try {
            Thread.sleep(1000);
            this.data = data;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            logger.info("释放写锁:" + stamp);
            stampedLock.unlockWrite(stamp);
        }
    }
}

两个线程并发读:

public static void main(String[] args) throws InterruptedException {
 	MyStampedLock myStampedLock = new MyStampedLock(1);
    new Thread(() -> {
      myStampedLock.getData();
    }, "t1").start();
    Thread.sleep(500);
    new Thread(() -> {
        myStampedLock.getData();
    }, "t2").start();
}

在这里插入图片描述
一个线程读,一个线程写:

public static void main(String[] args) throws InterruptedException {
 	MyStampedLock myStampedLock = new MyStampedLock(1);
    new Thread(() -> {
      myStampedLock.getData();
    }, "t1").start();
    Thread.sleep(500);
    new Thread(() -> {
        myStampedLock.setData(2);
    }, "t2").start();
}

在这里插入图片描述

三. Semaphore 信号量

用来限制同时访问共享资源的线程数量上限,可用于限流。

Semaphore semaphore = new Semaphore(n);
semaphore.acquire();
semaphore.release();

用法示例:当许可数量为2时,4个线程只有2个能获得许可,其他线程将被阻塞,直到许可被释放后,其他线程才能重新获得许可。(可代替 wait/notify )

public static void main(String[] args) {
	Semaphore semaphore = new Semaphore(2);
    for (int i = 0; i < 4; i++) {
        new Thread(() -> {
            try {
                semaphore.acquire();
                logger.info("获得许可");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release();
            }
            logger.info("释放许可……");

        }).start();
    }
}

在这里插入图片描述

四. CountDownLatch

用于倒计时等待所有线程结束。类似于 join,但是 join 属于比较底层的 API,join 必须等到线程完全结束,释放连接才行,无法配合线程池使用。

CountDownLatch 配合线程池使用示例:

public class CountDownLatchTest {
    private static final Logger logger = LoggerFactory.getLogger(CountDownLatchTest.class);
    public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(3);
        CountDownLatch countDownLatch = new CountDownLatch(3);
        service.execute(() -> {
            try {
                logger.info("线程0启动");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            countDownLatch.countDown();
            logger.info("线程0结束--" + countDownLatch.getCount());
        });
        service.execute(() -> {
            try {
                logger.info("线程1启动");
                Thread.sleep(1500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            countDownLatch.countDown();
            logger.info("线程1结束--" + countDownLatch.getCount());
        });
        service.execute(() -> {
            try {
                logger.info("线程2启动");
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            countDownLatch.countDown();
            logger.info("线程2结束--" + countDownLatch.getCount());
        });
        logger.info("等待子线程结束……");
        countDownLatch.await();
        logger.info("等待结束!");

    }
}

在这里插入图片描述
实际上如果有返回结果需要同步,可以考虑使用 Future 接收结果。

五. CyclicBarrier

CountDownLatch 的局限性是不能重复使用,即当计数减为1时,无法重新设置count 值继续计数。CyclicBarrier 也是通过计数等待所有线程结束,但是弥补了这一缺陷。当 count 值减为 0 时,如果继续调用 CyclicBarrier 对象的 await() 方法,该对象的 count 值又会从初始值开始

public class CyclicBarrierTest {
    private static final Logger logger = LoggerFactory.getLogger(CyclicBarrierTest.class);
    public static void main(String[] args) {
        //CyclicBarrier 可重用,当计数减到0时,下次调用await又会从初始值开始
        CyclicBarrier cyclicBarrier = new CyclicBarrier(2, ()->{
            logger.info("线程1、2同步完毕"); //参数二为同步完成后执行的代码
        });

        ExecutorService service = Executors.newFixedThreadPool(2);

        for (int i = 0; i < 3; i++) {
            service.execute(() -> {
                try {
                    logger.info("线程1开始执行");
                    Thread.sleep(1000);
                    cyclicBarrier.await(); //计数减一,进入同步等待,当 count 减为0时退出等待
                    logger.info("线程1执行完毕");
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            });

            service.execute(() -> {
                try {
                    logger.info("线程2开始执行");
                    Thread.sleep(2000);
                    cyclicBarrier.await(); //计数减一,进入同步等待,当 count 减为0时退出等待
                    logger.info("线程2执行完毕");
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            });
        }
        service.shutdown();
    }
}

在这里插入图片描述

注意:CyclicBarrier 初始化的 count 值必须与线程数保持一致,否则不能起到同步的效果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值