• synchronized原理


    1:问题

    举例引出问题

    public class test {
        static int count = 0;
    
        public static void main(String[] args) throws InterruptedException {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    add();
                }
            };
    
            Thread thread = new Thread(runnable);
            Thread thread1 = new Thread(runnable);
            thread.start();
            thread1.start();
    
            thread.join();
            thread1.join();
    
            System.out.println("count:" + count);
        }
    
        public static void add() {
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    在这里插入图片描述
    上述代码中,启动两个线程,对变量count进行数值操作,两个线程分别执行5000次,按照预期结果来看,count的最终结果应该是10000,但是实际得到的数值却不是10000,并且实际得到的数值不是确定的。

    那么为什么会出现这种情况呢?

    因为JAVA中的++操作,它在语言层面虽然只是一行代码,看似是原子操作的,但是站在字节码的角度,它却不是原子操作的,所以两个线程在对count进行数值操作时,会出现线程之间的交错运行,导致count达不到预期的结果。

    为什么线程之间交错运行就会导致count达不到预期结果呢?

    因为两个线程进行计算的变量是属于共享变量,而不是线程私有的变量,所以两个线程进行计算时会出现一个线程将count复制到自己的空间进行计算了但是还没赋值给count,此时另一个线程读取count进行计算赋值,这种情况会导致count最终结果达不到预期。

    如何解决这种情况?
    由于count++操作在字节码的角度不是原子操作的,我们可以站在宏观的角度去解决,JAVA提供了synchronized关键字来帮助我们保证一组代码在执行时不会有别的线程来执行,这样子就可以保证在进行count++操作时只有一个线程在执行。

    2:synchronized的使用方法

    synchronized的使用可以作用于代码块或者是方法上

    public class test {
        public void function(){
            synchronized (锁对象){
                ......
            }
        }
        
        public synchronized void function1(){
            ......
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在使用synchronized时要分清楚锁对象是谁,线程间所用的锁对象是否是同一个,如果线程间使用的锁对象不是同一个,则synchronized其实是没有意义的。

    		synchronized (this){
                // 当前对象
            }
    
            Object lock = new Object();
            synchronized (lock){
                // lock对象
            }
            
            synchronized (XXX.class){
                // XXX类
            }
    		public synchronized void function(){
            	// 当前对象
        	}
    
    		public static synchronized void function(){
            	// 当前类
        	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    总结:

    1. 对于普通同步方法,锁就是当前实例
    2. 对于静态同步方法,锁就是当前类的Class对象
    3. 对于同步方法块,锁就是synchronized里配置的对象

    3:synchronized原理

    知道了synchronized的基本用法,synchronized需要与一个类或者对象进行关联,可以把类或者对象理解为一把锁,那么底层是怎么实现的呢?

    在JVM规范中提到了synchronized在JVM中的实现原理,JVM是基于进入和退出Monitor对象来实现方法同步和代码块同步,但是两者的实现不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,方法同步的细节,JVM规范中没有详细说明,但是方法同步也是使用这两个指令进行实现的。

    在这里插入图片描述

    首先编译这段代码查看字节码

    在这里插入图片描述
    可以看到代码块同步中,monitorenter是在代码同步块开始的地方,monitorexit是在代码块方法结束的地方和异常处。

    接下来查看同步方法的字节码

    在这里插入图片描述
    在这里插入图片描述
    可以看到代码块同步可以看到monitorenter与monitorexit指令,但是方法块同步在编译后却没有看到。相关书籍中说明同步方法是使用另外一种方式进行实现的,但是本质都是使用monitorenter与monitorexit指令来进行实现。

    4:Java对象头

    synchronized用的锁是存在于java对象头里的。如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型的,则用2个字宽来存储对象头。在32位虚拟机中,一个字宽等于4个字节,即32bit。

    在这里插入图片描述
    java对象头中的Mark Word里默认存储对象的HashCode、分代年龄、锁标记。32位的JVM的Mark Word默认存储如图所示。

    在这里插入图片描述
    在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。

    在这里插入图片描述
    上图中可以看到轻量级,重量级,偏向锁,GC与正常状态下的Mark Word的存储内容

    5:轻量级锁

    轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以
    使用轻量级锁。

    5.1:轻量级锁加锁:

    线程执行同步代码块之前,JVM会现在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的Mark Word信息复制到锁记录中,官方称之为 Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,则当前线程获取到锁,如果失败则表示则表示有其他线程再竞争锁,当前线程尝试使用自旋来获取锁,当然 自旋已经是属于重量级锁的优化了。

    在这里插入图片描述

    上图是获取到轻量级锁的示意图,当前线程存储了对象头的Mark Word的信息,且CAS替换了对象头中的Mark Word,替换为指向锁记录的指针。

    在这里插入图片描述
    可重入锁的示意图,常见的情况就是一个线程执行A方法获取XXX锁成功,然后A方法中调用B方法,B方法中也需要获取XXX锁,由于当前线程已经在执行A方法时获取到XXX锁了,此时调用B方法获取XXX锁时也会在栈帧中创建一条锁记录。

    5.2:轻量级锁解锁:

    轻量级锁解锁时,会使用CAS操作将Displaced Mark Word替换回对象头中,如果成功,则表示没有发生竞争,如果失败了,则表示当前锁存在竞争,锁就会膨胀为重量级锁,进行重量级锁的解锁操作。

    在这里插入图片描述
    上图中,Thread-0已经获取到了轻量级锁,此时Thread-1尝试获取锁对象,进行了Displaced Mark Word操作后进行CAS尝试替换对象头的Mark Word来指向Thread-1时会失败,因为此时的对象头的Mark Word信息指向了Thread-0,此时Thread-1会尝试 自旋 重试来获取锁,一定次数后如果获取不到锁,则会进行锁膨胀,将对象头(Object)申请Monitor锁,使对象头的Mark Word指向Monitor对象的地址,Monitor的Owner指向Thread-0,同时Thread-1进行Monitor的EntryList进行睡眠。

    在这里插入图片描述
    当Thread-0解锁后,会发现此时的锁已经不是轻量级锁,而升级位重量级锁,Thread-0会重置Monitor的Owner并唤醒EntryList中的线程来竞争锁。

    在这里插入图片描述

    1. 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

    2. 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会
      高,就多自旋几次;反之,就少自旋甚至不自旋。

    3. Java 7 之后不能控制是否开启自旋功能

    4. 因为自旋是会消耗CPU资源的,为了避免无用的自旋,(比如获得锁的线程被阻塞住了),一旦锁升级为重量级锁,就不会再恢复到轻量级锁的状态,当锁处于这个状态下,其他线程尝试获取锁时,都会被阻塞住进入EntryList中,当持有锁的线程释放锁时会唤醒这些线程,被唤醒的线程开始锁的竞争。

    6:偏向锁

    HotSopt的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,为了让线程获取锁的代价更低而引入了偏向锁。当一个线程访问同步代码块并获取锁时,会在对象头和栈帧中的锁记录里存放锁偏向的线程ID,以后该线程在进入和退出同步代码块时不需要使用CAS操作来进行加锁和解锁,只需要简单的测试对象头的Mark Word中是否存储着指向当前线程的偏向锁,如果测试成功,则表示当前线程已经获取到锁了,如果失败了,则需要再次测试Mark Word中的偏向锁的标识是否设置为了1(表示当前是偏向锁),如果没有设置,则使用CAS来竞争(此时就是轻量级锁或者重量级锁了),如果设置了,则尝试将对象头的偏向锁指向当前线程。

    关于轻量级锁,重量级锁,偏向锁,GC和正常状态下的Mark Word的格式,上文已经描述。

    接下来使用jol来查看对象的Mark Word信息

    <dependency>
        <groupId>org.openjdk.jol</groupId>
        <artifactId>jol-core</artifactId>
        <version>0.10</version>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    6.1:测试偏向锁

    @Slf4j
    public class test {
        public static void main(String[] args) throws InterruptedException {
            Object lock = new Object();
            log.info(ClassLayout.parseInstance(lock).toPrintable());
            synchronized (lock){
                log.info(ClassLayout.parseInstance(lock).toPrintable());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    上述代码中,默认情况lock对象应该是偏向锁的状态,进入同步代码块中也应该是偏向锁的状态
    在这里插入图片描述
    可以看到一开始对象的状态为001,是无锁的状态,在进入同步代码块中,变成了000,是轻量级锁的状态。

    这是因为偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 来禁用延迟

    -XX:BiasedLockingStartupDelay=0 
    
    • 1

    所以上述代码中,程序刚刚启动,偏向锁还没有启动,就进行了加锁操作,直接变为了轻量级锁

    @Slf4j
    public class test {
        public static void main(String[] args) throws InterruptedException {
            TimeUnit.SECONDS.sleep(5);
            Object lock = new Object();
            log.info(ClassLayout.parseInstance(lock).toPrintable());
            synchronized (lock){
                log.info(ClassLayout.parseInstance(lock).toPrintable());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    此时我们在方法开始时睡眠5s,再去创建锁对象,来查看锁对象的Mark Word信息
    在这里插入图片描述
    可以看到对象头的Mark Word信息是偏向锁的状态了。

    可以添加 VM 参数禁用偏向锁

     -XX:-UseBiasedLocking
    
    • 1

    如果禁用了偏向锁,那么一开始就直接是轻量级锁

    在这里插入图片描述

    @Slf4j
    public class test {
        public static void main(String[] args) throws InterruptedException {
            TimeUnit.SECONDS.sleep(5);
            Object lock = new Object();
            log.info("初始化状态:{}",ClassLayout.parseInstance(lock).toPrintable());
    
            new Thread(()->{
                synchronized (lock){
                    log.info("第一次打印:{}",ClassLayout.parseInstance(lock).toPrintable());
                }
            },"t1").start();
    
            TimeUnit.SECONDS.sleep(1);
            new Thread(()->{
                synchronized (lock){
                    log.info("第二次打印:{}",ClassLayout.parseInstance(lock).toPrintable());
                }
            },"t2").start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这里插入图片描述
    可以看到一开始就是无锁状态,在获取锁时是轻量级锁

    @Slf4j
    public class test {
        public static void main(String[] args) throws InterruptedException {
            TimeUnit.SECONDS.sleep(5);
            Object lock = new Object();
            log.info("初始化状态:{}",ClassLayout.parseInstance(lock).toPrintable());
    
            new Thread(()->{
                synchronized (lock){
                    log.info("第一次打印:{}",ClassLayout.parseInstance(lock).toPrintable());
                }
            },"t1").start();
            
            new Thread(()->{
                synchronized (lock){
                    log.info("第二次打印:{}",ClassLayout.parseInstance(lock).toPrintable());
                }
            },"t2").start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述
    可以看到 我将main线程的休眠代码去掉,两个线程就变成了竞争的状态,锁就直接变成了重量级锁

    6.2:偏向锁的撤销

    偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。他会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否还活着,如果线程不处于活动状态,则将对象头设置为无锁状态;如果线程还活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程

    6.2.1:对象的hashcode

    @Slf4j
    public class test {
        public static void main(String[] args) throws InterruptedException {
            TimeUnit.SECONDS.sleep(5);
            Object lock = new Object();
            log.info("初始化状态:{}",ClassLayout.parseInstance(lock).toPrintable());
    
            lock.hashCode();
    
            log.info("调用hashcode后:{}",ClassLayout.parseInstance(lock).toPrintable());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述
    上述例子可以得出结论,在调用锁对象的hashcode后,会禁用掉偏向锁

    为什么?

    在这里插入图片描述
    处于偏向锁状态的锁对象的Mark Word中保存的信息大部分都是ThreadID,已经没有足够的空间去保存hashCode了,那为什么调用hashCode后可以使用轻量级和重量级锁呢?

    因为轻量级和重量级锁中,锁对象的信息会存放于线程的栈帧中(轻量级)或者Monitor中(重量级)。

    6.2.2 两个线程使用同一个锁

    @Slf4j
    public class test {
        public static void main(String[] args) throws InterruptedException {
            TimeUnit.SECONDS.sleep(5);
            Object lock = new Object();
            new Thread(()->{
                log.info("t1初始化:{}",ClassLayout.parseInstance(lock).toPrintable());
                synchronized (lock){
                    log.info("t1获取锁:{}",ClassLayout.parseInstance(lock).toPrintable());
                }
                log.info("t1执行完同步代码块:{}",ClassLayout.parseInstance(lock).toPrintable());
                synchronized (test.class){
                    test.class.notify();
                }
            },"t1").start();
    
    
            new Thread(()->{
                synchronized (test.class){
                    try {
                        test.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.info("t2初始化:{}",ClassLayout.parseInstance(lock).toPrintable());
                synchronized (lock){
                    log.info("t2获取锁:{}",ClassLayout.parseInstance(lock).toPrintable());
                }
                log.info("t2执行完同步代码块:{}",ClassLayout.parseInstance(lock).toPrintable());
            },"t2").start();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    在这里插入图片描述
    在这里插入图片描述

    可以看到t2获取锁,锁对象的Mark Word信息变成了轻量级锁

    6.2.3 批量重偏向

    @Slf4j
    public class test {
        public static void main(String[] args) throws InterruptedException {
            TimeUnit.SECONDS.sleep(5);
            Vector<Dog> list = new Vector<>();
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 30; i++) {
                    Dog d = new Dog();
                    list.add(d);
                    synchronized (d) {
                        log.info(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                    }
                }
                synchronized (list) {
                    list.notify();
                }
            }, "t1");
            t1.start();
    
            Thread t2 = new Thread(() -> {
                synchronized (list) {
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.info("===============> ");
                for (int i = 0; i < 30; i++) {
                    Dog d = list.get(i);
                    log.info(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                    synchronized (d) {
                        log.info(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                    }
                    log.info(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                }
            }, "t2");
            t2.start();
        }
    }
    class Dog{
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至
    加锁线程。

    6.2.4 批量撤销

    @Slf4j
    public class test {
        static Thread t1, t2, t3;
    
        public static void main(String[] args) throws InterruptedException {
            TimeUnit.SECONDS.sleep(5);
            Vector<Dog> list = new Vector<>();
            int loopNumber = 39;
            t1 = new Thread(() -> {
                for (int i = 0; i < loopNumber; i++) {
                    Dog d = new Dog();
                    list.add(d);
                    synchronized (d) {
                        log.info(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                    }
                }
                LockSupport.unpark(t2);
            }, "t1");
            t1.start();
    
            t2 = new Thread(() -> {
                LockSupport.park();
                log.info("===============> ");
                for (int i = 0; i < loopNumber; i++) {
                    Dog d = list.get(i);
                    log.info(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                    synchronized (d) {
                        log.info(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                    }
                    log.info(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                }
                LockSupport.unpark(t3);
            }, "t2");
            t2.start();
    
            t3 = new Thread(() -> {
                LockSupport.park();
                log.info("===============> ");
                for (int i = 0; i < loopNumber; i++) {
                    Dog d = list.get(i);
                    log.info(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                    synchronized (d) {
                        log.info(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                    }
                    log.info(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                }
            }, "t3");
            t3.start();
            t3.join();
            log.info(ClassLayout.parseInstance(new Dog()).toPrintable());
        }
    }
    
    class Dog {
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象
    都会变为不可偏向的,新建的对象也是不可偏向的

    在这里插入图片描述

    7:锁的优缺点对比

    优点缺点适用场景
    偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级别的差距如果线程间存在竞争,会带来锁的撤销的消耗适用于只有一个线程访问的场景
    轻量级锁竞争的线程不会阻塞,提高了相应速度如果得不到锁竞争的线程会有自旋的额外CPU开销追求响应时间,同步块执行速度非常快
    重量级锁线程竞争不属于自旋,不会消耗CPU线程会阻塞,响应时间慢追求吞吐量
  • 相关阅读:
    POJ 2836 Rectangular Covering 状态压缩DP(铺砖问题)
    element-ui 表单验证注意事项
    洛谷P4683 [IOI2008] Type Printer 题解
    CSS 基础知识 选择器
    从零开始,开发一个 Web Office 套件(4):新的问题—— z-index
    2024.05.21 校招 实习 内推 面经
    抖音小程序开发教学系列(3)- 抖音小程序页面开发
    C# 第五章『面向对象』◆第10节:委托
    macOS端React的项目WebPack热更新(HMR)失效问题分析及解决,原因竟是Windows文件系统不区分大小写导致
    国庆当天打卡北京拍飞机网红地:西湖园
  • 原文地址:https://blog.csdn.net/weixin_45690465/article/details/127477931