Java基础之并发篇(二)

1、前言

本篇主要基于Java基础之并发篇(一)继续梳理java中关于并发相关的基础只是。本篇基于网络整理,和自己编辑。在不断的完善补充哦。

2、synchronized 的原理是什么?

synchronized是 Java 内置的关键字,它提供了一种独占的加锁方式。

synchronized的获取和释放锁由JVM实现,用户不需要显示的释放锁,非常方便。然而,synchronized 也有一定的局限性。当线程尝试获取锁的时候,如果获取不到锁会一直阻塞。

如果获取锁的线程进入休眠或者阻塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待。

2.1、当一个线程进入某个对象的一个 synchronized 的实例方法后,其它线程是否可进入此对象的其它方法?

  • 如果其他方法没有 synchronized 的话,其他线程是可以进入的。
  • 所以要开放一个线程安全的对象时,得保证每个方法都是线程安全的。

3、同步方法和同步块,哪个是更好的选择?

同步块是更好的选择,因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。

同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁。

4、在监视器(Monitor)内部,是如何做线程同步的?

监视器和锁在 Java 虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码

5、Java 如何实现“自旋”(spin)

代码如下:

public class SpinLock {

    private AtomicReference<Thread> sign =new AtomicReference<>();

    public void lock() { // <1>
        Thread current = Thread.currentThread();
        while(!sign .compareAndSet(null, current)) {
            // <1.1>
        }
    }

    public void unlock () { // <2>
        Thread current = Thread.currentThread();
        sign .compareAndSet(current, null);
    }

}

  • <1> 处,#lock() 方法,如果获得不到锁,就会“死循环”,直到或得到锁为止。考虑到“死循环”会持续占用 CPU ,可能导致其它线程无法获得到 CPU 执行,可以在 <1.1> 处增加 Thread.yiead() 代码段,出让下 CPU 。
  • <2> 处,#unlock() 方法,释放锁。

6、volatile 实现原理

6.1、volatile 有什么用?

volatile 保证内存可见性和禁止指令重排。

同时,volatile 可以提供部分原子性。

简单来说,volatile 用于多线程环境下的单次操作(单次读或者单次写)。

6.2、volatile 变量和 atomic 变量有什么不同?

  • volatile 变量,可以确保先行关系,即写操作会发生在后续的读操作之前,但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。
  • AtomicInteger 类提供的 atomic 方法,可以让这种操作具有原子性。例如 #getAndIncrement() 方法,会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。

 6.3、可以创建 volatile 数组吗?

Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。

同理,对于 Java POJO 类,使用 volatile 修饰,只能保证这个引用的可见性,不能保证其内部的属性。

6.4、volatile 能使得一个非原子操作变成原子操作吗?

一个典型的例子是在类中有一个 long 类型的成员变量。如果你知道该成员变量会被多个线程访问,如计数器、价格等,你最好是将其设置为 volatile 。为什么?因为 Java 中读取 long 类型变量不是原子的,需要分成两步,如果一个线程正在修改该 long 变量的值,另一个线程可能只能看到该值的一半(前 32 位)。但是对一个 volatile 型的 long 或 double 变量的读写是原子。

6.5、volatile 类型变量提供什么保证?

volatile 主要有两方面的作用:

  1. 避免指令重排
  2. 可见性保证

例如,JVM 或者 JIT 为了获得更好的性能会对语句重排序,但是 volatile 类型变量即使在没有同步块的情况下赋值也不会与其他语句重排序。

  • volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。
  • 某些情况下,volatile 还能提供原子性,如读 64 位数据类型,像 long 和 double 都不是原子的(低 32 位和高 32 位),但 volatile 类型的 double 和 long 就是原子的。不过需要在 64 位的 JVM 虚拟机上

