java 并发-并发工具原理与使用

AQS

AQS:AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,许多同步类实现都依赖于该同步器

image

自定义锁

//自定义锁(不可重入)
class Mylock implements Lock{

    //同步器,锁的大部分功能由下面的抽象同步器实现提供
    class MySync extends AbstractQueuedSynchronizer{
        @Override//尝试获取锁,尝试一次
        protected boolean tryAcquire(int arg) {
            //锁状态state,默认是0,改为1表示独占
            if (compareAndSetState(0,1)){
                //设置当前线程为owner
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;

        }

        @Override//释放锁
        protected boolean tryRelease(int arg) {
            //此处不用同步,因为释放锁的线程一定是持有锁的线程
            //设置锁的owner为空
            //state变量是volatile,exclusiveOwnerThread则不是,setState(0)放在后面,可以保证这俩变量对其他线程可见
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

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

        public Condition newCondition(){
            return new ConditionObject();
        }
    }

    private MySync sync=new MySync();

    @Override//加锁
    public void lock() {
        //此处会调用上面重写的tryAcquire,尝试加锁一次,如果失败了,放到队列中等待,下次再来加锁
        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();
    }
}

ReentrantLock原理

ReentrantLock实现了Lock接口,Sycn是ReentrantLock内部的一个抽象同步器,继承自AbstractQueuedSynchronizer,Sycn由两个实现NonFairSync和FairSync,默认非公平,可创建‌公平锁。
image

非公平锁的实现原理

第一个线程来,没有竞争。
image
image
接着第二个线程来,有竞争,尝试加锁,失败,执行上面的acquire(1),再尝试获取,若失败,会调用
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
image
image
接下来会创建一个链表,节点之间为双向,会关联线程对象,这其实一个等待队列。图里的head和tail不是节点,head指向的是头节点(head),tail指向尾节点
image
image
第3点解释,Dummy的waitStatus改为-1,那么以后有Dummy来唤醒线程1,因为线程1一直获取锁失败后会进入队列等待,需要有能唤醒的节点。如果再来一个线程2,失败了,那么节点线程1的waitStatus改为-1,有线程1唤醒线程2,这就是前驱节点唤醒后继节点。
image
park,进入队列阻塞等待
image
多个线程竞争失败
image
释放锁
image
image
此时有两种情况,
1.成功获取锁
image
image
2.新来了一个线程4(注意,不在队列的),竞争锁,线程1失败,进入重新抢锁的流程。
image

可重入原理

同一个线程,锁重入的过程
image
同一个线程释放锁的过程,入参是1,关键是锁重入次数(state)减去1。举例说明,state为5,表示重入了4次,5减了4次,最后为1表示当前线程要释放锁了。
image

可打断原理

先从1.不可打断模式开始理解

总的来说,线程被打断后,调用Thread.interrupted()被打断后会返回true(打断标记为true),表示被打断了,但是同时又将打断标记设为true,接下来又再一次进入循环,又开始获取锁,又可以park进入等待获取锁。
image
image

2.打断模式

直接抛异常,不再获取锁
image

公平锁

1.先了解下非公平锁

线程上来就直接抢锁,不会检查等待队列
image

2.公平锁

image
image

条件变量

await

image
线程0调用ConditionObject的await方法,进入等待的流程
image
接着,线程0进入队列的节点,节点状态(黄色三角)为-2,表示条件变量的等待,然后将线程0持有的锁全部释放(因为可能发生锁重入了),同步器的state设为0(释放了)
image
没有其他新来的线程竞争锁的话,线程1获取锁
image

signal

唤醒开始,由持有锁的线程才能唤醒,否则会抛异常
image
取条件变量等待中的第一个,此例中为线程0,尝试唤醒线程0,如果成功进入等待队列抢锁,也有可能失败,因为线程0可能被打断,也有可能超时等待不再获取锁了,此时会再找下一个在条件变量的节点(线程)
image
成功的情况
image

读写锁

ReentrantReadWriteLock

读-写、写-读,这俩操作加锁,读-读不用加锁。默认非公平,通过构造函数传入 true,即可创建‌公平锁。由于读读不加锁,体现了共享锁的思想。
image

读-读示例

读操作获取读锁,模拟读时长1秒,可以看到2个线程几乎能同时获取锁,进行读取,1秒后都释放锁,可以看到,不互斥。
image
image

读-写示例

w.lock()要获取写锁,要等1秒后,读锁释放。
image
image
image

写-读示例

先写,等写完再读

@Slf4j(topic = "c.test")
public class Test41 {
    public static void main(String[] args) throws InterruptedException {
        ReadWriteLock lock=new ReadWriteLock();
        new Thread(()->{
            log.debug("启动");
            lock.write();
        },"t1").start();

        Thread.sleep(100);

        new Thread(()->{
            log.debug("启动");
            lock.read();
        },"t2").start();
    }
}

@Slf4j(topic = "c.test")
class ReadWriteLock{

    private Object data;
    private ReentrantReadWriteLock rw=new ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock r = rw.readLock();
    private ReentrantReadWriteLock.WriteLock w = rw.writeLock();

