synchronized
1,synchronized关键字用法
①修饰代码块,指定加锁对象,表示进入当前代码块要获得指定对象的锁。
②修饰实例方法:对当前对象加锁,进入同步方法要获得当前对象的锁。
③修饰静态方法,给当前类加锁,作用于类的所有对象,访问静态方法要获得class锁。
2,介绍一下synchronized
①,解决多个线程访问资源的同步性,synchronized关键字保证它修饰的方法或者代码块在任意时刻只有一个线程访问。
②java早期版本sychronized是重量级锁,效率较低
因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock
来实现的,Java 的线程是映射到操作系统的原生线程之上的。
如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,
这个状态之间的转换需要相对比较长的时间,时间成本相对较高。
③后期优化,引入偏向锁,轻量级锁等。
2.1,底层原理:
①java中每一个对象都可以作为锁,每一个Object对像都有一个Monitor监视器,
②synchronized同步语句块使用monitorenter和monitorexit指令分别指向同步代码块的开始与结束,当执行monitorenter时,尝试获取对象锁,
对象锁计数器为0则表示该对象锁可获取,获取后,对象锁加1.
③当同步方法时,用到ACC_SYNCHRINIZED标识,表示该方法是同步方法。monitorenter和monitorexit指令与ACC_SYNCHRINIZED标识
都是为了获取对象锁。
3,synchronized(底层原理)
syncronized用的锁存在java对象的对像头里。
3.1,什么是对象头?
①Java对象在堆中的基本的基本结构分三部分(有点像计算机网络中报文或者帧的结构,前缀+数据+后缀):header(对相头)+实例数据+对齐填充。
②对象头,非数组对象头为两个字宽(32位一字宽位332bit,64位为64bit)为Mark Word(标识位,标识锁状态等信息,)部分和,Class Metadata Address
(储存到对象类型数据的指针),如果是数组对象用三个字宽,加上数组长度Array length。
③Mark Word的储存结构:
(锁轻量,重量,偏向指的是synchronized锁升级后引入的),对象头里储存锁状态信息,锁的类型,包括持有锁的线程ID等信息。
其中重量锁(升级前只有重量锁)状态下,Mark Word中有指向对象关联的Monitor(监视器锁)的指针。偏向锁状态下,储存有当前
持有该对象的线程ID.
3.2,什么是Monitor?
①一种实现同步的工具,每个java对象都可以关联一个Monitor对象,(每个java对象都可以是锁。)
②是实现synchronized内置锁的关键。
- Monitor对象如何与java对象关联,
<1>Java对象被某线程获取锁,对象头Mark Word中会储存指向Monitor的指针。
<2>Monitor对象的Owner字段会存放相关联的对象的获取其对象锁的线程ID,
每个对象关联一个Monitor锁,Monitor锁,有一个阻塞队列,用于存放阻塞等待抢锁线程
3.3,synchronized锁优化
3.3.1synchronized是一种排他锁(只能有一个线程持有锁)也是一种可重入锁(已持有锁的线程再次获得锁。避免自己将自己阻塞,排他,不排自己)
优化之前(加锁机制):
①如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的owner
②如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
③如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权
优化之后:
引入偏向锁,轻量级锁,重量级锁,自旋锁。
优化和通过synchronized加锁的过程是: 偏向锁-->轻量级锁-->重量级锁
3.3.2偏向锁:为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径
<1>即当某线程第一个获得某对象锁,设置为偏向锁,Mark Word中标识为偏向锁,并储存当前偏向线程ID,
<2>当下一个线程来竞争锁的时候,先比较偏向线程ID,如果是偏向线程,直接获得锁,不需要指行3.3.1中的②步骤,
对锁标识位每增加1,就要有一个-1与之对应。如果不是偏向线程,则先CAS操作抢锁,CAS成功(原偏向线程执行完或者中断退出)
若CAS不成功,就产生竞争,准备撤销偏向锁。
<3>撤销过程:(图片参考(复制)自synchronized原理)
3.3.3,轻量级锁:引入自旋锁,减少竞争失败立马阻塞带来的系统开销
- 线程阻塞开销:
java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,
需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,
专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的
一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
- 但自旋(死循环,失败了不立马阻塞,接着抢一会,)同样带来CPU消耗,若以要规定自旋时长(重复抢锁次数)到一定
还没抢到,升级重量级锁,在抢。
<1>线程通过CAS获取锁,成功-获取锁执行。不成功则有竞争,进入自旋抢锁阶段,自旋一定次数仍抢不到表示竞争激烈
升级为重量级锁。
3.3.4,重量级锁:线程竞争失败会进入阻塞队列,挂起阻塞等待唤醒。
3.3.5,优化总结:
①引入偏向锁的目的:在只有单线程执行情况下,尽量减少不必要的轻量级锁执行路径,轻量级锁的获取及释放依赖多次CAS原子指令,
而偏向锁只依赖一次CAS原子指令置换ThreadID,之后只要判断线程ID为当前线程即可,偏向锁使用了一种等到竞争出现才释放锁的机制,
消除偏向锁的开销还是蛮大的。如果同步资源或代码一直都是多线程访问的,那么消除偏向锁这一步骤对你来说就是多余的,
可以通过-XX:-UseBiasedLocking=false来关闭
②引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗(用户态和核心态转换),但是如果多个
线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁
③重入:对于不同级别的锁都有重入策略,偏向锁:单线程独占,重入只用检查threadId等于该线程;轻量级锁:重入将栈帧中lock record的header
设置为null,重入退出,只用弹出栈帧,直到最后一个重入退出CAS写回数据释放锁;重量级锁:重入_recursions++,重入退出_recursions--,_recursions=0时释放锁
3.4,synchronized锁,等待/唤醒机制:
3.4.1,进入重量级锁阻塞队列的线程,需要外界来唤醒,
Object类中:
wait() 调用该对象的线程,进入WATING状态,只能等待另外线程唤醒。调用wait()后会释放对象锁。
wait(long) 超时唤醒,若在设置时间(毫秒)内没有被其他线程唤醒,则自动唤醒位RUNNABLE状态,竞争抢锁。
wait(long,int) 时间单位更精确(可达到纳秒)
notify() 通知等待队列的一个线程,使其从wait()方法中返回,返回的前提是获取到对象锁
notifyAll() 通知所有该对象等待队列线程。
3.4.2,wait(),和sleep()的区别:
-
-
- wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
- wait() 会释放锁,sleep() 不会
-
参考: