转载

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

  1. 简介

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

  2. 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 的状态变化表

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

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替换回到对象头,如果成功,则表示没有发生竞争。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。过程如下图所示:

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

(3) 锁的优缺点对比

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