    public Object read(){
        r.lock();
        try {
            log.debug("开始读");

            log.debug("读完");
        }finally {
            r.unlock();
        }
        return data;
    }
    public void write(){
        w.lock();
        try {
            log.debug("开始写");
            Thread.sleep(2000);
            log.debug("写完");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            w.unlock();
        }
    }
}

输出:

10:55:26.538 [t1] c.test - 启动
10:55:26.539 [t1] c.test - 开始写
10:55:26.649 [t2] c.test - 启动
10:55:28.544 [t1] c.test - 写完
10:55:28.544 [t2] c.test - 开始读
10:55:28.544 [t2] c.test - 读完

注意事项

读锁不支持条件变量,写锁支持
同一个线程不能持有读锁的情况下,再获取写锁。
image
支持降级,体现了锁重入。
image

示例代码说明降级升级:

class CachedData {
    //缓存的数据
   Object data;
   //缓存是否有效,true有效
   volatile boolean cacheValid;
   final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

   void processCachedData() {
    //获取读锁
     rwl.readLock().lock();
     //情况1.缓存失效
     if (!cacheValid) {
        // 在获取写锁之前必须释放读锁,因为不支持锁升级
        rwl.readLock().unlock();
        rwl.writeLock().lock();
        try {
        // 重新检查状态,因为另一个线程可能在我们之前已经获取了写锁并更改了状态。举个例子,
        //线程1执行完rwl.readLock().unlock(),准备执行rwl.writeLock().lock(),线程2就执行了
        //rwl.writeLock().lock(),并且改了数据,缓存失效,cacheValid为false
        //线程2改完,线程1也要改,他改他的,我改我的
          if (!cacheValid) {
            data = ...
            cacheValid = true;
          }
          // 通过在释放写锁之前获取读锁来进行锁降级,这样能让其他线程读取缓存
          rwl.readLock().lock();
        } finally {
          rwl.writeLock().unlock(); // 释放写锁,仍然持有读锁
        }
     }
     //情况2.缓存有效,使用或处理数据
     try {
       use(data);
     } finally {
       rwl.readLock().unlock();
     }
   }
 }

读写锁应用之缓存

读写锁的原理

stampedlock

介绍

image

演示

write,写方法,加锁写,返回时间戳,写完后,解锁,再将时间戳传入,时间戳已改
image
read,读方法,readTime只是为了测试用,tryOptimisticRead读取锁的时间戳;
举例说明:
情况1: 线程1和线程2同时读取时间戳,接下来验证戳有没有改变,没有改变,直接返回数据。
情况2: 线程1和线程2同时读取时间戳,线程3写操作,改了时间戳,线程1、2接下来验证戳有没有改变,改变,readLock加上读锁,返回最新的数据。
image

Semaphore

image

代码演示

可创建是否公平
image
如果不使用信号量,10个线程同时执行,10个线程同时启动运行,1秒后都结束。
image
image
使用信号量,限制为至多3个线程同时运行,acquire方法要放在线程的函数方法内,才能起作用。
image
3个线程运行后,其他的才能运行
image

原理

假设最多允许3个线程,state为3,state就是之前写的AQS同步器里的变量state
image
acquire原理
image
release原理
image

Semaphore应用之改进连接池

CountDownLatch

介绍,倒计锁
image
示例代码,下面的代码含义,倒数计数值为3,主线程调用latch.await方法,主线程等待,会卡在这一行,等三个线程执行完。一个线程执行后将倒数计数减1,三个线程执行完后,倒数计数为0,主线程结束等待。

@Slf4j(topic = "c.test")
public class Test43 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch =new CountDownLatch(3);
        log.debug("wait");
        new Thread(() -> {
            log.debug("running");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            latch.countDown();
            log.debug("end");
        },"t1").start();

        new Thread(() -> {
            log.debug("running");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            latch.countDown();
            log.debug("end");
        },"t2").start();

        new Thread(() -> {
            log.debug("running");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            latch.countDown();
            log.debug("end");
        },"t3").start();
        latch.await();
        log.debug("end wait");
    }
}
如果加多一个线程t4(执行了6秒),倒计数还是3,那么t1、t2、t3个线程执行完,就结束等待,不会等t4。
        new Thread(() -> {
            log.debug("running");
            try {
                Thread.sleep(6000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            latch.countDown();
            log.debug("end");
        },"t4").start();
如果有2个线程,倒计数还是3,那么主线程就不会结束,因为计数最后为1,不为0
下面是错误用法,代码一开始就减去1,会导致线程没执行完,主线程就结束等待。必须线程执行完才减去1

image

在线程池中使用CountDownLatch

4个线程,线程1、2、3运行,线程4等待前面3个线程运行结束

@Slf4j(topic = "c.test")
public class Test44 {

    public static void main(String[] args) {
        CountDownLatch latch =new CountDownLatch(3);
        ExecutorService pool = Executors.newFixedThreadPool(4);
        pool.submit(() -> {
            log.debug("running");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            latch.countDown();
            log.debug("end");
        });
        pool.submit(() -> {
            log.debug("running");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            latch.countDown();
            log.debug("end");
        });
        pool.submit(() -> {
            log.debug("running");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            latch.countDown();
            log.debug("end");
        });
        //第4个线程等待前面的3个线程结束
        pool.submit(() -> {
            try {
                log.debug("wait");
                latch.await();
                log.debug("end wait");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        pool.shutdown();
    }
}

输出如下:

17:37:32.109 [pool-1-thread-1] c.test - running
17:37:32.109 [pool-1-thread-3] c.test - running
17:37:32.109 [pool-1-thread-2] c.test - running
17:37:32.109 [pool-1-thread-4] c.test - wait
17:37:33.112 [pool-1-thread-1] c.test - end
17:37:34.126 [pool-1-thread-3] c.test - end
17:37:35.114 [pool-1-thread-2] c.test - end
17:37:35.114 [pool-1-thread-4] c.test - end wait
posted @ 2026-02-12 11:47  dvdhellohaha  阅读(10)  评论(0)    收藏  举报