Java锁机制

本文详细介绍了Java的锁机制,包括原始的synchronized关键字、CAS无锁算法及其在JUC并发包中的应用。文章讨论了锁的发展,强调了避免用户态与内核态切换和优化读业务的重要性。同时,文章详细分析了AQS(AbstractQueuedSynchronizer)的工作原理,以及JUC包中的CountDownLatch、CyclicBarrier、Semaphore和Exchanger等并发工具类的使用和应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.什么是锁

在并发情况下多个线程会对同一资源进行抢夺从而导致数据不一致性的问题,很多编程语言都会引入锁机制,锁是一种抽象的概念,目的是对资源进行锁定从而达到线程同步的目的

2.原始锁synchronized

为什么把synchronized叫原始锁,因为它是Java一开始就提供的关键字,它是建立在JVM指令的层命来实现的,我知道很多人也在用lock锁,lock锁是在JDK5后由Doug Lea大师伴随着concurrent包引入的,synchronized的底层实现关键点在于monitor监视及配合monitorenter和monitorexit两条字节码指令,任何编程语言都是建立在操作系统的层面上的,Java作为更高级语言也不例外,Java线程其实是对操作系统底层线程的映射,monitor最终也会调用操作系统的mutexlock指令,所以这样的加锁操作要进行操作系统用户态和内核态的切换,我觉得这是锁机制发展历程的核心,虽然在JDK6之后优化引入了无锁、偏向锁、轻量级锁、重量级锁,当然这四种状态你也可以在对象头里的MarkWord里清楚看到它们的标志为信息等等,锁只能不断升级不能降级。
在最初的无锁状态即没有对资源进行锁定,所有线程都可以访问到资源,这样就会有两种情况:1.资源不会出现在多线程的情况或者即使出现在多线程的情况下也不会形成竞争,这样就无需担心同步问题;2.资源会被竞争,但是不想对资源进行锁定,原因就是我上面说的,可以通过一些类如CAS的机制来控制线程//无锁到偏向锁再研究研究
然后变为偏向锁,偏向锁顾名思义即偏向于某一线程的锁,因为HotSpot作者发现大部分情况下,加锁的代码不仅仅不存在多线程竞争,而且总是由同一个线程多次获得,所以引入偏向锁,偏向锁的实现依赖于线程ID,在MarkWord中也可以发现,根据锁标志位是否为01以及是否偏向位为1即可确定为偏向锁,通过确认线程ID确认线程是不是老顾客来把锁直接交出去,不需要通过底层mutex lock以及CAS来获取锁,显然这样是十分高效的,当确定线程ID不符合从而确定当前环境下线程竞争激烈从而去升级为轻量级锁;
与偏向锁不同的是,偏向锁只需要通过线程ID就可以实现线程和锁的绑定,而偏向锁绑定实现在于自己虚拟机栈中的owner指针去指向对象,成功获取偏向锁的线程就可以进入同步代码块,其他线程进行自旋等待,自旋同样也是高效方式的体现,如果对象的锁很快就会释放的话,不断自选轮询的某个线程就会去直接获取锁,而如果是以被操作系统挂起阻塞的方式还需要进行系统中断和现场恢复,自旋其实相当于CPU空转,长时间自选其实也不是什么好事情,根据自选等待的线程以及自选时间系统会将轻量级锁升级为种量级锁;
种量级锁即最初始的没有优化前的需要通过monitor调用底层同步源语的方式,或者说其实前三种方式都不算是真正意义上的加锁,而种量级锁才是真正的加锁

3.完美的CAS

