并发编程之常用锁与AQS

目录

一、常用锁(除Synchronized)

LongAddr

ReentrantLock

CountDownLatch

CyclicBarrier

Phaser

ReadWriteLock

Semaphore

Exchanger

LockSupport

二、AQS

三、ThreadLocal


一、常用锁(除Synchronized)

LongAddr

        首先声明LongAddr不为锁,他是一个原子操作类,类似于AtomicLong。通过上文我们可以知道count++这种操作它不是一个原子性操作,如果想让它变成原子性操作,我们可以通过加锁的方式事项。除了加锁以外我们也可以通过原子类实现,例如AtomicLong的increamentAndGet()方法或者LongAddr的increment()方法。

        那么这两个原子类有什么区别的呢。AtomicLong和LongAddr底层是通过CAS自旋锁的方式实现,但是AtomicLong内部只有一个锁,而LongAddr中是分段锁,就好像它的内部会将数据分为一个数组,针对数组中的每个元素进行加锁,最后将数组整体相加。

        通过下面的一段简单的程序,做一个比较:

static long count = 0L;
    static AtomicLong countAtomic = new AtomicLong(0L);
    static LongAdder countLongAddr = new LongAdder();

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[2000];

        Object lock = new Object();
        for (int i = 0; i < threads.length; i++){
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 50000; j++){
                    synchronized (lock){
                        count++;
                    }
                }
            });
        }
        Long start = System.currentTimeMillis();
        for (Thread thread : threads) thread.start();
        for (Thread thread : threads) thread.join();
        System.out.println("synchronized耗时:"+(System.currentTimeMillis()-start));

        for (int i = 0; i < threads.length; i++){
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 50000; j++){
                    countAtomic.incrementAndGet();
                }
            });
        }
        start = System.currentTimeMillis();
        for (Thread thread : threads) thread.start();
        for (Thread thread : threads) thread.join();
        System.out.println("AtomicLong耗时:"+(System.currentTimeMillis()-start));

        for (int i = 0; i < threads.length; i++){
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 50000; j++){
                    countLongAddr.increment();
                }
            });
        }
        start = System.currentTimeMillis();
        for (Thread thread : threads) thread.start();
        for (Thread thread : threads) thread.join();
        System.out.println("LongAddr耗时:"+(System.currentTimeMillis()-start));
    }

下面是他们的耗时信息

synchronized耗时:4550
AtomicLong耗时:1636
LongAddr耗时:244

通过比较发现,他们的耗时Synchronized>Atomic>LongAddr,但是当线程数量比较少或者每个线程循环次数比较少时,Atomic还和LongAddr的效率并不一定谁高,需要进行进一步的比较。

ReentrantLock

        先看下面一段代码

