Java基础(补充)—线程安全集合类+并发编程+ThreadLocal+线程池+内存模型

线程安全 就是:一段操纵共享数据的代码能够保证在同一时间内被多个线程执行而仍然保持其正确性的,就被称为是线程安全的。

1.主要集合类包括:

在这里插入图片描述

  • List:有序集合,允许元素重复。主要实现类有ArrayList、LinkedList和Vector等。
    ArrayList:基于动态数组实现,适合频繁的随机访问
    LinkedList:基于双向链表实现,适合频繁插入和删除
    Vector:与ArrayList类似同样维护一个数组, 但通过加synchronized来保证线程安全
    • Stack:继承自Vector也是线程安全的
  • Set:无序集合,不允许元素重复。主要实现类有HashSet、LinkedHashSet、TreeSet。
    HashSet:基于哈希表实现,不保证集合的迭代顺序
    LinkedHashSet:继承自HashSet,通过双向链表维护元素插入顺序
    TreeSet:基于红黑树实现,可以对集合中的元素进行排序,(有序
  • Map:键值对集合,一个键最多只能映射到一个值。主要实现类有HashMap、LinkHashMap、TreeMap。
    HashMap:基于哈希表实现,不保证映射顺序,由数组+链表组成的,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树
    LinkedHashMap:继承自HashMap,通过双向链表维护键值对的插入顺序
    TreeMap:基于红黑树实现,可以对键进行排序(有序
    HashTable:线程安全,不允许有 null 键和 null 值,否则会抛出异常。
  • Queue:元素有序,可重复,按特定的排队规则来确定先后顺序。
    PriorityQueue: 优先队列 Object[] 数组来实现小顶堆。
    ArrayDeque: Deque双向队列接口的实现,可扩容动态双向数组。

线程安全容器类:

在这里插入图片描述
(1)HashTable:
HashTable中的key、value都不可以为null;具有无序特性;
HashTable使用synchronized来修饰方法函数来保证线程安全,但是在多线程运行环境下效率表现非常低下。因为当一个线程访问HashTable的同步方法时,其他线程也访问同步方法就会粗线阻塞状态。比如当一个线程在添加数据时候,另外一个线程即使执行获取其他数据的操作也必须被阻塞,大大降低了程序的运行效率。
(2)ConcurrentHashMap
ConcurrentHashMap是HashMap的线程安全版
ConcurrentHashMap允许多个修改操作并发运行,其原因在于使用了锁分段技术:首先讲Map存放的数据分成一段一段的存储方式,然后给每一段数据分配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。这样就保证了每一把锁只是用于锁住一部分数据,那么当多线程访问Map里的不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率。

上述的处理机制明显区别于HashTable是给整体数据分配了一把锁的处理方法。
为此,在多线程环境下,常用ConcurrentHashMap在需要保证数据安全的场景中去替换HashMap,而不会去使用HashTable,同时在最新版的JDK中已经推荐废弃使用HashTable。
(3)CopyOnWriteArrayList
CopyOnWriteArrayList实现了List接口,提供的数据更新操作都使用了ReentrantLock的lock()方法来加锁,unlock()方法来解锁。
当增加元素的时候,首先使用Arrays.copyOf()来拷贝形成新的副本,在副本上增加元素,然后改变原引用指向副本。
读操作不需要加锁,而写操作类实现中对其进行了加锁。因此,CopyOnWriteArrayList类是一个线程安全的List接口的实现,在高并发的情况下,可以提供高性能的并发读取,并且保证读取的内容一定是正确的,这对于读操作远远多于写操作的应用非常适合(注意: 如上述更新操作会带来较大的空间与性能开销,如果更新操太过频繁,反而不太合适使用)。
CopyOnWriteArrayList类是一个线程安全的List接口的实现,在高并发的情况下,可以提供高性能的并发读取,并且保证读取的内容一定是正确的,这对于读操作远远多于写操作的应用非常适合。
(4)CopyOnWriteArraySet
锁特性同上
(5)ConcurrentLinkedQueue
ConcurrentLinkedQueue可以被看作是一个线程安全的LinkedList,使用了非阻塞算法实现的一个高效、线程安全的并发队列;
其本质是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当添加一个元素时会添加到队列的尾部;当获取一个元素时,会返回队列头部的元素。
ConcurrentLinkedQueue应该算是在高并发环境中性能最好的队列,没有之一。
(6)Vector
Vector通过数组保存数据,继承了Abstract,实现了List;所以,其本质上是一个队列。
但是和ArrayList不同,Vector中的操作是线程安全的,它是利用synchronized同步锁机制进行实现,其实现方式与HashTable类似。

(7)StringBuffer
StringBuffer是通过对方法函数进行synchronized修饰实现其线程安全特性,实现方式与HashTable、Vector类似。

ArrayList 的扩容

当添加元素时,如果元素个数+1> 当前数组长度 【size + 1 > elementData.length】时,进行扩容,扩容后的数组大小是: oldCapacity + (oldCapacity >> 1)
将旧数组内容通过Array.copyOf全部复制到新数组,此时新旧列表的size大小相同,但elementData的长度即容量不同
注意:扩容并不是严格的1.5倍,是扩容前的数组长度右移一位(一半) + 扩容前的数组长度

为什么按大约1.5倍扩容

扩容因子的大小选择,需要考虑如下情况:
扩容容量不能太小,防止频繁扩容,频繁申请内存空间 + 数组频繁复制
扩容容量不能太大,需要充分利用空间,避免浪费过多空间
为了能充分使用之前分配的内存空间,最好把增长因子设为 1<k<2.
k=1.5时,就能充分利用前面已经释放的空间。如果k >= 2,新容量刚刚好永远大于过去所有废弃的数组容量
并且充分利用移位操作(右移一位),减少浮点数或者运算时间和运算次数

2.并发编程

进程线程

【并发性是进程的基本特征】
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。例:在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。一个 Java 程序的运行是 main 线程和多个其他线程同时运行。
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

【操作系统中:进程是资源分配的基本单位,线程是被系统独立调度和分派的基本单位】

Java线程与操作系统中的线程区别

JDK 1.2 之前,Java 线程是基于绿色线程(Green Threads)实现的,这是一种用户级线程(用户线程),也就是说 JVM 自己模拟了多线程的运行,而不依赖于操作系统
在 JDK 1.2 及以后,Java 线程改为基于原生线程(Native Threads)实现,也就是说 JVM 直接使用操作系统原生的内核级线程(内核线程)来实现 Java 线程,由操作系统内核进行线程的调度和管理。

用户线程:由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用)。
内核线程:由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问)。