synchronized虽然经过了优化,在线程竞争激烈的情况下这不还是很快就会演变为种量级锁,这不还是老样子,这也是为什么常言道的syn锁在线程竞争不是很激烈的情况下效率高,而lock锁在线程竞争激烈的情况下效率高。回到最初的问题,我认为锁机制的发展还是关键在于解决两个点:1.不要调用操作系统底层源语从而导致用户态和内核态的切换;2.在如今大量的读业务情况下,并且还可能同步代码块里微妙的代码执行时间远小于线程间切换的时间;
如果可以不去调用操作系统底层源语避免用户态和内核态切换的开销,并且不用去锁定资源就可以实现线程的同步,这样的方式一定是最高效的,显然CAS就是这样;
CAS(compare and swap比较并交换),它是一种思想或者说是一种算法,CAS包含三个操作数 —— 内存位置(V)、原预期值(A)、新值(B)。
其实逻辑上很简单,思想跟锁一样,线程如果进入资源就去改变资源的状态,内存V作为资源的原状态值,线程用跟资源状原态值一模一样的预期值A来判断资源是否有别的线程在操作,如果没有就进入资源并用自己的新值B替换掉A,别的线程就无法用预期值A再次判断出内存V,无法进入并且自旋不断地重试CAS
简单点说就是,当更新变量时,只有当变量的预期值A和内存地址V当中的值相同时,才会将内存地址V对应的值修改为B
思想还是跟加锁相似,但实现上CAS是在操作系统层面上,而且我们常用的X86,ARM架构的CPU也都提供了指令级别的CAS原子操作,即不需要通过操作系统的同步原语,CPU可以原生的支持CAS,这样从根本上就可以不再依赖锁来进行线程同步,万丈高楼平地起,计算机世界也是如此,底层开放了新特性,那么上层就会有更多的编程花样,有了CPU原生指令级的CAS支持,就会促进出很多无锁编程的思路,相比于syn锁的悲观锁,用CAS进行无锁编程或者也叫乐观锁会更加高效
在JAVA中,CAS操作依赖于Unsafe类中的方法,Unsafe类存在于sun.misc包中,其内部方法可以像C的指针一样直接操作内存,从名称看来就可以知道该类是非安全的,毕竟Unsafe拥有着类似于C的指针操作,Unsafe类中的所有方法都是native修饰的,也就是Unsafe类中的方法都直接调用操作系统底层资源执行相应任务,关于Unsafe类的主要功能点如下:

4.JUC并发包

在有了CAS的基础上,在JDK5后util包下继而引入了concurrent包,主要由并发大师Doug Lea完成,通常所说的concurrent包基本有3个package组成
java.util.concurrent:提供大部分关于并发的接口和类,如BlockingQueue,Callable,ConcurrentHashMap,ExecutorService, Semaphore等
java.util.concurrent.atomic:提供所有原子操作的类, 如AtomicInteger, AtomicLong等;
java.util.concurrent.locks:提供锁相关的类, 如Lock, ReentrantLock, ReadWriteLock, Condition等;
在这里插入图片描述

5.AQS

在Java已经提供了CAS能力的基础上,如何去利用它去开发一个通用的去对竞争资源同步的框架是一件很重要的事情,那么这样的框架设计思路是怎样的?
首先,谈到框架一定具有通用性,在实现底层同步机制的同时需要开放一定的空间去给上层进行业务逻辑的编写
其次,利用CAS的原子性,将内存V作为状态标记位,以此来阻碍其他线程的调用
最后,在没有获取到资源被阻碍的线程中有两种场景,一是有的线程只是想去快速尝试获取一下共享资源而已,获取不到也没关系它会去做其他的业务处理;二是有的线程必须去获取执行共享资源才能进行下一步处理,必须一直等待;针对第一种场景直接利用CAS去返回true或者false即可,针对第二种场景也可以去给予返回值,然后让它去进行轮询,但是轮询相当于CPU空转,高并发的场景下CPU负载会更加严重,降低系统性能,并且让上层业务去主动处理也会增加上层业务开发的难度这不是我们想要的目的,因而可以设计一个队列将这些线程排队,待共享资源空闲下来队列里的线程就可以依次去获取
显然AQS就是这样的,JUC中的很多工具以及现在许多主流的开源中间件都使用了AQS,AbstractQueuedSynchronizer,队列同步框架。

