一, 前言
Synchronized 在多线程环境下是不可缺少的, 那么对于 Synchronized 又了解多少呢. 下面就系统总结, 而对于 Synchronized 的基本使用, 请参看另一篇博客.
1.1,Synchronized 作用
确保线程互斥的访问同步代码
保证共享变量的修改能够及时可见
有效解决重排序问题
二, 从 JVM 理解 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 锁也是可重入锁.
以上内容均是学习总结, 如有不适之处欢迎留言指正.
来源: https://www.cnblogs.com/fenjyang/p/11594556.html