当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。
CAP理论
我们把一个或者多个操作在CPU执行的过程中不被中断的特性称为原子性.
当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。
在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。如果在被线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。
BASE理论
volatile
关键字 volatile 是 Java 虚拟机提供的最轻量级的同步机制。
因为volatile不确保原子性,所以不能完全保证线程安全,仅在以下条件才应该使用:
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:
ThreadLocal
ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
ThreadLocal内存泄露问题
线程池使用完及时释放value
线程的创建
终止线程的4种方式
线程相关的基本方法
wait()
:调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。
sleep()
:sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态
yield ()
:yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。
interrupt()
:中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。
join()
:等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。
notify()
:随机唤醒此对象上等待的单个线程。
notifyAll()
:唤醒此对象上等待的所有线程。
如何在两个线程之间共享数据
Java 里面进行多线程通信的主要方式就是共享内存的方式
Vector、HashTable
ConcurrentHashMap、CopyOnWriteArrayList
ConcurrentQueue、ConcurrentSkipListMap、ConcurrentSkipListSet
JAVA集合
闭锁
闭锁可以用来确保某些活动指导其他活动都完成都才继续执行。
闭锁包括一个计数器,该计数器被初始化为一个整数,表示需要等待的时间数量。countDown方法递减计数器,表示有一个时间已经发生了,而await方法等待计数器达到零,这表示所有需要等待的时间都已经发生。吐过计数器的值非零,那么await会一直阻塞知道计数器为零,或者等待的线程中断或超时。
CountDownLatch和FutureTask都是闭锁的实现。
信号量
计数信号量(Counting Semaphore)用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的输了。还可以用来实现某种资源池,或者对容器施加边界。
Semaphore中管理这一组虚拟的许可(permit),许可的初始数量可通过构造函数来指定。在执行操作时可以首先获得许可,并在使用以后释放许可。如果没有许可,那么acquire将阻塞知道有许可(或者中断或超时)。release方法将返回一个许可给信号量。
栅栏
栅栏(Barrier)类似于闭锁,它能阻塞一组线程直到某个事件发生。栅栏与闭锁的关键区别在于,所有线程必须同时达到栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用户等待其他线程。
当线程到达栅栏门位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。
CountDownLatch、CyclicBarrier和Semaphore 使用示例及原理
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) { ... }
当向线程池提交一个新的任务时
newFixedThreadPool:固定长度线程池,线程池的核心线程数和最大线程数设置为指定的值,使用无界队列。
newCachedThreadPool:可缓存的线程池,线程池的最大线程数为Integer.MAX_VALUE,核心线程数为0,超时时间设置为1分钟,使用无界队列。
newSingleThreadExecutor:单线程的线程池
newScheduledThreqadPool:定时执行的线程池
Q: new Thread()和newSingleThreadExecutor()都是创建一个线程处理,为什么还需要存在单个线程的线程池呢?
A: new Thread()每次创建新的线程,newSingleThreadExecutor()使用同一个线程,减少线程创建和销毁的消耗。
(1)预防死锁
破坏死锁形成的四个条件
(2)避免死锁
预防死锁和避免死锁的区别:
预防死锁是设法至少破坏产生死锁的四个必要条件之一,严格的防止死锁的出现,而避免死锁则不那么严格的限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁。避免死锁是在系统运行过程中注意避免死锁的最终发生。
常用避免死锁的方法:
常用算法:
资源分配图算法
、银行家算法
资源分配图算法与银行家算法
(3)检测死锁
死锁检测与恢复是指系统设有专门的机构,当死锁发生时,该机构能够检测到死锁发生的位置和原因,并能通过外力破坏死锁发生的必要条件,从而使得并发进程从死锁状态中恢复出来。
(4)解除死锁
死锁解除的主要方法有:
死锁,死锁的四个必要条件以及处理策略
(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中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。
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锁升级详细过程
(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 锁详解
Condition 类和 Object 类锁方法区别区别
可重入锁
可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
(2)AbstractQueueSynchronizer(AQS)
AQS 定义了一套多线程访问共享资源的同步器框架,AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器, 比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask(jdk1.7) 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。
它维护了一个 volatile int state
(代表共享资源)和一个 FIFO 线程等待队列
(多线程争用资源被阻塞时会进入此队列)。
AQS 定义两种资源共享方式:Exclusive 独占资源-ReentrantLock,Share 共享资源-Semaphore/CountDownLatch
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒 时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
同步器的实现是 ABS 核心(state 资源状态计数)
同步器的实现是 ABS 核心,以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念
。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。
AQS 详细介绍
AQS为什么使用双向链表
双向链表可以支持 常量O(1) 时间复杂度的情况下找到前驱结点,基于这样的特点。
双向链表在插入和删除操作的时候,要比单向链表简单、高效。
第一个方面,没有竞争到锁的线程加入到阻塞队列,并且阻塞等待的前提是,当前线程所在节点的前置节点是正常状态,这样设计是为了避免链表中存在异常线程导致无法唤醒后续线程的问题。所以线程阻塞之前需要判断前置节点的状态,如果没有指针指向前置节点,就需要从head节点开始遍历,性能非常低。
第二个方面,在Lock接口里面有一个,lockInterruptibly()方法,这个方法表示处于锁阻塞的线程允许被中断。也就是说,没有竞争到锁的线程加入到同步队列等待以后,是允许外部线程通过interrupt()方法触发唤醒并中断的。这个时候,被中断的线程的状态会修改成CANCELLED。被标记为CANCELLED状态的线程,是不需要去竞争锁的,但是它仍然存在于双向链表里面。意味着在后续的锁竞争中,需要把这个节点从链表里面移除,否则会导致锁阻塞的线程无法被正常唤醒。在这种情况下,如果是单向链表,就需要从Head节点开始往下逐个遍历,找到并移除异常状态的节点。同样效率也比较低,还会导致锁唤醒的操作和遍历操作之间的竞争。
第三个方面,为了避免线程阻塞和唤醒的开销,所以刚加入到链表的线程,首先会通过自旋的方式尝试去竞争锁。但是实际上按照公平锁的设计,只有头节点的下一个节点才有必要去竞争锁,后续的节点竞争锁的意义不大。否则,就会造成羊群效应,也就是大量的线程在阻塞之前尝试去竞争锁带来比较大的性能开销。所以为了避免这个问题,加入到链表中的节点在尝试竞争锁之前,需要判断前置节点是不是头节点,如果不是头节点,就没必要再去触发锁竞争的动作。所以这里会涉及到前置节点的查找,如果是单向链表,那么这个功能的实现会非常复杂。
(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
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);
}
内存模型是定义了线程和主内存之间的抽象关系,即 JMM 定义了 JVM 在计算机内存(RAM)中的工作方式。
Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量(线程共享的变量)存储到内存和从内存中取出变量这样底层细节。
Java 内存模型中规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间无法直接访问对方本地内存中的变量。
全面学习掌握Java内存模型
本文主要用作知识点梳理,以上很多内容都为概述,详细内容附有写的不错的博客地址。日后有时间和需求再做详细解析和补充。
参考书籍:《JAVA并发编程实战》