AQS成员属性:

    /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
    private transient volatile Node head;

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     */
    private transient volatile Node tail;

    /**
     * The synchronization state.
     */
    private volatile int state;//判断共享资源是否被占用的标记位

AQS成员变量很简单,三个属性,vloatile修饰的state属性来确保线程的可见性,这里的state是用int修饰而不是boolean,因为线程获取锁的有两种模式,独占和共享,在共享模式下,线程获取锁后其他以共享模式获取锁的线程也可以去获取锁并且可以去增强锁,从而一起访问共享资源,因而int可以去代表当前占用资源的线程数量,其余两个属性头节点和尾节点维护了一个Node类型的先进先出(FIFO)的双向链表,队列中的节点同样有独占和共享两种模式

Node的具体结构:

static final class Node {
        // 共享模式下等待的标记
        static final Node SHARED = new Node();
        // 独占模式下等待的标记
        static final Node EXCLUSIVE = null;

         // 线程的等待状态 表示线程已经被取消
        static final int CANCELLED =  1;
        // 线程的等待状态 表示后继线程需要被唤醒
        static final int SIGNAL    = -1;
        // 线程的等待状态 表示线程在Condtion上
        static final int CONDITION = -2;
        // 表示下一个acquireShared需要无条件的传播
        static final int PROPAGATE = -3;

        //线程在队列中的等待状态 
        volatile int waitStatus;

        //当前线程节点的前继节点  
        volatile Node prev;

        //当前线程节点的后继节点
        volatile Node next;

        //线程对象
        volatile Thread thread;

        //该属性用于条件队列或者共享锁 
        Node nextWaiter;
        
        //......

就成员属性而言重要的属性大致分为4类:thread线程对象本身、waitStatus线程等待状态、前后节点、4中状态值
如何利用state状态和FIFO的队列来操作管理线程,这些操作都被封装为了方法,按场景来分类,第一种是尝试获取锁,如果不成功直接返回结果即可不想去等待,第二种即愿意去加入队列中等待
其中第一种场景对应tryAcquire方法,第二种场景对应acquire方法

tryAcquire方法:

    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

源码中的tryAcquire是一个被protected修饰,int作为参数,返回boolea的方法,boolea返回值即代表是否成功获取锁,参数值int代表对state状态的修改,此方法的实现只有1行是抛出一个异常,因而该方法的意图很明显,AQS只是一个框架,它需要一个继承类用户自己去重写实现该方法并且加入自己的业务逻辑,否则就直接抛出异常,例如:

public class Syncer extends AbstractQueuedSynchronizer {
    @Override
    protected boolean tryAcquire(int arg) {
        //上层业务逻辑,比如
        if(arg != 1)
            return false;
        if(getState() == 1)
            return false;
        return compareAndSetState(0,1);
    }
}

外层调用Syncer即可

acquire方法:

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

acquire方法的修饰符是public final,即所有的继承类都可以直接去调用并且不允许继承类擅自去重写,即此方法一定可以得到锁
if判断条件中包含两个条件:!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
如果tryAcquire获得锁则 !tryAcquire为false,啪的一下很快啊就跳出整个 if 了,直接执行selfInterrupt方法,如果tryAcquire没有获得锁,则会去执行后面的acquireQueued(Node,arg)方法进行排队等待锁,而其中的Node参数嵌套了addWaiter(Node.EXCLUSIVE)方法

addWaiter方法:

addWaiter方法即将当前线程封装为Node加入到等待队列中

