1、synchronized 简介
synchronized
关键字是解决多线程之间访问资源的同步性。synchronized
关键字可以保证被其修饰的方法或者代码块在同一时刻只能被一个线程访问。
在JDK1.6
之前,synchronized
是重量级锁,开销大,因为监视器锁是依赖于底层的操作系统来实现的,Java
的线程是映射到操作系统的原生线程之上的,就是是说要挂起或者唤醒一个线程,需要操作系统的帮忙,而操作系统实现线程之间的切换需要从用户空间切换到内核空间,这个切换耗时长,所有时间成本高,导致效率低下。
在JDK1.6
之后,Java
官方在JVM
层面上对synchronized
有较大优化,提高了synchronized
性能,引入了如自旋锁、适应性自旋锁、锁消除、偏向锁等技术。
2、怎么使用synchronized
synchronized
关键字不仅可以用在方法上也可以用在代码块上,当用在实例方法时,锁住的是实例对象,用在静态方法时,锁住的是这个类对象。在代码块上也是如此,但是需要注意的是,当锁住的是类对象时,尽管new多个实例对象,但是它们都属于同一个类,依然会被锁住,来保持线程之间的同步关系。
3、对象锁(monitor)机制
通过一段代码来看
public class SynchronizedDemo {
public static void main(String[] args) {
synchronized (SynchronizedDemo.class) {
}
method();
}
private static void method() {
}
}
synchronized
修饰的同步代码块,锁住的是这个类对象,同时也锁住了下面的这个静态方法。要执行同步代码块中的逻辑,线程要先执行monitorenter
命令,退出时执行monitorexit
命令。
使用synchronized
来同步,关键在于获取该对象的监视器monitor
,获取到后才能往下执行,获取不到则进入同步队列等待,这个获取的过程是互斥
的,即同一时刻只有一个线程能获取到monitor
。上面的代码中,执行完同步代码块后,会接着去执行同步静态方法,这个时候线程是不需要再次获取monitor
的,这就是锁的重入性
,即在同一个锁程中,线程不用再次获取同一把锁。
每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。
synchronized
修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的确实是 ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。
不过两者的本质都是对对象监视器 monitor 的获取。
4、锁获取和锁释放对内存的操作
原本内存中的是a = 0
,当线程A获取到锁时,会先强制把主内存中的数据读取到本地内存中,然后在对数据进行操作,a+1
,这时A线程的本地内存中a的值为1,当A释放锁时,也会强制将本地内存中的数据再写入到主内存中,这时主内存中a的值更新为1,同时A释放后,B线程马上获取到锁,B线程从主内存中读取数据,得到a的值为1。从整体上看,也就是A线程的操作结果对B线程是可见的。
5、synchronized优化
通过上面的介绍可以看出,synchronized
最大的特征就是同一时刻只能有一个线程获得对象的监视器monitor
,也就是互斥性,但是这种效率低下,因为每次只能通过一个线程。在JDK1.6
之后对其做了相关优化,提高了效率。
先理解两个概念:
(1)CAS操作
a.什么是CAS
线程获取锁的策略是悲观锁策略
,即假设每一次执行同步代码都会碰到冲突,所有当前线程获取到锁时就是阻塞其他线程获取到该对象的锁。而CAS
则是乐观锁策略
,就是假设所有线程访问共享资源时都不会发生冲突,就不会阻塞其他线程。如果出现了冲突,无锁操作就是使用CAS
来鉴别线程是否出现冲突,出现冲突则重试当前操作直到没有冲突为止。
b.CAS怎么操作
CAS
比较交换的过程通俗理解为CAS(v,o,n)
,v
是内存地址中存放的实际值
;o
是预期的值(旧值)
;n
是更新的新值
。当v
和o
相同时,则说明没有线程修改过共享资源的值,也就是当前共享资源没有被占用,则直接把n
值赋予给v
;当v
和o
不同时,说明值被其他线程修改过,则返回v
值。所有当有多个线程同时使用CAS
访问一个变量时,只会有一个线程会成功,其他的会失败,失败的线程可以选择重试,或者挂起。
(2)Java对象头
在执行同步代码的时候线程会获取对象的monitor
,即获取对象的锁,这里的锁其实也就是对象的标识信息。这个标识信息就是存放在Java
对象的对象头中的Mark Word
里。
Java对象头包含两部分:Mark Word
和类型指针。
即Mark Word
存放了锁状态,hashCode,分代年龄,是否偏向锁和锁标志位信息
。
类型指针
指向对象的类元数据,虚拟机通过这个指针确定该对象是哪个类的实例。
Java1.6
后,锁一共有四种状态,级别从低到高依次为,无锁、偏向锁、轻量级锁和重量级锁
。这几种状态会随着线程的竞争而升级,锁可以升级但是不能降级
。这种策略是为了提升获取锁和释放锁的效率。对象的Mark Word
变化如下图所示:
(3)偏向锁
当一个线程访问同步代码并获取锁时,会在对象头和栈帧的锁记录中记录当前线程的id
,当下次访问时,就是判断对象头中记录的线程id
是否指向当前线程,如果是的话,则不用CAS
操作来获取锁和释放锁。表示线程已经获取了锁,如果不匹配的话,则进一步检查是否偏向锁存的值,是的话,则尝试用CAS
将对象头的偏向锁指向当前线程,不是的话,则通过CAS
来竞争锁。
偏向锁的释放使用了一种只有等到竞争出现才释放的机制
,当有其他线程尝试竞争锁时,持有锁的线程才会释放锁。
偏向锁在Java6、7中默认是开启的,可通过-XX:-UseBiasedLocking=false
来关闭,则程序会默认进入轻量级锁状态。
同时偏向锁开启在程序启动后是有几秒的延迟的,可通过-XX:BiasedLockingStartupDelay=0
来关闭延迟。
(4)轻量级锁
线程在执行同步代码之前,JVM
会在当前线程的栈帧中创建存储锁记录
的空间,并将对象头的Mark Word
复制到锁记录中,然后线程尝试用CAS
将对象头的Mark Word
替换为指向锁记录的指针,如果成功,则获取了锁,如果失败,则表示其他线程竞争锁,当前线程通过自旋尝试获取锁。
轻量级锁解锁时,会尝试将栈帧锁记录中的Mark Word
替换回到对象头中,如果成功,则说明当前锁没有其他线程竞争,如果失败,则有其他线程竞争,锁就会升级为重量级锁。
(5)各种锁的比较
相关应用
使用synchronized实现线程安全的单例模式Volatile和Synchronized关键字实现单例模式(线程安全)
volatile相关详解Java高并发之volatile 关键字