Java synchronized
synchronized是java提供线程间同步的重要机制
保证在同一时刻, 被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果
java内存模型:
先通过一个生产者消费者例子来了解如何使用synchronized
package com.example.demo;
public class SyncTest {
static Object obj = new Object();
static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Thread c1 = new Thread(new Consumer());
Thread p1 = new Thread(new Producer());
c1.start();
Thread.sleep(1000);
p1.start();
c1.join();
p1.join();
}
static class Consumer implements Runnable {
@Override
public void run() {
synchronized (obj) {
System.out.println("Consumer executing...");
while (!flag) {
System.out.println("no product, waiting");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Consumer waiting end....");
}
flag = false;
System.out.println("consuming.....");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("exit consumer.....");
}
}
}
static class Producer implements Runnable {
@Override
public void run() {
synchronized (obj) {
System.out.println("Producer executing......");
try {
System.out.println("Producing.....");
Thread.sleep(2000);
flag = true;
obj.notify();
Thread.sleep(2000);
System.out.println("Producer exit......");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
当然上面是代码块的方式,另一种方式是用在方法上。
- synchronized锁的是谁?
- 锁是如何实现的?
- wait()和notify()发生了什么?
synchronized锁的是谁?
synchronized有两种使用方式,分别来进行讨论
- 代码块
我们知道,使用代码块的时候synchronized后面要传入一个对象(Object)或者类(obj.class),所以结论就是传的是谁锁的就是谁。 - 方法
方法又分为静态方法和非静态方法
- 静态方法:锁的是方法所在的类
- 非静态方法:锁的是调用该方法的对象
锁是如何实现的?
关键词:Monitor对象,monitorenter和monitorexit指令,markword
monitor:每个对象都是一个监视器锁(monitor),当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权。
Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为 “重量级锁”。
再具体一点,Monitor是由ObjectMonitor实现的,其源码是用C++语言编写的,
// src/share/vm/runtime/objectMonitor.hpp
ObjectMonitor() {
_header = NULL;
_count = 0; //锁的计数器,获取锁时count数值加1,释放锁时count值减1
_waiters = 0, //等待线程数
_recursions = 0; // 线程重入次数
_object = NULL; // 存储Monitor对象
_owner = NULL; // 持有当前线程的owner
_WaitSet = NULL; // wait状态的线程列表
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; // 阻塞在EntryList上的单向线程列表
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁状态block状态的线程列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
获取Monitor和释放Monitor的流程如下:
当多个线程同时访问同步代码块时,首先会找到对象或类对应的Monitor对象,进入到EntryList中,然后通过CAS的方式尝试将Monitor中的owner字段设置为当前线程,如果成功count加1;如果之前的owner的值就是指向当前线程的,owner和recursions都需要加1。如果CAS尝试获取锁失败,则进入到EntryList中。
当获取锁的线程调用wait()方法(只有获取到锁才能用wait方法),则会将owner设置为null,同时count减1,recursions减1,当前线程加入到WaitSet中,等待被唤醒。
当前线程执行完同步代码块时,则会释放锁,count减1,recursions减1。当recursions的值为0时,说明线程已经释放了锁。
wait(), notify()
Jdk1.6为什么要对synchronized进行优化?
因为Java虚拟机是通过进入和退出Monitor对象来实现代码块同步和方法同步的,而Monitor是依靠底层操作系统的Mutex Lock来实现的,操作系统实现线程之间的切换需要从用户态转换到内核态,这个切换成本比较高,对性能影响较大。
锁升级
以32位机器为例
当我们创建一个对象之后,对象头部的markword中有25bit hashcode,4bit分代年龄,1bit偏向锁标记位,2bit锁标记位
Q: 一个对象new出来之后,markword里面的hashcode位置有没有真正存放对象的hashcode?
A: 如果仅仅new了一个对象,并没有隐式或显示的调用Object的hashcode()方法,那么对象hashcode并不存在,即存在的前提是调用父类Object的hashcode()方法,否则这25个bit就是0。
显示调用:比如在构造函数里面执行super.hashcode()
。
隐式调用:new对象之后马上添加到HashMap/HashSet里,因为hashmap的put方法的第一个参数就是hash值,来源就是调用了Object的hashcode方法。
Q: 如果类重写了hashcode方法,并且在构造函数中调用hashcod方法,那么markword里面hashcode的25个bit是否存在?
A:不会,只有调用Object的hashcode方法才行
锁标志位
无锁:偏向锁标志位0+锁标志位01
偏向锁:偏向锁标志位1+锁标志位01
无锁升级为偏向锁
前提条件:markword里面没有存放对象的hashcode,如果有就不能加偏向锁(不然hashcode会覆盖)
升级之后,markword里面的25bit hashcode部分变成了23bit线程id+2bit epoch,且偏向锁标记位从0变为1
偏向锁升级为轻量级锁
必要条件:发生竞争
但不是充分条件
eg
执行完毕的争抢:
线程A获取到对象的偏向锁,线程B来访问看到当前对象已经有偏向,检测一下线程A的存活状态,如果A已经在临界区之外了(不在执行同步代码),B直接将对象置为无锁状态,再去抢锁,如果B抢到了,那么就获取了对象的偏向锁,偏向B;如果B争抢失败(比如C进入竞争且成功),那么C就获取了对象的偏向锁。