码迷,mamicode.com
首页 > 编程语言 > 详细

Java Synchronized 锁的实现原理与应用 (偏向锁,轻量锁,重量锁)

时间:2019-03-20 20:46:18      阅读:202      评论:0      收藏:0      [点我收藏+]

标签:i++   优化   而且   暂停   text   process   64bit   字节   内容   

  • 简介

    在Java SE 1.6之前,Synchronized被称为重量级锁.在SE 1.6之后进行了各种优化,就出现了偏向锁,轻量锁,目的是为了减少获得锁和释放锁带来的性能消耗.

  • Synchroized的使用(三种形式)
    (1) 对于普通同步方法,锁是当前实例对象.如下代码示例:
    解释:对于set和get方法来说,都是在方法上使用了同步关键字,所以他们是同步方法,锁的就是当前的实例对象,怎么理解了,看下面的main方法,就是这个new的实例对象.所以他们的锁对象都是synchronizedMethod 这个实例.

    private int i = 0;
    
    public synchronized void setNum (int number) {
        this.i = number;
    }
    
    public synchronized int getNum () {
        return i;
    }
    
    public static void main (String[] args) {
        // 启动两个线程调用get和set方法
        SynchronizedMethod synchronizedMethod = new SynchronizedMethod();
        new Thread(() -> {
            synchronizedMethod.setNum(5);
        },"set").start();
        new Thread(() -> {
            int num = synchronizedMethod.getNum();
            System.out.println(num);
        },"get").start();
    }

    (2) 对于静态同步方法,锁是当前类的Class对象.看代码示例:
    解释:如下两个方法都是静态同步方法.所以锁是当前类的class对象,这么理解吧,静态方法是类调用的,所以锁就是这个类对象.如下代码运行结果,只有当类的第一个静态同步方法执行完毕,第二个才能执行.

    /**
    * synchronized 静态方法
    */
    public class SynchroizedStaticMethod {
    
    private static int i = 0;
    
    public static synchronized void addNum () {
        for (;;) {
            i++;
            System.out.println(Thread.currentThread().getName()+"----"+i);
            if(i >= 100){
                break;
            }
        }
    }
    
    public static synchronized void getNum () {
        System.out.println(Thread.currentThread().getName()+"----"+i);
    }
    
    public static void main (String[] args) {
        new Thread(() -> {
            SynchroizedStaticMethod.addNum();
        },"addNum").start();
        new Thread(() -> {
            SynchroizedStaticMethod.getNum();
        },"getNum").start();
    
    }
    }

    一部分执行结果
    addNum----92
    addNum----93
    addNum----94
    addNum----95
    addNum----96
    addNum----97
    addNum----98
    addNum----99
    addNum----100
    getNum----100

  • Process finished with exit code 0
    (3) 对于同步代码块,锁就是Synchronized括号里面配置的对象.如下代码实例:
    解释:通过如下代码可以证明锁就是括号里面的对象,当两个方法是一个对象时,只能是获取到对象锁的方法 执行,但是是两个锁对象时,那么两个方法获取的就是不同的锁对象,所以结果不一样了.

    /**
     * 代码块
     */
    public class SynchroizedCodeBlock {
    
        private Object object = new Object();
    
        public void printOne () {
            synchronized (object) {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + "---" + 1);
                }
            }
        }
    
        public void printTwo () {
            synchronized (object) {
                System.out.println(Thread.currentThread().getName()+"---"+2);
            }
        }
    
        public static void main (String[] args) {
            SynchroizedCodeBlock codeBlock = new SynchroizedCodeBlock();
            new Thread(() -> {
                codeBlock.printOne();
            },"printOne").start();
            new Thread(() -> {
                codeBlock.printTwo();
            },"printTwo").start();
        }
    }

    执行结果
    printOne---1
    printOne---1
    printOne---1
    printOne---1
    printOne---1
    printOne---1
    printOne---1
    printOne---1
    printOne---1
    printOne---1
    printTwo---2

    Process finished with exit code 0
    改变下括号里面的对象

    public void printTwo () {
            synchronized (this) {
                System.out.println(Thread.currentThread().getName()+"---"+2);
            }
        }

    执行结果(与第一次不一样了)
    printTwo---2
    printOne---1
    printOne---1
    printOne---1
    printOne---1
    printOne---1
    printOne---1
    printOne---1
    printOne---1
    printOne---1
    printOne---1

    Process finished with exit code 0
    3.锁在什么地方(Java 对象头)

    Synchronized用的锁是存在Java的对象头里的.如果对象时数组类型,则虚拟机用3个字宽存储对象头..Java对象头里的Mark Word里默认储存对象的HashCode.分代年龄和锁标记位

    长度 内容 说明
    32/64bit Mark Word 存储对象的hashcode或锁信息等
    32/64bit Class Metadata Address 存储对象数据类型的指针
    32/64bit Array length 数组的长度(如果当前对象时数组)

    Mark Word 的状态变化表
    技术图片

    4.JSE1.6对锁的优化(锁的升级与对比)

    在Java SE1.6中,锁一共有4中状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

    (1)偏向锁
    why:在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁.
    what:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里储存偏向的线程ID,以后该线程在进入和退出同步代码块时不需要进行cas操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word里是否储存着指向当前线程的偏向锁。如果测试成功,表示该线程获得了锁。如果测试失败,则需要在测试一下Mark Word中偏向锁的表示是否设置成1(表示当前是偏向锁):如果没有设置,则使用cas竞争锁;如果设置了,则尝试使用cas将对象头的偏向锁指向当前线程。
    偏向锁的撤销:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其它线程尝试竞争偏向锁时,持有线程才会释放锁。偏向锁的撤销,需要等待全局安全节点(在这个时间点上没有正在执行的字节码)。
    偏向锁的升级:如果有线程来竞争偏向锁,那么就需要判断对象头的Mark Word的线程ID和当前线程ID是否一致,如果不一致说明发送了竞争,那么就需要检查拥有偏向锁的线程是否还存活;如果没有存活,那么将对象头设置为无锁状态,当前线程和其它线程都可以去竞争偏向锁;如果存活,暂停拥有偏向锁的线程,遍历栈帧信息,判断这个线程是否还要使用这个锁对象,如果还需要,就撤销偏向锁,升级为轻量锁,如果不要继续使用,标记为无锁状态,重新偏向其它线程。如果升级为轻量锁后,应该还是拥有锁的线程先去执行。
    (2) 轻量锁
    why:轻量锁是为线程竞争不是很多,每个线程的执行时间不长而准备的,因为轻量锁发生竞争时,不阻塞线程,而是采用的自旋;如果竞争时就阻塞线程,而锁很快就释放了,这个线程的状态切换也是很大的消耗。
    waht:线程在执行同步代码块前,jvm会先在当前线程的栈帧中创建一个用于存储锁记录的空间,并将对象头中的Mark Word替换为为指向锁记录的指针。如果成功,当前线程获取锁,如果失败,表示其它线程竞争锁,当前线程尝试使用自旋来获取锁。这一块其实有些绕,就是怎么判断锁这一块具体参考这篇文档
    轻量锁的解锁:轻量级解锁时,会使用cas操作将disolaced Mark Word替换回到对象头,如果成功,则表示没有发生竞争。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。过程如下图所示:
    技术图片
    (3) 锁的优缺点对比

    优点 缺点 使用场景
    偏向锁 加锁和解锁不需要额为的消耗,和执行非同步方法相比,仅存在纳秒级别的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问的同步块场景
    轻量锁 竞争线程不会阻塞,提高了程序的响应速度 如果始终得不到锁竞争的线程,使用自旋会消耗cpu 追求响应时间,同步块执行速度非常快
    重量级锁 线程竞争不使用自旋,不消耗cpu 线程阻塞,响应时间缓慢 追求吞吐量,同步块执行速度较长

    Java Synchronized 锁的实现原理与应用 (偏向锁,轻量锁,重量锁)

    标签:i++   优化   而且   暂停   text   process   64bit   字节   内容   

    原文地址:https://blog.51cto.com/14220760/2366218

    (0)
    (0)
       
    举报
    评论 一句话评论(0
    登录后才能评论!
    © 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
    迷上了代码!