简单说说synchronized(参考http://ddrv.cn/a/36370/amp)
由monitor管程去的ObjectMonitor来实现请求和等待的,ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入_EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。准确来讲上面讲的属于Synchronized重量级锁的实现部分,Synchronized在进行重量级锁之前会先历经,偏向锁,轻量级锁和自旋锁,不直接切换成重量级锁的原因是重量级锁需要进行操作系统Mutex Lock来实现,线程之间的切换需要从用户状态到核心态。
Synchronized
synchronized 它可以把任意一个非 NULL 的对象当作锁。
(1)作用于方法时,锁住的是对象的实例(this);
(2) 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen (jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁, 会锁所有调用该方法的线程;
(3)synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。
分类
按修饰对象分类
同步方法
- 非静态 public synchronized void methodName(){}
- 静态方法 public synchronized static void methodName(){}
同步代码块
- synchronized(this | object){}
按获取的锁来分类
对象锁
- 修饰非静态方法
- 每个对象有个monitor对象,这个对象其实就是 Java 对象的锁,类的每个对象有各自的monitor对象
- 某一线程占有这个对象的时候,先判断monitor 的计数器是不是0,如果是0还没有线程占有,这个时候线程占有这个对象,并且对这个对象的monitor+1;如果不为0,表示这个线程已经被其他线程占有,这个线程等待。当线程释放占有权的时候,monitor-1
- 同一线程可以对同一对象进行多次加锁,具有重入性
- synchronized(类.class){}
类锁
- 修饰静态方法
- 类锁实际上是通过对象锁实现的,即类的 Class 对象锁。每个类只有一个 Class 对象,所以每个类只有一个类锁,即这个类的所有对象用的一把锁
Synchronize核心组件
- Wait Set:哪些调用wait方法被阻塞的线程被放置在这里;
- Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
- Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;
- OnDeck:任意时刻,多只有一个线程正在竞争锁资源,该线程被成为OnDeck;
- Owner:当前已经获取到所资源的线程被称为Owner;
- !Owner:当前释放锁的线程
- JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下, ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将 一部分线程移动到EntryList中作为候选竞争线程。
- Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定 EntryList中的某个线程为OnDeck线程(一般是先进去的那个线程)。
- Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck, OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在 JVM中,也把这种选择行为称之为“竞争切换”。
- OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList 中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify 或者notifyAll唤醒,会重新进去EntryList中。
- 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统 来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。
- Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先 尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是 不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁 资源。
- 每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加 上monitorenter和monitorexit指令来实现的,方法加锁是通过一个标记位来判断的
- synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线 程加锁消耗的时间比有用操作消耗的时间更多。
- Java1.6,synchronized进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做 了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。
- 锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;
synchronized的执行过程(这里)
1. 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁
2. 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1
3. 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
4. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁
5. 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
6. 如果自旋成功则依然处于轻量级状态。
7. 如果自旋失败,则升级为重量级锁。
ReentrantLock 与 synchronized
- ReentrantLock 通过方法lock()与unlock()来进行加锁与解锁操作,与synchronized会 被 JVM 自动解锁机制不同,ReentrantLock 加锁后需要手动进行解锁。为了避免程序出 现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作。
- ReentrantLock相比synchronized的优势是可中断、公平锁、多个锁。这种情况下需要 使用ReentrantLock
Condition 和 Object锁方法区别
- Condition类的awiat方法和Object类的wait方法等效
- Condition类的signal方法和Object类的notify方法等效
- Condition类的signalAll方法和Object类的notifyAll方法等效
- ReentrantLock类可以唤醒指定条件的线程,而object的唤醒是随机的
锁优化
锁消除
-
JIT在编译的时候把不必要的锁去掉
粗化(ReentrantReadWriteLock )
-
大部分情况下我们是要让锁的粒度最小化,锁的粗化则是要增大锁的粒度;在以下场景下需要粗化锁的粒度:假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都进出一次临界区,效率是非常差的;
场景:
-
ReentrantReadWriteLock 是一个读写锁,读操作加读锁,可以并发读,写操作使用写锁,只能单线程写;
-
CopyOnWriteArrayList 、CopyOnWriteArraySet,CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
-
CopyOnWrite并发容器用于读多写少的并发场景,因为,读的时候没有锁,但是对其进行更改的时候是会加锁的,否则会导致多个线程同时复制出多个副本,各自修改各自的;
-
cas如果需要同步的操作执行速度非常快,并且线程竞争并不激烈,这时候使用cas效率会更高,因为加锁会导致线程的上下文切换,如果上下文切换的耗时比同步操作本身更耗时,且线程对资源的竞争不激烈,使用volatiled+cas操作会是非常高效的选择;
减少锁的时间 和 减少锁的粒度
- 时间:不需要同步执行的代码,能不放在同步快里面执行就不要放在同步快内,可以让锁尽快释放;
- 粒度:它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争。它的思想也是用空间来换时间;
场景:
- LongAdder 实现思路也类似ConcurrentHashMap,LongAdder有一个根据当前并发状况动态改变的Cell数组,Cell对象里面有一个long类型的value用来存储值;开始没有并发争用的时候或者是cells数组正在初始化的时候,会使用cas来将值累加到成员变量的base上,在并发争用的情况下,LongAdder会初始化cells数组,在Cell数组中选定一个Cell加锁,数组有多少个cell,就允许同时有多少线程进行修改,最后将数组中每个Cell中的value相加,在加上base的值,就是最终的值;cell数组还能根据当前线程争用情况进行扩容,初始长度为2,每次扩容会增长一倍,直到扩容到大于等于cpu数量就不再扩容,这也就是为什么LongAdder比cas和AtomicInteger效率要高的原因,后面两者都是volatile+cas实现的,他们的竞争维度是1,LongAdder的竞争维度为“Cell个数+1”为什么要+1?因为它还有一个base,如果竞争不到锁还会尝试将数值加到base上;
- LinkedBlockingQueue也体现了这样的思想,在队列头入队,在队列尾出队,入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高;
- sun.misc.Contended注解会在变量前面添加128字节的padding将当前变量与其他变量进行隔离;
锁分离
- 常见的锁分离就是读写锁ReadWriteLock。比如 LinkedBlockingQueue 从头部取出,从尾部放数据。
工具和方法
Jconsole、 Jstack pid、Javap -V 反编译
反编译发现
代码块的加锁
monitorenter和monitorExit配合使用
- 每一个 JAVA 对象都会与一个监视器 monitor 关联,我们 可以把它理解成为一把锁,当一个线程想要执行一段被 synchronized 修饰的同步方法或者代码块时,该线程得先 获取到 synchronized 修饰的对象对应的 monitor。 monitorenter 表示去获得一个对象监视器。monitorexit 表 示释放 monitor 监视器的所有权,使得其他被阻塞的线程 可以尝试去获得这个监视器 monitor 依赖操作系统的 MutexLock(互斥锁)来实现的, 线 程被阻塞后便进入内核(Linux)调度状态,这个会导致系 统在用户态与内核态之间来回切换,严重影响锁的性能
前面我们在讲 synchronized 的时候,发现被阻塞的线程什 么时候被唤醒,取决于获得锁的线程什么时候执行完同步 代码块并且释放锁。那怎么做到显示控制呢?我们就需要 借 助 一 个 信 号 机 制 : 在 Object 对 象 中 , 提 供 了 wait/notify/notifyall,可以用于控制线程的状态
注::三个方法都必须在 synchronized 同步关键 字 所 限 定 的 作 用 域 中 调 用 , 否 则 会 报 错 java.lang.IllegalMonitorStateException ,意思是因为没有 同步,所以线程对对象锁的状态是不确定的,不能调用这 些方法。 另外,通过同步机制来确保线程从 wait 方法返回时能够感 知到感知到 notify 线程对变量做出的修改
注:
wait、notify为什么定义在Object里面 (参考https://www.cnblogs.com/lirenzhujiu/p/5927241.html)
多个线程通信 -> 共享内存 -> 条件控制(共享资源) -> 共享资源竞争 -> 互斥特性 -> Synchronized 对象有关 ->
锁的相关数据是存储在对象头的 Synchronized(共享资源){
// 一定是对象
共享资源.wait();
Thread.wait();
Thread.notify();
共享资源.notify(); }
so
- 从synchronized说:synchronized中的这把锁可以是任意对象,所以任意对象都可以调用wait()和notify()
- 从方法来说:这些方法在操作同步线程时,都必须要标识它们操作线程的锁,只有同一个锁上的被等待线程,可以被同一个锁上的notify唤醒,不可以对不同锁中的线程进行唤醒。也就是说,等待和唤醒必须是同一个锁。而锁可以是任意对象,所以可以被任意对象调用的方法是定义在object类中
- 共享资源来说,是对对象属性的共享
在jdk1.5以后,将同步synchronized替换成了Lock,将同步锁对象换成了Condition对象,并且Condition对象可以有多个,Condition中的await(), signal().signalAll()代替Object中的wait(),notify(),notifyAll()。白话说就是设置条件,唤醒哪种线程,从而提高性能。
对方法的加锁的标志
ACC_SYNCHRONIZED
使用synchronized注意的问题
与moniter关联的对象不能为空
synchronized作用域太大
不同的monitor企图锁相同的方法
多个锁的交叉导致死锁
synchronized锁存在四种状态(参考这)
加锁的基础
对象头
hashCode的作用:HashSet
无锁状态:锁标志位01
偏向锁
(总是由同一个线程多次获得推荐使用):注意点:线程Id
在对象第一次被某一线程占有的时候,偏向锁置1,锁标志位为01,写入线程ID。
当有其他的线程访问的时候
- 原获得偏向锁的线程如果已经退出了临界区,也就是同 步代码块执行完了,那么这个时候会把对象头设置成无 锁状态并且争抢锁的线程可以基于 CAS 重新偏向当前线程
- 如果原获得偏向锁的线程的同步代码块还没执行完,处 于临界区之内,这个时候会把原获得偏向锁的线程升级 为轻量级锁后继续执行同步代码块
可通过jvm参数UseBiasedLocking 来设置开启或关闭偏向锁
轻量级锁:
线程竞争性不是很强
升级过程
- 线程在自己的栈桢中创建锁记录 LockRecord。
- 将锁对象的对象头中的MarkWord复制到线程的刚刚创 建的锁记录中。
- 将锁记录中的 Owner 指针指向锁对象
- 将锁对象的对象头的 MarkWord替换为指向锁记录的指 针。
之后将Mark Word的锁标志位设置为“00”
自旋锁:指当有另外一个线程来竞争锁时,竞争失败的时候,不是马上转化级别,而是执行几次空循环
重量级锁:
强互斥。在轻量锁中,通过 CAS 操作把线程栈帧(可以多个线程)中的 LockRecord 替换回到锁对象的 MarkWord 中,如果成功表示没有竞争。如果失败,表示 当前锁存在竞争,那么轻量级锁就会膨胀成为重量级锁
小总:https://blog.csdn.net/zqz_zqz/article/details/70233767中的图