Synchronized及其实现原理
并发编程中synchronized一直是元老级角色,我们称之为重量级锁。主要用在三个地方:
1、修饰普通方法,锁是当前实例对象。
2、修饰类方法,锁是当前类的Class对象。
3、修饰代码块,锁是synchronized括号里面的对象。
一、synchronized实现原理
当一个线程试图访问同步代码块时,必须得到锁。在退出或抛出异常时必须释放锁,JVM是基于进入和退出Monitor来实现方法同步和代码块同步。
我们来看下synchronized的字节码:
public class SynchronizedTest { public void addNum1(String userName) { } public void addNum2(String userName) { synchronized(this) { } } public synchronized void addNum3(String userName) { } }
在字节码里可以看到,用synchronizde修饰的同步代码块多了两个指令:monitorenter、monitorexit;
代码块同步是使用monitorenter、monitorexit指令实现的,而方法同步是使用另外一种方式实现的,但是方法同步也可以使用这两个指令来实现。
monitorenter指令是编译后插入到同步代码块的开始位置,而monitorexit是插入到方法的结束和异常位置。任何一个对象都有一个monitor与之关联。线程执行到monitorenter指令处时,会尝试获取对象所对应的monitor所有权,即尝试获得对象的锁。
二、修饰普通方法 锁是当前实例对象
我们先来看下将实例对象作为锁的概念:
public class AddNumTest { private int num = 0; public synchronized void addNum(String str) { try { if ("a".equals(str)) { num = 10; System.out.println("add a"); Thread.sleep(2000); } else { num = 20; System.out.println("add b"); } System.out.println(str + " num = " + num); } catch (InterruptedException e) { e.printStackTrace(); } } }
public class AddNumThreadOne implements Runnable { private AddNumTest at; public AddNumThreadOne(AddNumTest at) { this.at = at; } @Override public void run() { at.addNum("a"); } }
public class AddNumThreadTwo implements Runnable { private AddNumTest at; public AddNumThreadTwo(AddNumTest at) { this.at = at; } @Override public void run() { at.addNum("b"); } }
public class AddNum { public static void main(String[] args) { //注意,这里传入同一个实例对象 AddNumTest at = new AddNumTest(); //AddNumTest bt = new AddNumTest(); Thread t1 = new Thread(new AddNumThreadOne(at)); Thread t2 = new Thread(new AddNumThreadTwo(at)); t1.start(); t2.start(); } }
执行结果:
add a a num = 10 add b b num = 20
前面解释过关键字synchronized的实现原理是使用对象的monitor来实现的,取的锁都是对象锁,而不是把一段代码或者函数作为锁。在并发情况下,如果并发情况下多线程竞争的是同一个对象,那么先来的获取该对象锁,后面的线程只能排队,等前面的线程执行完毕释放锁。
上面介绍的是同一个对象锁,我们来观察下获取不同的对象锁会是什么情况:
public static void main(String[] args) { //注意,这里传入的不同的实例对象 AddNumTest at = new AddNumTest(); AddNumTest bt = new AddNumTest(); Thread t1 = new Thread(new AddNumThreadOne(at)); Thread t2 = new Thread(new AddNumThreadTwo(bt)); t1.start(); t2.start(); }
执行结果:
add a add b b num = 20 a num = 10
这里线程1、2抢占的是不同的锁,尽管线程1先到达同步代码块的位置,但是由于monitor不一样,所以不能阻塞线程2的执行。
三、synchronized锁重入
锁重入:当一个线程得到一个对象锁后,再次请求此对象锁时可以再次得到该对象的锁。但是这里有维护一个计数器,同一个线程每次得到对象锁计数器都会加1,释放的时候减1,直到计数器的数值为0的时候,才能被其他线程所抢占
public class AgainLock { public synchronized void print1() { System.out.println("do work print1"); print2(); } public synchronized void print2() { System.out.println("do work print2"); print3(); } public synchronized void print3() { System.out.println("do work print3"); } }
public class AgainLockTest { public static void main(String[] args) { Thread t = new Thread(new Runnable() { @Override public void run() { AgainLock al = new AgainLock(); al.print1(); } }); t.start(); } }
执行结果:
do work print1 do work print2 do work print3
这里面三个同步方法,使用的锁都是该实例对象的同步锁,同一个线程在执行的时候,每次都是在锁没有释放的时候,就要重新再去获取同一把对象锁。从运行结果可以看出,关键字synchronized支持同一线程锁重入。
四、synchronized同步代码块
用synchronized同步方法的粒度过大,有时候一个方法里面的业务逻辑很多,但是我们想对同步的部分进行单独定制,这时候就可以使用synchronized来同步代码块。
public class SynchronizedTest1 { public void doWorkTask() { for(int i=0;i<100;i++) { System.out.println("nosynchronized thread name =" + Thread.currentThread().getName() + ";i=" + i); } synchronized(this) { for(int i=0;i<100;i++) { System.out.println("thread name =" + Thread.currentThread().getName() + ";i=" + i); } } } }
如果在并发情况下,调用这个类的同一个实例,线程A和B可以同时执行使用synchronized同步之前的代码逻辑,但是使用关键字同步的部分是互斥的,先到达的线程占有对象锁,后面的线程会被阻塞,直到对象锁被前面的线程释放。
四、总结
synchronized是一种悲观锁:
1、多线程竞争的情况下,频繁的加锁解锁导致过多的线程上下文切换,由于java线程是基于操作系统内核线程实现的,所以如果阻塞或者唤醒线程都需要切换到内核态操作,这会极大的浪费CPU资源。
2、一个线程持有锁,会导致后面其他请求该锁的线程挂起。
synchronized锁优化:自旋锁
很多应用中共享数据的锁定,只会持续很短的时间,如果因为这很短的时间做线程的挂起和恢复,会造成很大的性能消耗(因为java线程对应操作系统的内核线程),让当前线程不停地的在循环体内执行不被挂起,这就是自旋锁的实现,后面会展开详细介绍。