6.6、volatile 和 synchronized 的区别?

  1. volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取。synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  2. volatile 仅能使用在变量级别。synchronized 则可以使用在变量、方法、和类级别的。
  3. volatile 仅能实现变量的修改可见性,不能保证原子性。而synchronized 则可以保证变量的修改可见性和原子性。
  4. volatile 不会造成线程的阻塞。synchronized 可能会造成线程的阻塞。
  5. volatile 标记的变量不会被编译器优化。synchronized标记的变量可以被编译器优化。

另外,会有面试官会问 volatile 能否取代 synchronized 呢?答案肯定是不能,虽然说 volatile 被称之为轻量级锁,但是和 synchronized 是有本质上的区别,原因就是上面的几点落。

 6.7、什么场景下可以使用 volatile 替换 synchronized ?

  1. 只需要保证共享资源的可见性的时候可以使用 volatile 替代,synchronized 保证可操作的原子性一致性和可见性。
  2. volatile 适用于新值不依赖于旧值的情形。
  3. 1 写 N 读。
  4. 不与其他变量构成不变性条件时候使用 volatile 。

7、什么是死锁、活锁?

死锁,是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

7.1、产生死锁的必要条件:

  • 互斥条件:所谓互斥就是进程在某一时间内独占资源。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

7.2、死锁的解决方法:

  • 撤消陷于死锁的全部进程。
  • 逐个撤消陷于死锁的进程,直到死锁不存在。
  • 从陷于死锁的进程中逐个强迫放弃所占用的资源,直至死锁消失。
  • 从另外一些进程那里强行剥夺足够数量的资源分配给死锁进程,以解除死锁状态。

7.3、什么是活锁?

活锁,任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。

7.4、死锁与活锁的区别?

活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”,而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。

实际上,死锁就是悲观锁可能产生的结果,而活锁是乐观锁可能产生的结果。

8、什么是悲观锁、乐观锁?

8.1、悲观锁

悲观锁,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

  • 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
  • 再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。

8.2、乐观锁

乐观锁,顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。

  • 像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。

    例如,version 字段(比较跟上一次的版本号,如果一样则更新,如果失败则要重复读-比较-写的操作)

  • 在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

8.3、乐观锁的实现方式:

  • 使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。
  • Java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

9、Java AQS

java.util.concurrent.locks.AbstractQueuedSynchronizer 抽象类,简称 AQS ,是一个用于构建锁和同步容器的同步器。事实上concurrent 包内许多类都是基于 AQS 构建。例如 ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,等。AQS 解决了在实现同步容器时设计的大量细节问题。

AQS 使用一个 FIFO 的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态 waitStatus 。

10、什么是 Java Lock 接口?

java.util.concurrent.locks.Lock 接口,比 synchronized 提供更具拓展行的锁操作。它允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。它的优势有:

  • 可以使锁更公平。
  • 可以使线程在等待锁的时候响应中断。
  • 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间。
  • 可以在不同的范围,以不同的顺序获取和释放锁。

11、什么是可重入锁(ReentrantLock)?

举例来说明锁的可重入性。代码如下:

public class UnReentrant{

    Lock lock = new Lock();

    public void outer() {
        lock.lock();
        inner();
        lock.unlock();
    }

    public void inner() {
        lock.lock();
        //do something
        lock.unlock();
    }

}

  • #outer() 方法中调用了 #inner() 方法,#outer() 方法先锁住了 lock ,这样 #inner() 就不能再获取 lock 。
  • 其实调用 #outer() 方法的线程已经获取了 lock 锁,但是不能在 #inner() 方法中重复利用已经获取的锁资源,这种锁即称之为不可重入。
  • 可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码块。

synchronized、ReentrantLock 都是可重入的锁,可重入锁相对来说简化了并发编程的开发。

简单来说,ReenTrantLock 的实现是一种自旋锁,通过循环调用 CAS 操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。

12、synchronized 和 ReentrantLock 异同?

12.1、相同点:

  • 都实现了多线程同步和内存可见性语义。
  • 都是可重入锁。

12.2、不同点:

        1、同步实现机制不同

synchronized 通过 Java 对象头锁标记和 Monitor 对象实现同步。