总之:现在的 Java 线程的本质其实就是操作系统的线程

线程与进程的关系,区别及优缺点?

一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,
但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈
总结: 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
(1)程序计数器:当前线程所执行的字节码的行号指示器,字节码解释器通过改变程序计数器来依次读取指令;在多线程的情况下,程序计数器用于记录当前线程执行的位置。所以:程序计数器私有主要是为了线程切换后能恢复到正确的执行位置

(2)虚拟机栈:每个方法在执行的同时都会在Java 虚拟机栈中创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息;【栈帧就是Java虚拟机栈中的下一个单位】

(3)本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
(4)堆和方法区:是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器**编译后的代码(字节码)**等数据。

线程创建

1.继承Thread类2.实现Runnable接口3.使用Callable和Future4.使用线程池ExecutorService

多线程 Thread、Runnable、Callable的区别

在Java中,Thread、Runnable和Callable是用于实现多线程的三种主要方式,它们在用法和功能上有一些区别。
(1)Thread:Thread是Java的一个内置类,用于表示一个线程。当我们直接继承Thread类并覆盖其run()方法时,就可以创建一个新的线程。
(2)Runnable:Runnable是一个接口,它定义了一个
run()方法
,当线程开始执行时,run()方法将被调用。
与Thread类不同,Runnable不继承Thread类,而是将Runnable接口传递给Thread构造函数
(3)Callable:Callable也是一个接口,与Runnable类似,它定义了一个call()方法,当线程开始执行时,call()方法将被调用。
与Runnable不同的是,Callable可以返回结果,并且可以抛出异常
为了获取Callable任务的结果,我们需要使用FutureTask类或者Future和ExecutorService接口

可以直接调用 Thread 类的 run 方法吗(为什么用start()启动线程,不直接调用run())

new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作
但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结:调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行

线程的生命周期和状态

在这里插入图片描述
在这里插入图片描述

(1)Thread.sleep(1000L)在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),但是,该线程不会释放锁
(2)Thread.yield()方法针对的是时间片的划分与调度,yield线程让步,会释放当前线程cpu执行权限,让步给更高优先级或者同优先级的线程,注意,此时该线程是从运行状态立刻回到就绪状态,并且不会释放资源锁
yield方法将会暂停当前正在执行的线程对象,并执行其它线程,他始终都是RUNNABLE状态,不过要注意,yield只是一种建议性的,如果调用了yield方法,对CPU时间片的分配进行了“礼让”,它仍旧有可能继续获得时间片,并且继续执行,所以一次调用yield 并不一定肯定会发生什么。
(3)join()能够保证线程的顺序问题,比如现在有 A和 B两个线程,如果B线程调用A.join方法,则必学先让A线程执行完毕,才能执行B线程,底层通过通过wait和notify实现,核心思想,保证执行顺序。
例:用jon实现,主线程调用t1.join,主线程程变为阻塞状态,同时释放锁,必须等待t1线程执行完毕的情况下,主线程才能继续执行。我们来实现下

