小言_互联网的博客

深入synchronized关键字

435人阅读  评论(0)

synchronized关键字

基本概念

synchronized是利用锁的机制来实现同步的。锁机制有如下两种特性:

  • 互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块(复合操作)进行访问。互斥性我们也往往称为操作的原子性。
  • 可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作从而引起不一致。

synchronized的用法

简单的代码:

package com.ph;

import java.util.concurrent.TimeUnit;

/**
 * @author ph
 * @version 1.0
 * @date 2020/6/19 8:58
 * @description demo
 */
public class TestSynchronized {

    public static int i;

    //修饰非静态方法
    public synchronized  void test01(){
        try {
            i++;
            TimeUnit.SECONDS.sleep(2);
            System.out.println(Thread.currentThread().getName()+" is runing"+"i="+i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //修饰静态方法
    public synchronized static void test02(){
        try {
            i++;
            TimeUnit.SECONDS.sleep(2);
            System.out.println(Thread.currentThread().getName()+" is runing"+"i="+i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //修饰非静态方法

    //代码块1(对象)this指的是当前对象
    public  void test03(){
        synchronized(this){
            try {
                i++;
               TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName()+" is runing"+"i="+i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    //代码块1(CLASS类)
    public  void test04(){
        synchronized(TestSynchronized.class){
            //有Class对象的所有的对象都共同使用这一个锁
            try {
                i++;
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName()+" is runing"+"i="+i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    public static void main(String[] args) {
//        for (int i = 0; i < 5; i++) {
//            new Thread(()->test02()).start();
//        }
        TestSynchronized testSynchronized = new TestSynchronized();
        for (int i = 0; i < 5; i++) {
            new Thread(()->testSynchronized.test01()).start();
        }
//        for (int i = 0; i < 5; i++) {
//            new Thread(()->testSynchronized.test03()).start();
//        }
//        for (int i = 0; i < 5; i++) {
//            new Thread(()->testSynchronized.test04()).start();
//        }

    }
}


分析:格局synchronized修饰的的对象分类

  • 同步方法
    1)同步非静态方法
    2)同步静态方法
  • 同步代码块
    1)获取对象锁
    synchronized(this|object) {}
    在 Java 中,每个对象都会有一个 monitor 对象,这个对象其实就是 Java 对象的锁,通常会被称为“内置锁”或“对象锁”。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。
    2)获取类锁
    synchronized(类.class) {}
    在 Java 中,针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的 Class 对象锁。每个类只有一个 Class 对象,所以每个类只有一个类锁。

根据修饰对象的不同分析:

  • 通过反编译发现:当synchronized修饰代码块时


底层通过Monitorenter和Monitorexit指令完成加锁和释放锁,并且存在两个 Monitorexit,前面一个为线程正常退出释放锁,后一个为发生异常时释放锁,避免程序死锁

在 Java 中,每个对象都会有一个 monitor 对象,监视器。

  1. 某一线程占有这个对象的时候,先查看monitor 的计数器是不是0,如果是0表示还没有线程占有,这个时候线程占有这个对象,并且对这个对象的monitor+1;如果不为0,表示这个线程已经被其他线程占有,这个线程等待。当线程释放占有权的时候,monitor-1;
  2. 同一线程可以对同一对象进行多次加锁,monitor+1、+1.体现了synchronized的可重入性。
  • 当修饰方法时:

    通过标志位ACC_SYNCHRONIZED来标志加锁
    当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

Java虚拟机对synchronized的优化

  • jdk1.6之前synchronized是重量锁

  • jdk1.6之后引入锁升级,以Hotspot 虚拟机详细讲解:

  • java实例对象:
    1)对象头:由MarkWord和Klass Point(类型指针)组成,其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据。如果对象是数组对象,那么对象头占用3个字宽(Word),如果对象是非数组对象,那么对象头占用2个字宽。
    2)实例变量:对象的属性信息
    3)填充数据:java虚拟机要求对象是8字节的整数倍,用于凑齐整数倍

  • markWord

锁升级过程

  • 锁的四种状态:无锁状态、偏向锁、轻量级锁、重量级锁
  1. 偏向锁:大多时候不存在锁竞争,常常是一个线程多次获得同一把锁,为了降低锁竞争的获取锁的消耗,引入偏向锁。
    当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。
  2. 偏向锁的取消:
    偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,如果不想有这个延迟,那么可以使用-XX:BiasedLockingStartUpDelay=0;
    如果不想要偏向锁,那么可以通过-XX:-UseBiasedLocking = false来设置;
  3. 轻量级锁:轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了就干脆不阻塞这个线程,让它自旋这等待锁释放。
  4. 轻量级锁什么时候升级为重量级锁?
    线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;
    如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。
    但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

参考:

https://blog.csdn.net/tongdanping/article/details/79647337


转载:https://blog.csdn.net/weixin_42343100/article/details/106836541
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场