ReentrantLock lock = new ReentrantLock();
    void first(){
        try {
            lock.lock();
            for (int i = 0; i < 5; i++){
                System.out.println(i);
                if (i == 3){
                    second();
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
           lock.unlock();
        }
    }
    void second(){
        try {
            lock.lock();
            System.out.println("second");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        D_Reentrant reentrant = new D_Reentrant();
        reentrant.first();
    }

看一下执行输出:

0
1
2
3
second
4

        通过以上执行结果,可以看到,ReentrantLock为可重入锁。Synchronized也是可重入锁,那么ReentrantLock的优势在那里呢。因为它更加灵活,有一些更丰富的功能,看下面的代码片段:

// 创建公平锁
ReentrantLock reentrantLock = new ReentrantLock(true);
// 尝试获取锁
Boolean lockFlag = reentrantLock.tryLock();
// 在5秒内尝试获取锁
lockFlag = reentrantLock.tryLock(5, TimeUnit.SECONDS);
// 可以被打断的加锁,当其它线程调用interrupt方法的时候,抛出异常,并且释放锁
reentrantLock.lockInterruptibly();

CountDownLatch

        它能对线程进行阻塞,在创建的时候传入一个值,当这个值不为0的时候,进行阻塞,当线程为零的时候放行,内部的线程安全通过CAS实现,具体看如下代码:

// 创建CountDownLatch,并且入参为10
        CountDownLatch countDownLatch = new CountDownLatch(10);
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i++){
            threads[i] = new Thread(() -> {
                System.out.println("thread start");
                // 对countDownLatch中的值减1,通过cas来保证线程安全
                countDownLatch.countDown();
            });
        }
        for (Thread thread : threads){
            thread.start();
        }
        // 进行阻塞,当countDownLatch中的值为0的时候放行
        countDownLatch.await();
        System.out.println("end");

输出结果:

thread start
thread start
thread start
thread start
thread start
thread start
thread start
thread start
thread start
thread start
end

CyclicBarrier

        它和CountDownLatch正好相反,CountDownLatch是计数--,当减到0以后,放行;而CyclicBarrier是计数++,当加到指定值以后,放行。CyclicBarrier是可以循环使用,不需要重新设置值的。具体使用看下面的代码:

// 创建CyclicBarrier,并且指定当阻拦的线程数量为25是执行指定的runnable
CyclicBarrier cyclicBarrier = new CyclicBarrier(25, new Runnable() {
    @Override
    public void run() {
        System.out.println("桌满,开席");
    }
});
for (int i = 0; i < 100; i++){
    new Thread(()->{
        try {
            // 线程进行阻塞
            cyclicBarrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
    }).start();
}

按照上面的解释,没阻塞满25个线程执行一次打印,并且可以循环利用,那么上面的代码会打印4次,具体打印结果如下:

桌满,开席
桌满,开席
桌满,开席
桌满,开席

Phaser

        按照阶段的不同,来控制线程的执行。可以把它理解成一个带阶段的CyclicBarrier,每达到一个阶段去做不同的事情。在实际项目中使用的不是很多,它是在1.7以后才出现的类,通过下面的代码对它来做一个了解即可:

    /**
     * 自定义自己的Phaser,然后定义各个阶段,
     * 需要注意阶段是从0开始
     */
    static class MarryPhaser extends Phaser{
        @Override
        protected boolean onAdvance(int phase, int registeredParties) {
            switch (phase) {
                case 0:
                    System.out.println("所有人到齐了!"+registeredParties+"个人");
                    return false;
                case 1:
                    System.out.println("所有人吃完了!"+registeredParties+"个人");
                    return false;
                case 2:
                    System.out.println("所有人离开了!"+registeredParties+"个人");
                    return false;
                case 3:
                    System.out.println("入洞房!"+registeredParties+"个人");
                    return true;
                default:
                    return true;
            }

        }
    }

    static MarryPhaser marryPhaser = new MarryPhaser();

    public static void waitSleep(int number) {
        try {
            Thread.sleep(number);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    static Random random = new Random();
    static class Person extends Thread {
        private String name;
        public Person(String name){
            this.name = name;
        }
        public void arrive(){
            waitSleep(random.nextInt(1000));
            // 第一阶段,所有人必须都到达以后才能去进行下一步
            marryPhaser.arriveAndAwaitAdvance();
        }
        public void eat(){
            waitSleep(random.nextInt(1000));
            // 第二阶段,所有人必读都吃完以后才能进行下一步
            marryPhaser.arriveAndAwaitAdvance();
        }
        public void leave(){
            waitSleep(random.nextInt(1000));
            // 第三阶段,所有人必须都离开以后才能进行下一步
            marryPhaser.arriveAndAwaitAdvance();
        }
        public void sleep(){
            if ("新郎".equals(name) || "新娘".equals(name)){
                waitSleep(random.nextInt(1000));
                // 第四阶段,新郎和新娘睡觉就是入洞房,结束
                marryPhaser.arriveAndAwaitAdvance();
            }else {
                // 第四阶段不需要除新郎和新娘以外的人参与
                marryPhaser.arriveAndDeregister();
            }
        }
        @Override
        public void run(){
            arrive();
            eat();
            leave();
            sleep();
        }
    }

    public static void main(String[] args) {
        marryPhaser.bulkRegister(7);
        for (int i = 0; i < 5; i++){
            new Thread(new Person(i+"person")).start();
        }
        new Thread(new Person("新郎")).start();
        new Thread(new Person("新娘")).start();
    }

看一下输出结果:

所有人到齐了!7个人
所有人吃完了!7个人
所有人离开了!7个人
入洞房!2个人

ReadWriteLock

        读写锁,这种锁针对读和写进行不同锁处理方式,读锁时共享锁,写锁是排他锁。看下面一段代码:

    public static void deal(Lock lock, String deal){
        try {
            lock.lock();
            System.out.println(deal);
            Thread.sleep(1000);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }


    public static void main(String[] args) {
        // 创建ReadWriteLock
        ReadWriteLock lock = new ReentrantReadWriteLock();
        // 获取读锁
        Lock readLock = lock.readLock();
        // 获取写锁
        Lock writeLock = lock.writeLock();

        Runnable readR = () -> deal(readLock, "read");
        Runnable writeR = () -> deal(writeLock, "write");
        for (int i = 0; i < 10; i++) new Thread(readR).start();
        for (int i = 0; i < 2; i++) new Thread(writeR).start();
    }

如果读锁和写锁都是排它锁,那么它会执行将近12s左右,但是运行完上述代码你回发现read几乎是同时完成,而write是一条一条完成。通过上述代码可以证明,读锁是共享锁,写锁是排它锁。

Semaphore

        在创建Semaphore的时候会指定一个信号量,这个信号量代表的就是允许多少个线程同时执行,它可以起到一个限流的作用。看下面这段代码:

        // 创建Semaphore对象
        Semaphore semaphore = new Semaphore(2);
        new Thread(() -> {
            try {
                // 获取信号量
                semaphore.acquire();
                System.out.println("first start");
                Thread.sleep(1000);
                System.out.println("first end");
                // 释放信号量
                semaphore.release();
            }catch (Exception e){
                e.printStackTrace();
            }

        }).start();
        new Thread(() -> {
            try {
                // 获取信号量
                semaphore.acquire();
                System.out.println("second start");
                Thread.sleep(1000);
                System.out.println("second end");
                // 释放信号量
                semaphore.release();
            }catch (Exception e){
                e.printStackTrace();
            }

        }).start();
        new Thread(() -> {
            try {
                // 获取信号量
                semaphore.acquire();
                System.out.println("third start");
                Thread.sleep(1000);
                System.out.println("third end");
                // 释放信号量
                semaphore.release();
            }catch (Exception e){
                e.printStackTrace();
            }

        }).start();

下面为输出结果:

first start
second start
first end
second end
third start
third end

从输出结果可以看出,由于信号量为2,因此两个线程同时运行,当其中一个线程释放信号量以后,另一个线程得到信号量,然后继续输出。

Exchanger

        Exchanger能够让线程之间彼此通信,交换信息,需要注意的是线程数量要求为两个。看下面这段代码:

        // 创建Exchanger对象
        Exchanger<String> exchanger = new Exchanger<>();
        new Thread(() -> {
            try {
                // 与另外一个线程进行通信,将本线程的T1交换给另一个线程
                String str = exchanger.exchange("T1");
                System.out.println("T1线程"+str);
            }catch (Exception e){
                e.printStackTrace();
            }

        }).start();
        new Thread(() -> {
            try {
                // 与另外一个线程进行通信,将本线程的T2交换给另一个线程
                String str = exchanger.exchange("T2");
                System.out.println("T2线程"+str);
            }catch (Exception e){
                e.printStackTrace();
            }

        }).start();

下面看一下输出结果:

T2线程T1
T1线程T2

通过上面的输出结果可以看到,两个线程发生了信息 交换,分别打印了在 自己线程中交换的信息。

LockSupport

        它可以在不使用锁的前提之下,暂停线程。并且它可以随时阻塞线程以及唤醒线程,通过下面这段代码对它有一个更好的了解,下面这段代码实现了1A2B3C4D这样的交叉打印:


    static Thread t1 = null;
    static Thread t2 = null;
    public static void main(String[] args) {
        char[] letterArray = "ABCDEEF".toCharArray();
        char[] numberArray = "1234567".toCharArray();

        t1 = new Thread(() -> {
          for (char number : numberArray){
              System.out.println(number);
              // 恢复t2线程
              LockSupport.unpark(t2);
              // 暂停t1线程
              LockSupport.park();
          }
        });
        t2 = new Thread(() -> {
            for (char letter : letterArray){
                // 暂停t2线程
                LockSupport.park();
                System.out.println(letter);
                // 恢复t1线程
                LockSupport.unpark(t1);
            }
        });
        t1.start();
        t2.start();

    }

这种交叉打印也可以通过wait和notify实现,需要注意的是wait会释放锁,notify不会释放锁,看下面的代码:

        char[] letterArray = "ABCDEF".toCharArray();
        char[] numberArray = "123456".toCharArray();
        Object object = new Object();
        new Thread(() -> {
            synchronized (object){
                for (char number : numberArray){
                    try {
                        System.out.println(number);
                        // 通知另一个线程启动
                        object.notify();
                        // 当前线程阻塞,并释放锁
                        object.wait();
                    }catch (Exception e){
                        e.printStackTrace();
                    }

                }
                // 防止线程未结束
                object.notify();
            }

        }).start();
        new Thread(() -> {
            synchronized (object){
                for (char letter : letterArray){
                    try {
                        System.out.println(letter);
                        // 通知另一个线程启动
                        object.notify();
                        // 当前线程阻塞,并释放锁
                        object.wait();
                    }catch (Exception e){
                        e.printStackTrace();
                    }

                }
                // 防止线程未结束
                object.notify();
            }

        }).start();

通过这种交叉打印的方式,补充一个ReentrantLock的知识点condition,看如下代码:

        // 创建ReentrantLock锁
        ReentrantLock reentrantLock = new ReentrantLock();
        // 创建线程1的condition
        Condition first = reentrantLock.newCondition();
        // 创建线程2的condition
        Condition second = reentrantLock.newCondition();
        char[] letterArray = "ABCDEF".toCharArray();
        char[] numberArray = "123456".toCharArray();

        new Thread(() -> {
            try {
                reentrantLock.lock();
                for(char number : numberArray){
                    System.out.println(number);
                    // 唤醒线程2
                    second.signal();
                    // 线程1等待并释放锁
                    first.await();
                }
                // 最终唤醒线程2,用于线程2运行结束
                second.signal();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                reentrantLock.unlock();
            }

        }).start();
        new Thread(() -> {
            try {
                reentrantLock.lock();
                for(char letter : letterArray){
                    System.out.println(letter);
                    // 唤醒线程1
                    first.signal();
                    // 阻塞线程2并释放锁
                    second.await();
                }
                // 最终唤醒线程1,用于线程1运行结束
                first.signal();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                reentrantLock.unlock();
            }
        }).start();

二、AQS

        AQS(AbstractQueuedSynchronizer),很多锁的内部都是通过AQS来实现的,例如上面的CountDownLatch,ReentrantLock等。AQS主要有两部分组成,一个是代表锁状态的state,一个存放线程信息双向链表,这个集合的每个元素为Node,包含线程的相关信息。大致结构如下:

        再介绍AQS之前,先看一下ReentrantLock的lock()方法的源码:

    // ReentrantLock的lock方法,调用了sync的lock方法
    public void lock() {
        sync.lock();
    }

    // ReentrantLock的匿名内部类Sync继承了AbstractQueuedSynchronizer类,也就是AQS
    abstract static class Sync extends AbstractQueuedSynchronizer{...}

    // ReentrantLock的匿名内部类NonfairSync继承了Sync,根据继承的可传递性,也继承了AQS
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        
        final void lock() {
            // 调用AQS的cas方法进行加锁,如果加锁成功,则进行加锁处理
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 如果调用AQS的cas方法加锁失败,则调用AQS的acquire方法
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
    // AQS的设置state的方法,也就是获取锁
    protected final boolean compareAndSetState(int expect, int update) {
        // 调用unsafe的cas方法
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
    
    
    // AQS的acquire的方法
    public final void acquire(int arg) {
        // if条件里面第一步判断是否能获取到锁,因为使用的短路且,如果能获取到锁,则条件判断结束
        // 如果没有获取到锁,则尝试将线程放入队列中
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    // AQS的tryAcquire方法实现这里采用了模板的设计模式,由子类去进行具体的实现
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    // AQS的addWaiter方法,通过CAS的方式往AQS的队列中的尾部添加新的线程节点
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
    // AQS的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);
        }
    }
    // ReentrantLock的匿名内部类NonfairSync的tryAcquire方法
    protected final boolean tryAcquire(int acquires) {
        // 调用ReentrantLock的匿名内部类Sync的nonfairTryAcquire方法
        return nonfairTryAcquire(acquires);
    }
    
    // ReentrantLock的匿名内部类Sync的nonfairTryAcquire方法
    final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            // 获取AQS的当前锁状态
            int c = getState();
            // 如果当前锁没被使用
            if (c == 0) {
                // 调用AQS的cas方法上锁,如果上锁成功,则放回成功
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 如果当前锁被占用,并且占用线程未当前线程,则岁state进行+1处理
            // 也是通过这种方式实现可重入
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

其实在上面已经对AQS的源码做了一部分的解读,接下来再看一下他的state和双向链表:

// 通过state来标识锁状态,0为无锁    
private volatile int state;
// 双向链表的头结点
private transient volatile Node tail;
// 双向链表的尾结点
private transient volatile Node head;
// AQS的内部类Node
static final class Node {
      
        static final Node SHARED = new Node();
        
        static final Node EXCLUSIVE = null;

        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;

        // 当前线程的等待状态
        volatile int waitStatus;

        // 当前节点的前节点
        volatile Node prev;

        // 当前节点的尾结点
        volatile Node next;

        // 当前节点的线程信息
        volatile Thread thread;

        /**
         * Link to next node waiting on condition, or the special
         * value SHARED.  Because condition queues are accessed only
         * when holding in exclusive mode, we just need a simple
         * linked queue to hold nodes while they are waiting on
         * conditions. They are then transferred to the queue to
         * re-acquire. And because conditions can only be exclusive,
         * we save a field by using special value to indicate shared
         * mode.
         */
        Node nextWaiter;

        /**
         * Returns true if node is waiting in shared mode.
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        // 获取当前节点的前节点
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

在jdk1.9以后添加了Varhandle类型用来表示指向某个变量的引用,如下图:

 这个类型的作用在于,他可以将普通对象进行原子操作,例如compareAndSet()方法和getAndAdd()方法。它是通过C实现的,可以理解为直接操作二进制码。

三、ThreadLocal

        ThreadLocal它是线程私有,放入ThreadLocal的对象只对当前对象可见,通过下面这段代码进一步了解:

// 创建threadLocal对象
    static ThreadLocal<Product> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                // 线程1沉睡两秒
                Thread.sleep(2000);
            }catch (Exception e){
                e.printStackTrace();
            }
            // 然后输出threadLocal中的对象
            System.out.println(threadLocal.get());
        }).start();
        new Thread(() -> {
            try {
                Thread.sleep(1000);
            }catch (Exception e){
                e.printStackTrace();
            }
            // 线程2沉睡一秒以后往threadlocal中存放对象
            threadLocal.set(new Product());
        }).start();
    }
    static class Product{
        private String name = "product";
    }

如果ThreadLocal对象不是线程私有的话,那么线程1应该输出代用“product”值的对象,那么我们看一下输出结果:

null

输出结果为null,证明ThreadLocal是线程私有,看一下源码,可以对它有一个更好的了解:

    // ThreadLocal的set方法
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            // 如果map不为null,则将值存放到map中
            map.set(this, value);
        else
            // 如果map为null,则创建map
            createMap(t, value);
    }

    // ThreadLocal的getMap方法,获取的是当前线程的THreadLocalMap(它是Thread的内部类)
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    // ThreadLocal的createMap方法
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    

        在前文的JVM垃圾回收中我们可以知道ThreadLocal中的ThreadLocalMap实现是通过弱引用来实现的,但是弱引用只针对entry中的key,但是value还存在,所以在使用结束以后,在finally中执行一下remove()方法,防止出现oom。

        在我们的开发中它有一个很重要的作用,就是spring中的事物,如果同一个事务中有多个数据库操作,如果这些操作使用的数据库链接不同,那么是没有办法进行管理的。这时候就用到了ThreadLocal,将第一个获取到的数据库链接放入当前线程的ThreadLocal中,当前线程的其它数据库操作都从ThreadLocal中拿去数据库链接,这样就能保证事务的正常运行。

猜你喜欢

转载自blog.csdn.net/weixin_38612401/article/details/124051573