Synchronized详解
sychronized是java多线程非常关键的一个知识点,这篇博客将从synchronized几个用法以及代码来学习。
sychronized的作用是能够保证同一时间只有一个线程来运行这块代码,达到并发效果,如果没有保证并发的话,在多线程编码中就会产生致命问题,比如经典的i++,这也是数据库并发中经典的案例,i++并不是原子操作,分为三步,取数,操作,写数,参考这段代码,可以运行一下看下结果
public class showUnsafe1 implements Runnable{
static int i=0;
@Override
public void run() {
for(int j=0;j<10000;j++){
i++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new showUnsafe1());
Thread thread2 = new Thread(new showUnsafe1());
thread1.start(); // 启动thread1,在合适的时刻运行
thread2.start(); // 启动thread2,在合适的时刻运行
thread1.join(); // 让主线程等待thread1运行完
thread2.join(); // 让主线程等待thread2运行完
System.out.println(i);
}
}
一、synchronized四种用法
synchronized为啥这么神奇,无它,加锁而已,不少八股文喜欢分为两种锁,一种是对象锁,一种是类锁,还可以分为方法锁,代码块锁,静态锁,class锁,我们通过代码学习他们如何使用
1.对象锁:方法锁
方法锁是用synchronized修饰的一个类方法,作用方法即是方法作用域,除了这个方法要同步,其余不需要
public class SynchronizedObjectMethod implements Runnable{
private static SynchronizedObjectMethod instance=new SynchronizedObjectMethod();
public synchronized void method(){
System.out.println("我是对象锁的方法修饰符形式。我叫"+Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"运行结束");
}
@Override
public void run() {
method();
}
public static void main(String[] args) {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
while(thread1.isAlive()||thread2.isAlive()){
}
System.out.println("finish");
}
}
2. 对象锁:代码块形式
代码块锁就是常用的同步方法块,synchronized锁住的是它里面的对象,作用域就是synchonized{}里面的代码
public class SynchronizedObjectCodeBlock implements Runnable{
private static SynchronizedObjectCodeBlock instance=new SynchronizedObjectCodeBlock();
Object lock1=new Object();
@Override
public void run() {
synchronized (lock1){
System.out.println("我是对象锁的代码块形式。我叫"+Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"运行结束");
}
}
public static void main(String[] args){
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
while(thread1.isAlive()||thread2.isAlive()){
}
System.out.println("finish");
}
}
3. 类锁:class形式
class形式说的是synchronized()括号里使用的锁是class对象,所谓class对象指得是java文件对应的一个java.lang.class对象,所有该类生成的对象共有这个class对象 类加载机制,所以这个锁锁住了这个类生成的所有对象
public class SynchronizedClassClass implements Runnable{
private static SynchronizedClassClass instance1=new SynchronizedClassClass();
private static SynchronizedClassClass instance2=new SynchronizedClassClass();
public void method(){
synchronized (SynchronizedClassClass.class){
System.out.println("我是类锁的形式之一:修饰.class。我叫"+Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"运行结束");
}
}
@Override
public void run() {
method();
}
public static void main(String[] args) {
Thread thread1 = new Thread(instance1);
Thread thread2 = new Thread(instance2);
thread1.start();
thread2.start();
while(thread1.isAlive()||thread2.isAlive()){
}
System.out.println("finish");
}
}
在这个案例中,存在两个SynchronizedClassClass对象,但是不能同时访问同步代码
4.类锁:static形式
static形式说的是static修饰synchronized修饰的方法,即static synchronized methodName,作用方法还是这个类的class对象
public class SynchronizedClassStatic implements Runnable{
private static SynchronizedClassStatic instance1=new SynchronizedClassStatic();
private static SynchronizedClassStatic instance2=new SynchronizedClassStatic();
public static synchronized void method(){
System.out.println("我是类锁的形式之一:加static修饰。我叫"+Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"运行结束");
}
public void method2(){
System.out.println("我是非静态方法,我叫"+Thread.currentThread().getName());
}
@Override
public void run() {
method();
method2();
}
public static void main(String[] args) {
Thread thread1 = new Thread(instance1);
Thread thread2 = new Thread(instance2);
thread1.start();
thread2.start();
while(thread1.isAlive()||thread2.isAlive()){
}
System.out.println("finish");
}
}
二、synchronized的原理
让我们从字节码来看看synchronized的神秘面纱, 我们反编译synchronized修饰object的java文件
public class Decompilation {
private Object object=new Object();
public void insert(Thread thread){
synchronized (object){
}
}
}
反编译指令为 javap -c -v YourName.java
所以就是这个小小的Monitor Enter和monitorexit指令完成了同步操作,关于Monitor Enter和monitorexit的定义可以查看jvm文档 中的注释
先看看 Monitor Enter,注意紫色标注
再看看Monitor Exit
这里就解释清楚为什么synchronized上的锁是可重入的,对于更深入的理解
重点还是提到的Monitor这个概念
我们再反编译synchronize修饰的同步方法,其结果是
与之前不一样,这里是在方法的访问标识上添加了ACC_SYNCHRONIZED,它在jvm文档中是这么解释
大致意思就是当调用设置了 ACC_SYNCHRONIZED 的方法时,执行线程进入监视器(monitor),然后执行这个方法,方法执行完毕后退出监视器。在执行线程拥有监视器期间,没有其他线程可以进入这个方法.
同样,这里涉及了Monitor
2.1 深入Monitor
虽然HotSpot的JDK代码没有开元,但好在还有OpenJDK,大家有时间可以看看ObjectMonitor.hpp和ObjectMonitor.cpp,如果c语言功力不好的同学,看看注释,大致知道每个变量啥意思即可。
主要是_owner
,_recursions
,_entryList
,_waitSet
,此外还有header
这个对象头将对象和Monitor
联系起来。
_owner
顾名思义就是锁的拥有者,recursions
就是锁的进入次数,初始为0,而_entryList
是存放Blocked状态的线程的,waitSet
是存放Waiting状态的线程。
在HotSpot中,一个对象是在Heap中的存储布局有三个部分:对象头(Header),实例数据(Instance Data)以及对齐填充部分,而对象头一般由两个部分组成:MarkWord和类型指针,如果是数组对象,那么还有数组长度信息。
而MarkWord就是连接Monitor和对象的关键东西。它存储了对象运行时的一些数据,比如HashCode,GC年龄,锁的状态,线程所持有的锁等。
总之,monitor才是synchronized并发的关键,monitor是底层用cpp实现的一个对象,实现了锁的状态转换,获取释放等方法,通过markword与java对象联系一起,而markword是嵌入在java头部的。
三、synchornized的各种锁以及优化
3.1 锁的升级
从jdk1.6开始,synchronized锁有四种状态,级别由低到高是
无锁,偏向锁,轻量锁,重量锁
锁的升级过程是一个很麻烦的事情,本质就是如果发生了锁的竞争就升级锁,直到升级到重量锁为止,期间使用到了CAS和自旋来避免线程直接进入阻塞状态。有兴趣的同学可以看看《阿里巴巴java性能调优实战》这本书。
3.2 JIT 实现锁消除和锁粗化
锁消除的概念比较容易理解,就是如果编译器认定一个锁只会被单个线程访问,那么这个锁就可以被消除。而锁粗化,简单的说就是JIT动态编译时发现相邻的同步块使用的是同一个锁实例,那么就合并他们,避免频繁加锁释放锁。
3.3 减少锁的粒度
这是我们平时编程的时候,我们可以控制的,有的同学(比如我)图省事往往可以加对象锁的,直接加类锁,这样就是不地道的。
《阿里巴巴java性能调优实战》举例说明减少锁的粒度的好处,比如被抛弃的HashTable和新宠ConcurrentHashMap的转换,就是使用了减少锁的粒度方法。