synchronized实现原理

前言:

synchronized的简介

Synchronized的用法就是为了避免因资源抢占导致的数据错乱,从而让线程进行同步,保证同步内的对象只有一个线程来执行值更改使用操作,是并发控制中必不可少的部分,懂的都懂,今天来深扒一下它

synchronized的特性

原子性、指一个操作或多个操作,要么全部执行并且执行的过程并不会被任何因素打断,要么就都不执行,比如

int i=1;这个操作就是原子性要么执行要么不执行,而i++就不是原子性的,因为包含了读取、计算、赋值几步,原值可能还没完成时就已经被赋值了,而原子性保证了执行过程不会被中断

synchronized与volatile的最大区别就是原子性,volatile不具备原子性

可见性、指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的,比如

int i =1;这个值如果是全局变量,在synchronized内执行变化它时,其他线程也可以读取到当前i的值

有序性、程序执行的顺序都是按照代码先后顺序执行,由于Java允许编译器和处理器对指令进行重排,但他并不影响单线程的顺序,而是影响了多线程的并发执行顺序性,synchronized则保证了同步代码块内是有顺序的

可重入性、就是拥有了这个锁还能重复申请

一、synchronized的使用

1.修饰实例方法

锁的是当前实例对象,进入同步代码获得当前实例

public synchronized void add(){...};

反编译时,add方法的flags多了一个ACC_SYNCHRONIZED标志,这标志用来告诉JVM这是一个同步方法,在进入该方法之前先获取相应的锁,锁的计数器加1,方法结束后计数器-1,如果获取失败就阻塞住,直到该锁被释放。

2.修饰静态方法

锁的是当前类,进入同步代码前获取当前类对象

public static synchronized void add(){...};

3.同步代码块

锁括号内的对象或当前类

public class Test{

  private Test instance = new Test();

  synchronized(instance);  //锁对象

  synchronized(Test.class){...} //锁类

}

反编译时,同步代码块是由monitorenter指令进入,然后monitorexit释放锁

二、为什么任何对象都可以作为锁

详见:JVM内存模型中对象的组成结构

在JVM 中每个对象分为三部分存在:对象头、示例数据、对齐填充

对象头中又有MarkWorld(运行时元数据)

锁状态标志中便记录了加锁的信息

Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的:

偏向锁、轻量级锁、重量级锁的具体使用会在下面具体介绍区别和联系

而查看对象头信息

总结

由于每个对象的Mark Word中都有储存锁的信息,可以说 锁是对象,任何对象都可以作为锁

偏向锁、轻量级锁、重量级锁

偏向锁

只有第一个申请锁的线程会使用锁,有其他线程竞争就膨胀为轻量级锁

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的所记录里存储锁偏向的线程ID,以后该线程进入和退出同步块不需要CAS(Compare and Swap,比较并替换)操作来争夺锁资源。对一个线程的偏向,如果有其他线程竞争才去释放(再替换线程ID即可),当新线程发起替换对象头中的线程ID为自身的CAS请求时,回去判断拥有此偏向锁的线程是否还活着,如果不活着,则置位无锁状态,如果或者则挂起线程,并将只想当前线程的锁记录地址放入头对象,膨胀成轻量级锁,然后恢复持有锁的线程

ps:当前线程挂起再恢复的过程中并没有发声锁的转移,只是“将头对象中的线程ID变更为只想锁记录地址的指针”,偏向锁是在单线程执行代码块时使用的机制,如果多线程并发时(线程A并未执行完同步代码块,B线程发起了锁的申请),则一定会转化为轻量级锁或重量级锁。

轻量级锁

一个线程自旋等待持有锁线程释放锁,若自旋后还没获得膨胀为重量级锁

每次线程想进入同步代码块的时候,都得通过CAS尝试将对象头中的所指针替换为自身栈中的记录,如果没有成功,则进入而自适应的自旋(动态改变自旋等待次数,有另外一个线程来竞争锁时,线程在原地循环等待而不是阻塞,获得锁的线程释放后就立马获得锁),如果自适应自旋转时还没有获得锁,则膨胀为重量锁

重量级锁

实际的多线程阻塞等待,切换锁的过程

通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

应用场景

偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁

轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的所竞争

重量级锁:有实际竞争,且所竞争时间长

三、synchronized的底层实现

每个对象都有一个监视器锁monitor 当monitor被占用时就会处于锁定状态,执行monitorenter指令时,尝试获取monitor的所有权,过程如下:

前提:

ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程

