当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。
- 原子性
我们把一个或者多个操作在CPU执行的过程中不被中断的特性称为原子性.
- 可见性
当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。
- 顺序性
在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。如果在被线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
volatile
关键字 volatile 是 Java 虚拟机提供的最轻量级的同步机制。
- 保证此变量对所有线程的可见性。但是操作并非原子操作,并发情况下不安全。(每次更新的值都会同步到主内存)
- 禁止指令重排序优化。(通过插入内存屏障保证一致性。)
因为volatile不确保原子性,所以不能完全保证线程安全,仅在以下条件才应该使用:
- 对变量的写入操作不依赖变量的当前值,或者确保只有单个线程更新变量的值
- 该变量不会与其他状态变量一起纳入不变性条件中
- 在访问变量时不需要加锁
ThreadLocal
ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本。
ThreadLocal变量类似于全局变量,它能降低代码的可重用性并在类之间引入隐含的耦合行。
在数据量大时会占用大量内存,且有内存溢出的风险。
Vector、HashTable
ConcurrentHashMap、CopyOnWriteArrayList
ConcurrentQueue、ConcurrentSkipListMap、ConcurrentSkipListSet
JAVA集合
1.3 同步工具类闭锁
闭锁可以用来确保某些活动指导其他活动都完成都才继续执行。
闭锁包括一个计数器,该计数器被初始化为一个整数,表示需要等待的时间数量。countDown方法递减计数器,表示有一个时间已经发生了,而await方法等待计数器达到零,这表示所有需要等待的时间都已经发生。吐过计数器的值非零,那么await会一直阻塞知道计数器为零,或者等待的线程中断或超时。
CountDownLatch和FutureTask都是闭锁的实现。
信号量
计数信号量(Counting Semaphore)用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的输了。还可以用来实现某种资源池,或者对容器施加边界。
Semaphore中管理这一组虚拟的许可(permit),许可的初始数量可通过构造函数来指定。在执行操作时可以首先获得许可,并在使用以后释放许可。如果没有许可,那么acquire将阻塞知道有许可(或者中断或超时)。release方法将返回一个许可给信号量。
栅栏
栅栏(Barrier)类似于闭锁,它能阻塞一组线程直到某个事件发生。栅栏与闭锁的关键区别在于,所有线程必须同时达到栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用户等待其他线程。
当线程到达栅栏门位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。
CountDownLatch、CyclicBarrier和Semaphore 使用示例及原理
二、线程池 2.1 线程池参数public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) { ... }
- corePoolSize(核心线程数:线程池的初始大小,没有任务执行时的线程池大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。
- maximumPoolSize(最大线程数):表示可同时活动的线程数量上限。
- keepAliveTime(存活时间 :如果某个线程的空闲时间超过了存活时间,将被标记为可回收,当线程数量超过核心线程数大小时,这个线程将被终止。
- unit(时间单位)
- workQueue(任务队列):任务提交速度大于线程池处理速度时,会被加入到BlockingQueue队列中等待。任务队列分为3种:无界队列,有界队列和同步移交(同步移交不放入队列,直接移交给线程,如果没有空闲线程则新建线程,如线程数达到最大,则执行拒绝策略)。
- threadFactory(线程工厂):线程池的创建都是通过线程工厂的newThread方法调用来创建,可以通过定制线程工厂方法来实现个性化的需求(如指定一些异常捕获或者信息记录等)。
- handler(拒绝策略):当达到最大线程数,且队列是有界时,会执行拒绝策略。有4种拒绝策略:
- Abort(中止):抛出异常RejectExecutionException
- Discard(抛弃):新任务被提交后直接被丢弃掉。
- Discard-Oldest(抛弃最旧的) :策略会抛弃下一个将被执行的任务,也就是队列最前面的,然后提交新的任务。
- Caller-Runs(调用者运行) :把这个任务交于提交任务的线程执行。由于主线程执行任务所以有一段时间不能提交任务,也让线程池的任务有一定时间处理。
newFixedThreadPool:固定长度线程池,线程池的核心线程数和最大线程数设置为指定的值,使用无界队列。
newCachedThreadPool:可缓存的线程池,线程池的最大线程数为Integer.MAX_VALUE,核心线程数为0,超时时间设置为1分钟,使用无界队列。
newSingleThreadExecutor:单线程的线程池
newScheduledThreqadPool:定时执行的线程池
Q: new Thread()和newSingleThreadExecutor()都是创建一个线程处理,为什么还需要存在单个线程的线程池呢?
A: new Thread()每次创建新的线程,newSingleThreadExecutor()使用同一个线程,减少线程创建和销毁的消耗。
- 互斥条件: 进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
- 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
- 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
- 预防死锁:通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来防止死锁的发生。
- 避免死锁:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
- 检测死锁:允许系统在运行过程中发生死锁,但可设置检测机构及时检测死锁的发生,并采取适当措施加以清除。
- 解除死锁:当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来。
(1)预防死锁
破坏死锁形成的四个条件
- 破坏“互斥”条件
就是在系统里取消互斥。若资源不被一个进程独占使用,那么死锁是肯定不会发生的。但一般来说在所列的四个条件中,“互斥”条件是无法破坏的。因此,在死锁预防里主要是破坏其他几个必要条件,而不去涉及破坏“互斥”条件。
注意:互斥条件不能被破坏,否则会造成结果的不可再现性。 - 破坏“请求与保持”条件
破坏“占有并等待”条件,就是在系统中不允许进程在已获得某种资源的情况下,申请其他资源。即要想出一个办法,阻止进程在持有资源的同时申请其他资源。
方法一:创建进程时,要求它申请所需的全部资源,系统或满足其所有要求,或什么也不给它。这是所谓的 “ 一次性分配”方案。
方法二:要求每个进程提出新的资源申请前,释放它所占有的资源。这样,一个进程在需要资源S时,须先把它先前占有的资源R释放掉,然后才能提出对S的申请,即使它可能很快又要用到资源R。 - 破坏“不可抢占”条件
破坏“不可抢占”条件就是允许对资源实行抢夺。
方法一:如果占有某些资源的一个进程进行进一步资源请求被拒绝,则该进程必须释放它最初占有的资源,如果有必要,可再次请求这些资源和另外的资源。
方法二:如果一个进程请求当前被另一个进程占有的一个资源,则操作系统可以抢占另一个进程,要求它释放资源。只有在任意两个进程的优先级都不相同的条件下,方法二才能预防死锁。 - 破坏“循环等待”条件
破坏“循环等待”条件的一种方法,是将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。
(2)避免死锁
预防死锁和避免死锁的区别:
预防死锁是设法至少破坏产生死锁的四个必要条件之一,严格的防止死锁的出现,而避免死锁则不那么严格的限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁。避免死锁是在系统运行过程中注意避免死锁的最终发生。
常用避免死锁的方法:
- 加锁顺序(线程按照一定的顺序加锁)
- 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
- 死锁检测
常用算法:
资源分配图算法、银行家算法
资源分配图算法与银行家算法
(3)检测死锁
死锁检测与恢复是指系统设有专门的机构,当死锁发生时,该机构能够检测到死锁发生的位置和原因,并能通过外力破坏死锁发生的必要条件,从而使得并发进程从死锁状态中恢复出来。
(4)解除死锁
死锁解除的主要方法有:
- 资源剥夺法。挂起某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但应防止被挂起的进程长时间得不到资源,而处于资源匮乏的状态。
- 撤销进程法。强制撤销部分、甚至全部死锁进程并剥夺这些进程的资源。撤销的原则可以按进程优先级和撤销进程代价的高低进行。
- 进程回退法。让一(多)个进程回退到足以回避死锁的地步,进程回退时自愿释放资源而不是被剥夺。要求系统保持进程的历史信息,设置还原点。
死锁,死锁的四个必要条件以及处理策略
四.高级主题 4.1 显式锁 4.1.1 synchronized(1)Monitor
Monitor 被翻译为监视器或管程
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针。
在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
-
synchronized用在方法上:标志ACC_SYNCHRONIZED。代表的是当线程执行到方法后会检查是否有这个标志,如果有的话就会隐式的去调用monitorenter和monitorexit两个命令来将方法锁住。
-
synchronized用在代码块上:在同步块的前后形成monitorenter和monitorexit两个字节码指令。
- 在执行monitorenter指令的时候,首先要去尝试获取对象的锁(获取对象锁的过程,其实是获取 monitor对象的所有权的过程)。
- 如果这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加一。
- 而在执行monitorexit指令时会将锁计数器减一。一旦计数器的值为零,锁随即就被释放了。
- 如果获取对象锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止。
(2)JAVA对象
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。
- 对象头:Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
- 实例数据:存放类的属性数据信息,包括父类的属性信息;
- 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;
Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)。其中 Class Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。
Mark Word部分的存储结构(32位虚拟机):
(3)对象头中Mark Word与线程中Lock Record
在线程进入同步代码块的时候,如果此同步对象没有被锁定,即它的锁标志位是01,则虚拟机首先在当前线程的栈中创建我们称之为“锁记录(Lock Record)”的空间,用于存储锁对象的Mark Word的拷贝,官方把这个拷贝称为Displaced Mark Word。整个Mark Word及其拷贝至关重要。
Lock Record是线程私有的数据结构,每一个线程都有一个可用Lock Record列表,同时还有一个全局的可用列表。每一个被锁住的对象Mark Word都会和一个Lock Record关联(对象头的MarkWord中的Lock Word指向Lock Record的起始地址),同时Lock Record中有一个Owner字段存放拥有该锁的线程的唯一标识(或者object mark word),表示该锁被这个线程占用。如下图所示为Lock Record的内部结构:
synchronized详解
(4)synchronized的优化
JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
自旋锁
线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。
何谓自旋锁?
所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。
适应自旋锁
JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
锁消除
为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。
锁粗化
锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。如上面实例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。
synchronized 锁的升级过程
synchronized 锁的四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁,状态锁的状态根据竞争激烈的程度从低到高不断升级。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。其中识别是不是同一个线程一只获取锁的标志是在上面提到的对象头Mark Word(标记字段)中存储的。(花销除了第一次CAS,后续只需要判断Mark Word中的线程id是否为访问的线程)
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。(相比偏向锁需要自旋以及CAS操作替换线程id,但不会阻塞线程)
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候(默认10次),还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。这时候也就成为了原始的Synchronized的实现。
synchronized锁升级详细过程
4.1.2 Lock(1)ReentrantLock
ReentrantLock实现Lock接口,Lock接口中定义了lock与unlock相关操作,并且还存在newCondition方法,表示生成一个条件。
ReentrantLock 类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与 FairSync类继承自 Sync类,Sync类继承自 AbstractQueuedSynchronizer抽象类。
ReentrantLock结构:
通过分析 ReentrantLock的源码,可知对其操作都转化为对 Sync对象的操作,由于 Sync继承了 AQS,所以基本上都可以转化为对 AQS的操作。如将 ReentrantLock的 lock函数转化为对 Sync的 lock函数的调用,而具体会根据采用的策略(如公平策略或者非公平策略)的不同而调用到 Sync的不同子类。所以可知,在 ReentrantLock的背后,是 AQS对其服务提供了支持。
synchronized和ReentrantLock的区别:
| 区别 | ReentrantLock | synchronized |
|---|---|---|
| 底层实现 | API层面 | JAVA关键字 |
| 锁机制 | 基于AQS | 基于Monitor |
| 释放形式 | 手动释放 | 自动释放 |
| 灵活性 | 支持响应中断(lockInterruptibly)、尝试获取锁(trylock)、超时(timeout) | 不灵活 |
| 锁类型 | 非公平锁&公平锁 | 非公平锁 |
| 条件绑定 | 通过Condition绑定多个条件 | 不支持 |
在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一中高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的、可轮轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用synchronized。
ReentrantLock 锁详解
(2)AbstractQueueSynchronizer(AQS)
AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器, 比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask(jdk1.7) 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒 时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
AQS 详细介绍
(3)CompareAndSwap(CAS)
CAS是乐观锁的一种实现,CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。
CPU对CAS的支持:
CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。所以利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。
A: ABA问题:假设有一个变量 A ,修改为B,然后又修改为了 A,实际已经修改过了,但 CAS 可能无法感知,造成了不合理的值修改操作。
Q: 加上版本号,更新的时候检查版本号,并更新引用的值和版本号。
一文彻底搞懂CAS实现原理
(4)原子变量类
AcomicInteger、AtomicLong、AtomicBoolean等
- 维护一个volatile修饰的int型变量value
- 使用CAS保证原子性操作
//维护一个volatile修饰的int型变量value
private volatile int value;
public AtomicInteger(int initialValue) {
value = initialValue;
}
//基于CAS的原子性操作
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
4.2 JAVA内存模型(Java Memory Model)
内存模型是定义了线程和主内存之间的抽象关系,即 JMM 定义了 JVM 在计算机内存(RAM)中的工作方式。
Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量(线程共享的变量)存储到内存和从内存中取出变量这样底层细节。
Java 内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间无法直接访问对方本地内存中的变量。
全面学习掌握Java内存模型
本文主要用作知识点梳理,以上很多内容都为概述,详细内容附有写的不错的博客地址。日后有时间和需求再做详细解析。
参考书籍:《JAVA并发编程实战》



