1 线程
1.1 线程的概述
1.1.1 线程与【进程】
- 进程:是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
- 线程:与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
- 多线程:就是在一个进程中多个执行路径同时执行。
- 总结:线程是进程 划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反
1.1.2 多线程的好处和弊端
- 多线程的好处
- 解决了一个进程里面可以同时运行多个任务(执行路径);
- 高资源的利用率,而不是提高效率。
- 多线程的弊端
- 降低了一个进程里面线程的执行频率;
- 对线程进行管理需要额外的CPU开销,线程的使用会给系统带来上下文切换的额外开销;
- 公有变量的同时读或写,当多个线程需要对公有变量进行写操作时,后一个线程往往会修改掉前一个线程存放的数据,发生线程安全问题;
- 线程的死锁。即较长时间的等待或资源竞争以及死锁等多线程症状。
1.2 线程的创建方式
- 方式一
- 继承Thread类;
- 重写run方法,把要执行**的任务放在run方法中;
- 调用start()方法启动线程。
- 使用细节:
- 线程的启动使用父类Thread的start()方法;
- 如果线程对象直接调用run(),那么JVM不会当做线程来执行,会当做是普通的函数来调用;
- 线程的启动只能用一次,否则抛出异常;
- 可以直接创建Thread类的对象并启动该线程,但是如果没有重写run(),什么也不会执行;
- 匿名内部类的线程实现方式。
- 方式二
- 实现Runable接口;
- 重写Runable接口中的run方法;
- 通过Thread类建立线程对象;
- 将Runable接口的子类对象作为实参,传递给Thread类构造方法;
- 调用Thread类的start方法开启线程,并调用Runable接口子类run方法。
- 为什么要将Runable接口的子类对象传递给Thread的构造函数
因为自定义的run方法所属对象是Runable接口的子类对象,所以要让线程去执行指定对象的run方法
- 方式三
- 创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
- 使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
- 使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
-
代码示例
public class Main { public static void main(String[] args){ MyThread3 th=new MyThread3(); //使用Lambda表达式创建Callable对象 //使用FutureTask类来包装Callable对象 FutureTask<Integer> future=new FutureTask<Integer>((Callable<Integer>)()->{ return 5; } ); new Thread(task,"有返回值的线程").start();//实质上还是以Callable对象来创建并启动线程  try{ System.out.println("子线程的返回值:"+future.get());//get()方法会阻塞,直到子线程执行结束才返回 }catch(Exception e){ e.printStackTrace(); } } } -
与Runnable接口的区别**
- Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大
- call()方法可以有返回值
- call()方法可以声明抛出异常
-
Future接口定义了几个公共方法控制它关联的Callable任务
- boolean cancel(boolean mayInterruptIfRunning):视图取消该Future里面关联的Callable任务
- V get():返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值
- V get(long timeout,TimeUnit unit):返回Callable里call()方法的返回值,最多阻塞timeout时间,经过指定时间没有返回抛出TimeoutException
- boolean isDone():若Callable任务完成,返回True
- boolean isCancelled():如果在Callable任务正常完成前被取消,返回True
- 三种方式的对比
实现Runnable和实现Callable接口的方式基本相同,不过是后者执行call()方法有返回值,后者线程执行体run()方法无返回值,因此可以把这两种方式归为一种这种方式与继承Thread类的方法之间的差别如下:
- 线程只是实现Runnable或实现Callable接口,还可以继承其他类。
- 这种方式下,多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
- 但是编程稍微复杂,如果需要访问当前线程,必须调用Thread.currentThread()方法。
- 继承Thread类的线程类不能再继承其他父类(Java单继承决定)。
- 注:一般推荐采用实现接口的方式来创建多线程
1.3 线程的状态

- 创建:新建了一个线程对象;
- 可运行:线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的执行权;
- 运行:就绪状态的线程获取了CPU执行权,执行程序代码;
- 阻塞:阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
- 死亡:线程执行完它的任务时。
1.4 线程的常见方法
- Thread(String name) 初始化线程的名字
- getName() 返回线程的名字
- setName() 设置线程的名字
- sleep() 线程睡眠指定的毫秒数
- getPriority() 返回当前线程对象的优先级(默认线程的优先级是5,范围从1-10)
- setPriority() 设置线程的优先级(虽然设置了线程的优先级,但是具体的实现取决于底层的操作系统的实现
- currentThread() 返回CPU正在执行的线程的对
1.5 start()方法和run()方法的比较
- new一个Thread,线程进入了新建状态;调用start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start()会执行线程的相应准备工作,然后自动执行run()方法的内容,这是真正的多线程工作。
- 而直接执行run()方法,会把run方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
- 总结: 调用start方法方可启动线程并使线程进入就绪状态,而run方法只是thread的一个普通方法调用,还是在主线程里执行。
1.6 线程阻塞
1.6.1 什么是阻塞
阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)
1.6.2 阻塞原因

1.6.3 wait(),notify()和suspend(),resume()之间的区别
区别的核心
- wait(),notify()阻塞时会释放占用的锁, 前面叙述的方法,包括suspend(),resume()都不会释放占用的锁
- wait(),notify()直接隶属于Object类, 前面叙述的方法,包括suspend(),resume()都直接隶属于Thread类
- wait(),notify()必须在synchronized方法或块中调用, 前面叙述的方法,包括suspend(),resume()都可在任何位置调用
1.6.4 wait和notify方法需要注意的地方
- 第一:调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。
- 第二:除了 notify(),还有一个方法 notifyAll() 也可起到类似作用,唯一的区别在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。
1.6.5 wait和sleep的区别
- sleep()来自Thread类,而wait()来自Object类。调用sleep()方法的过程中,线程不会释放对象锁。而 调用 wait 方法线程会释放对象锁
- sleep()睡眠后不出让系统资源,wait让其他线程可以占用CPU
- sleep(milliseconds)需要指定一个睡眠时间,时间一到会自动唤醒.而wait()需要配合notify()或者notifyAll()使用
1.7 什么是并发和并行?
- 并发: 同一时间段,多个任务都在执行 (单位时间内不一定同时执行);
- 并行:单位时间内,多个任务同时执行。
1.8 线程生命周期
-
- 正常终止 :当线程的run()执行完毕,线程死亡。
-
- 使用标记停止线程
- 注意:Stop方法已过时,就不能再使用这个方法。
1.9 后台线程
1.9.1 实现
- setDaemon(boolean on)
1.9.2 特点
- 当所有的非后台线程结束时,程序也就终止了同时还会杀死进程中的所有后台线程,也就是说,只要有非后台线程还在运行,程序就不会终止,执行main方法的主线程就是一个非后台线程。
- 必须在启动线程之前(调用start方法之前)调用setDaemon(true)方法,才可以把该线程设置为后台线程。
- 一旦main()执行完毕,那么程序就会终止,JVM也就退出了。
- 可以使用isDaemon() 测试该线程是否为后台线程(守护线程)。
1.9.3 Thread的join方法
- join可以用来临时加入线程执行
1.10 线程之间共享数据
通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据**而设计的。
1.11 ThreadLocal
- 线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。
- Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式
- 但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。
- ThreadLocal的作用
简单说ThreadLocal就是一种以空间换时间的做法在每个Thread里面维护了一个ThreadLocal.ThreadLocalMap把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。
- ThreadLocal的源码分析
- ThreadLocal的内存泄漏问题呢
1.12 线程池
可参照我的另一篇博客:Java线程池小结
1.13 java中用到的线程调度算法什么
抢占式:一个线程用完CPU后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片某一个线程执行。
1.14 Thread.sleep(0)的作用是什么
由于java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
2 锁
2.1 如何创建锁对象
- 可以使用this关键字作为锁对象,也可以使用所在类的字节码文件对应的class对象作为锁对象
- 类名.class
- 对象.getClass()
2.2 注意
- 只能同步方法(代码块),不能同步变量或者类
- 每个对象只有一个锁
- 不必同步类中的所有方法,类可以同时具有同步方法和非同步方法
- 如果两个线程要执行一个类中的一个同步方法,并且他们使用的是了类的同一个实例(对象)来调用方法,那么一次只有一个线程能够执行该方法,另一个线程需要等待,直到第一个线程完成方法调用,总结就是:一个线程获得了对象的锁,其他线程不可以进入该对象的同步方法。
- 如果类同时具有同步方法和非同步方法,那么多个线程仍然可以访问该类的非同步方法
- 同步会影响性能(甚至死锁),优先考虑同步代码块。
- 如果线程进入sleep()睡眠状态,该线程会继续持有锁,不会释放。
2.3 死锁
当多个线程完成功能需要同时获取多个共享资源的时候可能会导致死锁。
2.3.1 产生条件
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
2.3.2 如何避免死锁?
只要破坏死锁产生的四个条件之一即可。
- 破坏互斥条件 这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
- 破坏请求与保持条件 一次性申请所有的资源。
- 破坏不剥夺条件 占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件 靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
2.4 线程的通讯
线程间的通讯其实就是多分线程在操作同一个资源,但操作动作不同。
- 等待唤醒机制
- wait:告诉当前线程放弃执行权,并放弃监视器(锁)并进入阻塞状态,直到其他线程获得执行权,并持有了相同的监视器(锁),并调用notify为止。
- notify:唤醒持有同一个监视器(锁)中调用wait的第一个线程,注意:被唤醒的线程进入了可运行状态,等待CPU的执行权;
- notifyAll:唤醒持有同一监视器中调用wait的所有线程。
- 线程间通信其实就是多个线程在操作同一个资源,但操作动作不同,wait,notify(),notifyAll()都使用在同步中,因为要对持有监视器(锁)的线程操作,所以要使用在同步中,因为只有同步才具有锁。
- 为什么这些方法定义在Object类中
因为这些方法在操作线程时,都必须要标识他们所操作线程持有的锁,只有同一个锁上的被等待线程,可以被统一锁上notify唤醒,不可以对不同锁中的线程进行唤醒,就是等待和唤醒必须是同一个锁。而锁由于可以是任意对象,所以可以被任意对象调用的方法定义在Object类中。
2.5锁的种类
2.5.1 自旋锁
自旋锁在JDK1.6之后就默认开启了。基于之前的观察,共享数据的锁定状态只会持续很短的时间,为了这一小段时间而去挂起和恢复线程有点浪费,所以这里就做了一个处理,让后面请求锁的那个线程在稍等一会,但是不放弃处理器的执行时间,看看持有锁的线程能否快速释放。为了让线程等待,所以需要让线程执行一个忙循环也就是自旋操作。在jdk6之后,引入了自适应的自旋锁,也就是等待的时间不再固定了,而是由上一次在同一个锁上的自旋时间及锁的拥有者状态来决定。
2.5.2 偏向锁
在JDK1.之后引入的一项锁优化,目的是消除数据在无竞争情况下的同步原语。进一步提升程序的运行性能。 偏向锁就是偏心的偏,意思是这个锁会偏向第一个获得他的线程,如果接下来的执行过程中,改锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步。偏向锁可以提高带有同步但无竞争的程序性能,也就是说他并不一定总是对程序运行有利,如果程序中大多数的锁都是被多个不同的线程访问,那偏向模式就是多余的,在具体问题具体分析的前提下,可以考虑是否使用偏向锁。
2.5.3 轻量级锁
为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,所以在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。
2.6 synchronize和ReentranLock
2.6.1 synchronize和ReentranLock的区别
synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。而且,synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API。 既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:
- ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁
- ReentrantLock可以获取各种锁的信息
- ReentrantLock可以灵活地实现多路通知
- 另外,二者的锁机制其实也是不一样的:ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word。
2.6.2 synchronized的优化
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
【详参】Synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 和ReenTrantLock 的对比
2.6.3底层原理
- 【synchronized的底层原理】 啃碎并发(七):深入分析Synchronized原理
- 【ReetranLock的底层原理】 深入理解ReentrantLock的实现原理
2.7 sleep()方法和wait()方法区别和共同点?
- 两者最主要的区别在于:sleep方法没有释放锁,而wait方法释放了锁 。
- 两者都可以暂停线程的执行。
- Wait通常被用于线程间交互/通信,sleep通常被用于暂停执行。
- wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。sleep()方法执行完成后,线程会自动苏醒。
2.8 乐观锁和悲观锁
2.8.1 乐观锁
乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
2.8.2 悲观锁
悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。
2.8.3 什么是CAS
CAS,全称为Compare and Swap,即比较-替换。假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。
2.9 ConcurrentHashMap
2.9.1 ConcurrentHashMap的并发度是什么?
ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap
2.9.2 ConcurrentHashMap的工作原理
- jdk 1.6
- ConcurrentHashMap是线程安全的,但是与Hashtablea相比,实现线程安全的方式不同。Hashtable是通过对hash表结构进行锁定,是阻塞式的,当一个线程占有这个锁时,其他线程必须阻塞等待其释放锁。ConcurrentHashMap是采用分离锁的方式,它并没有对整个hash表进行锁定,而是局部锁定,也就是说当一个线程占有这个局部锁时,不影响其他线程对hash表其他地方的访问。
- 具体实现: ConcurrentHashMap内部有一个Segment.
- jdk 1.8
- 在jdk 8中,ConcurrentHashMap不再使用Segment分离锁,而是采用一种乐观锁CAS算法来实现同步问题,但其底层还是“数组+链表->红黑树”的实现。
2.10 volatile关键字
2.10.1 可以volatile数组吗?
Java 中可以创建 volatile类型数组,不过只是一个指向数组的引用,而不是整个数组。如果改变引用指向的数组,将会受到volatile 的保护,但是如果多个线程同时改变数组的元素,volatile标示符就不能起到之前的保护作用了。
2.10.2 volatile能使得一个非原子操作变成原子操作吗?
一个典型的例子是在类中有一个 long 类型的成员变量。如果你知道该成员变量会被多个线程访问,如计数器、价格等,你最好是将其设置为 volatile。为什么?因为 Java 中读取 long 类型变量不是原子的,需要分成两步,如果一个线程正在修改该 long 变量的值,另一个线程可能只能看到该值的一半(前 32 位)。但是对一个 volatile 型的 long 或 double 变量的读写是原子。
一种实践是用 volatile 修饰 long 和 double 变量,使其能按原子类型来读写。double 和 long 都是64位宽,因此对这两种类型的读是分为两部分的,第一次读取第一个 32 位,然后再读剩下的 32 位,这个过程不是原子的,但 Java 中 volatile 型的 long 或 double 变量的读写是原子的。volatile 修复符的另一个作用是提供内存屏障(memory barrier),例如在分布式框架中的应用。简单的说,就是当你写一个 volatile 变量之前,Java 内存模型会插入一个写屏障(write barrier),读一个 volatile 变量之前,会插入一个读屏障(read barrier)。意思就是说,在你写一个 volatile 域时,能保证任何线程都能看到你写的值,同时,在写之前,也能保证任何数值的更新对所有线程是可见的,因为内存屏障会将其他所有写的值更新到缓存。
2.10.3 volatile类型变量提供什么保证
- 避免指令重排
- 可见性保证
例如,JVM 或者 JIT为了获得更好的性能会对语句重排序,但是 volatile 类型变量即使在没有同步块的情况下赋值也不会与其他语句重排序。
- volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。某些情况下,volatile 还能提供原子性
如读 64 位数据类型,像 long 和 double 都不是原子的(低32位和高32位),但 volatile 类型的 double 和 long 就是原子的。
2.11 CyclicBarrier和CountDownLatch区别
这两个类非常类似,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:
- CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行。
- CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个
- CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了。
2.12 Automic原子类
JUC包中的原子类的类型
- 基本类型
- AtomicInteger:整形原子类
- AtomicLong:长整型原子类
- AtomicBoolean:布尔型原子类
- 数组类型
- AtomicIntegerArray:整形数组原子类
- AtomicLongArray:长整形数组原子类
- AtomicReferenceArray:引用类型数组原子类
- 引用类型
- AtomicReference:引用类型原子类
- AtomicStampedRerence:原子更新引用类型里的字段原子类
- AtomicMarkableReference :原子更新带有标记位的引用类型 对象的属性修改类型
- AtomicIntegerFieldUpdater:原子更新整形字段的更新器
- AtomicLongFieldUpdater:原子更新长整形字段的更新器
- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
AtomicInteger 类的原理
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
2.13 AQS
AQS (AbstractQueuedSynchronizer)是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。
2.13.1 AQS 原理分析
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten) 队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。 AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
【底层原理出门右转】 Java并发之AQS详解
2.13.2 AQS 对资源的共享方式
- Exclusive(独占):只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁:
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
- Share(共享):多个线程可同时执行,如 Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
2.13.3 AQS 底层使用了模板方法模式
- 同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
- 使用者继承 AbstractQueuedSynchronizer 并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放)
- 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
- 这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。
2.13.4 AQS组件
- Semaphore(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量) 可以指定多个线程同时访问某个资源。
- CountDownLatch (倒计时器): CountDownLatch 是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
- CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await() 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
2.14 Concurrent包
可参考 浅析Java并发编程(四)java.util.concurrent
本文参考: