synchronized的底层实现原理及各种优化

synchronized的底层实现原理及各种优化

synchronized概述

synchronized,单词译为同步,是Java的内建锁,用来确保线程安全,是解决并发问题的一种重要手段。synchronized可以保证在多线程状态下,每次仅有一个线程访问共享资源。

synchronized的作用主要有以下三个:

  • 原子性:线程互斥的访问同步代码块,可以将小原子合成大原子。
  • 可见性:synchronized解锁之前,必须将工作内存中的数据同步到主内存,其它线程操作该变量时每次都可以看到被修改后的值。
  • 有序性:一个线程的加锁,必须等到其它线程将锁释放;一个线程要释放锁,首先要加锁。
synchronized同步原理

synchronized仅是Java中的一个关键字,在使用的过程中并没有看到显示的加锁和解锁过程。因此有必要通过javap命令,查看相应的字节码文件。

synchronized修饰代码块
public class Test implements Runnable {
    @Override
    public void run() {
        // 加锁操作
        synchronized (this) {
            System.out.println("hello");
        }
    }

    public static void main(String[] args) {
        Test test = new Test();
        Thread thread = new Thread(test);
        thread.start();
    }
}

javap查看相应的class文件:
在这里插入图片描述
可以看出在执行同步代码块之前之后都有一个monitor字样,其中前面的是monitorenter,后面的是离开monitorexit,不难想象一个线程也执行同步代码块,首先要获取锁,而获取锁的过程就是monitorenter ,在执行完代码块之后,要释放锁,释放锁就是执行monitorexit指令。

为什么会有两个monitorexit呢?

这个主要是防止在同步代码块中线程因异常退出,而锁没有得到释放,这必然会造成死锁(等待的线程永远获取不到锁)。因此最后一个monitorexit是保证在异常情况下,锁也可以得到释放,避免死锁。

synchronized修饰方法
public class Test implements Runnable {
    @Override
    public synchronized   void run() {
            System.out.println("hello again");
    }

    public static void main(String[] args) {
        Test test = new Test();
        Thread thread = new Thread(test);
        thread.start();
    }
}

在这里插入图片描述
仅有ACC_SYNCHRONIZED这么一个标志,该标记表明线程进入该方法时,需要monitorenter,退出该方法时需要monitorexit。

synchronized可重入的原理

**重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。**底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。

锁优化

JDK1.6之前,synchronized是一个重量级锁,何谓重量级锁?就是多个线程竞争同一把锁,未获得锁的线程都会被阻塞,等到持有锁的线程将锁释放之后,这些线程又被唤醒。其中线程的阻塞和唤醒都与操作系统有关,是一个极其耗费CPU资源的过程。因此为了提高synchronized的性能特地在JDK1.6做了优化。据说在JDK1.4已经优化完成,不过默认是关闭状态。

在了解锁优化之前需要先了解一些概念:
Java对象内存模型

在这里插入图片描述
一个Java对象由,对象标记,类型指针,真实数据,内存对齐四部分组成。

  • 对象标记也称Mark Word字段,存储当前对象的一些运行时数据。
  • 类型指针,JVM根据该指针确定该对象是哪个类的实例化对象。
  • 真实数据自然是对象的属性值。
  • 内存补齐,是当数据不是对齐数的整数倍的时候,进行调整,使得对象的整体大小是对齐数的整数倍方便寻址。典型的以空间换时间的思想。

其中对象标记和类型指针统称为Java对象头。

Mark Word字段

Mark Word用于存储对象自身运行时的数据,如hashcode,GC分代年龄,锁状态标志位,线程持有的锁,偏向线程ID,等等。

在这里插入图片描述

为社么Java的任意对象都可以作为锁?

在Java对象头中,存在一个monitor对象,每个对象自创建之后在对象头中就含有monitor对象,monitor是线程私有的,不同的对象monitor自然也是不同的,因此对象作为锁的本质是对象头中的monitor对象作为了锁。这便是为什么Java的任意对象都可以作为锁的原因。

