synchronized底层原理

synchronized底层原理

常见三种使用方法:

1)普通同步方法,锁是当前实例;
2)静态同步方法,锁是当前类的Class实例,Class数据存在永久代中,是该类的一个全局锁;
3)对于同步代码块,锁是synchronized括号里配置的对象。

1、对象锁机制(monitor):

先看一段代码:

public class Test{
    private static Object object = new Object();
    public static void main(String[] args) {
        synchronized (object) {
            System.out.println("hello world");
        }
    }
}

执行同步代码块首先要执行 monitorenter指令 ,退出的时候monitorexit指令。通过分析之后可以看出,使用Synchronized进行同步,其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,否则就只能等待。而这个获取的过程是互斥的,即同一时刻只有一个线程能够获取monitor。

字节码中包含一个monitorenter指令以及多个monitorexit指令 . 这是因为Java虚拟机需要确保所获得的锁在正常执行路路径,以及异常执行路径上都能够被解锁。


下面我们再来看看用Synchronized修饰的同步方法又会进行怎样的操作?

代码:

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

当用 synchronized 标记方法时,你会看到字节码中方法的访问标记包括 ACC_SYNCHRONIZED。该标记表示在进入该方法时, Java 虚拟机需要进行 monitorenter 操作。而在退出该方法时,不管是正常返回,还是向调用者抛异常, Java 虚拟机均需要进行 monitorexit 操作。

这里 monitorenter 和 monitorexit 操作所对应的锁对象是隐式的。对于实例方法来说,这两个操作对应的锁对象是 this;对于静态方法来说,这两个操作对应的锁对象则是所在类的 Class 实例。

关于 monitorenter 和 monitorexit 的作用,我们可以抽象地理解为每个锁对象拥有一个锁计数器一个指向持有该锁的线程的指针。

当执行 monitorenter 时,如果目标锁对象的计数器为 0,那么说明它没有被其他线程所持有。在这个情况下, Java 虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加 1。

在目标锁对象的计数器不为 0 的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁 。

当执行 monitorexit 时, Java 虚拟机则需将锁对象的计数器减 1。当计数器减为 0 时,那便代表该锁已经被释放掉了。

之所以采用这种计数器的方式,是为了允许同一个线程重复获取同一把锁。举个例子,如果一个 Java类中拥有多个 synchronized 方法,那么这些方法之间的相互调用,不管是直接的还是间接的,都会涉及对同一把锁的重复加锁操作。因此,我们需要设计这么一个可重入的特性,来避免编程里的隐式约束。

如何来证明同一个线程再次获得锁时可以获取成功而其他线程获取锁会阻塞呢?想办法证明锁的可重入性。

2、JDK1.5提供的Lock锁

范例:使用ReentrantLock进行同步处理

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Test2 {
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        Thread t1 = new Thread(mt, "⻩牛A");
        Thread t2 = new Thread(mt, "⻩牛B");
        Thread t3 = new Thread(mt, "⻩牛C");
        t1.setPriority(Thread.MIN_PRIORITY);
        t2.setPriority(Thread.MAX_PRIORITY);
        t3.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
        t3.start();
    }
}
class MyThread implements Runnable {
    private int ticket = 500;
    private Lock ticketLock = new ReentrantLock() ;
    @Override
    public void run() {
        for (int i = 0; i < 500; i++) {
            ticketLock.lock();
            try {
                if (this.ticket > 0) { // 还有票
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } // 模拟⽹网络延迟
                    System.out.println(Thread.currentThread().getName() + ",还有" + this.ticket-- +
                            " 张票");
                }
            } finally {
                ticketLock.unlock();
            }
        }
    }
}

在JDK1.5中, synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java提供的Lock对象,性能更高一些。

到了JDK1.6,发生了变化,对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。 导致在JDK1.6上synchronize的性能并不不比Lock差。 官方也表示,他们也更支持synchronized,在未来的版本中还有优化余地,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步 。

3、synchronized的优化

现在我们对Synchronized应该有所印象了,它最大的特征就是在同一时刻只有一个线程能够获得对象的监视器(monitor),从而进入到同步代码块或者同步方法之中,即表现为互斥性(排它性)。 这种方式肯定效率低下,每次只能通过一个线程,既然每次只能通过一个,这种形式不不能改变的话,那么我们能不能让每次通过的速度变快一点呢。

在聊到锁的优化也就是锁的几种状态前,有两个知识点需要先关注:(1) CAS操作 (2) Java对象头,
这是理理解下面知识的前提条件。

4、CAS操作
4.1 什么是CAS( Compare and swap )

使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。 而CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。 那么,如果出现冲突了怎么办?无锁操作是使用CAS(compare and swap)又叫做比较交换来别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。

4.2、CAS的操作过程

CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为: V 内存地址存放的实际值; O预期的值(旧值); N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了了,自然而然可以将新值N赋值给V。 反之, V 和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量时,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程 。

CAS的实现需要硬件指令集的支撑,在JDK1.5后虚拟机才可以使用处理器提供的CMPXCHG指令实现。

元老级的Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。而CAS并不是武断的将线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。这是两者主要的区别!

CAS的问题

  • ABA问题 因为CAS会检查旧值有没有变化,这里里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。在JDK1.5后的atomic包中提供了AtomicStampedReference来解决ABA问题,解决思路路就是这样的。

  • 自旋会浪费大量的处理器资源

    与线程阻塞相比,自旋会浪费大量的处理器资源。这是因为当前线程仍处于运行行状况,只不过跑的是无用 指令。它期望在运行无用指令的过程中,锁能够被释放出来。

    我们可以用等红绿灯作为例子。 Java 线程的阻塞相当于熄火停车,而自旋状态相当于怠速停车。如果红灯的等待时间非常长,那么熄火停车相对省油一些;如果红灯的等待时间非常短,比如我们在同步代码块中只做了一个整型加法,那么在短时间内锁肯定会被释放出来,因此怠速停车更合适。

    然而,对于JVM来说,它并不不能看到红灯的剩余时间,也就没法根据等待时间的长短来选择是自旋还是阻塞。 JVM给出的方案是自适应自旋,根据以往自旋等待时能否获取锁,来动态调整自旋的时间(循环数)。

    就我们的例子来说,如果之前不熄火等待了绿灯,那么这次不熄火的时间就长一点;如果之前熄火等待绿灯,那么这次不熄火的时间就短一点

  • 公平性

    自旋状态还带来另外一个副作用,不公平的锁机制。处于阻塞状态的线程,无法立刻竞争被释放的锁。
    然而,处于自旋状态的线程,则很有可能优先获得这把锁。

    内建锁无法实现公平机制,而lock体系可以实现公平锁。

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

Java虚拟机中synchronized关键字的实现,按照代价由高到低可以分为重量级锁、轻量锁和偏向锁三种!

1、 重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。 JVM采用了自适应自旋,来避免线程在面对非常小的synchronized代码块时,仍会被阻塞、唤醒的情况。

2、 轻量级锁采用CAS操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。 它针对的是多个线程在不同时间段申请同一把锁的情况。

3、 偏向锁只会在第一次请求时采用CAS操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况。

4.4、死锁

同步的本质在于:一个线程等待另外一个线程执行完毕,执行完成后才可以继续执行。但是如果现在相关的几个线程彼此之间都在等待着,那么就会造成死锁。

死锁一旦出现之后,整个程序就将中断执行,所以死锁属于严重性问题。过多的同步会造成死锁,对于资源的上锁一定要注意不要成"环"。

死锁产生的条件

  • 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用 。

  • 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。

  • 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。

  • 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了
    一个等待环路

    通俗的讲:两个资源1和2,在不同的线程A、B中分别持有对方申请的资源(线程A持有1,申请2;线程B持有2,申请1)

解决的方案:

  • 一次性申请资源(相当与所有资源都在一个线程中申请成功)
  • 在线程满足条件时,释放掉已占有的资源
  • 按顺序申请资源

举例:

public class Test {
    private static Pen pen = new Pen() ;
    private static Book book = new Book() ;
    public static void main(String[] args) {
        new Test().deadLock();
    }

    private void deadLock() {
        Thread thread1 = new Thread(new Runnable() { // 笔线程
            @Override
            public void run() {
                synchronized (pen) {
                    System.out.println(Thread.currentThread()+" :我有笔,我就不不给你");
                    synchronized (book) {
                        System.out.println(Thread.currentThread()+" :把你的本给我! ");
                    }
                }
            }
        },"Pen") ;
        Thread thread2 = new Thread(new Runnable() { // 本⼦子线程
            @Override
            public void run() {
                synchronized (book) {
                    System.out.println(Thread.currentThread()+" :我有本⼦子,我就不不给你! ");
                    synchronized (pen) {
                        System.out.println(Thread.currentThread()+" :把你的笔给我! ");
                    }
                }
            }
        },"Book") ;
        thread1.start();
        thread2.start();
    }
}
class Pen {
    private String pen = "笔" ;
    public String getPen() {
        return pen;
    }
}
class Book {
    private String book = "本" ;
    public String getBook() {
        return book;
    }
}

运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HEculCDO-1585203215001)(C:\Users\惠秋丽\AppData\Roaming\Typora\typora-user-images\1585114424146.png)]

5、AQS

AbstractQueuedSynchronizer (抽象的同步器)

基于CAS 原理实现 同步队列:无条件自旋,尝试添加/删除队列元素。失败就一直循环调用添加/删除操作,一直到执行成功。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值