Synchronized 在多线程环境下是不可缺少的,那么对于Synchronized 又了解多少呢。下面就系统总结,而对于Synchronized的基本使用,请参看另一篇博客。
1.1 Synchronized 作用
确保线程互斥的访问同步代码
保证共享变量的修改能够及时可见
有效解决重排序问题
首先使用JDK自带的反编译工具查看Synchronized编译后的字节码,打开cmd进入到.class文件所在文件目录,输入javap -v 类名.class
先看如下代码:
package com.mult;
public class Demo {
private static int value = 10;
public static void main(String[] args) {
System.out.println(new Demo().method());
}
public synchronized int method() {
synchronized (Demo.class) {
if (value > 5) {
return value;
} else {
return 0;
}
}
}
}
从上图可以看出Synchronized 是通过monitorenter和monitorexit两个字节码指令实现的。在每一个对象中都会存在一个Monitor监视器,而monienter和monitorexit两者之间是互斥关系,monienter用于获取对象锁,而moniexit释放对象锁。
在JVM规范文档中有以下说明:
如果 Monitor 的计数器为 0,则该线程进入 Monitor,然后将计数器值设置为 1,该线程即为 Monitor 的所有者,也就是说此时获取到对象锁。
如果线程已经占有该 Monitor,只是重新进入,则进入 Monitor 的计数器加 1。
如果其他线程已经占用了 Monitor,则该线程进入阻塞状态,直到 Monitor 的计数器为 0,再重新尝试获取 Monitor 的所有权。
当计数器为0时,Monitor便会释放对象锁,那么其他阻塞的线程就可以尝试申请获取对象锁。
总结这里,就要引出另一个内容,就是Synchronized是可重入锁。
可重入锁: 一个线程已经获取到对象锁时,其他线程处于阻塞状态。但获取到对象锁的线程再次去请求自己所持有的对象锁资源时,这种情况成为可重入锁。
请看实例代码:
public class Demo {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
Demo demo = new Demo();
demo.method_1();
}
}).start();
}
public synchronized void method_1() {
System.out.println(Thread.currentThread().getName()+"-->method_1....");
method_2();
}
public synchronized void method_2() {
System.out.println(Thread.currentThread().getName()+"-->method_2....");
}
}
以上代码中只有一个demo对象锁,在method_1中调用method_2结果依然可以打印,证明Synchronized是可重入锁。反之,如果不是可重入锁,那么在method_1中获取到对象锁,接着调用method_2便会产生死锁,另外两个方法的线程名称是相同的,也可以证明该线程拿到的就是同一个对象锁。
注意:当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。
在JDK6之后,对Synchronized的实现进行了优化,引入了偏向锁、轻量级锁,锁,它们之间的关系为;
无锁->偏向锁->轻量级锁->重量级锁
注意:以上级别之间的转换是单向的,只能从低级转向高级,反之不可。
4.1 偏向锁
在某一环境下,一个线程可能会多次获得对象锁。那么频繁的申请锁释放锁势必会对性能造成一定影响,因此引入偏向锁概念。当一个线程频繁获得对象锁时,会在对象头中存储锁偏向的线程ID,然后当该线程再次申请或释放锁时,就不再需要做其他的同步操作,因而在一定程度上可以提高系统性能。
4.2 轻量级锁
轻量级锁在偏向锁的上一级,在偏向锁不再适用的情况下,就会向上升级。当升级为轻量级锁时,Mark Word的结构也会相应的变化。线程在栈帧中创建锁记录,接着将锁对象中Mark Word复制到线程创建的所记录中,而锁对象中的Mark Word则被替换为指向锁记录的指针,完成轻量级锁的实现。而轻量级锁的引入是为解决在重量级锁中,多线程之间的性能消耗问题。
4.3 自旋锁
自选锁顾名思义就是“自己旋转”。同样在多线程的环境下,其中一条线程获得对象锁,而其他的线程则在原地循环等待其他线程释放锁,而不是处于线程阻塞状态。这种原地循环等待的情况是会消耗CPU资源的,默认情况下循环10次。自旋锁的使用一般是小城获取锁的时间较短,让其他线程稍微等待一段时间进而再获得对象锁,比如对于同步代码块的执行一般是较快的。如果线程循环时间较长,那么操作系统便会将此线程挂起,避免资源的更多浪费。
对于自旋的概念可能不太好理解,下面写个小Demo。
public static void main(String[] args) {
// 线程1
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开始执行了...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"执行完毕...");
}
}).start();
// 线程2
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开始执行了...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"执行完毕...");
}
}).start();
System.out.println("全部线程执行完毕...");
}
运行结果:
分析:以上案例本来的目的是当全部线程执行完毕后,再打印全部线程执行完毕。但是在多线程情况下这是无法保证的,下面进行优化。
while(Thread.activeCount() != 1){
}
System.out.println("全部线程执行完毕...");
重复的代码就不再展示,只是在最后一句打印前添加死循环,让其一直判断当前活动的线程是否只剩下一个,如果是则退出while循环。那么while循环就是一直在不停循环的等待过程,直到活动线程为最后一个。
适应性自旋
是不固定自旋10次一下。它可以根据它前面线程的自旋情况,从而调整它的自旋,甚至是不经过自旋而直接挂起。
4.4 重量级锁
当轻量级锁膨胀到重量级锁之后,表示线程只能被挂起阻塞来等待被唤醒了,那么这种锁机制效率就相对比较慢,同时比较损耗系统资源。
到这里关于Synchronized的总结就结束了,还有一种ReentrantLock锁也是可重入锁。