优化手段
偏向锁:

偏向锁针对的是锁不存在竞争,每次仅有一个线程来获取该锁,为了提高获取锁的效率,因此将该锁偏向该线程。提升性能。

偏向锁的获取:

1.首先检测是否为可偏向状态(锁标识是否设置成1,锁标志位是否为01).
2.如果处于可偏向状态,测试Mark Word中的线程ID是否指向自己,如果是,不需要再次获取锁,直接执行同步代码。
3.如果线程Id,不是自己的线程Id,通过CAS获取锁,获取成功表明当前偏向锁不存在竞争,获取失败,则说明当前偏向锁存在锁竞争,偏向锁膨胀为轻量级锁。

偏向锁的撤销:

偏向锁只有当出现竞争时,才会出现锁撤销。

1。等待一个全局安全点,此时所有的线程都是暂停的,检查持有锁的线程状态,如果能找到说明当前线程还存活,说明还在执行同步块中的代码,首相将该线程阻塞,然后进行锁升级,升级到轻量级锁,唤醒该线程继续执行代同步码。
2.如果持有偏向锁的线程未存活,将对象头中的线程置null,然后直接锁升级。

轻量级锁:

偏向锁考虑的是不存在多个线程竞争同一把锁,而轻量级锁考虑的是,多个线程不会在同一时刻来竞争同一把锁。

轻量级锁的获取:

1.在线程的栈帧中创建用于存储锁记录得空间,
2.并将Mark Word复制到锁记录中,(这一步不论是否存在竞争都可以执行)。
3.尝试使用CAS将对象头中得Mark word字段变成指向锁记录得指针。
4 操作成功,不存在锁竞争,执行同步代码。
5操作失败,锁已经被其它线程抢占了,这时轻量级锁膨胀为重量级锁。

轻量级锁得释放:

反替换,使用CAS将栈帧中得锁录空间替换到对象头,成功没有锁竞争,锁得以释放,失败说明存在竞争,那块指向锁记录得指针有别的线程在用,因此锁膨胀升级为重量级锁。

重量级锁:

重量级锁描述同一时刻有多个线程竞争同一把锁。

当多个线程共同竞争同一把锁时,竞争失败得锁会被阻塞,等到持有锁的线程将锁释放后再次唤醒阻塞的线程,因为线程的唤醒和阻塞是一个很耗费CPU资源的操作,因此此处采取自适应自旋来获取重量级锁来获取重量级锁。

锁的升级

无锁 – > 偏向锁 -----> 轻量级锁 ---- > 重量级锁

其它优化
自旋锁:

线程未获得锁后,不是一昧的阻塞,而是让线程不断尝试获取锁。

缺点:若线程占用锁时间过长,导致CPU资源白白浪费。

解决方式:当尝试次数达到每个值得时候,线程挂起。

自适应自旋锁:

自旋得次数由上一次获取锁的自旋次数决定,次数稍微延长一点点。

锁消除

对于线程的私有变量,不存在并发问题,没有必要加锁,即使加锁编译后,也会去掉。

锁粗化

当一个循环中存在加锁操作时,可以将加锁操作提到循环外面执行,一次加锁代替多次加锁,提升性能。

