java锁的相关术语及synchronized原理详解

java锁的相关术语及synchronized原理详解

1、几种锁的概念

(1)自旋锁

​ CPU循环的使用CAS技术对数据尝试更新,直至成功。

(2)悲观锁

​ 线程假定会发生并发冲突,同步所有对数据的操作,从读操作开始就上锁。

(3)乐观锁

​ 线程假定没有冲突,在修改数据时发现数据和一开始获取的不一致,则读取最新数据并再次尝试修改。

(4)独享锁(写)

​ 线程给资源加上写锁,并且可以修改资源值,其他线程不能;(单写)

(5)共享锁(读)

​ 线程给资源加上读锁后只能读不能写,其他线程也只能加读锁,不能加写锁;(多读)

(6)可重入锁/不可重入锁

​ 线程拿到锁以后,可以自由进入同一把锁所同步的其他代码,则为可重入锁;反之,则为不可重入锁。

(7)公平锁/非公平锁

​ 争抢锁的顺序 ,如果按照先来后到,则为公平锁;反之,则为非公平锁。

2、同步关键字synchronized

写在前面

CAS:传入一个旧值A(期望操作前的值)和一个新值B,在操作前比较之前存储的一开始的旧值A和现在读取的旧值A’是否发生变化(因为在操作前可能会有其他线程改变了旧值),没有发生变化则将旧值交换为新值,发生了变化则不交换。

synchronized原理

​ synchronized是基于对象监视器实现的。java中的每个对象都与一个监视器关联,一个线程可以锁定或者解锁监视器,从而拿到对应对象监视器的操作权;一次只有一个线程可以锁定监视器;当一个线程锁定一个监视器以后,其他所有试图锁定该监视器的线程都会被阻塞,直到它们可以锁定该监视器为止。

  • 特性:可重入、独享、悲观锁

  • 锁的范围:类锁、对象锁(此处理解为“实例锁”更为恰当,因为在java中一切皆对象,包括类)、锁消除、锁粗化

    public class ObjectSyncDemo1 {
    
        static Object temp = new Object();
    
        public static synchronized void test1() { 
                try {
                    System.out.println(Thread.currentThread() + " 我开始执行");
                    Thread.sleep(3000L);
                    System.out.println(Thread.currentThread() + " 我执行结束");
                } catch (InterruptedException e) {
                }
    
        }
        
    //    public synchronized void test1() { 
    //        synchronized(ObjectSyncDemo1.class){
    //            try {
    //                System.out.println(Thread.currentThread() + " 我开始执行");
    //                Thread.sleep(3000L);
    //                System.out.println(Thread.currentThread() + " 我执行结束");
    //            } catch (InterruptedException e) {
    //            }
    //        }
    //    }
    
        public static void main(String[] args) throws InterruptedException {
            new Thread(() -> {
                new ObjectSyncDemo1().test1();
            }).start();
    
            Thread.sleep(1000L); // 等1秒钟,让前一个线程启动起来
            new Thread(() -> {
                new ObjectSyncDemo1().test1();
            }).start();
        }
    }
    

在这里插入图片描述

public class ObjectSyncDemo1 {

    static Object temp = new Object();
    //改为非静态方法
    public synchronized void test1() { 
            try {
                System.out.println(Thread.currentThread() + " 我开始执行");
                Thread.sleep(3000L);
                System.out.println(Thread.currentThread() + " 我执行结束");
            } catch (InterruptedException e) {
            }

    }
    
//    public synchronized void test1() { 
//        synchronized(this){
//            try {
//                System.out.println(Thread.currentThread() + " 我开始执行");
//                Thread.sleep(3000L);
//                System.out.println(Thread.currentThread() + " 我执行结束");
//            } catch (InterruptedException e) {
//            }
//        }
//    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            new ObjectSyncDemo1().test1();
        }).start();

        Thread.sleep(1000L); // 等1秒钟,让前一个线程启动起来
        new Thread(() -> {
            new ObjectSyncDemo1().test1();
        }).start();
    }
}

在这里插入图片描述

synchronized在非静态方法上:锁的对象 是 类的一个实例 ; 在静态方法上:锁的对象 是 类本身,即类锁。除此之外,public static synchronized void test1() 等价于 synchronized (ObjectSyncDemo1.class),都是类锁;public synchronized void test1() 等价于 synchronized (this),都是实例锁。

​ 实例锁可以理解为锁住的是一个类的一个实例,当再建一个实例的时候,之前一个实例的锁对新建的这个实例对象没有锁的作用。所以第二段代码中两个线程可以同时执行,不用等到第一个线程中的实例方法(Thread.sleep(3000L))执行完再执行第二个线程中的方法;同理,类锁锁住的是类,无论建多少个类的实例,只要一个实例中的类锁没有释放,同一个类的实例都要阻塞直至上一个实例的类锁释放,这也是为什么第一段代码中的两个线程会先后执行的原因。