    private Node addWaiter(Node mode) {
         //创建出新的Node节点
        Node node = new Node(Thread.currentThread(), mode);
        //绑定到尾节点后
        Node pred = tail;
        //在尾节点不为空的情况下通过CAS将当前节点置为尾节点
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                //再将前置节点的后置绑定到当前节点
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

如果程序没有进入第一个if块,也就是尾节点为空或第一次CAS失败,则会进入完整的入队方法enq

enq方法:

    private Node enq(final Node node) {
        for (;;) { //无限循环即自旋CAS直到把节点插入为止
            Node t = tail;
            if (t == null) { 
                //获取尾节点为null直接初始化,再此循环进来进入else
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                //CAS入队操作
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

因而在之前只用addWaiter尝试快速入队,在不成功的情况下用enq强制入队,但是AQS并不是类似于生产者消费者模型有消费者不断的从队列中获取节点

acquireQueued方法:

该方法配合relase方法可以对线程进行挂起和响应

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;//局部变量是否失败,直到return才会变为false
        try {
            boolean interrupted = false;//是否中断
            for (;;) {//自旋
                final Node p = node.predecessor();
                //当Node前驱节点是头并且tryAcquire成功了
                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);
        }
    }

在AQS中头节点其实是一个虚节点,充当一个摆设,第二个节点才是真正要去拿锁的节点,当第二个节点拿到锁之后它就会变为头节点,然后头节点出队,shouldParkAfterFailedAcquire和parkAndCheckInterrupt两个方法用来让当前节点找到一个合适的地方开始等待
shouldParkAfterFailedAcquire方法返回true则代表当前节点需要被挂起,parkAndCheckInterrupt则执行真正的挂起

shouldParkAfterFailedAcquire方法:

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;//得到前驱节点的状态
        if (ws == Node.SIGNAL)//如果前驱节点的状态也为signal说明也在等待获取锁
            return true;//即返回true挂起休息
        if (ws > 0) {//大于0前驱节点状态只能是CANCEL
            do {
                node.prev = pred = pred.prev;//将其从队列中删除
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {//如果是其他状态
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//通过CAS将前置节点置为SIGNAL
        }
        return false;//返回false进行下一轮的判断
    }

parkAndCheckInterrupt方法:

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);//调用操作系统原语挂起线程
        return Thread.interrupted();//返回当前线程的中断标识位并将其复位为false
    }

因而整个线程获取资源的步骤大致如下:
1.通过acquire方法去获取资源,acquire底层通过tryAcquire去直接获取资源,程过直接返回
2.失败则进入addWaiter方法再配合enq方法将线程加入队列尾部,并标记为独占模式
3.并且执行acquireQueued方法去判断线程是否处于第二个节点位置上,如果是就不断去CAS获取锁
4.否则进入shouldPark方法来将线程挂起

待资源被释放后就需要去唤醒被挂起的线程

release方法:

    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }
    
    public final boolean release(int arg) {
        if (tryRelease(arg)) {//尝试释放锁成功
            Node h = head;//获取到当前队列头节点
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

与tryAcquire一样tryRealease一样是开放给上层自行去实现的方法,如果当前线程tryRelease尝试释放锁成功,就需要唤醒其他线程,此时将头节点作为参数传入unparkSuccessor方法

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);
    }

而在acquiredQueued方法会完美的处理等待线程并将其挂起,最终被置为头节点的一定是获取到共享资源处理完毕后才会被置为头节点

6.JUC包下的工具类:

CountDownLatch

CountDownLatch类可以使一个线程等待其他线程各自执行完毕后再执行。
CountDownLatch在初始化时需要指定一个count参数,主要有两个方法:

//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };   
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  
//将count值减1
public void countDown() { };  

因而只要在合适的位置调用await和countDown方法,CountDownLatch既可以让多个线程等待单个线程执行结束后执行,也可以让单个线程等待多个线程执行结束后执行
比如:

CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 0; i < 5; i++) {
    new Thread(() -> {
        try {
            //准备完毕……运动员都阻塞在这,等待号令
            countDownLatch.await();
            String parter = "【" + Thread.currentThread().getName() + "】";
            System.out.println(parter + "开始执行……");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}

Thread.sleep(2000);// 裁判准备发令
countDownLatch.countDown();// 发令枪:执行发令

运行结果:

【Thread-0】开始执行……
【Thread-1】开始执行……
【Thread-4】开始执行……
【Thread-3】开始执行……
【Thread-2】开始执行……
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
    final int index = i;
    new Thread(() -> {
        try {
            Thread.sleep(1000 + ThreadLocalRandom.current().nextInt(1000));
            System.out.println("finish" + index + Thread.currentThread().getName());
            countDownLatch.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}

countDownLatch.await();// 主线程在阻塞,当计数器==0,就唤醒主线程往下执行。
System.out.println("主线程:在所有任务运行完成后,进行结果汇总");

运行结果:

finish4Thread-4
finish1Thread-1
finish2Thread-2
finish3Thread-3
finish0Thread-0
主线程:在所有任务运行完成后,进行结果汇总

源码分析:

public class CountDownLatch {

    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        Sync(int count) {
            setState(count);
        }

        int getCount() {
            return getState();
        }

        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }

    private final Sync sync;


    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

     while waiting
 
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

  
    public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }


    public void countDown() {
        sync.releaseShared(1);
    }


    public long getCount() {
        return sync.getCount();
    }


    public String toString() {
        return super.toString() + "[Count = " + sync.getCount() + "]";
    }
}

//原理还是随后再搞吧。。。。。。

CyclicBarrier

CyclicBarrier字面意思“循环栅栏”,CyclicBarrier做的事情是等待,比如在生活中某个活动需要等人全部到齐了才能开始,线程也一样,一个线程组的线程需要等待所有线程完成任务后再继续执行下一次任务
其构造方法如下:

//parties 是参与线程的个数
public CyclicBarrier(int parties)
// Runnable 参数的意思是最后一个到达线程要做的任务
public CyclicBarrier(int parties, Runnable barrierAction)

其重要的方法为await( ) ,表示线程已经到达了栅栏, 与CountDownLatch不同的是Cyclicbarrier是做加法,CountDownLatch是做减法
示例如下:

public class CyclicBarrierDemo {

    static class TaskThread extends Thread {
        
        CyclicBarrier barrier;
        
        public TaskThread(CyclicBarrier barrier) {
            this.barrier = barrier;
        }
        
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
                System.out.println(getName() + " 到达栅栏 A");
                barrier.await();
                System.out.println(getName() + " 冲破栅栏 A");
                
                Thread.sleep(2000);
                System.out.println(getName() + " 到达栅栏 B");
                barrier.await();
                System.out.println(getName() + " 冲破栅栏 B");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    public static void main(String[] args) {
        int threadNum = 5;
        CyclicBarrier barrier = new CyclicBarrier(threadNum, new Runnable() {
            
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " 完成最后任务");
            }
        });
        
        for(int i = 0; i < threadNum; i++) {
            new TaskThread(barrier).start();
        }
    }
    
}

运行结果:

Thread-1 到达栅栏 A
Thread-3 到达栅栏 A
Thread-0 到达栅栏 A
Thread-4 到达栅栏 A
Thread-2 到达栅栏 A
Thread-2 完成最后任务
Thread-2 冲破栅栏 A
Thread-1 冲破栅栏 A
Thread-3 冲破栅栏 A
Thread-4 冲破栅栏 A
Thread-0 冲破栅栏 A
Thread-4 到达栅栏 B
Thread-0 到达栅栏 B
Thread-3 到达栅栏 B
Thread-2 到达栅栏 B
Thread-1 到达栅栏 B
Thread-1 完成最后任务
Thread-1 冲破栅栏 B
Thread-0 冲破栅栏 B
Thread-4 冲破栅栏 B
Thread-2 冲破栅栏 B
Thread-3 冲破栅栏 B

Semaphore

Semaphore即信号量,与操作系统中的信号量一样,控制访问特定资源的线程数目。
构造方法:

//permits 表示许可线程的数量
public Semaphore(int permits)
//fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程
public Semaphore(int permits, boolean fair)

其重要方法主要有两个:

//表示阻塞并获取许可
public void acquire() throws InterruptedException
//表示释放许可
public void release()

举个例子:现在停车场上有3个停车位,但是有6辆汽车,要求这些停车位一旦有空闲的,汽车就要过去停车。这个有点类似于PV操作对临界区资源的访问

public class SemaphoreDemo {

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);	//设置3个停车位
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();	//获取停车位资源
                    System.out.println(Thread.currentThread().getName() + "\t抢占到了车位");	
                    TimeUnit.SECONDS.sleep(5);	//休眠5秒
                    System.out.println(Thread.currentThread().getName() + "\t离开到了车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release();	//释放停车位资源
                }
            },String.valueOf(i)).start();

        }
    }
}