public static void main(String[] args) {
    Thread t1 = new Thread(new ThreadDemo(),"线程一");
    Thread t2 = new Thread(new ThreadDemo(),"线程二");
    Thread t3 = new Thread(new ThreadDemo(),"线程三");
    t1.start();
    t1.join();
    t2.start();
    t2.join();
    t3.start();
    t3.join();
}

(4)wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。“直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入“就绪状态”),注意只能用于synchronized代码块/方法中
wait(long timeout)让当前线程处于“等待(阻塞)状态”,“直到其他线程调用此对象的notify()方法或 notifyAll() 方法,或者超过指定的时间量”,当前线程被唤醒(进入“就绪状态”)。
(5)notify/notifyAll:notify唤醒等待资源锁中优先级最高的线程,notifyAll唤醒所有等待的线程进入就绪状态,注意只能用于synchronized代码块/方法中;

线程的几种状态例子 这里的例子没太明白,如下:
在这里插入图片描述
主线程启动,执行t1.start(),进入t1,执行t1将wait(1000L),此时t1让出锁。在t1超时等待的同时,主线程睡眠。当后,这里主线程抢到锁,t1的状态为【TIMED_WAITING】。这时主线程执行object.notify(),但这时锁还没有释放,t1还没有获取到锁,所以t1状态【BLOCKED】。之后主线程释放锁,t1获得锁,执行object.wait(),这时t1的状态【WAITING】。然后回到主线程,并获得锁,执行object.notify()。此时t1线程被唤醒并处于运行状态【RUNNABLE】。t1执行完成,状态为【TERMINATED】。

Thread sleep() 方法和 Object wait()

方法对比共同点:两者都可以暂停线程的执行。
区别:sleep() 方法没有释放锁,而 wait() 方法释放了锁 。wait() 通常被用于线程间交互/通信,sleep()通常被用于暂停执行。
wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。
sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。
sleep() 是 Thread 类的静态本地方法,wait() 则是 Object 类的本地方法。

为什么 wait() 方法不定义在 Thread 中?wait() 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(Object)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object)而非当前的线程(Thread)。
类似的问题:为什么 sleep() 方法定义在 Thread 中?因为 sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁

多线程

(1)并发与并行的区别
并发:两个及两个以上的作业在同一 时间段 内执行。
并行:两个及两个以上的作业在同一 时刻 执行。
最关键的点是:是否是 同时 执行。

(2)同步和异步的区别
同步:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待
异步:调用在发出之后,不用等待返回结果,该调用直接返回

单核 CPU 是支持 Java 多线程的。操作系统通过时间片轮转的方式,将 CPU 的时间分配给不同的线程。尽管单核 CPU 一次只能执行一个任务,但通过快速在多个线程之间切换,可以让用户感觉多个任务是同时进行的。

(3)使用多线程可能带来什么问题?
并发编程的目的就是为了能提高程序的执行效率进而提高程序的运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。

(4)如何理解线程安全和不安全?
线程安全指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性
线程不安全则表示在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失。

死锁

线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

产生死锁的四个必要条件:
互斥条件:该资源任意一个时刻只由一个线程占用。
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

如何预防和避免线程死锁? 如何预防死锁?
破坏死锁的产生的必要条件即可:
破坏请求与保持条件:一次性申请所有的资源。
破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源
破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。

如何避免死锁?
避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。

ThreadLocal

ThreadLocal,即线程本地变量
如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是在操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了并发场景下的线程安全问题。
线程独有的变量

为什么要使用ThreadLocal

并发场景下,会存在多个线程同时修改一个共享变量的场景。这就可能会出现线性安全问题
为了解决线性安全问题,可以用加同步锁的方式,比如使用synchronized 或者Lock。但是加锁的方式,可能会导致系统变慢。
使用ThreadLocal,以空间换时间的方式,会在每个线程的本地,都保存一份共享变量的拷贝副本。
多线程对共享变量修改时,实际上操作的是这个变量副本,从而保证线性安全。

使用ThreadLocal的好处

1.保存每个线程绑定的数据,在需要的地方可以直接获取,避免直接传递参数带来的代码耦合问题;
2.各个线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失。

ThreadLocal 的工作原理

ThreadLocal 的关键在于它是如何为每个线程维护一个独立的变量副本的。实际上,每个线程都持有一个 ThreadLocalMap,这个 ThreadLocalMap 的键是 ThreadLocal 对象,值是线程局部变量的实际值

