synchronized

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() 不会

  

 

参考:

synchronizeed原理-很全

深入理解java中的锁

深入浅出多线程

javaguide

 

 

 

 

posted on 2020-10-09 14:16  呵呵哒9876  阅读(303)  评论(0编辑  收藏  举报