在这里插入图片描述
原理:
在信号量上我们定义两种操作:
acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。
release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。
信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

Exchanger

Exchanger用于两个线程之间进行数据交换,简单说就是一个线程在完成一定的事务后想与另一个线程交换数据,则第一个先拿出数据的线程会一直等待第二个线程,直到第二个线程拿着数据到来时才能彼此交换对应数据。每个线程调用exchage方法到达各自的同步点,当且仅当两个线程都达到同步点的时候,才可以交换信息,否则先到达同步点的线程必须等待。

Exchanger 泛型类型,其中 V 表示可交换的数据类型,对外提供的接口很简单,具体如下:

Exchanger():无参构造方法。

V exchange(V v):等待另一个线程到达此交换点(除非当前线程被中断),然后将给定的对象传送给该线程,并接收该线程的对象。

V exchange(V v, long timeout, TimeUnit unit):等待另一个线程到达此交换点(除非当前线程被中断或超出了指定的等待时间),然后将给定的对象传送给该线程,并接收该线程的对象。

可以看出,当一个线程到达 exchange 调用点时,如果其他线程此前已经调用了此方法,则其他线程会被调度唤醒并与之进行对象交换,然后各自返回;如果其他线程还没到达交换点,则当前线程会被挂起,直至其他线程到达才会完成交换并正常返回,或者当前线程被中断或超时返回。

 public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<>();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                String method1 = "method1";
                log.info("线程1到达同步点...");
                try {
                    String strFromThread2 = exchanger.exchange(method1);
                    log.info("线程1获取的交换数据是{}", strFromThread2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("线程1运行结束");
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                String method2 = "method2";
                try {
                    log.info("线程2开始执行...");
                    // 让线程1先达到同步点
                    TimeUnit.SECONDS.sleep(3);
                    log.info("线程2达到同步点...");
                    String strFromThread1 = exchanger.exchange(method2);
                    log.info("线程2获取的交换数据是{}", strFromThread1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("线程2运行结束");
            }
        });

        thread1.start();
        thread2.start();
    }
03:02:43.733 [Thread-0] INFO com.example.concurrent.ExchangerTest - 线程1到达同步点...
03:02:43.733 [Thread-1] INFO com.example.concurrent.ExchangerTest - 线程2开始执行...
03:02:46.740 [Thread-1] INFO com.example.concurrent.ExchangerTest - 线程2达到同步点...
03:02:46.740 [Thread-1] INFO com.example.concurrent.ExchangerTest - 线程2获取的交换数据是method1
03:02:46.740 [Thread-0] INFO com.example.concurrent.ExchangerTest - 线程1获取的交换数据是method2
03:02:46.749 [Thread-1] INFO com.example.concurrent.ExchangerTest - 线程2运行结束
03:02:46.749 [Thread-0] INFO com.example.concurrent.ExchangerTest - 线程1运行结束

7.并发集合

HashMap与CourrentHashMap单独整理出来写了一篇,这里只简单说一下CopyOnWriterArrayList
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
其add方法源码如下:

private transient volatile Object[] array;//原集合数组用volatile修饰
public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            //赋值出新的集合数组
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

重点容器维护的数组使用volatile修饰的,保证及时可见性!
读取的源码:
读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的

public E get(int index) {
    return get(getArray(), index);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值