ReentrantLock 通过CAS、AQS(AbstractQueuedSynchronizer)和 LockSupport(用于阻塞和解除阻塞)实现同步。
 

        2、可见性实现机制不同

synchronized 依赖 JVM 内存模型保证包含共享变量的多线程内存可见性。

ReentrantLock 通过 ASQ 的 volatile state 保证包含共享变量的多线程内存可见性。

        3、使用方式不同

synchronized 可以修饰实例方法(锁住实例对象)、静态方法(锁住类对象)、代码块(显示指定锁对象)。

ReentrantLock 显示调用 tryLock 和 lock 方法,需要在 finally 块中释放锁。

        4、功能丰富程度不同

synchronized 不可设置等待时间、不可被中断(interrupted)。

ReentrantLock 提供有限时间等候锁(设置过期时间)、可中断锁(lockInterruptibly)、condition(提供 await、condition(提供 await、signal 等方法)等丰富功能

        5、锁类型不同

synchronized 只支持非公平锁。

ReentrantLock 提供公平锁和非公平锁实现。当然,在大部分情况下,非公平锁是高效的选择。

在 synchronized 优化以前,它的性能是比 ReenTrantLock 差很多的,但是自从 synchronized 引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用 synchronized 。

并且,实际代码实战中,可能的优化场景是,通过读写分离,进一步性能的提升,所以使用 ReentrantReadWriteLock 。

13、ReadWriteLock 是什么?

ReadWriteLock ,读写锁是,用来提升并发程序性能的锁分离技术的 Lock 实现类。可以用于 “多读少写” 的场景,读写锁支持多个读操作并发执行,写操作只能由一个线程来操作。

ReadWriteLock 对向数据结构相对不频繁地写入,但是有多个任务要经常读取这个数据结构的这类情况进行了优化。ReadWriteLock 使得你可以同时有多个读取者,只要它们都不试图写入即可。如果写锁已经被其他任务持有,那么任何读取者都不能访问,直至这个写锁被释放为止。

ReadWriteLock 对程序性能的提高主要受制于如下几个因素:

  1. 数据被读取的频率与被修改的频率相比较的结果。
  2. 读取和写入的时间
  3. 有多少线程竞争
  4. 是否在多处理机器上运行

14、Condition 是什么?

在没有 Lock 之前,我们使用 synchronized 来控制同步,配合 Object 的 #wait()#notify() 等一系列方法可以实现等待 / 通知模式。在 Java SE 5 后,Java 提供了 Lock 接口,相对于 synchronized 而言,Lock 提供了条件 Condition ,对线程的等待、唤醒操作更加详细和灵活。

15、LockSupport 是什么?

LockSupport 是 JDK 中比较底层的类,用来创建锁和其他同步工具类的基本线程阻塞。

  • Java 锁和同步器框架的核心 AQS(AbstractQueuedSynchronizer),就是通过调用 LockSupport#park()和 LockSupport#unpark() 方法,来实现线程的阻塞和唤醒的。
  • LockSupport 很类似于二元信号量(只有 1 个许可证可供使用),如果这个许可还没有被占用,当前线程获取许可并继续执行;如果许可已经被占用,当前线程阻塞,等待获取许可。

17、Java 内存模型

17.1、什么是 Java 内存模型?

Java 虚拟机规范中试图定义一种 Java 内存模型(Java Memory Model,JMM)来屏蔽掉各层硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。

Java 内存模型规定了所有的变量都存储在主内存(Main Memory)中。每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间的变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的关系如下图:

010edc234449f0d54d70e0a40f2f3aa4.png

线程、主内存、工作内存

18、两个线程之间是如何通信的呢?

线程之间的通信方式,目前有共享内存和消息传递两种。

18.1、共享内存

在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。典型的共享内存通信方式,就是通过共享对象进行通信。

6e281e1b2b1b48f587421266cc13162a.png

例如上图线程 A 与 线程 B 之间如果要通信的话,那么就必须经历下面两个步骤:

  1. 首先,线程 A 把本地内存 A 更新过得共享变量刷新到主内存中去。
  2. 然后,线程 B 到主内存中去读取线程 A 之前更新过的共享变量。

18.2、消息传递

在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。在 Java 中典型的消息传递方式,就是 #wait() 和 #notify() ,或者 BlockingQueue 。

931360d6c045031a00158b4a8512c285.png

19、为什么代码会重排序?

在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:

  • 在单线程环境下不能改变程序运行的结果。
  • 存在数据依赖关系的不允许重排序

需要注意的是:重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义

20、什么是内存屏障?

内存屏障,又称内存栅栏,是一组处理器指令,用于实现对内存操作的顺序限制。

20.1、内存屏障为何重要?

对主存的一次访问一般花费硬件的数百次时钟周期。处理器通过缓存(caching)能够从数量级上降低内存延迟的成本这些缓存为了性能重新排列待定内存操作的顺序。也就是说,程序的读写操作不一定会按照它要求处理器的顺序执行。当数据是不可变的,同时/或者数据限制在线程范围内,这些优化是无害的。如果把这些优化与对称多处理(symmetric multi-processing)和共享可变状态(shared mutable state)结合,那么就是一场噩梦。

当基于共享可变状态的内存操作被重新排序时,程序可能行为不定。一个线程写入的数据可能被其他线程可见,原因是数据写入的顺序不一致。适当的放置内存屏障,通过强制处理器顺序执行待定的内存操作来避免这个问题。

21、Java 并发容器

21.1、什么是并发容器的实现?

何为同步容器?可以简单地理解为通过 synchronized来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。

  • 比如 Vector,Hashtable,以及 Collections#synchronizedSet()Collections#synchronizedList() 等方法返回的容器。
  • 可以通过查看 Vector,Hashtable 等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字 synchronized 。

并发容器,使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性。

  • 例如在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁。在这种锁机制下,允许任意数量的读线程并发地访问 map ,并且执行读操作的线程和写操作的线程也可以并发的访问 map ,同时允许一定数量的写操作线程并发地修改 map ,所以它可以在并发环境下实现更高的吞吐量。
  • 再例如,CopyOnWriteArrayList 。

22、SynchronizedMap 和 ConcurrentHashMap 有什么区别?

22.1、SynchronizedMap

一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为 map 。

22.2、ConcurrentHashMap

使用分段锁来保证在多线程下的性能。ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将 hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提升是显而易见的。【注意,这块是 JDK7 的实现。在 JDK8 中,具体的实现已经改变】

另外 ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中,当 iterator 被创建后集合再发生改变就不再是抛出 ConcurrentModificationException 异常,取而代之的是在改变时 new 新的数据从而不影响原有的数据,iterator 完成后再将头指针替换为新的数据 ,这样 iterator 线程可以使用原来老的数据,而写线程也可以并发的完成改变。

23、Java 中 ConcurrentHashMap 的并发度是什么?

在 JDK8 前,ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16 ,这样在多线程情况下就能避免争用。

在 JDK8 后,它摒弃了 Segment(锁段)的概念,而是启用了一种全新的方式实现,利用 CAS 算法。同时加入了更多的辅助变量来提高并发度,具体内容还是查看源码吧。

24、ConcurrentHashMap 为何读不用加锁?

24.1、在 JDK7 以及以前

  • HashEntry 中的 keyhashnext 均为 final 型,只能表头插入、删除结点。
    • HashEntry 类的 value 域被声明为 volatile 型。
    • 不允许用 null 作为键和值,当读线程读到某个 HashEntry 的 value 域的值为 null 时,便知道产生了冲突——发生了重排序现象(put 方法设置新 value 对象的字节码指令重排序),需要加锁后重新读入这个 value 值。
  • volatile 变量 count 协调读写线程之间的内存可见性,写操作后修改 count ,读操作先读 count,根据 happen-before 传递性原则写操作的修改读操作能够看到。

24.1、在 JDK8 开始

  • Node 的 val 和 next 均为 volatile 型。
  • #tabAt(..,) 和 #casTabAt(...) 对应的 Unsafe 操作实现了 volatile 语义。

25、CopyOnWriteArrayList 可以用于什么应用场景?

CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModificationException 异常。在 CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。

  • 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 ygc 或者 fgc 。
  • 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然 CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求。

CopyOnWriteArrayList 透露的思想:

  • 读写分离,读和写分开
  • 最终一致性
  • 使用另外开辟空间的思路,来解决并发冲突

CopyOnWriteArrayList 适用于读操作远远多于写操作的场景。例如,缓存。

26、Java 阻塞队列

26.1、什么是阻塞队列?有什么适用场景?

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:

  • 在队列为空时,获取元素的线程会等待队列变为非空。
  • 当队列满时,存储元素的线程会等待队列可用。

阻塞队列常用于生产者和消费者的场景:

  • 生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程
  • 阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

BlockingQueue 接口,是 Queue 的子接口,它的主要用途并不是作为容器,而是作为线程同步的的工具,因此他具有一个很明显的特性:

  • 当生产者线程试图向 BlockingQueue 放入元素时,如果队列已满,则线程被阻塞。
  • 当消费者线程试图从中取出一个元素时,如果队列为空,则该线程会被阻塞。
  • 正是因为它所具有这个特性,所以在程序中多个线程交替向BlockingQueue中 放入元素,取出元素,它可以很好的控制线程之间的通信。

阻塞队列使用最经典的场景,就是 Socket 客户端数据的读取和解析:

  • 读取数据的线程不断将数据放入队列。
  • 然后,解析线程不断从队列取数据解析。

26.2、Java 提供了哪些阻塞队列的实现?

JDK7 提供了 7 个阻塞队列。分别是:

Java5 之前实现同步存取时,可以使用普通的一个集合,然后在使用线程的协作和线程同步可以实现生产者,消费者模式,主要的技术就是用好 wait、notify、notifyAll、sychronized 这些关键字。

而在 Java5 之后,可以使用阻塞队列来实现,此方式大大简少了代码量,使得多线程编程更加容易,安全方面也有保障。

ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。

此队列按照先进先出(FIFO)的原则对元素进行排序,但是默认情况下不保证线程公平的访问队列,即如果队列满了,那么被阻塞在外面的线程对队列访问的顺序是不能保证线程公平(即先阻塞,先插入)的。

LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。

此队列按照先出先进的原则对元素进行排序

PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。

DelayQueue:支持延时获取元素的无界阻塞队列,即可以指定多久才能从队列中获取当前元素。

SynchronousQueue:一个不存储元素的阻塞队列。

每一个 put 必须等待一个 take 操作,否则不能继续添加元素。并且他支持公平访问队列。

LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。

相对于其他阻塞队列,多了 tryTransfer 和 transfer 方法。

  • transfer 方法:如果当前有消费者正在等待接收元素(take 或者待时间限制的 poll 方法),transfer 可以把生产者传入的元素立刻传给消费者。如果没有消费者等待接收元素,则将元素放在队列的 tail 节点,并等到该元素被消费者消费了才返回。
  • tryTransfer 方法:用来试探生产者传入的元素能否直接传给消费者。如果没有消费者在等待,则返回 false 。和上述方法的区别是该方法无论消费者是否接收,方法立即返回。而 transfer 方法是必须等到消费者消费了才返回。

LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

优势在于多线程入队时,减少一半的竞争。​​

26.3、阻塞队列提供哪些重要方法?

方法处理方式抛出异常返回特殊值一直阻塞超时退出
插入方法add(e)offer(e)put(e)offer(e, time, unit)
移除方法remove()poll()take()poll(time, unit)
检查方法element()peek()不可用不可用

26.4、ArrayBlockingQueue 与 LinkedBlockingQueue 的区别?

Queue阻塞与否是否有界线程安全保障适用场景注意事项
ArrayBlockingQueue阻塞有界一把全局锁生产消费模型,平衡两边处理速度用于存储队列元素的存储空间是预先分配的,使用过程中内存开销较小(无须动态申请存储空间)
LinkedBlockingQueue阻塞可配置存取采用 2 把锁生产消费模型,平衡两边处理速度无界的时候注意内存溢出问题,用于存储队列元素的存储空间是在其使用过程中动态分配的,因此它可能会增加 JVM 垃圾回收的负担。

27、延迟队列的实现方式,DelayQueue 和时间轮算法的异同?

JDK 的 Timer 和 DelayQueue 插入和删除操作的平均时间复杂度为 O(nlog(n)) ,而基于时间轮可以将插入和删除操作的时间复杂度都降为 O(1) 。

28、简述 ConcurrentLinkedQueue 和 LinkedBlockingQueue 的用处和不同之处?

在 Java 多线程应用中,队列的使用率很高,多数生产消费模型的首选数据结构就是队列(先进先出)。

Java 提供的线程安全的 Queue 可以分为

1、阻塞队列,典型例子是 LinkedBlockingQueue 。

适用阻塞队列的好处:多线程操作共同的队列时不需要额外的同步,另外就是队列会自动平衡负载,即那边(生产与消费两边)处理快了就会被阻塞掉,从而减少两边的处理速度差距。

2、非阻塞队列,典型例子是 ConcurrentLinkedQueue 。

当许多线程共享访问一个公共集合时,ConcurrentLinkedQueue 是一个恰当的选择。

具体的选择,如下:

  • LinkedBlockingQueue 多用于任务队列。
    • 单生产者,单消费者
    • 多生产者,单消费者
  • ConcurrentLinkedQueue 多用于消息队列。
    • 单生产者,多消费者
    • 多生产者,多消费者

29、Java 原子操作类

29.1、什么是原子操作?

原子操作(Atomic Operation),意为”不可被中断的一个或一系列操作”。

  • 处理器使用基于对缓存加锁或总线加锁的方式,来实现多处理器之间的原子操作。
  • 在 Java 中,可以通过锁和循环 CAS 的方式来实现原子操作。CAS操作 —— Compare & Set ,或是 Compare & Swap ,现在几乎所有的 CPU 指令都支持 CAS 的原子操作。

原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。

  • int++ 并不是一个原子操作,所以当一个线程读取它的值并加 1 时,另外一个线程有可能会读到之前的值,这就会引发错误。
  • 为了解决这个问题,必须保证增加操作是原子的,在 JDK5 之前我们可以使用同步技术来做到这一点。到 JDK5 后,java.util.concurrent.atomic 包提供了 int 和 long 类型的原子包装类,它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。

java.util.concurrent 这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。

  • 原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference 。
  • 原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray 。
  • 原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater 。
  • 解决 ABA 问题的原子类:AtomicMarkableReference(通过引入一个boolean 来反映中间有没有变过),AtomicStampedReference(通过引入一个 int 来累加来反映中间有没有变过)。

30、CAS 操作有什么缺点?

30.1、ABA 问题

比如说一个线程 one 从内存位置 V 中取出 A ,这时候另一个线程 two 也从内存中取出 A ,并且 two 进行了一些操作变成了 B ,然后 two 又将 V 位置的数据变成 A ,这时候线程 one 进行 CAS 操作发现内存中仍然是 A ,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。

从 Java5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。

30.2、循环时间长开销大

对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized 。

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

当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。

31、总结

本篇文章梳理了java并发相关的基础知识,希望对你有帮助哦。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/320054.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux CentOS 7.6安装JDK详细保姆级教程

一、检查系统是否自带jdk java --version 如果有的话&#xff0c;找到对应的文件删除 第一步&#xff1a;先查看Linux自带的JDK有几个&#xff0c;用命令&#xff1a; rpm -qa | grep -i java第二步:删除JDK&#xff0c;执行命令&#xff1a; rpm -qa | grep -i java | xarg…

【AI的未来 - AI Agent系列】【MetaGPT】2. 实现自己的第一个Agent

在MetaGPT中定义的一个agent运行示例如下&#xff1a; 一个agent在启动后他会观察自己能获取到的信息&#xff0c;加入自己的记忆中下一步进行思考&#xff0c;决定下一步的行动&#xff0c;也就是从Action1&#xff0c;Action2&#xff0c;Action3中选择执行的Action决定行动…

时间序列数据的季节性检测

时间序列分析是统计学和数据科学的一个基本研究领域&#xff0c;它为理解和预测序列数据中的模式提供了一个强大的框架。特别是时间序列数据&#xff0c;它捕获连续时间间隔内的信息&#xff0c;使分析师能够揭示趋势&#xff0c;季节性模式和其他时间依赖性。在时间序列分析的…

Django教程第6章 | web开发实战-文件上传(导入文件、上传图片)

专栏系列&#xff1a;Django学习教程 导入文件 目标&#xff1a;导入部门清单excel&#xff0c;解析excel数据存储到数据库。 1.准备要导入的excel文件 2.编写模板HTML <div class"panel panel-default"><!-- Default panel contents --><div class…

第 7 章 排序算法

文章目录 7.1 排序算法的介绍7.3 算法的时间复杂度7.3.1 度量一个程序(算法)执行时间的两种方法7.3.2 时间频度7.3.3 时间复杂度7.3.4 常见的时间复杂度7.3.5 平均时间复杂度和最坏时间复杂度 7.4 算法的空间复杂度简介7.4.1 基本介绍 7.5 冒泡排序7.5.1 基本介绍7.5.2 演示冒泡…

Python——VScode安装

⼀、下载安装 [root192 ~]# rpm --import https://packages.microsoft.com/keys/microsoft.asc[root192 ~]# sh -c echo -e "[code]\nnameVisualStudio Code\nbaseurlhttps://packages.microsoft.com/yumrepos/vscode\nenabled1\ngpgcheck1\ngpgkeyhttps://packages.mi…

企业级大数据安全架构(三)修改集群节点hostname

作者&#xff1a;楼高 在后续安装FreeIPA的过程中&#xff0c;要求机器名必须包含完整的域名信息。如果之前在Ambari集群节点上的机器名不符合这个要求&#xff0c;可以按照以下步骤在Ambari上修改所有节点的机器名&#xff1a; 1.部署节点说明 本次测试是三台 ambari 节点&…

polar CTF CB链

一、题目 二、解答 1、通过jar包&#xff0c;可以看到/user路由下有反序列化操作 看到存在commons-beanutils依赖且版本为1.9.2&#xff0c;可利用CB链Getshell。 使用ysoserial项目中的CommonsBeanutils1链写一个POC&#xff0c;注意确保ysoserial项目中的pom.xml中的comm…

编程艺术之Unix哲学

Unix 哲学不算是一种正规设计方法&#xff0c;它并不打算从计算机科学的理论高度来产生理论上完美的软件。那些毫无动力、松松垮垮而且薪水微薄的程序员们&#xff0c;能在短短期限内&#xff0c;如神灵附体般开发出稳定而新颖的软件——这只不过是经理人永远的梦呓罢了。 1 Un…

[ACM题目练习] 前后手

题目1 A为了让数字总和最大&#xff0c;但是B想让数字总和最小。 题解 因为A先操作B后操作&#xff0c;所以B的策略一定是把当前剩下的数字中前1到 x 大的元素给乘上-1&#xff0c;那么A的策略是怎样的(通常这种题A没有策略&#xff0c;都是遍历所有的情况) (再接着优化&#…

如何基于 Gin 封装出属于自己 Web 框架?

思路 在基于 Gin 封装出属于自己的 Web 框架前&#xff0c;你需要先了解 Gin 的基本用法和设计理念。 然后&#xff0c;你可以通过以下步骤来封装自己的 Web 框架&#xff1a; 封装路由&#xff1a;Gin 的路由是通过 HTTP 方法和 URL 路径进行匹配的&#xff0c;你可以根据自己…

力扣 第 121 场双周赛 解题报告 | 珂学家 | 数位DP

前言 整体评价 T3, T4 都是典题 T1. 大于等于顺序前缀和的最小缺失整数 思路: 模拟 class Solution { public:int missingInteger(vector<int>& nums) {set<int> s(nums.begin(), nums.end());int acc nums[0];for (int i 1; i < nums.size(); i) {if …

Java--业务场景:在Spring项目启动时加载Java枚举类到Redis中(补充)

文章目录 前言步骤测试结果 前言 通过Java–业务场景&#xff1a;在Spring项目启动时加载Java枚举类到Redis中,我们成功将Java项目里的枚举类加载到Redis中了&#xff0c;接下来我们只需要写接口获取需要的枚举值数据就可以了&#xff0c;下面一起来编写这个接口吧。 步骤 在…

OSPF : 区域 / 为什么非骨干互访需要经过骨干

概述 OSPF系列第二篇 , 今天来围绕着区域这个概念展开写一篇博客 分区背景 先来讨论一下技术背景 , 也就是为什么要分区 ? 所有设备都在一个区域不行吗 会有什么问题呢 . 首先明确一个知识点 : 正常状态下一个区域内的所有设备的LSDB都是一样的.区域内的路由器必须为所属的…

tr seq cut sort

一. tr 对字符进行处理 tr 命令用于字符转换、替换和删除&#xff0c;主要用于删除文件中的控制符或进行字符串转换等。 ① 转换 格式&#xff1a; tr 当前字符 需要转换成的字符 ​ ​ 将所有小写变成大写 ​ ② 压缩 格式&#xff1a; tr -s ​ ③ 删除 …

抖音小店怎么选品?分享如何培养选爆品的思维,每个人都要学会

选品定店铺生死。 一个店铺能不能出单&#xff0c;能不能赚钱&#xff0c;店铺的商品占主要部分&#xff0c;商品才是电商店铺最核心的内容&#xff0c;一个货真价实&#xff0c;物美价廉的产品才是店铺的核心竞争力&#xff0c;运营和找达人都是让产品卖的更多&#xff0c;更…

Redis未授权访问漏洞复现与工具安装

目录 一、漏洞简介 二、靶场搭建 三、漏洞检测 四、工具安装 五、远程连接 六、利用Redis写入webshell 七、redis-getShell工具 八、ssh公私钥免密登录 九、其他 一、漏洞简介 redis是一个数据库&#xff0c;默认端口是6379&#xff0c;redis默认是没有密码验证的&…

python查看安装包所依赖的包版本

python查看安装包所依赖的包版本 1. 找到包的位置 site-packages 文件夹的位置import gevent # ctrl 点进去就行了2. 返回包环境文件夹的上一层&#xff0c;会看到下面有一个 gevent-{版本号}.dist-info的文件夹3. 查看 METADATA 文件Requires-Dist: greenlet >2.0.0 ...#…

DHCP自动获取实验和DNS正向解析实验

一、服务程序 1.1DHCP定义 DHCP&#xff08;动态主机配置协议&#xff09;是一个局域网的网络协议。指的是由服务器控制一段IP地址范围&#xff0c;客户机登录服务器时就可以自动获得服务器分配的IP地址和子网掩码。默认情况下&#xff0c;DHCP作为Windows Server的一个服务组…

spring基于XML方式的组件管理

基本介绍 依赖注入是一种处理对象间依赖关系的技术。在Spring中&#xff0c;依赖注入有构造方法注入和设值注入两种方式。 设值注入是将依赖作为成员变量&#xff0c;通过主调类的setter方法注入依赖。构造方法注入则是在Bean的构造方法中注入依赖。 本次我们将通过具体例子来…