每个 ThreadLocalMap 是一个数组,数组的每个元素都是一个 Entry,它继承自 WeakReference,键是 ThreadLocal 的弱引用,值是实际存储的线程局部变量值。这种设计可以在 ThreadLocal 对象被回收时,允许垃圾收集器回收对应的 Entry,避免内存泄漏。

内存泄露问题

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄露堆积将会导致内存溢出。

由于 ThreadLocalMap 使用的是 ThreadLocal 的弱引用,但值是强引用,如果线程池长时间不释放线程,可能会导致内存泄漏。因此,使用 ThreadLocal 时应在不再需要时调用 remove() 方法清理变量。因此要确保每次使用完ThreadLocal之后都调用remove()去除Entry(key为null的Entry)!

ThreadLocal的应用场景

  1. 在重入方法中替代参数的显式传递。方法都需要用到同一个对象时,可以使用ThreadLocal替代参数的传递或者static静态全局变量。这是因为使用参数传递造成代码的耦合度高,使用静态全局变量在多线程环境下不安全。
    当该对象用ThreadLocal包装过后,就可以保证在该线程中独此一份,同时和其他线程隔离。
    ​ 例如在Spring的@Transaction事务声明的注解中就使用ThreadLocal保存了当前的Connection对象,避免在本次调用的不同方法中使用不同的Connection对象。

  2. 全局存储用户信息。使用ThreadLocal替代Session的使用,当用户要访问需要授权的接口的时候,可以现在拦截器中将用户的Token存入ThreadLocal中;之后在本次访问中任何需要用户用户信息的都可以直接在ThreadLocal中拿取数据。

  3. 解决线程安全问题
    ​ 依赖于ThreadLocal本身的特性,对于需要进行线程隔离的变量可以使用ThreadLocal进行封装。

threadLocal为每一个线程提供了一个变量副本实现了线程的隔离Map <threadId, object>,不保证变量同步,只保证变量隔离。

线程池

什么是线程池?
顾名思义,线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。

使用线程池的好处:
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

如何创建线程池?

方式一:通过ThreadPoolExecutor构造函数来创建(推荐)。
方式二:通过 Executor 框架的工具类 Executors 来创建。
FixedThreadPool:固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
SingleThreadExecutor: 只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
CachedThreadPool: 可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
ScheduledThreadPool:给定的延迟后运行任务或者定期执行任务的线程池

还有很对相关知识详见 Javaguide线程池

Java内存模型

Java 内存模型(JMM) 抽象了线程和主内存之间的关系,就比如说线程之间的共享变量必须存储在主内存中。
在 JDK1.2 之前,Java 的内存模型实现总是从 主存 (即共享内存)读取变量,是不需要进行特别的注意的。
而在当前的 Java 内存模型下,线程可以把变量保存 本地内存 (比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。

Java 内存区域和 JMM 有何区别?

Java 内存区域和内存模型是完全不一样的两个东西:
JVM 内存结构和 Java 虚拟机的运行时区域相关,定义了 JVM 在运行时如何分区存储程序数据,就比如说堆主要用于存放对象实例。
Java 内存模型(JMM)和 Java 的并发编程相关,抽象了线程和主内存之间的关系就比如说线程之间的共享变量必须存储在主内存中,规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。

volatile 关键字-一种轻量级同步机制

volatile 关键字能保证数据的可见性,但不能保证数据的原子性synchronized 关键字两者都能保证
volatile 关键字可以保证变量的可见性,如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。而且可以防止 JVM 的指令重排序

并发编程的三个重要特性

原子性:一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。
在 Java 中,可以借助synchronized、各种 Lock 以及各种原子类(CAS操作)、可能也会用到 volatile或者final关键字 实现原子性。

可见性:多线程下,一个线程修改了变量的值,其他线程能立即看到修改的值。
在 Java 中,可以借助synchronized、volatile 以及各种 Lock 实现可见性。
当一个变量被volatile修饰后,表示线本地内存无效,当一个线程修改共享变量后,会被立即更新到主内存中,其他线程从主内存中读取该共享变量。

有序性:程序执行的顺序按照代码的先后顺序执行。
指令重排序不会影响单线程的运行结果,但对多线程有影响。【单线程就是串行执行,多线程所有操作是无序的】
在 Java 中,volatile 关键字可以禁止指令进行重排序优化。
在这里插入图片描述

AQS(抽象队列同步器)

AQS 就是一个抽象类,主要用来构建锁和同步器。
AQS 为构建锁和同步器提供了一些通用功能的实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue等等皆是基于 AQS 的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值