Java内存模型是共享内存的并发模型,线程之间主要通过读-写共享变量(堆内存中的实例域,静态域和数组元素)来完成隐式通信。
Java 内存模型(JMM)控制 Java 线程之间的通信,决定一个线程对共享变量的写入何时对另一个线程可见。
线程之间的共享变量存储在主内存(Main Memory)中
每个线程都有一个私有的本地内存(Local Memory),本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。本地内存中存储了该线程以读/写共享变量的拷贝副本。
从更低的层次来说,主内存就是硬件的内存,而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
Java内存模型中的线程的工作内存(working memory)是cpu的寄存器和高速缓存的抽象描述。而JVM的静态内存储模型(JVM内存模型)只是一种对内存的物理划分而已,它只局限在内存,而且只局限在JVM的内存。
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的
lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM中的happens-before 原则
倘若在程序开发中,仅靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,幸运的是,在Java内存模型中,还提供了happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据,happens-before 原则内容如下:
1程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
2锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
3 volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
4线程启动规则 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
5传递性 A先于B ,B先于C 那么A必然先于C
6线程终止规则 线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
7线程中断规则 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
8对象终结规则 对象的构造函数执行,结束先于finalize()方法。
原子性:指一个操作是不可中断的,即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰
可见性:指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。显然,对于串行程序来说,可见性问题不存在。因为修改某个变量后,读取这个变量的值,一定是修改后的新值。但是在并行程序,如果一个线程修改了某一个全局变量,那么其他线程未必可以马上知道这个改动。
有序性:对于一个线程的执行代码而言,我们总是习惯地认为代码的执行时从先往后,依次执行的。这样的理解也不能说完全错误,因为就一个线程而言,确实会这样。但是在并发时,程序的执行可能就会出现乱序。给人直观的感觉就是:写在前面的代码,会在后面执行。有序性问题的原因是因为程序在执行时,可能会进行指令重排,重排后的指令与原指令的顺序未必一致(指令重排后面会说)。
多线程读同步与可见性问题:
可见性(共享对象可见性):线程对共享变量修改的可见性。当一个线程修改了共享变量的值,其他线程能够立刻得知这个修改。
解决:
volatile关键字:volatile关键字可以保证直接从主存中读取一个变量,如果这个变量被修改后,总是会被写回到主存中去。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是:volatile的特殊规则保证了新值能立即同步到主内存,以及每个线程在每次使用volatile变量前都立即从主内存刷新。因此我们可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。
synchronized关键字:同步块的可见性是由“如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值”、“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”这两条规则获得的。
final关键字:final关键字的可见性是指,被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那么在其他线程就能看见final字段的值(无须同步)
重排序导致的可见性(有序性)问题:
volatile关键字本身就包含了禁止指令重排序的语义
synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入
多线程写同步与原子性问题:
如果两个或者更多的线程共享一个对象,多个线程在这个共享对象上更新变量,就有可能发生竞争。
原子性:指一个操作是按原子的方式执行的。要么该操作不被执行;要么以原子方式执行,即执行过程中不会被其它线程中断。
问题:可能AB两线程同时执行10次对i的累加,AB同时读到i是2然后同时累加,此时并不是i++,而是直接+2了。
解决:
只能通过同步块,synchronized。
作用:
1保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总数可以被其他线程立即得知。
可见性是指变量在线程之间是否可见,JVM 中默认情况下线程之间不具备可见性。
在 JVM 内存模型中内存分为主内存和工作内存,各线程有独自的工作内存,对于要操作的数据会从主内存拷贝一份到工作内存中,默认情况下工作内存是相互独立的,也就是线程之间不可见,而 volatile 最重要的作用之一就是使变量实现可见性。
保证了变量的可见性(visibility)。被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象。
2禁止指令重排序优化。从而避免多线程环境下程序出现乱序执行的现象。
(参见单例模式的双重锁机制,用volatile)
3但是不保证原子性。无法保证线程安全!
解决volatile原子性的问题的话一般用锁。
指令为什么会重排序呢
为了执行指令更快,指令间没有相互依赖(读后写、写后写、写后读)的话,可以乱序执行
从硬件架构来说,CPU为什么会重排序
答:前面指令如果依赖的数据发生缓存缺失,那么需要去内存磁盘读取数据,这个过程很耗时,如果不乱序执行的话,后面所有的指令都会被block住,这样CPU的吞吐量上不去,所有会有乱序执行机制,让后面没有数据依赖关系的指令可以不用等前面指令执行完了再执行(IPC,指令级并发)
volatile和synchronized的区别:
volatile不具备互斥性(当一个线程持有锁时,其他线程进不来,这就是互斥性)。
volatile不具备原子性。
synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在:
volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。
volatile用在如下的几个地方:
1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
2、多任务环境下各任务间共享的标志应该加volatile;
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
另外,以上这几种情况经常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),在1中可以通过关中断来实现,2中可以禁止任务调度,3中则只能依靠硬件的良好设计了。
有序性举例:
单例模式中未设置volatile:
public class DoubleCheckLock {
private static DoubleCheckLock instance;
private DoubleCheckLock(){
}
public static DoubleCheckLock getInstance(){
//第一次检测
if (instance==null){
//同步
synchronized (DoubleCheckLock.class){
if (instance == null){
//多线程环境下可能会出现问题的地方
instance = new DoubleCheckLock();
}
}
}
return instance;
}
}
这段代码在单线程环境下并没有什么问题,但如果在多线程环境下就可以出现线程安全问题。因为instance = new DoubleCheckLock();可以分为以下3步完成(伪代码):
memory = allocate(); //1.分配对象内存空间
instance(memory); //2.初始化对象
instance = memory; //3.设置instance指向刚分配的内存地址,此时instance!=null
由于步骤2和步骤3间可能会重排序,如下:
memory = allocate(); //1.分配对象内存空间
instance = memory; //3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
instance(memory); //2.初始化对象
由于步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以线程安全问题是:
当一条线程进行第一次检测时(访问instance不为null时),可能另一个线程先于他以及进入到创建实例的那一步了。但是可能在new的时候,先执行3导致instance!=null,正打算再执行2(此时instance实例还没有初始化完成!),我们第一个线程以为以为没问题,直接返回,也就造成了线程安全问题。那么该如何解决呢,很简单,我们使用volatile禁止instance变量被执行指令重排优化即可。
//禁止指令重排优化
private volatile static DoubleCheckLock instance;
原理:
有volatile变量修饰的共享变量进行写操作的时候会使用CPU提供的Lock前缀指令:
将当前处理器缓存行的数据写回到系统内存(当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存)。
这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效(当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中重新读取共享变量)。
锁的内存语义
当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
CPU底层实现原子操作:
通过总线锁定来保证原子性:
总线锁定其实就是处理器使用了总线锁,所谓总线锁就是使用处理器提供的一个 LOCK# 信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。但是该方法成本太大。
通过缓存锁来保证
所谓缓存锁定是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作写回内存时,处理器不在总线上声言LOCK#信号,而是修改内部地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效。
悲观锁:
每次操作都会加锁,会造成线程阻塞。
共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁:
每次操作不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止,不会造成线程阻塞。
乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
可以使用版本号机制和CAS算法实现。
版本号机制:
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
举例:
A读出version=1,并从其帐户余额中扣除50($100-$50)。
此时B 也读入此用户信息version=1,并扣除 $20($100-$20)。
A完成修改,将数据版本号加一version=2,连同扣除后余额balance=$50,提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,version 更新为 2 。
B完成后也将版本号version=2,试图向数据库提交数据balance=$80,但此时比对数据库记录版本时发现,B提交版本号为2,数据库记录当前版本也为2,不满足大于记录当前版本的乐观锁策略,因此,操作员B的提交被驳回。
CAS算法:
即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数
需要读写的内存值 V
进行比较的值 A
拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
两种锁的使用场景:
从上面对两种锁的介绍,我们知道两种锁各有优缺点。
乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
JAVA如何实现原子操作:锁和循环CAS。
CAS算法:
内存值V
预估值A
更新值B
当且仅当V==A时,才会把B的值赋给V,即V = B,否则不做任何操作。
整个比较并替换的操作是一个原子操作。CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
例子:
好处:
不加锁,超快,不用切换/休眠
问题:
1循环时间长开销很大:
我们可以看到getAndAddInt方法执行时,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
2只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
3什么是ABA问题?ABA问题怎么解决?
如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?
如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
解决:
追加版本号,
每次变量更新就把版本号加1,则A-B-A就变成1A-2B-3A。
或者加时间戳。
常见CAS应用:
并发包中的原子操作类(Atomic系列)
原子更新基本类型主要包括3个类:
AtomicBoolean:原子更新布尔类型
AtomicInteger:原子更新整型
AtomicLong:原子更新长整型
这3个类的实现原理和使用方式几乎是一样的,AtomicInteger主要是针对int类型的数据执行原子操作,它提供了原子自增方法、原子自减方法以及原子赋值方法等。
CAS和synchronized区别:
对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
用来控制线程同步的,保证我们的线程在多线程环境下,不被多个线程同时执行。
作用:
1可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),
2可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能),这点确实也是很重要的。
应用方式(方法锁、对象锁、类锁):
方法锁(对象锁):
1修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
修饰在方法上,多个线程调用同一个对象的同步方法会阻塞,调用不同对象的同步方法不会阻塞。(java对象的内存地址是否相同)
public synchronized void function(){
}
或者修饰代码块,但是在方法里面,给某个对象加锁。
public void function () {
String str=new String("lock");//在方法体内,调用一次就实例化一次,多线程访问不会阻塞,因为不是同一个对象,锁是不同的
synchronized (str) {
}
}
或者:
static AccountingSync instance=new AccountingSync();
synchronized(instance){
}
//this,当前实例对象锁
synchronized(this){
}
类锁:
由于一个class不论被实例化多少次,其中的静态方法和静态变量在内存中都只有一份。
java类可能会有很多对象,但是只有一个Class(字节码)对象,也就是说类的不同实例之间共享该类的Class对象。
Class对象其实也仅仅是个java对象,只不过有点特殊而已。
由于每个java对象都有1个互斥锁,而类的静态方法是需要Class对象。
所以所谓的类锁,只不过是Class对象的锁而已。获取类的Class对象的方法有好几种,最简单的是[类名.class]的方式。
2修饰静态方法,作用于当前class类对象加锁,进入同步代码前要获得当前类对象的锁
public static synchronized void function(){
}
3修饰代码块,类对象锁
static AccountingSync instance=new AccountingSync();
//class类对象锁
synchronized(AccountingSync.class){
}
总结:
synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类对象上锁。
synchronized 关键字加到实例方法上是给对象实例上锁。
尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!
synchronized锁机制:
在JDK 1.6之前,synchronized只有传统的锁机制(重量级),因此给开发者留下了synchronized关键字相比于其他同步机制性能不好的印象。
在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。
分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。当条件不满足时,锁会按偏向锁->轻量级锁->重量级锁 的顺序升级。
结构:
因为在Java中任意对象都可以用作锁,因此必定要有一个映射关系,存储该对象以及其对应的锁信息(比如当前哪个线程持有锁,哪些线程在等待)。所以最好的办法是将这个映射关系存储在对象头中,因为对象头本身也有一些hashcode、GC相关的数据,所以如果能将锁信息与这些信息共存在对象头中就好了。在JVM中,对象在内存中除了本身的数据外还会有个对象头,对于普通对象而言,其对象头中有两类信息:mark word和类型指针。另外对于数组而言还会有一份记录数组长度的数据。
synchronized重量级锁JVM底层实现原理:
重量级锁是我们常说的传统意义上的锁,其利用操作系统底层的同步机制去实现Java中的线程同步。
Java虚拟机中的同步(Synchronization)都是基于进入和退出Monitor对象实现,无论是显示同步(同步代码块)还是隐式同步(同步方法)都是如此。
同步代码块
monitorenter指令插入到同步代码块的开始位置。monitorexit指令插入到同步代码块结束的位置。JVM需要保证每一个monitorenter都有一个monitorexit与之对应。
任何对象,都有一个monitor与之相关联,当monitor被持有以后,它将处于锁定状态。线程执行到monitorenter指令时,会尝试获得monitor对象的所有权,即尝试获取锁。
同步方法
synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象。
synchronized用到的锁,是存储在对象头中的。(这也是Java所有对象都可以上锁的根本原因)
Java对象头:
内容 说明 备注
Mark Word 存储对象的Mark Word信息 -
Class Metadata Address 存储指向对象存储类型的指针 -
Array Length 数组的长度 只有数组对象有该属性
synchronized使用的锁是存放在Java对象头中的Mark Word中,下面将分析32 bits的JVM中的Mark Word的构成。
锁标志位 和 是否偏向锁 确定唯一的锁状态
其中 轻量锁 和 偏向锁 是JDK1.6之后新加的,用于对synchronized优化。
32位JVM的Mark Word存储结构:
Monitor是 synchronized 重量级 锁的实现关键。
锁的标识位为10。当然synchronized作为一个重量锁是非常消耗性能的,所以在JDK1.6以后做了部分优化,接下来的部分是讲作为重量锁的实现。
Monitor是线程私有的数据结构,每一个对象都有一个monitor与之关联。每一个线程都有一个可用monitor record列表(当前线程中所有对象的monitor),同时还有一个全局可用列表(全局对象monitor)。每一个被锁住的对象,都会和一个monitor关联。
当一个monitor被某个线程持有后,它便处于锁定状态。此时,对象头中 MarkWord的 指向互斥量的指针,就是指向锁对象的monitor起始地址。
一个monitor对象包括这么几个关键字段:cxq(下图中的ContentionList),EntryList ,WaitSet,owner。其中cxq ,EntryList ,WaitSet都是由ObjectWaiter的链表结构(两个队列 _EntryList 和 _WaitSet用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装成ObjectWaiter对象)),owner指向持有objectMonitor(持有锁)的线程。
Contention List:所有请求锁的线程将被首先放置到该竞争队列
Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List
Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set
OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck
Owner:获得锁的线程称为Owner
!Owner:释放锁的线程
新请求锁的线程将首先被加入到ContentionList中,当某个拥有锁的线程(Owner状态)调用unlock之后,如果发现 EntryList为空则从ContentionList中移动线程到EntryList。
当多个线程同时访问一个同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor后,会进入_owner 区域,然后把monitor中的 _owner 变量修改为当前线程,同时monitor中的计数器_count 会加1。
如果线程调用 wait() 方法,将释放当前持有的monitor,_owner变量恢复为null,_count变量减1,同时该线程进入_WaitSet 等待被唤醒。
当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当某个线程获取到对象的monitor(监视器锁) 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1。若线程调用 wait()方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。由此看来,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因。
当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个ObjectWaiter对象插入到cxq的队列尾部,然后暂停当前线程。当持有锁的线程释放锁前,会将cxq中的所有元素移动到EntryList中去,并唤醒EntryList的队首线程。
如果一个线程在同步块中调用了Object#wait方法,会将该线程对应的ObjectWaiter从EntryList移除并加入到WaitSet中,然后释放锁。当wait的线程被notify之后,会将对应的ObjectWaiter从WaitSet移动到EntryList中。
默认策略下,在A释放锁后一定是C线程先获得锁。因为在获取锁时,是将当前线程插入到cxq的头部,而释放锁时,默认策略是:如果EntryList为空,则将cxq中的元素按原有顺序插入到到EntryList,并唤醒第一个线程。也就是当EntryList为空时,是后来的线程先获取锁。这点JDK中的Lock机制是不一样的。
以上只是对重量级锁流程的一个简述,其中涉及到的很多细节,比如ObjectMonitor对象从哪来?释放锁时是将cxq中的元素移动到EntryList的尾部还是头部?notfiy时,是将ObjectWaiter移动到EntryList的尾部还是头部?
https://github.com/farmerjohngit/myblog/issues/15
锁有四种状态:无锁、偏向锁、轻量级锁和重量级锁
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。
无锁:
无锁状态,无锁即没有对资源进行锁定,所有的线程都可以对同一个资源进行访问,但是只有一个线程能够成功修改资源。无锁的特点就是在循环内进行修改操作,线程会不断的尝试修改共享资源,直到能够成功修改资源并退出,在此过程中没有出现冲突的发生,CAS 的原理和应用就是无锁的实现。
偏向锁:
解决只有在一个线程执行同步时提高性能。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。当一个线程获得锁,会将对象头的锁标志位设为01,进入偏向模式.偏向锁可以在让一个线程一直持有锁,在其他线程需要竞争锁的时候,再释放锁。
由于轻量级锁每次重入时,都需要进行CAS操作,对性能有损耗;Java6 中引入了偏向锁来优化这一问题,实现原理:只有第一次使用CAS将线程ID设置到Mark Word头,之后每次锁重入时,发现线程ID是自己,则不用执行CAS操作。
在JDK1.6中为了提高一个对象在一段很长的时间内都只被一个线程用做锁对象场景下的性能,引入了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只会执行几个简单的命令(查看一下是不是之前的那个线程,是的话不用再CAS了),而不是开销相对较大的CAS命令。
当锁已经发生偏向后,只要有另一个线程尝试获得偏向锁,则该偏向锁就会升级成轻量级锁。当然这个说法不绝对,因为还有批量重偏向这一机制。
轻量级锁:
是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋CAS的形式尝试获取锁,不会阻塞,提高性能。
轻量级锁的使用场景:如果一个对象虽然有多个线程需要加锁,当加锁时间是分开的,则可以用轻量级锁来优化。
轻量级锁加锁过程:
每个线程在其栈帧中都会创建一个所记录对象,所记录对象包含了锁记录地址、对象引用(记录加锁对象的地址);
JVM的开发者发现在很多情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。
多个线程交替(异步,并不同时,两个进程并没有竞争的时刻,可能一个结束了等了一会另一个才来)进入临界区时,偏向锁无法满足,膨胀到轻量级锁,锁标志位设为00。
重量级锁:
多个线程同时进入临界区。
是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。通过系统的线程互斥锁来实现的,代价最昂贵。
重量级锁性能差的原因:
当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cpu。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。
偏向锁和轻量级锁都是在用户态。
重量级锁实现:
synchronized——monitorenter/monitorexit——lock/unlock——mutex lock
重量级锁需要到OS的内核态,很耗性能
问:那为什么要到内核态
答:保护OS,有的指令不能让用户执行
问:计算机通过什么来区分什么是高优先级的,或者说需要在内核态执行的
答:指令会分级,Intel的x86会有R0、R1、R2、R3指令等级
synchronized的可重入性
从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有的对象锁的临界资源时,这种情况属于重入锁,请求将会成功,在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。
public void run() {
for(int j=0;j<1000000;j++){
//this,当前实例对象锁,第一次获得
synchronized(this){
i++;
increase();//synchronized的可重入性,第二次获得
}
}
}
public synchronized void increase(){
j++;
}
这里,假设线程A先获得第一次对象锁后,进入后又有第二个锁,他请求后发现竞争对象的的是自己!于是可以直接获得!
等待唤醒机制与synchronized
所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor ,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。
synchronized (obj) {
obj.wait();
obj.notify();
obj.notifyAll();
}
需要特别理解的一点是,与sleep方法不同的是,wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行,而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。
Lock:
Lock和Synchronized的共同点:
• 两者都是可重入锁
• 两者都保证了可见性与互斥性
都可以控制线程的同步,同一时间只能有一个线程操作上锁资源。
Lock在需要的时候去手动的获取锁和释放锁。在lock之后要unlock,否则如果一个写锁被获取之后没有释放的话,就不可能有锁获得锁了除非它自己本身。通用的做法是在try 块中进行业务处理,然后在finally中释放锁。
private Lock lock = new ReentrantLock();
public void func() {
lock.lock();
try {
// 临界区
} finally {
lock.unlock(); // 确保释放锁,从而避免发生死锁。
}
}
public static void main(String[] args) {
LockExample lockExample = new LockExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> lockExample.func());
executorService.execute(() -> lockExample.func());
}
Lock与Synchronized对比
• 在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化;另外,可读性上较好
• 由于ReentrantLock但是当同步非常激烈的时候,还能维持常态。所以比较适合高并发的场景
• 在一些synchronized所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多条件轮询锁
• 与目前的synchronized实现相比,争用下的ReentrantLock实现更具可伸缩性。这意味着当许多线程都在争用同一个锁时,使用ReentrantLock的总体开支通常要比synchronized少得多
Lock和Synchronized的区别:
• Lock是接口,而synchronized是关键字
• Lock是显式锁,可以手动开启和关闭,切结必须手动关闭,不然容易造成线程死锁。synchronized是隐式锁,出了作用域自动释放。
• Lock只有代码块锁,而synchronized既有代码块锁,也有方法锁。
• Lock可以提高多个线程进行读写的效率,例如读写锁,而synchronized不行
• Lock是同步非阻塞采用乐观并发策略,而synchronized是同步阻塞采用悲观并发策略
• Lock可以知道有没有成功获得锁,而synchronized不行
• Lock在发生异常时,如果没有主动unLock()解锁会出现死锁,必须在finally中解锁,而synchronized发生异常时,自动释放锁
• synchronized依赖于JVM(JVM级别)而ReenTrantLock依赖于API(API级别) ;synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
• ReenTrantLock可以等待中断,而synchronized不行
• ReenTrantLock可以有公平锁,而synchronized没有
• ReenTrantLock可以绑定多个Condition条件
相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:
①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
重入锁ReetrantLock
实现了Lock接口,作用与synchronized关键字相当,但比synchronized更加灵活。ReetrantLock本身也是一种支持重进入的锁,即该锁可以支持一个线程对资源重复加锁,同时也支持公平锁与非公平锁。需要注意的是ReetrantLock支持对同一线程重加锁,但是加锁多少次,就必须解锁多少次,这样才可以成功释放锁。
公平锁/非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。公平锁保证等待时间最长的线程将优先获得锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
synchronized只有非公平锁。
ReetrantLock 可以实现锁的公平性,通过AQS的来实现线程调度。
通过构造函数指定该锁是否是公平锁,默认是非公平锁。
非公平锁的优点在于吞吐量比公平锁大。一般而言非,非公平锁机制的效率往往会胜过公平锁的机制,但在某些场景下,可能更注重时间先后顺序,那么公平锁自然是很好的选择。
AQS
ReetrantLock是基于AQS并发框架实现的。
AQS核心思想:
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。
如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
AbstractQueuedSynchronizer又称为队列同步器,它是用来构建锁或其他同步组件的基础框架,内部通过一个int类型的成员变量state来控制同步状态,当state=0时,则说明没有任何线程占有共享资源的锁,当state=1时,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待,AQS内部通过内部类Node构成FIFO的同步队列来完成线程获取锁的排队工作,同时利用内部类ConditionObject构建等待队列,当Condition调用wait()方法后,线程将会加入等待队列中,而当Condition调用signal()方法后,线程将从等待队列转移动同步队列中进行锁竞争。注意这里涉及到两种队列,一种的同步队列,当线程请求锁而等待的后将加入同步队列等待,而另一种则是等待队列(可有多个),通过Condition调用await()方法释放锁后,将加入等待队列。
head和tail分别是AQS中的变量,其中head指向同步队列的头部,注意head为空结点,不存储信息。而tail则是同步队列的队尾,同步队列采用的是双向链表的结构这样可方便队列进行结点增删操作。state变量则是代表同步状态,执行当线程调用lock方法进行加锁后,如果此时state的值为0,则说明当前线程可以获取到锁,同时将state设置为1,表示获取成功。如果state已为1,也就是当前锁已被其他线程持有,那么当前执行线程将被封装为Node结点加入同步队列等待。
ReentrantLock内部存在3个实现类,分别是Sync、NonfairSync、FairSync,其中Sync继承自AQS实现了解锁tryRelease()方法,而NonfairSync(非公平锁)、 FairSync(公平锁)则继承自Sync,实现了获取锁的tryAcquire()方法,ReentrantLock的所有方法调用都通过间接调用AQS和Sync类及其子类来完成的。
从设计模式角度来看,AQS采用的模板模式的方式构建的,其内部除了提供并发操作核心方法以及同步队列操作外,还提供了一些模板方法让子类自己实现,如加锁操作以及解锁操作,为什么这么做?这是因为AQS作为基础组件,封装的是核心并发操作,但是实现上分为两种模式,即共享模式与独占模式,而这两种模式的加锁与解锁实现方式是不一样的,但AQS只关注内部公共方法实现并不关心外部不同模式的实现,所以提供了模板方法给子类使用,也就是说实现独占锁,如ReentrantLock需要自己实现tryAcquire()方法和tryRelease()方法,而实现共享模式的Semaphore,则需要实现tryAcquireShared()方法和tryReleaseShared()方法,这样做的好处是显而易见的,无论是共享模式还是独占模式,其基础的实现都是同一套组件(AQS),只不过是加锁解锁的逻辑不同罢了,更重要的是如果我们需要自定义锁的话,也变得非常简单,只需要选择不同的模式实现不同的加锁和解锁的模板方法即可。
NonfairSync非公平锁:(CAS+自旋+CLH同步队列)
1使用一个死循环进行CAS操作,可以解决多线程并发问题。这里做了两件事,一是如果还没有初始同步队列则创建新结点并使用compareAndSetHead设置头结点,tail也指向head,二是队列已存在,则将新结点node添加到队尾。注意这两个步骤都存在同一时间多个线程操作的可能,如果有一个线程修改head和tail成功,那么其他线程将继续循环,直到修改成功,这里使用CAS原子操作进行头结点设置和尾结点tail替换可以保证线程安全,从这里也可以看出head结点本身不存在任何数据,它只是作为一个牵头结点,而tail永远指向尾部结点(前提是队列不为null)。
2添加到同步队列后,结点就会进入一个自旋(死循环)过程,即每个结点都在观察时机待条件满足获取同步状态,然后从同步队列退出并结束自旋,回到之前的acquire()方法。
当且仅当前驱结点为头结点才尝试获取同步状态,这符合FIFO的规则,即先进先出,其次head是当前获取同步状态的线程结点,只有当head释放同步状态唤醒后继结点,后继结点才有可能获取到同步状态,因此后继结点在其前继结点为head时,才进行尝试获取同步状态,其他时刻将被挂起。
设置为node结点被设置为head后,其thread信息和前驱结点将被清空,因为该线程已获取到同步状态(锁),正在执行了,也就没有必要存储相关信息了,head只有保存指向后继结点的指针即可,便于head结点释放同步状态后唤醒后继结点。
从图可知更新head结点的指向,将后继结点的线程唤醒并获取同步状态,调用setHead(node)将其替换为head结点,清除相关无用数据。
总之,在AQS同步器中维护着一个同步队列,当线程获取同步状态失败后,将会被封装成Node结点,加入到同步队列中并进行自旋操作,当当前线程结点的前驱结点为head时,将尝试获取同步状态,获取成功将自己设置为head结点。在释放同步状态时,则通过调用子类(ReetrantLock中的Sync内部类)的tryRelease(int releases)方法释放同步状态,释放成功则唤醒后继结点的线程。
FairSync公平锁:
与非公平锁不同的是,在获取锁的时,公平锁的获取顺序是完全遵循时间上的FIFO规则,也就是说先请求的线程一定会先获取锁,后来的线程肯定需要排队。
方法唯一的不同是在使用CAS设置尝试设置state值前,调用了hasQueuedPredecessors()判断同步队列是否存在结点,如果存在必须先执行完同步队列中结点的线程,当前线程进入等待状态。这就是非公平锁与公平锁最大的区别,即公平锁在线程请求到来时先会判断同步队列是否存在结点,如果存在先执行同步队列中的结点线程,当前线程将封装成node加入同步队列等待。而非公平锁呢,当线程请求到来时,不管同步队列是否存在线程结点,直接尝试获取同步状态,获取成功直接访问共享资源,但请注意在绝大多数情况下,非公平锁才是我们理想的选择,毕竟从效率上来说非公平锁总是胜于公平锁。
Condition接口:
在并发编程中,每个Java对象都存在一组监视器方法,如wait()、notify()以及notifyAll()方法,通过这些方法,我们可以实现线程间通信与协作(也称为等待唤醒机制),如生产者-消费者模式,而且这些方法必须配合着synchronized关键字使用。
与synchronized的等待唤醒机制相比Condition具有更多的灵活性以及精确性,这是因为notify()在唤醒线程时是随机(同一个锁),而Condition则可通过多个Condition实例对象建立更加精细的线程控制,也就带来了更多灵活性了,我们可以简单理解为以下两点:
1通过Condition能够精细的控制多线程的休眠与唤醒。
2对于一个锁,我们可以为多个线程间建立不同的Condition。
每个Condition都对应着一个等待队列,也就是说如果一个锁上创建了多个Condition对象,那么也就存在多个等待队列。等待队列是一个FIFO的队列,在队列中每一个节点都包含了一个线程的引用,而该线程就是Condition对象上等待的线程。
一个Condition包含一个等待队列,Condition对象拥有首节点和尾节点的引用。
当一个线程调用了Condition.await()方法,那么该线程将会释放锁,构造成节点并加入等待队列的尾部进入等待状态。直到被唤醒、中断、超时才从队列中移出。
唤醒:
doSignal(first)方法中做了两件事,从条件等待队列移除被唤醒的节点,然后重新维护条件等待队列的firstWaiter和lastWaiter的指向。二是将从等待队列移除的结点加入同步队列(在transferForSignal()方法中完成的),如果进入到同步队列失败并且条件等待队列还有不为空的节点,则继续循环唤醒后续其他结点的线程。
到此整个signal()的唤醒过程就很清晰了,即signal()被调用后,先判断当前线程是否持有独占锁,如果有,那么唤醒当前Condition对象中等待队列的第一个结点的线程,并从等待队列中移除该结点,移动到同步队列中,如果加入同步队列失败,那么继续循环唤醒等待队列中的其他结点的线程,如果成功加入同步队列,那么如果其前驱结点是否已结束或者设置前驱节点状态为Node.SIGNAL状态失败,则通过LockSupport.unpark()唤醒被通知节点代表的线程,到此signal()任务完成,注意被唤醒后的线程,将从前面的await()方法中的while循环中退出,因为此时该线程的结点已在同步队列中,那么while (!isOnSyncQueue(node))将不在符合循环条件,进而调用AQS的acquireQueued()方法加入获取同步状态的竞争中,这就是等待唤醒机制的整个流程实现原理。
那么如何实现唤醒指定的线程呢?
只需采用多个condition,唤醒你指定的condition里的线程即可。
独占锁/共享锁
独占锁又叫做排他锁,是指锁在同一时刻只能被一个线程拥有,其他线程想要访问资源,就会被阻塞。比如ReentrantLock。
共享锁是指该锁可被多个线程所持有。如果某个线程对资源加上共享锁后,则其他线程只能对资源再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。比如ReentrantReadWriteLock 。它有两把锁:ReadLock 和 WriteLock,也就是一个读锁一个写锁,合在一起叫做读写锁。再进一步观察可以发现 ReadLock 和 WriteLock 是靠内部类 Sync 实现的锁。Sync 是继承于 AQS 子类的,AQS 是并发的根本,这种结构在CountDownLatch、ReentrantLock、Semaphore里面也都存在。
在 ReentrantReadWriteLock 里面,读锁和写锁的锁主体都是 Sync,但读锁和写锁的加锁方式不一样。读锁是共享锁,写锁是独占锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升。
共享锁底层原理:
AQS中通过state值来控制对共享资源访问的线程数,每当线程请求同步状态成功,state值将会减1,如果超过限制数量的线程将被封装共享模式的Node结点加入同步队列等待,直到其他执行线程释放同步状态,才有机会获得执行权,而每个线程执行完成任务释放同步状态后,state值将会增加1,这就是共享锁的基本实现模型。至于公平锁与非公平锁的不同之处在于公平锁会在线程请求同步状态前,判断同步队列是否存在Node,如果存在就将请求线程封装成Node结点加入同步队列,从而保证每个线程获取同步状态都是先到先得的顺序执行的。非公平锁则是通过竞争的方式获取,不管同步队列是否存在Node结点,只有通过竞争获取就可以获取线程执行权。
CountDownLatch
用来控制一个或者多个线程等待多个其他的线程。
维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。
final int totalThread = 10;
CountDownLatch countDownLatch = new CountDownLatch(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("run..");
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("end");
executorService.shutdown();
Semaphore
Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。
以下代码模拟了对某个服务的并发请求,每次只能有 3 个客户端同时访问,请求总数为 10。
final int clientCount = 3;
final int totalRequestCount = 10;
Semaphore semaphore = new Semaphore(clientCount);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalRequestCount; i++) {
executorService.execute(()->{
try {
semaphore.acquire();
System.out.print(semaphore.availablePermits() + " ");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
});
}
executorService.shutdown();
BlockingQueue
java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:
FIFO 队列 :LinkedBlockingQueue、ArrayBlockingQueue(固定长度)
优先级队列 :PriorityBlockingQueue
提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。
使用 BlockingQueue 实现生产者消费者问题
public class ProducerConsumer {
private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
private static class Producer extends Thread {
@Override
public void run() {
try {
queue.put("product");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("produce..");
}
}
private static class Consumer extends Thread {
@Override
public void run() {
try {
String product = queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("consume..");
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
Producer producer = new Producer();
producer.start();
}
for (int i = 0; i < 5; i++) {
Consumer consumer = new Consumer();
consumer.start();
}
for (int i = 0; i < 3; i++) {
Producer producer = new Producer();
producer.start();
}
}
produce..produce..consume..consume..produce..consume..produce..consume..produce..consume..
生产者消费者:
public class Consumer extends Thread {
List<Object> container;
/*表示当前线程共生产了多少件物品*/
private int count;
public Consumer(String name, List<Object> container) {
super(name);
this.container = container;
}
@Override
public void run() {
while(true){
synchronized (container) {
try {
if (container.isEmpty()) {
//仓库已空,不能消 只能等
container.wait(20);
} else {
// 消费
container.remove(0);
this.count++;
System.out.println("消费者:" + getName() + " 共消费了:" + this.count + "件物品,当前仓库里还有" + container.size() + "件物品");
container.notifyAll(); // 唤醒等待队列中所有线程
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class Producer extends Thread{
List<Object> container;
/*表示当前线程共生产了多少件物品*/
private int count;
public Producer(String name, List<Object> container) {
super(name);
this.container = container;
}
@Override
public void run() {
while (true) {
synchronized (container) {
try {
// 如果某一个生产者能执行进来,说明此线程具有container对象的控制权,其它线程(生产者&消费者)都必须等待
if (container.size() == 10) {
// 假设container最多只能放10个物品,即仓库已满
container.wait(10); //表示当前线程需要在container上进行等待
} else {
// 仓库没满,可以放物品
container.add(new Object());
this.count++;
System.out.println("生产者:" + getName() + " 共生产了:" + this.count + "件物品,当前仓库里还有" + container.size() + "件物品");
// 生产者生产了物品后应通知(唤醒)所有在container上进行等待的线程(生产者&消费者)
// 生:5, 消:5
// container.notify(); // 随机唤醒一个在等待队列中的线程
container.notifyAll(); // 唤醒等待队列中所有线程
}
} catch (InterruptedException e) {
e.printStackTrace();
}
} //
}
}
}
死锁:
static class FirstThread implements Runnable {
Object object1;
Object object2;
public FirstThread(Object object1, Object object2) {
this.object1 = object1;
this.object2 = object2;
}
@Override
public void run() {
synchronized (object1) {
// 获得锁对象1
try {
//sleep的目的:为了让两个线程都能分别获得自己的对象锁,
// 防止某个线程先启动,先获得第二把锁结束退出,从而构不成死锁
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (object2) {
// 获得锁对象2
}
}
}
}
static class SecondThread implements Runnable {
Object object1;
Object object2;
public SecondThread(Object object1, Object object2) {
this.object1 = object1;
this.object2 = object2;
}
@Override
public void run() {
synchronized (object2) {
// 获得锁对象2
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (object1) {
// 获得锁对象1
}
}
}
}
public static void main(String[] args) {
Object object1 = new Object();
Object object2 = new Object();
Thread thread1 = new Thread(new FirstThread(object1, object2));
Thread thread2 = new Thread(new SecondThread(object1, object2));
thread1.start();
thread2.start();
}
两线程1-100分别打印奇偶数(1+3种写法)
法一:乐观锁CAS
//volatile目的实现线程间的可见性,不然默认线程间的变量相互独立
public static volatile boolean flag = true;//为奇数
public static AtomicInteger num = new AtomicInteger(1);
public static void main(String[] args) {
Thread a = new Thread(new Single("奇数"));
Thread b = new Thread(new Double("偶数"));
a.start();
b.start();
}
public static class Single extends Thread{
String name;
public Single(String name) {
this.name=name;
}
@Override
public void run() {
while(num.get() < 100) {
if(flag) {
System.out.println(name + ":" + Thread.currentThread() + " " + num.getAndIncrement());
flag = false;
}
}
}
}
public static class Double extends Thread{
String name;
public Double(String name) {
this.name=name;
}
@Override
public void run() {
while(num.get() <= 100) {
if(!flag) {
System.out.println(name + ":" + Thread.currentThread() + " " + num.getAndIncrement());
flag = true;
}
}
}
}
法二:悲观锁synchronized
写法一:用一个lock对象来上锁,然后flag来判断(稍微麻烦了点)
注意 奇数是 < 100 偶数是<=100
//volatile目的实现线程间的可见性,不然默认线程间的变量相互独立
// flag = 0 now odd flag = 1 now even
public static volatile boolean flag = true;//为奇数
public static final Object lock = new Object();
public static volatile int num = 0;
public static void main(String[] args) {
Thread a = new Thread(new Single("奇数"));
Thread b = new Thread(new Double("偶数"));
a.start();
b.start();
}
public static class Single extends Thread{
String name;
public Single(String name) {
this.name=name;
}
@Override
public void run() {
while(num < 100) {
synchronized (lock) {
if (flag) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread() + " " + num++);
flag = true;
lock.notifyAll();
}
}
}
}
public static class Double extends Thread{
String name;
public Double(String name) {
this.name=name;
}
@Override
public void run() {
while(num <= 100) {
synchronized (lock) {
if (!flag) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread() + " " + num++);
flag = false;
lock.notifyAll();
}
}
}
}
写法二:直接用个counter对象来计数,包括上锁
public class ThreadTest3 {
public static void main(String[] args) {
Counter counter = new Counter();
new Thread(new PrintOdd(counter)).start();
new Thread(new PrintEven(counter)).start();
}
}
class Counter {
public int value = 1;
public boolean odd = true;
}
class PrintOdd implements Runnable {
public Counter counter;
public PrintOdd(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
while (counter.value <= 100) {
synchronized(counter) {
if (counter.odd) {
System.out.println(counter.value);
counter.value++;
counter.odd = !counter.odd;
//很重要,要去唤醒打印偶数的线程
counter.notify();
}
try {
counter.wait();
} catch (InterruptedException e) {
}
}
}
}
}
class PrintEven implements Runnable {
public Counter counter;
public PrintEven(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
while (counter.value <= 100) {
synchronized (counter) {
if (!counter.odd) {
System.out.println(counter.value);
counter.value++;
counter.odd = !counter.odd;
counter.notify();
}
try {
counter.wait();
} catch (InterruptedException e) {
}
}
}
}
}
写法三:最简洁的写法
因为这里我们就两个线程,没别的了,所以没必要搞什么判断,直接先唤醒另一个,再把当前这个休眠即可。
public class Demo2 {
private static volatile int i = 1;
public static void main(String[] args) throws Exception {
final Object obj = new Object();
Runnable runnable = new Runnable() {
@Override
public void run() {
synchronized (obj) {
for (; i < 10; ) {
System.out.println(Thread.currentThread().getName() + " " + (i++));
try {
obj.notifyAll();
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
obj.notifyAll();
}
}
};
Thread t1 = new Thread(runnable, "打印偶数的线程 ");
Thread t2 = new Thread(runnable, "打印奇数的线程 ");
t2.start();
t1.start();
}
}
三个线程交替顺序打印ABC
线程本来是抢占式进行的,要按序交替,所以必须实现线程通信,那就要用到等待唤醒。可以使用同步方法,也可以用同步锁。
public class TestLoopPrint {
public static void main(String[] args) {
AlternationDemo ad = new AlternationDemo();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
ad.loopA();
}
}
}, "A").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
ad.loopB();
}
}
}, "B").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
ad.loopC();
}
}
}, "C").start();
}
}
class AlternationDemo {
private int number = 1;//当前正在执行的线程的标记
private Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
public void loopA() {
lock.lock();
try {
if (number != 1) {
//判断
condition1.await();
}
System.out.println(Thread.currentThread().getName());//打印
number = 2;
condition2.signal();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
public void loopB() {
lock.lock();
try {
if (number != 2) {
//判断
condition2.await();
}
System.out.println(Thread.currentThread().getName());//打印
number = 3;
condition3.signal();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
public void loopC() {
lock.lock();
try {
if (number != 3) {
//判断
condition3.await();
}
System.out.println(Thread.currentThread().getName());//打印
number = 1;
condition1.signal();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
}
会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。ThreadLocal而是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据。
数据结构:
每个Thread中都有一个ThreadLocalMap对象,而ThreadLocalMap中存储的是多个ThreadLocal对象。
Thread类有一个类型为ThreadLocal.
ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。
ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。
每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
ThreadLocalMap有点类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构,只是单纯的数组实现。
解决哈希冲突的方式:
和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。
所以这里引出的良好建议是:每个线程只存一个变量即可,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal。【不建议:如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。】
会不会导致内存溢出
会的。无非就是线程的threadLocals放入的entry没有被及时的remove掉,这是一个坏习惯,正常我们在set后需要及时的remove掉。
一个线程对应一块工作内存,线程可以存储多个ThreadLocal。那么假设,开启1万个线程,每个线程创建1万个ThreadLocal,也就是每个线程维护1万个ThreadLocal小内存空间,而且当线程执行结束以后,假设这些ThreadLocal里的Entry还不会被回收,那么将很容易导致堆内存溢出。
JVM提供解决方案是把ThreadLocal里的Entry设置为弱引用,当垃圾回收的时候,回收ThreadLocal。
Key使用强引用:也就是上述说的情况,引用ThreadLocal的对象被回收了,ThreadLocal的引用ThreadLocalMap的Key为强引用并没有被回收,如果不手动回收的话,ThreadLocal将不会回收那么将导致内存泄漏。
Key使用弱引用:引用的ThreadLocal的对象被回收了,ThreadLocal的引用ThreadLocalMap的Key为弱引用,如果内存回收,那么将ThreadLocalMap的Key将会被回收,ThreadLocal也将被回收。value在ThreadLocalMap调用get、set、remove的时候就会被清除。
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
虽然JVM有保障了,但还是有内存泄漏风险。
ThreadLocalMap使用ThreadLocal对象作为弱引用,当垃圾回收的时候,ThreadLocalMap中Key将会被回收,也就是将Key设置为null的Entry。如果线程迟迟无法结束,也就是ThreadLocal对象将一直不会回收,回顾到上面存在很多线程+TheradLocal,那么也将导致内存泄漏。(内存泄露的重点)
首先来说,如果把ThreadLocal置为null,那么意味着Heap中的ThreadLocal实例不在有强引用指向,只有弱引用存在,因此GC是可以回收这部分空间的,也就是key是可以回收的。但是value却存在一条从Current Thread过来的强引用链。因此只有当Current Thread线程销毁时,value才能得到释放。
因此,只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间内不会被回收的,就发生了我们认为的内存泄露。最要命的是线程对象不被回收的情况,比如使用线程池的时候,线程结束是不会销毁的,再次使用的,就可能出现内存泄露。
其实,在ThreadLocal中,当调用remove、get、set方法的时候,会清除为null的弱引用,也就是回收ThreadLocal。
ThreadLocal提供一个线程(Thread)局部变量,访问到某个变量的每一个线程都拥有自己的局部变量。说白了,ThreadLocal就是想在多线程环境下去保证成员变量的安全。
ThreadLocal和Synchronized区别
ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,
不同的点是:
Synchronized是通过线程等待,牺牲时间来解决访问冲突;
ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。
正因为ThreadLocal的线程隔离特性,使他的应用场景相对来说更为特殊一些。在android中Looper、ActivityThread以及AMS中都用到了ThreadLocal。当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。
适用于无状态,副本变量独立后不影响业务逻辑的高并发场景。如果如果业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决,需要另寻解决方案。