并发编程(volatile和synchronized)

大家都知道,并发编程主要是运用多线程来提高程序的运算速度并提高机器的使用率。而并发程序运行的最大挑战就是公共资源的访问冲突。对于公共资源的访问主要分为读和写

  • 公共资源读:需要保证各个线程读到的公共资源时最新的

  • 公共资源写:需要防止多个线程对公共资源的写冲突

在Java中,利用volatile关键字和synchronized来实现多线程对资源访问控制

volatile

volatile主要用于修饰Java多线程的公共变量,当修改被volatile修饰的公共变量时,其修改后的值对所有的其他线程立即可见。同时为了防止多线程同时对其进行修改,在对其修改时需要加上排他锁。

并发编程(volatile和synchronized)

上边表格为CPU关于并发的一些术语定义

当写一个volatile修饰的变量时,会引发以下两个操作

  1. 会将当前处理器缓存行的数据回写到内存中

  2. 该内存对应的其他处理器中的缓存地址失效

接下来具体解释下这两种操作

LOCK#引发数据回写缓存

对于老式的处理器来说,当写一个volatile变量时,处理器会发送一个LOCK#信号将总线锁定,保证同一时间只有一个处理器可以对该变量进行修改。同时会将处理器中该变量的缓存刷新到内存中。而在一些新的处理器中不再使用LOCK#锁总线,而是使用缓存一致性原理保证数据刷新到内存并将其他处理器缓存该内存的地址置为失效

其他处理器缓存失效

当有处理器对volatile变量进行写时,其他处理器对该内存的缓存会置为失效,这样当下次读取缓存时会再次从内存中进行加载,从而保证各个处理器读到的数据都是最新的。

synchronized

对于Java程序员来说,synchronized关键字并不陌生。在jdk1.6之前,synchronized被称为重量级锁,1.6之后对其进行了优化,同时引入了偏向锁和轻量级锁。

synchronized是通过锁Java的对象来控制多线程对同步块的访问的

  • 对于普通方法来说,synchronized锁的是当前对象

  • 对于静态方法,synchronized锁的是当前Class对象

  • 对于同步方法块,synchronized锁的是括号里面的对象

对于synchronized的实现原理,JVM是通过monitor实现的。在同步代码块进入的地方插入monitorenter,在同步代码块退出的地方插入monitorexit。JVM保证每一个monitorentor必须有一个monitorexit与其对应。任何一个对象都有一个monitor与其对应。当代码执行monitorentor时,则表明该monitor被锁定。其他线程必须等到其解锁后才能对其进行重新获取。当线程执行到monitorexit时,表示其释放了monitor的所有权,其他线程可以对其进行重新获取。

Java对象头

上边说过,synchronized是通过锁Java对象来实现的,每一个Java对象都可以当做一个锁来使用。如下图所示为Java对象头的存储结构

并发编程(volatile和synchronized)

其中MarkWord中存储着对象的HashCode,分代年龄和锁信息

并发编程(volatile和synchronized)

在运行过程中,MarkWord中存储的数据也会改变

并发编程(volatile和synchronized)

锁分类

Jdk1.6之后为了减少获取和释放锁的消耗,对锁进行了优化,引入了偏向锁和轻量级锁。在1.6中,锁一共有四种状态,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁竞争的加剧,锁会从低级别升级为高级别的锁,但其级别不能逆向降级。

偏向锁

在实际运用过程中,大多数情况下,锁并不存在多线程竞争,而是被一个线程多次获取。为了减少锁获取和释放的开销,Java引入了偏向锁。在一个线程访问同步快并获取锁时,会在对象头和栈帧中锁记录里存储偏向的线程id,等该线程下一次要获取该锁时就不需要通过CAS来加锁和解锁了。只需要检测对象头的MarkWord中是否存储着该线程的偏向锁,如果存在则表示该线程已经获取锁,否则检测MarkWord中偏向锁标识是否设置了,如果没有设置则使用CAS竞争锁,如果设置了则使用CAS将对象头的偏向锁指向当前线程。

1. 偏向锁撤销

偏向锁只有等到其他线程竞争时才会释放锁。释放锁时必须等到全局安全点(没有正在执行的字节码)。首先暂停持有锁的线程,检查其是否活着,如果还活着则线程继续执行往下的同步代码,锁膨胀为轻量级锁。

并发编程(volatile和synchronized)

2. 关闭偏向锁

 

Java 1.6及之后默认是开启的,如果要关闭可以设置—XX:-UseBiasedLocking=false。如果应用程序启动后立即生效则可以设置-XX:-BiasedLockingStartupDelay=0

轻量级锁

1. 加锁

线程在执行同步块之前,会先在当前线程栈帧中创建用于存储锁记录的空间,并将对象头哦中的MarkWord复制到锁记录中,称为Displaced Mark Word。然后尝试使用CAS将Mark Word替换为指向锁记录的指针,如果成功则表示竞争到锁。失败则使用自选来获取锁

2. 释放锁

如果线程加锁失败(在一定时间内自选仍然没有获取到锁),则说明有其他线程长期占有锁,此时锁膨胀为重量级锁。

并发编程(volatile和synchronized)

锁优缺点

并发编程(volatile和synchronized)


《Java并发编程的艺术》 方腾飞 魏鹏 程晓明

相关文章: