JAVA并发编程:synchronized关键字深入解析

生活

天气贼好的一个礼拜二的吃完晚饭的晚上。
他们去听课了。
不想写代码。
我在这看点东西吧~

闲谈

对于synchronized的记忆是最早对同步的概念。那时候聊到同步,就会说到StringBuilder和StringBuffer,里面 的方法都是一样的,但是StringBuffer里的方法都加了synchronized来修饰,所以是线程安全的。
这个关键字在JDK1.6以前还是挺笨重的,性能很差,在1.6以后做了很多优化,引入了偏向锁、轻量级锁、重量级锁的概念,性能上已经和ReentrantLock不分上下了。所以今天就来讲讲这个JVM语义上的互斥锁。

本章的学习目的

1、对象头
2、锁优化

场景

下面来一个生活中的实际场景,多线程的1000人抢1000张票。
前面我们讲到volatile关键字起到可见性的作用,所以这里直接用volatile来修饰共享变量,看看会有啥效果呢?
下面直接上案例代码:

public class Test {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(100);
        for(int i=0;i<100;i++){
            new Thread(new BuyTicket(latch)).start();
        }
        latch.await();
        System.out.println(BuyTicket.ticket);
    }

    static class BuyTicket implements Runnable {
        public static volatile  int ticket = 100;
        private CountDownLatch latch;

        @Override
        public void run() {
            
                try {
                    Thread.sleep(5L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket--;
                latch.countDown();

        }

        public BuyTicket(CountDownLatch latch) {
            this.latch = latch;
        }
    }
}

每次运行后得到的余票都比0大,说明volatile这个关键字本身不是线程安全的。
需要加锁实现线程安全:

synchronized(BuyTicket.class){
  try {
                    Thread.sleep(5L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket--;
                latch.countDown();
}

加了锁以后会发现明细的卡慢,并且经过多次实现得到的余票都是0,说明用synchronized来实现线程安全是可行的。

使用

1、synchronized 修饰静态方法,加锁对象是 类名.class对象
2、synchronized 修饰非静态方法,加锁对象this
3、synchronized修饰 代码块,加锁对象即括号里的对象。

原理(反编译)

1、
关于synchronized的实现原理,要从两个命令说起,先来看下上述BuyTicket反编译的结果:
在这里插入图片描述
通过synchronized 修饰同步代码块,反编译后主要关注以上圈红的命令。
关于monitor:每个对象都有一个对象监视器,monitor;当monitor被占用时对象就处于锁定状态。
1.1 monitorenter
线程通过执行monitorenter指令尝试占用monitor对象的过程如下:
a:如果monitor的进入数为0,则该线程进入monitor,进入数+1,此时该线程占用monitor,即成为该monitor的所有者。
b:如果该线程已经占用monitor,只是重新进入,只需要monitor+1即可
c:如果其他线程已经占用monitor,则该线程进入阻塞状态,直到monitor进入数减为0,才会尝试去占用monitor.
1.2 monitorexit
执行monitorexit的线程必须是对应的monitor所有者线程。
执行monitorexit时,monitor进入数-1,直到进入数减为0,这个线程退出monitor,不再是这个monitor的所有者。其他被阻塞的线程将被唤醒参与monitor的竞争占用。

注意1:
可以看到上面反编译的结果里有两个monitorexit,第二个monitorexit其实是为了在出现异常时退出monitor使用的。
注意2:
synchronized依赖于monitor来实现同步,Object中的wait和notify其实也是依赖于monitor实现的,所以当不在同步块执行这两个方法时会报错:
java.lang.IllegalMonitorStateException

2、上面是同步代码块的反编译,下面来看下同步方法的反编译结果:
先来非静态同步方法:

 public synchronized void  say(){
        System.out.println("hello");
    }

反编译结果如下
在这里插入图片描述
可以看到在非静态同步方法下,反编译后,发现非静态同步方法通过对ACC_SYNCHRONIZED的设置标志是否同步。
在执行方法前,指定会先查询ACC_SYNCHRONIZED是否被标记,如果被标记,则尝试获取monitor,获取成功后执行方法体,执行完毕退出monitor。方法执行期间,其他线程无法获取monitor.

静态同步方法是一样的,无非是锁定的对象为类.class,还多个标志 ACC_STATIC,代表静态。

对象头

对象头也是个很重要的概念,记得第一次听到他还是半年前,那个时候面试的时候,问到synchronized的时候,面试官聊到这个对象头的时候就一脸懵逼了。

前面讲到同步的时候,要先获取占用对象的monitor,也就是获取对象的锁,那这个monitor是什么呢、这个monitor其实就是一个对象的标记,这个标记就是存在对象的对象头里的。
对象头里存放了对象的hashcode,分代年龄,锁标记位。

JDK1.6下,锁一共有四种状态,分别是无锁状态、偏向锁、轻量级锁、重量级锁。
这几种状态随着竞争激烈程度而随之升级。
锁可以升级但是不能降级,也就是说轻量级锁不能降级为偏向锁,之所以这么设计的原因是为了提高获取锁和释放锁的效率。

在这里插入图片描述

偏向锁、轻量级锁、重量级锁

偏向锁

偏向锁获取:
当一个线程访问同步块并获取锁时,会在对象头和栈帧的锁记录里存储锁偏向的线程ID,
以后该线程进入和退出同步块不需要通过cas来加锁和释放锁,只需测试对象头里markword里是否存储了当前线程的锁偏向ID,
如果测成功,则说明已经获取锁。
如果测试不成功,则判断锁标记位是否为1【代表偏向锁】。
如果不是1,则尝试cas竞争锁,
如果是1,则尝试通过cas将对象头的偏向锁指向当前线程。

偏向锁撤销:
偏向锁使用了一种等到竞争出现才会释放锁的机制。所以当其他线程尝试获取锁时,持有偏向锁的线程才会释放。

偏向锁的撤销,需要等待全局安全点(即没有字节码执行)。
首先暂停持有偏向锁的线程,然后判断这个线程是否活着,如果已经死亡就直接设置对象头为无锁。
如果还活着,拥有偏向锁的栈会执行。
栈中的锁记录和对象头里的markword要么偏向于其他线程,要么设置为无锁或者不适用偏向锁,升级为轻量级锁。最后唤醒暂停的线程。

如果关闭偏向锁:
偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态

轻量级锁

加锁:
线程在执行同步块之前,JVM会在当前线程的栈帧中创建存放锁记录的空间,并将对象头中的markword复制过来。然后尝试将对象头里的mark word指向锁记录的指针。如果成功,线程就获得锁,如果失败,线程将cas自旋尝试。

解锁:
轻量级锁解锁时,会cas尝试把线程栈帧中的锁记录(Displaced Mark Word)替换到对象头。如果成功,说明没加竞争。如果失败,说明存在锁竞争,锁就会膨胀成重量级锁。

重量级锁

轻量级锁一旦升级为重量级锁,就不能降级。
此时当有一个线程获得锁进入同步快执行时,其他线程都只能阻塞在同步快外面,等待这个线程执行完毕,释放锁,唤醒等待的线程去竞争。

后记

洗洗睡觉

猜你喜欢

转载自blog.csdn.net/qq_28605513/article/details/84575753