侧边栏壁纸
博主头像
lmg博主等级

  • 累计撰写 55 篇文章
  • 累计创建 6 个标签
  • 累计收到 2 条评论
标签搜索

Java并发2

lmg
lmg
2020-03-25 / 0 评论 / 0 点赞 / 561 阅读 / 11,519 字
温馨提示:
本文最后更新于 2022-04-16,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

Java中的锁

乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

两种锁的使用场景

各有优缺点,乐观锁适用于写比较少的情况下(多读场景),多写的场景下用悲观锁就比较合适。

乐观锁常见的两种实现方式

乐观锁一般会使用版本号机制或CAS算法实现。

1.CAS算法

compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

  • V表示要更新的变量
  • E表示预期的值
  • N表示新值

当且仅当 V 的值等于 E时,CAS通过原子方式用新值N来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试

2.版本号机制

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

乐观锁的缺点

ABA 问题是乐观锁一个常见的问题

1 ABA 问题

如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。你大爷还是你大爷,你大妈已经不是你大妈了

对于ABA问题,比较有效的方案是引入版本号

2 循环时间长开销大

自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。

设置自旋CAS的时间域值

3 只能保证一个共享变量的原子操作

CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

Atomic 原子类介绍

Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

所以,所谓原子类说简单点就是具有原子/原子操作特征的类。

基本数据类型原子类的优势

通过一个简单例子带大家看一下基本数据类型原子类的优势

①多线程环境不使用原子类保证线程安全(基本数据类型)

class Test {
        private volatile int count = 0;
        //若要线程安全执行执行count++,需要加锁
        public synchronized void increment() {
                  count++; 
        }

        public int getCount() {
                  return count;
        }
}

②多线程环境使用原子类保证线程安全(基本数据类型)

class Test2 {
        private AtomicInteger count = new AtomicInteger();

        public void increment() {
                  count.incrementAndGet();
        }
      //使用AtomicInteger之后,不需要加锁,也可以实现线程安全。
       public int getCount() {
                return count.get();
        }
}

AtomicInteger 线程安全原理简单分析

AtomicInteger 类的部分源码:

    // setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。

synchronized与Lock

Java中有两种加锁的方式:一种是用synchronized关键字,另一种是用Lock接口的实现类。

形象地说,synchronized关键字是自动档,可以满足一切日常驾驶需求。但是如果你想要玩漂移或者各种骚操作,就需要手动档了——各种Lock的实现类。

synchronized关键字

synchronized关键字主要的三种使用方式:

  • synchronized修饰非静态方法(普通方法)时,锁住的是对象的实例,即this对象

  • synchronized修饰静态方法时,也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)

所以如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁

  • synchronized修饰代码块,锁住的是在括号里面的对象。

总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!(会带来很不好的后果,如synchronized(a)和synchronized(b),可能a和b指向的是同一个对象。)

synchronized 关键字底层原理属于 JVM 层面。

① synchronized 同步语句块的情况

synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

② synchronized 修饰方法的的情况

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

synchronized锁升级:偏向锁 → 轻量级锁 → 重量级锁

偏向锁:

初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。**由于之前没有释放锁,这里也就不需要重新加锁。**如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。

轻量级锁:

轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。

重量级锁

如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起。

自旋锁(spin lock)与互斥量(mutex)的比较

  • 自旋锁是一种非阻塞锁,也就是说,如果某线程需要获取自旋锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取自旋锁。

  • 互斥量是阻塞锁,当某线程无法获取互斥量时,该线程会被直接挂起,该线程不再消耗CPU时间,当其他线程释放互斥量后,操作系统会激活那个被挂起的线程,让其投入运行。

互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间(次数)不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定,虚拟机变得越来越“聪明”了

锁清除

锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间(没必要就不要加锁嘛)。

锁粗化

把关联性强的锁操作合并成一个,避免频繁获取锁和释放锁,反而影响性能。

减少锁粒度

它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争。它的思想也是用空间来换时间;java中很多数据结构都是采用这种方法提高并发操作的效率:如ConcurrentHashMap

Synchronized的实现

它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

  1. Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
  2. Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;
  3. Wait Set:哪些调用wait方法被阻塞的线程被放置在这里;
  4. OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;
  5. Owner:当前已经获取到所资源的线程被称为Owner;
  6. !Owner:当前释放锁的线程。

Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。

ReentrantLock

ReentrantLock继承了Lock接口,是一个可重入的独占锁。

需要显示获取和释放锁。

支持公平锁和非公平锁的实现(先来先服务)。

不但提供了synchronized对锁的操作功能,还提供了诸如响应中断,可轮询锁,定时锁等避免多线程死锁。

谈谈 synchronized和ReentrantLock 的区别

① 两者都是可重入锁

支持一个线程对同一个资源执行多次加锁操作。

② synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API

③ ReentrantLock 比 synchronized 增加了一些高级功能

如可以实现公平锁,而synchronized是非公平锁;还可以定时锁,响应中断等。

volatile关键字

volatile是轻量级的synchronized,它保证了共享变量的 “可见性”:当一个线程修改一个共享变量时,另一个线程能读到这个修改的值。如果volatile使用得恰当,它比synchronized成本更低,因为它不会引起线程上下文的切换。

原理:修改volatile修饰的变量时。处理器会干两件事

1.)将当前处理器缓存行的数据写回到系统内存。

