Java synchronized的使用与原理
需要明确的几个问题:
- synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果 再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。
- 无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。
- 每个对象只有一个锁(lock)与之相关联。
- 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
synchronized关键字的作用域有二种:
- 某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线 程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的 synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
- 某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。
1、使用在方法上synchronized aMethod(){...}
使用相同的 object
public class synchTest { private String a= ""; private List<String> b= new ArrayList<>(); // 方法一 public void job() { System.out.println("job ....."); synchronized (b){ System.out.println("job 使用锁中 ...."); try { Thread.sleep(2000); } catch (InterruptedException e) { } } System.out.println("job end....."); } // 方法二 public synchronized void job2(){ System.out.println("job2 ....."); synchronized (b){ System.out.println("job22 使用锁中 ..."); try { Thread.sleep(2000); } catch (InterruptedException e) { } } System.out.println("job2 end....."); } public static void main(String[] args) { final synchTest rs = new synchTest(); new Thread() { public void run() { rs.job(); } }.start(); new Thread() { public void run() { rs.job2(); } }.start(); } } 结果: job ..... job 使用锁中 .... job2 ..... job end..... job22 使用锁中 ... job2 end.....
使用不同的object
public class synchTest { private String a= ""; private List<String> b= new ArrayList<>(); // 方法一 public void job() { System.out.println("job ....."); synchronized (a){ System.out.println("job 使用锁中 ...."); try { Thread.sleep(2000); } catch (InterruptedException e) { } } System.out.println("job end....."); } // 方法二 public synchronized void job2(){ System.out.println("job2 ....."); synchronized (b){ System.out.println("job22 使用锁中 ..."); try { Thread.sleep(2000); } catch (InterruptedException e) { } } System.out.println("job2 end....."); } public static void main(String[] args) { final synchTest rs = new synchTest(); new Thread() { public void run() { rs.job(); } }.start(); new Thread() { public void run() { rs.job2(); } }.start(); } } 结果: job ..... job 使用锁中 .... job2 ..... job22 使用锁中 ... job end..... job2 end.....
使用this关键词
public class synchTest { private String a= ""; private List<String> b= new ArrayList<>(); // 方法一 public void job() { System.out.println("job ....."); synchronized (this){ System.out.println("job 使用锁中 ...."); try { Thread.sleep(2000); } catch (InterruptedException e) { } } System.out.println("job end....."); } // 方法二 public synchronized void job2(){ System.out.println("job2 ....."); synchronized (b){ System.out.println("job22 使用锁中 ..."); try { Thread.sleep(2000); } catch (InterruptedException e) { } } System.out.println("job2 end....."); } public static void main(String[] args) { final synchTest rs = new synchTest(); new Thread() { public void run() { rs.job(); } }.start(); new Thread() { public void run() { rs.job2(); } }.start(); } } 结果: job ..... job 使用锁中 .... job end..... job2 ..... job22 使用锁中 ... job2 end.....
结论:
- synchronized(Object) object相同的情况下,修饰的内容会同步,等上一个执行完才能执行下一个方法的内容
- synchronized(Object) object不相同的情况下,修饰内容不会同步,两个方法可以一起执行
- this这个比较特殊,如果先执行修饰this这个方法的内容,会同步,否则 不会同步(可以测试下) 【慎用 this同步块,会锁对象】
2、使用在方法内部 synchronized(Oject){...}
public class synchTest { // 方法一 public synchronized void job() { System.out.println("job ....."); System.out.println("job 使用锁中 ...."); try { Thread.sleep(2000); } catch (InterruptedException e) { } System.out.println("job end....."); } // 方法二 public synchronized void job2(){ System.out.println("job2 ....."); System.out.println("job22 使用锁中 ..."); try { Thread.sleep(2000); } catch (InterruptedException e) { } System.out.println("job2 end....."); } public static void main(String[] args) { final synchTest rs = new synchTest(); new Thread() { public void run() { rs.job(); } }.start(); new Thread() { public void run() { rs.job2(); } }.start(); } } 结果: job ..... job 使用锁中 .... job end..... job2 ..... job22 使用锁中 ... job2 end.....
结论:
- 对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法,在对象内容所有的synchronized 的方法都会同步,必须等上一个方法执行完才能执行下一个方法
2、使用在方法内部 synchronized(Oject){...}、synchronized aMethod(){...}混用
public class synchTest { public String a = ""; // 方法一 public void job() { System.out.println("job ....."); synchronized (a){ System.out.println("job 使用锁中 ...."); try { Thread.sleep(2000); } catch (InterruptedException e) { } } System.out.println("job end....."); } // 方法二 public synchronized void job2(){ System.out.println("job2 ....."); System.out.println("job22 使用锁中 ..."); try { Thread.sleep(2000); } catch (InterruptedException e) { } System.out.println("job2 end....."); } public static void main(String[] args) { final synchTest rs = new synchTest(); new Thread() { public void run() { rs.job(); } }.start(); new Thread() { public void run() { rs.job2(); } }.start(); } } 结果: job ..... job 使用锁中 .... job2 ..... job22 使用锁中 ... job end..... job2 end.....
结论:
对象实例内 synchronized aMethod(){} 与synchronized(Object) 不会相互同步
二、原理
Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。
synchronized用的锁是存在Java对象头里的。
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步是使用monitorenter和monitorexit指令实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。
根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1;
相应地,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。
如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。
注意两点:
1、synchronized同步快对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
2、同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。
(图摘自:https://blog.csdn.net/javazejian/article/details/72828483)
Synchronized在jvm字节码上的体现
我们以之前的例子为例,使用javac编译代码,然后使用javap进行反编译。
反编译后部分片段如下图:
对于使用synchronized修饰的方法,反编译后字节码中会有ACC_SYNCHRONIZED关键字。
而synchronized修饰的代码块中,在代码块的前后会有monitorenter、monitorexit关键字,此处的字节码中有两个monitorexit是因为我们有try-catch语句块,有两个出口。
Synchronized与等待唤醒
等待唤醒是指调用对象的wait、notify、notifyAll方法。调用这三个方法时,对象必须被synchronized修饰,因为这三个方法在执行时,必须获得当前对象的监视器monitor对象。
另外,与sleep方法不同的是wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行。而sleep方法只让线程休眠并不释放锁。notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized代码块或synchronized方法执行结束后才自动释放锁。
自旋锁与自适应自旋锁
- 引入自旋锁的原因:互斥同步对性能最大的影响是阻塞的实现,因为挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来很大的压力。同时虚拟机的开发团队也注意到在许多应用上面,共享数据的锁定状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。
- 自旋锁:让该线程执行一段无意义的忙循环(自旋)等待一段时间,不会被立即挂起(自旋不放弃处理器额执行时间),看持有锁的线程是否会很快释放锁。自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启;在JDK1.6中默认开启。
- 自旋锁的缺点:自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好;反之,自旋的线程就会白白消耗掉处理器的资源,它不会做任何有意义的工作,这样反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,例如让其循环10次,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起(进入阻塞状态)。通过参数-XX:PreBlockSpin可以调整自旋次数,默认的自旋次数为10。
- 自适应的自旋锁:JDK1.6引入自适应的自旋锁,自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:如果在同一个锁的对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。简单来说,就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
- 自旋锁使用场景:从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。(见前面“轻量级锁”)
总结
- synchronized特点:保证内存可见性、操作原子性
- synchronized影响性能的原因:
- 1、加锁解锁操作需要额外操作;
- 2、互斥同步对性能最大的影响是阻塞的实现,因为阻塞涉及到的挂起线程和恢复线程的操作都需要转入内核态中完成(用户态与内核态的切换的性能代价是比较大的)
- synchronized锁:对象头中的Mark Word根据锁标志位的不同而被复用
- 偏向锁:在只有一个线程执行同步块时提高性能。Mark Word存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单比较ThreadID。特点:只有等到线程竞争出现才释放偏向锁,持有偏向锁的线程不会主动释放偏向锁。之后的线程竞争偏向锁,会先检查持有偏向锁的线程是否存活,如果不存货,则对象变为无锁状态,重新偏向;如果仍存活,则偏向锁升级为轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁
- 轻量级锁:在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,尝试拷贝锁对象目前的Mark Word到栈帧的Lock Record,若拷贝成功:虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向对象的Mark Word。若拷贝失败:若当前只有一个等待线程,则可通过自旋稍微等待一下,可能持有轻量级锁的线程很快就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁
- 重量级锁:指向互斥量(mutex),底层通过操作系统的mutex lock实现。等待锁的线程会被阻塞,由于Linux下Java线程与操作系统内核态线程一一映射,所以涉及到用户态和内核态的切换、操作系统内核态中的线程的阻塞和恢复。