锁(二)

Synchronized 与 锁原理


22.png

一个对象在 JVM 中的布局可以分为三个区域,分别是 对象头,实例数据,对齐填充。
实例数据:存放类的属性数据信息,包括父类的属性数据信息。
对齐填充:由于虚拟机要求,对象起始地址必须是 8 字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。
对象头:Java 对象头一般占有 2 个机器码(在32位虚拟机中,1个机器码等于4字节(32 bit),在64位虚拟机中,1个机器码8个字节(64 bit),但是,如果是数组类型,则需要3个机器码,因为 JVM 虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数据的元数据来确认数组的大小,所以用一块来记录数组长度。)

锁的本质实现就是 : 当一个线程获取到锁就是把它的线程 id 写入锁对象的对象头 , 来判断唯一。

对象头主要包括两部分数据:Mark Word 标记字段,Class Pointer 指针类型。

其中 Class Pointer 是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的对象,
Mark Word 用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。

对象头中 Mark Word 与线程中 Lock Record

Mark Word用于存储对象自身的运行时数据,如:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。

在线程进入同步代码块的时候,如果此同步对象没有被锁定,即他的锁标记位是 01,则虚拟机首先会在当前线程的栈中创建我们称之为 “锁记录(Lock Record)”的空间,用于存储对象的 Mark Word 的拷贝。

Lock Record 是线程私有的数据结构,每一个线程都有一个可用的 Lock Record 列表,同时还有哦一个全局的可用列表。每一个被锁住的对象 Mark Word 都哦会和一个 Lock Record 关联(对象头的 Mark Word 中的 Lock Word 指向 Lock Record 的起始地址),同时 Lock Record 中有一个 Owner 字段存放拥有该锁的线程的唯一标记。表示该锁被这个线程占用。

监视器(Monitor)
每一个对象都有一个 Monitor 对象与之关联,当一个 Monitor 被持有后,它将处于锁状态,synchornized 在 JVM 里面的实现都是基于进入和退出 Monitor 对象来实现方法同步和代码块同步。通过成对的 MonitorEnter 和 MontiorExit 指令来实现。

MonitorEnter 指令:插入在同步代码块的开始位置,当代码执行到该指令时,将尝试获取该对象 Monitor 的所有权,即尝试获取该对象的锁。
MonitorExit 指令,插入在方法结束和异常处,JVM 保证每个 MonitorEnter 必须有对应的 MonitorExit

Monitor 对象存在于每个 Java 对象的对象头 Mark Word 中,synchronidzed 锁便是通过这种方式获取锁的,也是为什么 Java 中任意对象都可以作为锁的原因,同时,notify,notifyAll,wait 等方法会使用到 Monitor 锁对象,所以必须在同步代码块中使用。

Monitor 是由 ObjectMonitor 实现的,其主要数据结构如下:
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}

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 与 Lock 锁的比较
锁的获取与释放

synchronized 是 Java 语言内置的实现,使用简单,无须关注锁的释放与获取,都是 JVM 自动管理。

Lock 相关的锁,则需要手动获取与释放,稍有不慎,忘记释放锁或者程序处理异常导致锁没有释放,则会造成死锁。所以,通常 unLock() 操作都要在 finally 语句块中进行释放锁操作。

但是,由于锁的释放与获取都由程序把控,能够实现更灵活的锁获取与释放。

Lock 锁可以非阻塞、响应中断、响应超时
● 使用 Lock 接口可以非阻塞地获取锁,具体API是tryLock()和tryLock(long time, TimeUnit unit)。
● 使用 Lock 接口可以响应中断和超时:synchronized一旦尝试获取锁,就会一直等待下去,直到获取锁;但是Lock接口可以响应中断或者超时。具体API是lockInterruptibly()和tryLock(long time, TimeUnit unit)。

Lock 锁提供了更丰富的锁
Lock 接口提供了更丰富的锁语义:Lock接口及其实现类,实现了读写锁、公平锁与非公平锁等,读写锁:ReadWriteLock;公平锁与非公平锁:以ReentrantLock为例,只要构造函数传入true,就可以使用公平锁,默认是非公平锁。synchronized 是支持重入的非公平锁。

实现原理
Lock 接口都是基于 AQS 实现的,锁的标志是 AQS 中的 state 标志位,当获取锁失败后,Lock 会进入自旋 +CAS 的形式实现的,以等待锁的获取。

synchronized 则是通过争抢对象关联的 Monitor 实现的,当线程争抢 Monitor 失败后,则会进入阻塞状态;

由于 Java 的线程时映射到操作系统原生的线程之上的,如果阻塞或唤醒一个线程就需要操作系统帮忙,这时就要从用户态转到核心态,因此线程状态转换需要花费很多的处理器时间,

