synchronized的分类和原理

简单说说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:当前释放锁的线程

参考这篇博客

  1. JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下, ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将 一部分线程移动到EntryList中作为候选竞争线程。
  2. Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定 EntryList中的某个线程为OnDeck线程(一般是先进去的那个线程)。
  3. Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck, OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在 JVM中,也把这种选择行为称之为“竞争切换”。
  4.  OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList 中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify 或者notifyAll唤醒,会重新进去EntryList中。
  5. 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统 来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。
  6. Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先 尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是 不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁 资源。
  7. 每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加 上monitorenter和monitorexit指令来实现的,方法加锁是通过一个标记位来判断的
  8. synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线 程加锁消耗的时间比有用操作消耗的时间更多。
  9. Java1.6,synchronized进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做 了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。
  10. 锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;

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  反编译

反编译发现

代码块的加锁

monitorentermonitorExit配合使用

  • 每一个 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。

当有其他的线程访问的时候

  1. 原获得偏向锁的线程如果已经退出了临界区,也就是同 步代码块执行完了,那么这个时候会把对象头设置成无 锁状态并且争抢锁的线程可以基于 CAS 重新偏向当前线程
  2. 如果原获得偏向锁的线程的同步代码块还没执行完,处 于临界区之内,这个时候会把原获得偏向锁的线程升级 为轻量级锁后继续执行同步代码块

可通过jvm参数UseBiasedLocking 来设置开启或关闭偏向锁

轻量级锁:

线程竞争性不是很强

升级过程

  1. 线程在自己的栈桢中创建锁记录 LockRecord。
  2. 将锁对象的对象头中的MarkWord复制到线程的刚刚创 建的锁记录中。
  3. 将锁记录中的 Owner 指针指向锁对象
  4. 将锁对象的对象头的 MarkWord替换为指向锁记录的指 针。

 之后将Mark Word的锁标志位设置为“00”

自旋锁指当有另外一个线程来竞争锁时,竞争失败的时候,不是马上转化级别,而是执行几次空循环

重量级锁:

强互斥。在轻量锁中,通过 CAS 操作把线程栈帧(可以多个线程)中的 LockRecord 替换回到锁对象的 MarkWord 中,如果成功表示没有竞争。如果失败,表示 当前锁存在竞争,那么轻量级锁就会膨胀成为重量级锁

小总:https://blog.csdn.net/zqz_zqz/article/details/70233767中的图

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值