基本介绍
-
synchronized 翻译成中文是同步的的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行
-
在 Java 早期版本中,synchronized 属于重量级锁,效率低下;因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的;如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高
-
不过,在 Java 6 之后,Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错;JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销
如何使用
-
synchronized 关键字最主要的三种使用方式:
- 修饰实例方法
- 修饰静态方法
- 修饰代码块 修饰实例方法(锁当前对象实例)
-
给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
synchronized void method() { //业务代码 }
修饰静态方法(锁当前类)
- 给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得当前 class 的锁,静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享
-
静态 synchronized 方法和非静态 synchronized 方法之间的调用不互斥,访问静态 synchronized 方法占用的锁是当前类的锁,访问非静态 synchronized 方法占用的锁是当前实例对象锁
synchronized static void method() { //业务代码 }
修饰代码块(锁指定对象/类)
-
对括号里指定的对象/类加锁
synchronized(object) // 表示进入同步代码库前要获得 给定对象的锁
synchronized(类.class) //表示进入同步代码前要获得 给定 Class 的锁
synchronized(this) { //业务代码 }
构造方法能否使用
- 构造方法本身就属于线程安全的,不存在同步的构造方法一说
- 构造方法不能使用 synchronized 关键字修饰
JDK1.6之后做了哪些优化
- JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销
- 锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级;注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率
- JDK1.6 为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程
底层原理
- Java对象头(存储锁类型)
- 在 HotSpot 虚拟机中,对象在内存中的布局分为三块区域:① 对象头 ②实例数据 ③对齐填充
- 对象头中包含两部分:MarkWord 和 类型指针(数组对象的对象头还有一部分是存储数组的长度)
- 多线程下 synchronized 的加锁就是对同一个对象的对象头中的 MarkWord 中的变量进行 CAS 操作
- 锁对象 Monitor
- synchronized 锁对象是存在哪里的呢?答案是存在锁对象的对象头 MarkWord 里
- MarkWord 在不同锁状态下存储的内容
- 重量级锁:锁标识位为10,其中指针指向的是 monitor 对象(也称为管程或监视器锁)的起始地址
- 重量级锁:锁标识位为10,其中指针指向的是 monitor 对象(也称为管程或监视器锁)的起始地址
- Monitor 对象部分存储内容
ObjectMonitor() {
_count = 0; //记录数
_recursions = 0; //锁的重入次数
_owner = NULL; //指向持有ObjectMonitor对象的线程
_WaitSet = NULL; //调用wait后,线程会被加入到_WaitSet
_EntryList = NULL ; //等待获取锁的线程,会被加入到该列表
}
- JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步,但两者的实现细节不一样
- 代码块同步:通过使用 monitorenter 和 monitorexit 指令实现的
- 同步方法:ACC_SYNCHRONIZED 修饰
锁升级的过程
32位虚拟机MarkWord存储内容
64位虚拟机MarkWord存储内容
-
无锁
当一个对象被创建之后,还没有线程进入,这个时候对象处于无锁状态,MarkWord的信息如上表所示 -
偏向锁
当锁处于无锁状态时,有一个线程A访问同步块并获取锁时,会在对象头和栈帧中的锁记录记录线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来进行加锁和解锁,只需要简单的测试一下啊对象头中的线程ID和当前线程是否一致 -
轻量级锁
在偏向锁的基础上,又有另外一个线程B进来,这时判断对象头中存储的线程A的ID和线程B不一致,就会使用CAS竞争锁,并且升级为轻量级锁,会在线程栈中创建一个锁记录(lock Record),将Mark Word复制到锁记录中,然后线程尝试使用CAS将对象头的Mark Word替换成指向锁记录的指针,如果成功,则当前线程获得锁;失败,表示其他线程竞争锁,当前线程便尝试CAS来获取锁 -
重量级锁
当线程没有获得轻量级锁时,线程会CAS自旋来获取锁,当一个线程自旋10次之后,仍然未获得锁,那么就会升级成为重量级锁 -
成为重量级锁之后,线程会进入阻塞队列(EntryList),线程不再自旋获取锁,而是由CPU进行调度,线程串行执行
与ReentrantLock的区别
- 两者都是可重入锁
- “可重入锁” 指的是自己可以再次获取自己的内部锁
- 比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁
- 同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁
- synchronized 依赖于 JVM,而 ReentrantLock 依赖于 API
- synchronized 依赖于 JVM实现的,在JDK1.6为 synchronized 关键字进行了很多优化,这些优化都是在虚拟机层面实现的,并没有直接暴露出来
- ReentrantLock 是JDK层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以可以通过查看它的源代码,来看它是如何实现的
- ReentrantLock 比 synchronized 增加了一些高级功能,主要来说主要有三点:
- 等待可中断:ReentrantLock 提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制,也就是说正在等待的线程可以选择放弃等待,改为处理其他事情
- 可实现公平锁:ReentrantLock 可以指定是公平锁还是非公平锁,而 synchronized 只能是非公平锁,所谓的公平锁就是先等待的线程先获得锁,ReentrantLock 默认情况是非公平的,可以通过 ReentrantLock 类的 ReentrantLock(boolean fair) 构造方法来制定是否是公平的
- 可实现选择性通知(锁可以绑定多个条件): synchronized 关键字与 wait() 和 notify()/notifyAll() 方法相结合可以实现等待/通知机制,ReentrantLock 类当然也可以实现,但是需要借助于 Condition 接口与 newCondition() 方法
- Condition是 JDK1.5 之后才有的,它具有很好的灵活性
- 比如可以实现多路通知功能也就是在一个 Lock 对象中可以创建多个 Condition 实例(即对象监视器),线程对象可以注册在指定的 Condition 中,从而可以有选择性的进行线程通知,在调度线程上更加灵活
- 在使用 notify()/notifyAll() 方法进行通知时,被通知的线程是由 JVM 选择的,用 ReentrantLock 类结合 Condition 实例可以实现"选择性通知",这个功能非常重要,而且是 Condition 接口默认提供的
- 而 synchronized 关键字就相当于整个 Lock 对象中只有一个 Condition 实例,所有的线程都注册在它一个身上
- 如果执行 notifyAll() 方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而 Condition 实例的 signalAll() 方法只会唤醒注册在该 Condition 实例中的所有等待线程