ObjectMonitor成员变量

  // initialize the monitor, exception the semaphore, all other fields

  // are simple integers or pointers

  ObjectMonitor() {

    _header      = NULL;

    _count        = 0;=============》计数器

    _waiters      = 0,

    _recursions  = 0;

    _object      = NULL;

    _owner        = NULL;===========》所持有该对象的线程

    _WaitSet      = NULL;==============》等待线程集合

    _WaitSetLock  = 0 ;========》保护等待队列简单的自旋锁

    _Responsible  = NULL ;

    _succ        = NULL ;

    _cxq          = NULL ;====》阻塞上entry上最近科大的县城列表,该列表是由waitNode构成,扮演者线程代理

    FreeNext      = NULL ;

    _EntryList    = NULL ;============》入口线程集合

    _SpinFreq    = 0 ;

    _SpinClock    = 0 ;

    OwnerIsThread = 0 ;

    _previous_owner_tid = 0;

  }

ObjectMonitor工作过程

1. 当多一个线程访问同一段同步代码块时,进入_EntryList集合

2.当线程获取到对象的monitor后进入_Owner区域,并把monitor中的owner变量设为当前线程,monitor是依赖于底层操作系统的mutex lock来实现互斥的,线程获取mutex成功,则会持有该mutex这时候其他线程无法获取该metux、并将monitor的count+1(此时表示当前线程持有当前对象并加锁)

3.当线程调用wait()方法后,释放当前持有的monitor既释放所持有的mutex,owner变量恢复为null,count-1,同时进入_WaitSet集合等待调用notify/notifyAll被唤醒

4.若当前线程执行完毕,释放当前持有的monitor,owner变量恢复为null

总结:同步锁在这种实现方式中,因为Monitor是依赖于底层的操作系统实现,这样就存在用户态和内核态之间的切换,所以会增加性能的开销


详细流程图

详细代码

头文件:

http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/runtime/objectMonitor.hpp

实现:

http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/runtime/objectMonitor.cpp

代码太长 详细解析可看具体代码解析

四、其他相关线程同步相关

Volatile实现原理

Volatile只能修饰变量,不能修饰方法或代码块

Volatile变量的可见性

Java虚拟机中定义了一种Java内存模型(Java Memory Model,即JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果,Java的内存模型目标:定义程序中各个变量的访问规则,既在虚拟机中将变量村存储到内存和从内从中去出变量这样的细节

而对于普通变量,线程A修改值后此时该值在此线程的工作内存,尚未同步到主内存时,若常出现B线程使用此变量,此时拿到的是主内存修改前的值,便发生了可见性不一致的问题

volatile可见性的实现就是借助了CPU的lock指令,通过在写volatile的机器指令前加上lock前缀,使写volatile具有以下两个原则:

当Volatile变量执行写操作后,JMM会把工作内存中的最新变量值强制刷新到主内存中

写操作会导致其他线程中的缓存无效

这样,其他线程使用缓存时,发现本地工作内存此变量无效,就会从主内存获取,这样获取到的就是最新的值,实现了线程的可见性

Volatile变量的有序性

volatile是通过编译器在生成字节码时,在指令序列中添加“内存屏障”来禁止指令重排序等

JVM的实现会在volatile读写前后均加上内存屏障,在一定程度上保证有序性。如下所示:

LoadLoadBarrier

volatile 读操作

LoadStoreBarrier

StoreStoreBarrier

volatile 写操作

StoreLoadBarrier

Volatile的使用

public class TestVolatile {

    public static volatile int counter = 1;

    public static void main(String[] args){

        counter = 2;

        System.out.println(counter);

    }

}

字节码层面

volatile在字节码层面,就是使用访问标志:ACC_VOLATILE来表示

Volatile与synchronized区别

1.volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住;

2.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的;

3.volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性;

4.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞;

Lock原理

Lock的使用

由于Lock是一个Java接口,所以需要new它的实现类常用的ReentrantLock

import java.util.concurrent.locks.Lock;

import java.util.concurrent.locks.ReentrantLock;

private Lock lock = new ReentrantLock();

lock.lock();//获取锁

//do something

lock.unlock()//释放锁

lock.trylock()//获取锁 如果锁被占有就放开

而lock的核心类是(AQS)AbstractQueuedSynchronizer 自旋锁

synchronized与Lock的区别

五、各种名称锁总结

详细介绍:https://tech.meituan.com/2018/11/15/java-lock.html

互斥锁/读写锁

互斥锁在Java中的具体实现就是ReentrantLock。

读写锁在Java中的具体实现就是ReadWriteLock。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容