2.)这个写回内存的操作会使其它CPU里缓存了该内存地址的数据无效。要重新从系统内存读取到缓存。

并发编程的三个重要特性

  1. 原子性 : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。synchronized和Lock可以保证代码片段的原子性。(由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。)
  2. 可见性 :当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile 关键字可以保证共享变量的可见性。
  3. 有序性 :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。

在Java中,对基本数据类型的变量的读取赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

x = 10; //赋值操作,直接将数值10写入到工作内存,是原子性操作
y = x;  //实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,不是原子性操作
x++;   //语句3
x = x + 1;  //语句4 包括3个操作:读取x的值,进行加1操作,写入新的值。

说说 synchronized 关键字和 volatile 关键字的区别

volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。

volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。

多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞

volatile在一定程度上保证有序性:在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。但前面和后面那块可能无序。

使用条件

您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

  • 对变量的写操作不依赖于当前值。
  • 该变量没有包含在具有其他变量的不变式中。

实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。

volatile的适用场景

不适用:第一个条件的限制使 volatile 变量不能用作线程安全计数器。虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由(读取-修改-写入)操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性。实现正确的操作需要使x 的值在操作期间保持不变,而 volatile 变量无法实现这点。

适用:比如实现一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。

ThreadLocal

ThreadLocal简介

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK中提供的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

ThreadLocal<String> localName = new ThreadLocal();
localName.set("小狼");
String name = localName.get();

在线程1中初始化了一个ThreadLocal对象localName,并通过set方法,保存了一个值小狼,同时在线程1中通过localName.get()可以拿到之前设置的值,但是如果在线程2中,拿到的将是一个null。

如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

原理:每个Thread中都具备一个ThreadLocalMap(ThreadLocalMap并没有实现Map接口,而是自己用数组"实现"了一个Map),而ThreadLocalMap可以存储以ThreadLocal对象(如上,localName)为key ,Object 对象为 value(如上,小狼)的键值对。

内存泄露

ThreadLocal可能导致内存泄漏,为什么? 先看看Entry的实现:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

通过之前的分析已经知道,当使用ThreadLocal保存一个value时,会在ThreadLocalMap中的数组插入一个Entry对象,按理说key-value都应该以强引用保存在Entry对象中,但在ThreadLocalMap的实现中,key被保存到了WeakReference(弱引用)对象中。

这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

ps: ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

解决

既然已经发现有内存泄露的隐患,自然有应对的策略,在调用ThreadLocal的get()、set()可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的value就没有GC Roots可达了,下次GC的时候就可以被回收,当然如果调用remove方法,肯定会删除对应的Entry对象。

如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。

ThreadLocal<String> localName = new ThreadLocal();
try {
    localName.set("小狼");
    // 其它业务逻辑
} finally {
    localName.remove();
}

AQS

AbstractQueuedSynchronizer,抽象队列同步器

AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。

给大家画一个图,看一下ReentrantLock和AQS之间的关系。

我们看上图,说白了,ReentrantLock内部包含了一个AQS对象,也就是AbstractQueuedSynchronizer类型的对象。这个AQS对象就是ReentrantLock可以实现加锁和释放锁的关键性的核心组件。

AQS 原理概览

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

好了,现在如果有一个线程过来尝试用ReentrantLock的lock()方法进行加锁,会发生什么事情?

很简单,这个AQS对象内部有一个核心的变量叫做state,是int类型的,代表了加锁的状态

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

初始状态下,这个state的值是0。

另外,这个AQS内部还有一个关键变量,用来记录当前加锁的是哪个线程,初始化状态下,这个变量是null。

接着线程跑过来调用ReentrantLock的lock()方法尝试进行加锁,这个加锁的过程,直接就是用CAS操作将state值从0变为1。如果成功,会设置state=1,代表已加锁。

大家看明白了那个state变量之后,就知道了如何进行可重入加锁!

其实每次线程1可重入加锁一次,会判断一下当前加锁线程就是自己,那么他自己就可以可重入多次加锁,每次加锁就是把state的值给累加1,别的没啥变化。

AQS维护一个同步队列,线程获取同步状态失败后,CAS加入队列尾部,然后CAS自旋前驱是否是头节点。移出队列的条件是:前驱节点是头节点成功获取了同步状态。

头节点是获取同步状态成功的节点,首节点的线程在释放同步状态后,将会唤醒后继节点。如果当前节点的前驱为头节点,尝试获取同步状态。

加入队列尾部的过程必须要保证线程安全(CAS),防止多个线程同时加入出现问题。

AQS定义两种资源共享方式

  • Exclusive

    (独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:

    • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
    • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
  • Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。

ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。只有一个线程可以写。

AQS 组件总结

  • Semaphore(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
  • CountDownLatch (倒计时器): CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。

CountDownLatch

CountDownLatch是一个同步工具类,用来协调多个线程之间的同步.

某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为new CountDownLatch(n),每当一个任务线程执行完毕,就将计数器减1 countdownLatch.countDown(),当计数器的值变为0时,在CountDownLatch上await()的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。

CyclicBarrier

字面意思是可循环使用的屏障。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截线程才会继续运行。

CountDownLatch和CyclicBarrier的区别

  1. CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。
  2. CountDownLatch: 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。
    CyclicBrrier: N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。

常见面试题

1.Java的同步方式

Synchronized,ReetrantLock,volatile,Atomic原子类

0

评论区