JAVA并发之Synchronized

本章主要介绍的是JAVA并发锁中很重要的一类:Synchronize锁。本文会先介绍线程的多种状态,synchronize的概念,synchronize的底层原理。

目录

一.线程的状态(生命周期)

1.新建状态 

2. 就绪状态

3.运行状态

4.阻塞状态

等待阻塞(o.wait方法进入等待队列)

同步阻塞(获得锁,进入EntrySet(锁池))

其他阻塞(sleep/join)

二.Synchronize

1.并发带来的问题

2.Synchronized

3.Synchronized原理

(1)对象头

(2)重量级锁

(3)轻量级锁

(4)偏向锁

4.Wait/Notify原理


一.线程的状态(生命周期)

在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞 (Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自 运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。

1.新建状态 

        当程序使用了new关键字创建一个线程之后,该线程就处于新建状态,此时仅有JVM为其分配内存,并初始化其成员变量的值。

2. 就绪状态

        当线程对象调用了start()方法之后,该线程就处于就绪状态。此时线程对象并不会马上进入运行状态,JAVA虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。

3.运行状态

        如果处于就绪状态的线程获得了CPU使用权,开始执行了run()方法,则该线程处于运行状态。

4.阻塞状态

        阻塞状态这里泛指线程因为某种原因放弃了cpu使用权,暂时停止了运行。知道线程进入可运行状态(就绪状态),才有机会再次获得cpu时间片,从而进入运行状态。阻塞的情况分为三种:

等待阻塞(o.wait方法进入等待队列)

        当运行的线程执行o.wait()方法时,JVM会将该线程放入等待队列(waitting set),此时只能等待其他线程调用notify/notifyall方法对等待队列的线程进行唤醒,随后进入就绪状态,等待获得CPU时间片,即可进入运行状态。

同步阻塞(获得锁,进入EntrySet(锁池))

        当运行的线程获取对象的同步锁时,(synchronize或者lock),若此时锁已被占用,则JVM会把该线程放入EntrySet中,只有当锁的使用权被释放了,锁池中的线程才会继续竞争锁。

其他阻塞(sleep/join)

        当运行的线程执行Thread.sleep方法或者t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态,当sleep状态超时、join()等待的线程终止或者超时、或者I/O处理完毕,线程重新转入就绪状态。

二.Synchronize

1.并发带来的问题

        当程序已并发的模式运行时,势必会有多线程对同一代码块的访问,若该代码块涉及到对共享变量(堆中的变量)的读写操作时(这时这个代码块称为临界区),就有可能会产生并发问题,即线程安全问题(本质上是内存安全问题)。

        当多个线程在临界区执行,由于代码的执行序列不同而导致结果与单线程情况下的结果不一致,称之为发生了竞争条件

          例如对于i++而言,实际在JVM中的字节码指令如下:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i

在java中,完成静态变量的自增需要在主存和工作内存中进行数据交换,当有多个线程对某个变量进行自增/自减操作时,可能会导致如下情况:

 线程1对i进行自增操作,线程2对i进行自减操作,如果按单线程状态,此时i的值应该不变,即为0,但上述情况在线程1对i进行加法时,还没来得及将结果存入到主存中,就发生了上下文切换(失去CPU使用权,其他线程获得cpu时间片),此时线程2读取的i还是之前的0,然后进行了完整的自减操作,随后线程1获得了cpu使用权,继续执行未完成的写入1的操作,最后i的值就变成了1,这就是线程安全问题。

2.Synchronized

        上述线程安全问题的产生是因为自增自减操作并未保证原子性。本节将介绍使用阻塞式的synchronized来解决上述线程安全问题。

        synchronized俗称对象锁,它采用互斥的方式,让同一时刻,至多只有一个线程能够持有对象锁,当其他线程想要获取锁则会被阻塞住,这样就能保证拥有锁的线程能够安全的执行临界区的代码,不用担心线程上下文切换。

注意

虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
//语法:
synchronized(对象) // 线程1, 线程2(blocked)
{
     临界区
}

上述线程安全问题就会如下解决:

 synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切 换所打断。

3.Synchronized原理

        Synchronized的原理是在底层使用了monitorenter进行加锁,monitorexit进行解锁,其中monitor的底层实现是通过C语言来实现的。在这里不介绍具体的Monitor的底层,只分析Synchronized的一个实现原理,其实JAVA中的ReentrantLock和Monitor是很类似的。

        Monitor被翻译为监视器或管程,每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针 Monitor 结构如下:

  • 刚开始 Monitor 中 Owner 为 null
  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一 个 Owner
  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足(wait()方法)进入 WAITING 状态的线程

注意

  • synchronized 必须是进入同一个对象的 monitor 才有上述的效果
  • 不加 synchronized 的对象不会关联监视器,不遵从以上规则

 在介绍Synchronized原理前,要介绍对象头以及Synchronized的三种状态:偏向锁、轻量级锁、重量级锁

(1)对象头

在JAVA对象中,每个对象其实不仅包含其真正的值,还包含了一个Object Header,其结构如下:

其中Mark Word包含了一些标记信息(是否加锁,锁的状态以及GC标记),而Klass Word包含的是其类的信息。

而对于Mark Word的结构如下(64位虚拟机为例):

(2)重量级锁

        重量级锁其实就是前面所提到的,直接将java对象与Monitor进行了关联,此时获得锁的线程会变成Monitor的Owner,阻塞的线程则会进入EntrySet,而条件不满足的线程会进入WaitingSet等待。

(3)轻量级锁

        如果一个对象虽然有多个线程要进行加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。其具体过程如下:

1.在线程的栈帧中创建一个锁记录对象(Lock Record),内部可以存储锁定对象的对象头之中的MarkWord

2.让锁记录中的Object reference指向锁对象,并尝试用CAS替换Object的Mark Word,将Mark Word的值存入锁记录 

如果CAS成功 则对象头中存储了锁记录地址和状态,表示此时该线程已经给对象加锁了(故不是直接关联的Monitor,为轻量级锁)

如果CAS失败,有两个情况:

      1.  如果是其他线程已经持有了该Object的轻量级锁,这时表明有竞争了,进入锁膨胀过程。

        膨胀成重量级锁,即引入Monitor。例如:当线程1进行轻量级加锁时,线程0已经对该对象加了轻量级锁,此时线程1加锁失败,进入锁膨胀过程:

  • 线程1为Object对象申请Monitor锁,让Object指向重量级锁地址。
  • 然后自己进入Monitor的EntryList阻塞

        当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁 流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

2.如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数(重入锁)

 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重 入计数减一

 

 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头

  • 成功,则解锁成功
  • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

(4)偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。

Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

总结:锁的进化流程:

初始时候,为偏向锁,当有其他线程对该锁进行获取到一定次数,且没有发生竞争时,锁升级为轻量级锁,在轻量级锁发生了竞争时,直接膨胀成重量级锁,此时竞争成功的线程会变成Monitor的Owenr,但是竞争失败的线程不会立马进入Monitor的EntryList挂起,而是会进行一定次数的自旋操作,来等待是否能够获得锁,这样做的目的是为了减少上下文切换的次数。如果自旋结束后还不能获得锁,则会进入Monitor的EntryList等待竞争锁。

4.Wait/Notify原理

  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒
  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值