一步一步学多线程-synchronized
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一步一步学多线程-synchronized相关的知识,希望对你有一定的参考价值。
当线程执行请求synchronized方法或块时,monitor会设置几个虚拟逻辑数据结构来管理这些多线程。
请求的线程会首先被加入到线程排队队列中,线程阻塞,当某个拥有线程锁的线程unlock之后,则排队队列里的线程竞争上岗(synchronized是不公平竞争锁),如果运行的线程调用对象wait()后就释放锁并进入wait线程集合那边,当调用对象的notify()或notifyall()后,wait线程就到排队那边。
重量级锁
在JVM规范中描述:每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1、 如果monitor的进入数为0,则该线程进入monitor,如果将进入数设置为1,该线程即为monitor的所有者。
2、 如果线程已经占有monitor,只是重新进入,则进入monitor的进入数加1.
3、 如果其他线程已经占用了monitor,则该线程进入阻塞状态,知道monitor的进入数为0,再重新尝试获取monitor的所有权。
Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。
但是其本质又是依赖于底层的操作系统的互斥锁(Mutex Lock)来实现的。而操作系统实现线程之间的切换这就需要用户转换到和心态,这个成本非常高,状态之间的转换需要相对较长的时间,这就是为什么Synchronized效率低的原因。
因此,这种依赖于操作系统互斥锁(Mutex Lock)所实现的锁我们称之为“重量级锁”。
当多线程环境进入synchronized区域的线程没有竞争时,JVM并不会马上创建重量级锁,而是使用偏向锁或者轻量级锁,当存在资源竞争的情况下才会使用重量级锁。
轻量级锁
轻量级锁的核心思想:被加锁的代码不会发生并发,如果发生并发,那就膨胀成重量级锁(膨胀即是锁升级)。
根据轻量级锁的实现,我们知道虽然轻量级锁不支持并发,遇到并发就要膨胀为重量级锁,但是轻量级锁可以支持多个线程以串行的方式访问同一个锁对象。
偏向锁
偏向锁的核心思想:假设加锁的代码自始至终只有一个线程调用,如果发现多于一个线程调用,即使没有线程间竞争,也会把锁升级为轻量级锁。
自旋锁
当线程阻塞后,如果进入排队队列需要CPU从用户态转为核心态,尤其当遇到频繁的阻塞和唤醒对CPU来说负荷很重。统计发现,很多对象锁的锁定状态持续的时间很短,此时在这么短的时间内进行线程频繁切换资源耗费严重。所以此时引出了自旋锁的概念。
所谓“自旋”,就是monitor并不把线程阻塞放入排队队列,而是去执行一个无意义的循环,循环结束后看看是否锁已释放并直接进行竞争上岗步骤,如果竞争不到继续自旋循环,循环过程中线程的状态一直处于running状态。明显自旋锁似的synchronized的对象锁方式在线程之间引入了不公平。但是这样可以保证大吞吐率和执行效率。
不过虽然自旋锁方式省去了阻塞线程的时间和空间(队列的维护等)开销,但是长时间自旋也是很低效的。所以自旋的次数一般控制在一个范围内,如10,50等(在JDK1.6中默认为10次),在超出这个范围后,线程就进入排队队列。
自适应自旋锁
就是自旋的次数是通过JVM在运行时收集的统计信息,动态调整自旋锁的自旋次数上界。
对象头
介绍了这几种锁,那么程序是通过什么来实现对象锁的呢?首先来看对象头的结构。
在Hotspot虚拟机的对象头上主要包括两部分数据:Mark Word(标记字段),Klass Pointer(类型指针)。其中Klass Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是各种锁的关键。
Mark Word中的结构大致如此
轻量级锁的获取和释放
获取锁
1、 判断当前对象是否处于无锁状态,若是,则JVM首先将当前线程的栈帧中建立一个名为所记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。否则执行步骤3。
2、 JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00(表示此对象处于轻量级锁状态),执行同步操作,如果失败则执行步骤3。
3、 判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态。
释放锁
轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下
1、 取出在获取轻量级锁保存在Mark Word中的数据;
2、 用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明锁释放成功,否则执行3.
3、 如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要在释放所的同时需要唤醒被挂起的线程。
偏向锁的释放和获取
获取锁
1、 检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁表示为01;
2、 若为可偏向状态,则测试线程ID是否为为当前线程ID,如果是,则执行步骤5,否则执行步骤3。
3、 如果线程ID不是当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行步骤4。
4、 通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块。
5、 执行同步代码块。
释放锁
偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程会是不会主动释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要等待全局安全点,步骤如下:
1、 暂停拥有偏向锁的线程,判断锁对象是否还处于被锁定状态
2、 撤销偏向锁,恢复到无锁状态或轻量级锁的状态。
参考资料
以上是关于一步一步学多线程-synchronized的主要内容,如果未能解决你的问题,请参考以下文章