Synchronized
简介
synchronized 关键字解决的是多个线程之间访问资源的同步性,它可以保证被它修饰的方法或者代码块在任意时刻都只会是一个线程执行。
版本对比
在 Java 早期1.2版本中,synchronized属于重量级锁,效率低,因为监视器锁(monitor)是依赖于底层的操作系统OS的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的,所以每次将挂起或者唤醒一个线程,都需要操作系统帮助完成。而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要较长的时间,时间成本相对较高。总结一句话,重量锁就是把所有事都交给了操作系统来完成。这也是为什么早期的 synchronized 效率低的原因。
而在 Java1.6 后不断优化,使的 synchronized 提供三种锁的实现,包括偏向锁、轻量级锁、重量级锁,还提供自动的升级和降级机制。
这里将对1.6优化后的 synchronized 进行展开。
自动的升级机制:
-
当前线程如果是第一个执行同步方法,那么会对同步方法 “贴上自己的标签“,也就是把自己的线程id写到对象头上mark word,上了偏向锁,那么下次回来便可直接使用,省去了再申请锁,减少了很多锁竞争的过程。
-
但是,一旦有其他的线程过来,那么在外阻塞的线程会把标记抹掉。此时,会升级到轻量级锁–自旋锁。在外的线程会不断while循环判断在同步方法内的线程执行结束没。如果说有线程在同步方法内执行时间过久,而在外的线程一直自旋,然而while有是很消耗的资源的,所以在这时候会再次升级。
-
从轻量级锁升级为重量级锁的情况还有点不同:
-
在 Java1.6 之前有两种方式:
- 自旋超过10次,可通过JVM调优参数
-XX:PreBlockSpin
修改 - 等待的线程超过CUP核的二分之一。例:CPU32核,等待已经16个。现在又来了一个,那么升级。
- 自旋超过10次,可通过JVM调优参数
-
在 Java1.6 之后增加了一个:自适应自旋 Adapative Self Spinning
- 自旋就不用再自己设置,由JVM自己决定。
-
关于偏向锁:
永远偏向第一个加锁的对象。
上文所说:当前线程如果是第一个执行同步方法,那么会对同步方法 “贴上自己的标签“,也就是把自己的线程id写到对象头上mark word,上了偏向锁,那么下次回来便可直接使用,省去了再次申请锁,减少了很多锁竞争的过程。
但它并不是锁,是一种优化。何种优化?
如Vactor、StringBuffer、HashTable线程安全的类在大多情况下只有单条线程来访问,所以出现锁竞争的情况很少,为了省资源可以设置个偏向锁,省去竞争。所以偏向锁是一种优化。
使用偏向锁是否会提高效率?为什么?
答:分情况
- 如果线程不多,大多数都是单线程的情况,是会提高效率。
- 如果明确知道有很多的线程会对一个同步方法进行竞争时,就没必要增加,因为产生锁竞争时对偏向锁的锁撤销是会消耗资源的。
轻量级锁就会比重量级锁效率高吗?
轻量级锁在自旋时的while必然会造成资源消耗,所以在线程多且执行时间长的情况下,轻量级锁效率低,重量级则就很不错。
相反,线程少执行短,那么轻量级效率高,重量级低。
Synchronized 实现
-
修饰代码块: 会对括号内的对象上锁
-
修饰实例方法: 会对调用者进行上锁,也就是实例对象
-
修饰静态方法: 会对当前类的Class对象上锁,也就从Class模板实例出的对象都会被上锁
举例说明:
public class SyncTest {
// 同步方法
public synchronized void test01(){
System.out.println("SyncTest => test01");
}
// 同步代码块
public void test02(){
synchronized (this) {
System.out.println("SyncTest => test02");
}
}
}
通过查看字节码我们可以看到:
进过编译,会在同步代码块的前后分别形成monitorenter和monitorexit这个两个字节码指令,然后执行两指令获取和释放monitor。
- 线程通过 monitorenter 进入时如果 monitor 为 0,那么该线程可以持有 monitor 后续代码,并将monitor 加1;如果当前线程已经持有了monitor,那么monitor 继续加1 (也就可重入);如果进入时 monitor 不为0,那么其他线程就会进入阻塞状态。
(可重入锁举例:当你进入家门后,自然还可以进入其他房门,其他线程被关在家门外)
修饰方法的的情况,同步方法使用ACC_SYNCHRONIZED来标识,而没有monitorenter和monitorexit。JVM 通过访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
先这样,写的有点急优点乱,后续有其他知识点再进行修改补充