### 回答1: synchronized关键字的底层实现原理涉及到Java对象头的概念。在Java对象头中,有一个表示锁状态的标志位,它用来标识对象的锁状态。当线程进入一个synchronized方法或代码块时,会尝试获取对象的锁。如果该锁没有被其他线程占用,则该线程会成功获取锁并进入临界区。如果该锁已经被其他线程占用,则该线程会进入阻塞状态,直到锁被释放。当线程执行完synchronized方法或代码块后,会释放锁。 在JVM中,synchronized关键字实现的锁有两种,分别为偏向锁和重量级锁。偏向锁是一种优化机制,它在对象创建时会将锁标志位初始化为偏向模式。当一个线程获取该对象的锁时,会将当线程的ID记录在对象头中,并将锁标志位设置为偏向模式。以后该线程再次获取该对象的锁时,无需竞争,可以直接获取。重量级锁则是一种比较传统的锁机制,它使用操作系统的互斥量来实现锁。当多个线程竞争同一对象的锁时,会进入阻塞队列,等待锁被释放。 因此,synchronized关键字的底层实现原理就是通过Java对象头中的标志位来实现锁状态的记录和判断,并通过偏向锁和重量级锁来优化锁的竞争。 ### 回答2: synchronized是Java中用来实现线程同步的关键字,它保证了在同一时间只有一个线程可以进入被synchronized修饰的代码块或方法。synchronized底层实现原理涉及到Java对象头、Monitor、线程间通信等。 每个Java对象在内存中都会有一个对象头,对象头中包含了一些元数据字段,其中有一个字段用来记录当对象的锁信息。当一个线程进入synchronized代码块时,首先会尝试对对象加锁,如果对象的锁信息表明已经被其他线程锁定,则该线程会进入阻塞状态,等待其他线程释放锁。如果对象的锁信息表明还没有被其他线程锁定,则将对象头中的锁信息设置为该线程,并且将一个Monitor关联到该对象上。 Monitor是Java中用来实现监视器锁的机制,它与每个Java对象关联。Monitor内部维护了一个线程等待队列和一个拥有锁的线程。每个Monitor对象只能拥有一个线程,其他线程需要获取锁时只能进入等待队列。当某个线程执行完synchronized代码块或方法时,会释放锁,并且唤醒等待队列中的一个线程来竞争锁。 线程间的通信是通过底层的wait()、notify()和notifyAll()方法实现的。当一个线程执行wait()方法时,它会释放锁并进入阻塞状态,等待其他线程调用notify()或notifyAll()方法来唤醒它。唤醒的线程将进入就绪状态,并与其他线程竞争锁,竞争成功后将继续执行。 总结起来,synchronized底层实现原理是通过Java对象头、Monitor和线程间的通信来实现的。它保证了在同一时间只有一个线程可以进入被synchronized修饰的代码块或方法,避免了多个线程对共享资源的并发访问造成的数据不一致问题。 ### 回答3: synchronized是Java中用于实现线程同步的关键字,可以用于修饰方法或代码块,保证多个线程对同一资源进行访问时的互斥。 synchronized底层实现原理是基于对象的监视器(Monitor)机制。在Java中的每一个对象都会有一个与之关联的Monitor对象,Monitor对象用于同步对共享资源的访问。当一个线程遇到synchronized修饰的代码块或方法时,它首先需要获得对象的Monitor对象的锁。若锁已经被其他线程持有,则该线程会进入阻塞状态,直到锁被释放。当该线程获得锁之后,它就可以执行临界区内的代码了。 当一个线程执行完synchronized代码块或方法后,会释放对Monitor对象的锁,其他处于等待的线程就有机会获得锁,进入临界区执行代码。这样就保证了在任意时刻,只有一个线程可以获得锁,其他线程需要等待,实现了对共享资源的互斥访问。 synchronized通过内置的锁机制来实现线程间的同步,确保了数据的一致性和完整性。它基于底层的Monitor机制利用了操作系统的原子性操作,保证了多线程并发执行时的正确性。但是,在synchronized的机制下,一个线程获得了对象的锁之后,其他线程必须等待,可能会造成线程的阻塞和延迟。此外,在一些特殊情况下,可能会出现死锁的问题,即多个线程相互等待对方释放锁。 总之,synchronized是一种可靠的线程同步机制,通过Monitor对象的锁机制实现对共享资源的互斥访问。它的底层实现原理是基于对象的监视器(Monitor)机制,利用锁和等待队列来控制线程的执行和互斥访问。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值