对于简单的同步块(比如被 synchronized 修饰的 get、set 方法),往往状态转换消耗的时间比用户代码执行的时间还要长,不过随着JDK1.6后,偏向锁、轻量级锁、锁消除、自旋锁、自适应自旋锁、锁粗化的出现,synchronized 的性能得到了很大的改善。

性能比较
ReadWriteLock 接口的实现类,可以实现读锁和写锁分离,极大提高了读多写少情况下系统性能。

从常规性能上来说,如果资源竞争不激烈,两者的性能是差不多的,但当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于 synchronized。

Volatile 的实现原理

共享性
数据共享性是线程安全的主要原因之一。

互斥性
资源互斥是指同时只允许一个访问者对其进行访问,具有唯一性和排它性。我们通常允许多个线程同时对数据进行读操作,但同一时间内只允许一个线程对数据进行写操作。
所以我们通常将锁分为共享锁和排它锁,也叫做读锁和写锁。

原子性
原子性就是指对数据的操作是一个独立的、不可分割的整体。换句话说,就是一次操作,是一个连续不可中断的过程,数据不会执行的一半的时候被其他线程所修改。

可见性


333.png

每个线程都有一个自己的工作内存(相当于CPU高级缓冲区,这么做的目的还是在于进一步缩小存储系统与CPU之间速度的差异,提高性能),对于共享变量,线程每次读取的是工作内存中共享变量的副本,写入的时候也直接修改工作内存中副本的值,然后在某个时间点上再将工作内存与主内存中的值进行同步。

这样导致的问题是,如果线程1对某个变量进行了修改,线程2却有可能看不到线程1对共享变量所做的修改。

有序性
为了提高性能,编译器和处理器可能会对指令做重排序。重排序可以分为三种:
● 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
● 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
● 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

volatile 是 轻量级的 synchronized。

volatile 的使用

  1. 防止重排序
    3 public class Singleton {
    4 public static volatile Singleton singleton;
    5
    9 private Singleton() {};
    10
    11 public static Singleton getInstance() {
    12 if (singleton == null) {
    13 synchronized (singleton) {
    14 if (singleton == null) {
    15 singleton = new Singleton();
    16 }
    17 }
    18 }
    19 return singleton;
    20 }
    21 }

实例化一个对象其实可以分为三个步骤:
  (1)分配内存空间。
  (2)初始化对象。
  (3)将内存空间的地址赋值给对应的引用。

但是由于操作系统可以对指令进行重排序,所以上面的过程也可能会变成如下过程:
  (1)分配内存空间。
  (2)将内存空间的地址赋值给对应的引用。
  (3)初始化对象

这样的话在多线程的情况下可能将一个没实例化的对象暴露出来,造成不可预料的效果。如果加上 volatile 就可以防止这个过程重排序

  1. 保证可见性

● 修改 volatile 变量时会强制将修改后的值刷新的主内存中。
● 修改 volatile 变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。

通过这两个操作,就可以解决 volatile 变量的可见性问题。

  1. 保证原子性
    volatile 只能保证单次 读/写 的原子性

final 关键字
final 可以申明成员变量、方法、类、本地变量。一旦将引用声明为 final,将无法再改变这个引用。final关键字还能保证内存同步。

final 的方法不能被重写,final 的类不能被继承。

final 好处
● 提高了性能,JVM在常量池中会缓存 final 变量
● final 变量在多线程中并发安全,无需额外的同步开销
● final 方法是静态编译的,提高了调用速度
● final 类创建的对象是只可读的,在多线程可以安全共享

死锁
死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

产生死锁的原因主要是:
● 因为系统资源不足。
● 进程运行推进的顺序不合适。
● 资源分配不当等。
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则
就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。

产生死锁的四个必要条件:

● 互斥条件:一个资源每次只能被一个进程使用。
● 占有且等待:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
● 不可强行占有:进程已获得的资源,在末使用完之前,不能强行剥夺。
● 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之
一不满足,就不会发生死锁。

处理死锁的基本方法:

*死锁预防:通过设置某些限制条件,去破坏死锁的四个条件中的一个或几个条件,来预防发生死锁。但由于所施加的限制条件往往太严格,因而导致系统资源利用率和系统吞吐量降低。

*死锁避免:允许前三个必要条件,但通过明智的选择,确保永远不会到达死锁点,因此死锁避免比死锁预防允许更多的并发。

*死锁检测:不须实现采取任何限制性措施,而是允许系统在运行过程发生死锁,但可通过系统设置的检测机构及时检测出死锁的发生,并精确地确定于死锁相关的进程和资源,然后采取适当的措施,从系统中将已发生的死锁清除掉。

*死锁解除:与死锁检测相配套的一种措施。当检测到系统中已发生死锁,需将进程从死锁状态中解脱出来。常用方法:撤销或挂起一些进程,以便回收一些资源,再将这些资源分配给已处于阻塞状态的进程。死锁检测盒解除有可能使系统获得较好的资源利用率和吞吐量,但在实现上难度也最大。

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

推荐阅读更多精彩内容