package com.study.lock.sync;

// 可重入
public class ObjectSyncDemo2 {

    public synchronized void test1(Object arg) {
        // 继续执行,保证能读取到之前的修改 JMM
        System.out.println(Thread.currentThread() + " 我开始执行 " + arg);
        if (arg == null) {
            test1(new Object());
        }
        System.out.println(Thread.currentThread() + " 我执行结束" + arg);
    }

    public static void main(String[] args) throws InterruptedException {
        new ObjectSyncDemo2().test1(null);
    }
}

在这里插入图片描述

​ 这个例子就体现了synchronized的可重入特性。当一个线程拿到锁后,其他线程就不能再进入锁的对象进行读或写操作,体现了synchronized的独享和悲观锁特性。

synchronized不仅实现了同步,根据JMM(Java Memory Model)还实现了可见性(读取最新主内存数据,并写入主内存)

锁粗化:可以理解为锁的范围扩大

package com.study.lock.sync;

// 锁粗化(运行时 jit 编译优化)
// jit 编译后的汇编内容, jitwatch可视化工具进行查看
public class ObjectSyncDemo3 {
    int i;

    public void test1(Object arg) {
        synchronized (this) {
            i++;
        // }
        //  两次锁中间什么事情也没干,而且解锁、加锁也会浪费资源,导致性能不高,因此jvm就会优化,把锁范围扩大,即 锁粗化
        // synchronized (this) {
            i++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10000000; i++) {
            new ObjectSyncDemo3().test1("a");
        }
    }
}

锁消除:在一些情况下不需要锁,jvm就会把锁去掉以提高性能

public void test1(Object arg) {
        // jit 优化, 消除了锁 (StringBuffer是方法内的 局部变量,没有竞态条件,所以不需要锁,但是StringBuffer本身就是线程安全的,它的实现里面是有锁的,所以jvm在真正执行过程中会把锁消除)
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("a");
        stringBuffer.append(arg);
        stringBuffer.append("c");
        // System.out.println(stringBuffer.toString());
    }
锁升级过程

在这里插入图片描述

图中tag有4种状态,分别是01(无锁)、00(轻量级锁)、10(重量级锁)、11(GC废弃)

​ 每个对象实例头部都有一个mark word,里面包含很多位信息,可以理解成一张表,里面包含很多字段信息,但是这些字段是根据锁状态动态生成的,并不是一直不变的。

​ 一个对象在被创建的时候,根据配置生成对象的内存结构,也就是上面所说的mark word。这个配置可以通过参数 -XX: -UseBiasedLocking 来禁用使用偏置锁定,也就是禁止开启偏向锁(偏向锁本质就是无锁),默认是开启偏向锁的。

​ 默认开启偏向锁时,偏向锁标记为1,tag状态为01(无锁)。当只有一个线程访问此对象时,会直接把“线程ID”设置为自己的线程ID,以后再次访问这个对象的时候,就直接比对“线程ID”的值是否与自己相同,如果相同就可以直接对对象进行操作,此时即为偏向锁;如果不同则说明是另外一个线程(或者多个线程)来访问对象,就会产生竞争,此时表结构就会发生变化,如上图所示。此时就升级为轻量级锁,偏向锁标记为0,并且多个线程将执行CAS操作将tag状态改为00(轻量级锁),当其中一个线程修改状态成功后,对象内存结构的表字段变成上图所示,会出现一个“lock锁记录地址”的 字段,它指向当前修改tag状态成功的线程栈。但是当线程比较多的时候, 频繁的进行CAS操作会消耗大量的CPU资源,所以当修改状态失败的线程CAS自旋一定次数以后,对象锁会升级为重量级锁。此时tag状态改为10,表结构中会出现一个“监视器对象地址”的字段,用来存储对象的监视器地址。并且线程都会去争抢这个监视器对象的锁,没有抢到锁的线程就会进入锁池中等待,以避免出现多线程循环CAS空转浪费资源的问题。

​ 当一个类的对象经常发生多线程争抢操作时,JVM会优化不会开启偏向锁了,直接从轻量级锁开始。

在这里插入图片描述

在这里插入图片描述

等待队列WaitSet是为wait/notify所设置的一个队列,用来放置wait的线程并等待notify。

为什么需要偏向锁?

​ 根据统计,当线程操作对象,对对象定义了锁之后,并发情况较少,所以为了减少资源开销,避免不必要的资源浪费,JVM定义了偏向锁,这样当只有一个线程操作时,不用浪费资源为对象创建监视器对象,不会有加锁、解锁的操作,性能也更高。

总结

同步关键字,优化内容:

偏向锁(减少在无竞争情况下的JVM资源消耗)

–> 出现两个及以上的线程争抢–>轻量级锁(CAS修改状态)
–>线程CAS自旋一定次数之后,升级为重量级锁(对象的mark word 内部会保存一个监视器锁的一个地址)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值