三:synchronized 关键字

目录

  • 1、共享带来的问题
  • 2、synchronized 用法
  • 3、类加载器对 Class 锁的影响
  • 4、synchronized 实现原理
    • 4.1、同步方法、同步代码块
    • 4.2、对象内存布局
    • 4.3、Monitor 对象定义
  • 5、synchronized 与原子性
  • 6、synchronized 与可见性
  • 7、synchronized 与有序性
  • 8、synchronized 锁升级
    • 8.1、概述
    • 8.2、偏向锁
      • 8.2.1、概念
      • 8.2.2、偏向锁的实现
      • 8.2.3、案例
      • 8.2.4、偏向锁的撤销
    • 8.3、轻量级锁
    • 8.4、重量级锁
  • 9、自旋锁
  • 10、自适应自旋锁
  • 11、锁消除
  • 12、锁粗化

1、共享带来的问题

public class ThreadTest {
    private static int count = 0;

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 100; i++) {
            Thread addThread = new Thread(() -> {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                count++;
            });
            addThread.start();
        }
        Thread.sleep(3000);
        System.out.println("count = " + count);
    }
}

把上述代码执行多次后,发现每次的结果不一样(自增并非是原子操作)。

在多个线程对共享资源读写操作时发生指令交错,就会出现问题

2、synchronized 用法

synchronized:俗称【对象锁】。它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换【保证一个共享资源在同一时间只会被一个线程访问到】

它有两种用法:同步方法和同步代码块。Java 中的每一个对象都可以作为锁。具体表现为以下 3 种形式:

  1. 对于普通同步方法,锁是当前实例对象
  2. 对于静态同步方法,锁是当前类的 Class 对象。多个线程同时访问静方法,线程会发生互斥(即一个线程访问,另一个线程只能等着),因为静态方法是依附于类对象而不是实例对象的
  3. 对于同步方法块,锁是 synchonized 括号里配置的对象

如:使用同步方法改造:

public class ThreadTest {
    private static int count = 0;

    public synchronized void incr() {
        count++;
    }

    public static void main(String[] args) throws Exception {
        ThreadTest threadTest = new ThreadTest();
        for (int i = 0; i < 100; i++) {
            Thread addThread = new Thread(() -> {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                threadTest.incr();
            });
            addThread.start();
        }
        Thread.sleep(3000);
        System.out.println("count = " + count);
    }
}

3、类加载器对 Class 锁的影响

在 JVM 里,Class 的唯一性是由 Class 全限定名和 classloader 决定的,同一个全限定名的 class 被不同的 classloader 加载,最终的 class 对象是不一样的

public class ThreadTest {
    public void one() {
        synchronized (this.getClass()) {
            try {
                System.out.println("start-" + Thread.currentThread().getName());
                Thread.sleep(1000);
                System.out.println("end-" + Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void two() {
        synchronized (this.getClass()) {
            try {
                System.out.println("start-" + Thread.currentThread().getName());
                Thread.sleep(1000);
                System.out.println("end-" + Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ThreadTest test = new ThreadTest();
        ThreadTest test2 = new ThreadTest();

        Runnable task1 = test::one;
        Runnable task2 = test2::two;

        new Thread(task1).start();
        new Thread(task2).start();
    }

}

执行结果如下:

start-Thread-0
end-Thread-0
start-Thread-1
end-Thread-1

结论:通过对 Class 对象(this.getClass()) 加锁,即使调用的是不同的实例对象,也能达到互斥访问的效果,因为它们的 Class 是相同的,竞争的是同一把锁

当我们的类加载器使用的不是同一个的情况下,会出现不同的 Class 对象

自定义一个类加载器:

public class MyClassLoader extends URLClassLoader {

    public MyClassLoader(URL[] urls) {
        super(urls);
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                // 类名
                if ("com.zzc.demos.threads.ThreadTest".equals(name)) {
                    c = findClass(name);
                } else {
                    return super.loadClass(name, resolve);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
}

再次执行:

public static void main(String[] args) throws Exception {
    URL url = new File("D:\\code\\test2\\test2\\target\\classes").toURL();
    MyClassLoader myClassLoader = new MyClassLoader(new URL[]{url});
    MyClassLoader myClassLoader2 = new MyClassLoader(new URL[]{url});

    //分别使用 myClassLoader 和 myClassLoader 2加载
    Class clazz1 = myClassLoader.loadClass("com.zzc.demos.threads.ThreadTest");
    Class clazz2 = myClassLoader2.loadClass("com.zzc.demos.threads.ThreadTest");
    Method method01 = clazz1.getMethod("one");
    Method method02 = clazz2.getMethod("two");

    Runnable task1 = () -> {
        try {
            method01.invoke(clazz1.newInstance());
        } catch (Exception e) {
            e.printStackTrace();
        }
    };
    Runnable task2 = () -> {
        try {
            method02.invoke(clazz2.newInstance());
        } catch (Exception e) {
            e.printStackTrace();
        }
    };
    new Thread(task1).start();
    new Thread(task2).start();
}

执行结果如下:

start-Thread-1
start-Thread-0
end-Thread-1
end-Thread-0

发现此时的 synchronized 没有同步的作用了(两个线程同时执行)

4、synchronized 实现原理

4.1、同步方法、同步代码块

先看一段代码(同步方法、同步代码块):

public class Test3 {

    public synchronized void test1() {}

    public void test2() {
        synchronized(this){}
    }
}

编译后,找到该类 class 文件目录,然后在命令行执行 javap 命令:

javap -verbose Test3.class > test.txt

可以把对应的字节码疏导 test.txt 中,内容如下:

在这里插入图片描述
反编译后,我们可以看到 Java 编译器为我们生成的字节码,发现 JVM 对于同步方法和同步代码块的处理方式不同。

  • 同步方法:JVM 采用 ACC_SYNCHRONIZED 标记符来实现同步
  • 同步代码快:JVM 采用 monitorentermonitorexit 两个指令来实现同步

在 The Java® Virtual Machine Specification 中有关于同步方法和同步代码块的实现原理的介绍:

方法级的同步是隐式的。同步方法的常量池中会有一个 ACC_SYNCHRONIZED 标志。当某个线程要访问某个方法的时候,会检查是否有 ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。

同步代码块使用 monitorentermonitorexit 两个指令实现。可以把执行 monitorenter 指令理解为加锁,执行 monitorexit 理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为 0,当一个线程获得锁(执行 monitorenter )后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行 monitorexit 指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁

无论是 ACC_SYNCHRONIZED 还是 monitorentermonitorexit 都是基于 Monitor 实现的,在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++ 实现的,由 ObjectMonitor 实现。

ObjectMonitor 类中提供了几个方法,如 enter、exit、wait、notify、notifyAll 等。sychronized 加锁的时候会调用 objectMonitor 的 enter 方法,解锁的时候会调用 exit 方法

4.2、对象内存布局

一个 Java 对象在内存中是如何存储的?

对象的实例保存在堆上,对象的元数据保存在方法区,对象的引用保存在栈上

在内存中,一个 Java 对象包含三部分:对象头、实例数据和对齐填充。其中,对象头是一个很关键的部分,因为对象头中包含锁状态标志、线程持有的锁等标志

对象头分为:对象的运行时数据(Mark Word)、类型指针

  • 对象的运行时数据:GC 分代年龄、哈希码、锁状态标识 等。这部分数据在 32 位和 64 位的虚拟机中分别占 32 位和 64 位。但是对象需要存储的运行时数据很多,32 位或者 64 位都不一定能存的下,考虑到虚拟机的空间效率,这个 Mark Word 被设计成一个非固定的数据结构,它会根据对象的状态复用自己的存储空间,对象处于不同状态的时候,对应的 bit 表示的含义可能会不一样
  • 类型指针:虚拟机可以通过这个指针来确认该对象是哪个类的实例(不是所有的虚拟机都必须以这种方式来确定对象的元数据信息)。对象的访问定位一般有句柄和直接指针两种,如果使用句柄的话,那么对象的元数据信息可以直接包含在句柄中(当然也包括对象实例数据的地址信息),也就没必要将这些元数据和实例数据存储在一起了

对 markword 的设计方式上,非常像网络协议报文头:将 mark word 划分为多个比特位区间,并在不同的对象状态下赋予比特位不同的含义。下图描述了在32位虚拟机上,在对象不同状态时 mark word各个比特位区间的含义:

在这里插入图片描述

4.3、Monitor 对象定义

ObjectMonitor() {
     _header       = NULL;
     _count        = 0;
     _waiters      = 0,//等待线程数
     _recursions   = 0;//重入次数
     _object       = NULL;
     _owner        = NULL;//持有锁的线程(逻辑上,实际上除了THREAD,还可能是Lock Record)
    _WaitSet      = NULL;//线程wait之后会进入该列表
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;//等待获取锁的线程列表,和_EntryList配合使用
    FreeNext      = NULL ;
    _EntryList    = NULL ;//等待获取锁的线程列表,和_cxq配合使用
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;//当前持有者是否为THREAD类型,如果是轻量级锁膨胀而来,还没有enter的话,
                       //_owner存储的可能会是Lock Record
    _previous_owner_tid = 0;
  }

当多个线程同时访问一段同步代码时,首先会进入 _EntryList 队列中,当某个线程获取到对象的 monitor 后进入 _Owner 区域并把 monitor 中的 _owner 变量设置为当前线程,同时 monitor 中的计数器 _count 加 1。即获得对象锁。

若持有 monitor 的线程调用 wait() 方法,将释放当前持有的 monitor_owner 变量恢复为 null,_count 自减 1,同时该线程进入 _WaitSet 集合中等待被唤醒。若当前线程执行完毕也将释放monitor ( 锁)并复位变量的值,以便其他线程进入获取 monitor (锁)

5、synchronized 与原子性

原子性:一个操作是不可中断的,要全部执行完成,要不就都不执行

线程是 CPU 调度的基本单位。CPU 有时间片的概念,会根据不同的调度算法进行线程调度。当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去 CPU 使用权。所以在多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。

在 Java 中,为了保证原子性,提供了两个高级的字节码指令 monitorentermonitorexit,可以保证被 synchronized 修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到

线程1在执行 monitorenter 指令的时候,会对 Monitor 进行加锁,加锁后其他线程无法获得锁,除非线程 1 主动解锁。即使在执行过程中,由于某种原因,比如 CPU 时间片用完,线程 1 放弃了 CPU,但是,他并没有进行解锁。而由于 synchronized 的锁是可重入的,下一个时间片还是只能被他自己获取到,还是会继续执行代码。直到所有代码执行完。这就保证了原子性

6、synchronized 与可见性

可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值

被 synchronized 修饰的代码,在开始执行时会加锁,执行完成后会进行解锁。而为了保证可见性,有一条规则是这样的:对一个变量解锁之前,必须先把此变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的值

7、synchronized 与有序性

有序性:程序执行的顺序按照代码的先后顺序执行

除了引入了时间片以外,由于处理器优化和指令重排等,CPU 还可能对输入代码进行乱序执行,可能存在有序性问题。

synchronized 是无法禁止指令重排和处理器优化的。也就是说,synchronized 无法避免上述提到的问题。

那为什么还说 synchronized 也提供了有序性保证呢?

Java 程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有操作都是天然有序的。如果在一个线程中观察另一个线程,所有操作都是无序的。这其实和 as-if-serial 语义有关

as-if-serial 语义:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守 as-if-serial 语义

as-if-serial 语义保证了单线程中,指令重排是有一定的限制的,而只要编译器和处理器都遵守了这个语义,那么就可以认为单线程程序是按照顺序执行的。当然,实际上还是有重排的,只不过我们无须关心这种重排的干扰

由于 synchronized 修饰的代码,同一时间只能被同一线程访问。那么也就是单线程执行的。所以,可以保证其有序性

8、synchronized 锁升级

8.1、概述

事实上,只有在 JDK1.6 之前,synchronized 的实现才会直接调用 ObjectMonitorenterexit,这种锁被称之为重量级锁

为什么说这种方式操作锁很重呢?

Java 的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到核心态,因此状态转换需要花费很多的处理器时间,对于代码简单的同步块(如被 synchronized 修饰的 getset 方法)状态转换消耗的时间有可能比用户代码执行的时间还要长,所以说 synchronized 是 java 语言中一个重量级的操作

所以,在 JDK1.6 中出现对锁进行了很多的优化,进而出现轻量级锁,偏向锁,锁消除,适应性自旋锁,锁粗化(自旋锁在 1.4 就有 只不过默认的是关闭的,jdk1.6 是默认开启的),这些操作都是为了在线程之间更高效的共享数据 ,解决竞争问题【这些其实对于使用它的开发者来说是屏蔽掉了的,也就是说,作为一个 Java 开发,你只需要知道你想在加锁的时候使用 synchronized 就可以了,具体的锁的优化是虚拟机根据竞争情况自行决定的】

锁升级: JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级。

        当没有竞争出现时,默认会使用偏向锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销
        如果有另外的线程试图锁定某个已经被偏向过的对象,JVM 就需要撤销(revoke)偏向锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。

在这里插入图片描述

8.2、偏向锁

8.2.1、概念

多线程的情况下,锁不仅不存在多线程竞争,还存在锁由同一线程多次获得的情况(偏向锁:为了解决只有在一个线程执行同步时提高性能)。而且,在实际应用运行过程中发现:“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。那么,只需要在锁第一次被拥有的时候,记录下偏向线程 ID。

这样,偏向线程就一直持有着锁。后续这个线程进入和退出这段加了同步锁的代码时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁,如果相等,表示偏向锁是偏向于当前线的,就不需要再尝试获得锁了,直到竞争发生才释放锁

以后每次同步,检查锁的偏向线程 ID 与当前线程 ID 是否一致:如果一致,直接进入同步。无需每次加锁解锁都去 CAS 更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高;如果不一致,意味着发生了竞争,锁已经不是总是偏向于同一个线程了,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的

8.2.2、偏向锁的实现

一个 synchronized 方法被一个线程抢到了锁时,那这个方法所在的对象就会在其所在的 Mark Word 中将偏向锁修改状态位,同时还会有占用前 54 位来存储线程指针作为标识。若该线程再次访问同一个synchronized 方法时,该线程只需去对象头的 Mark Word 中去判断一下是否有偏向锁指向本身的 ID,无需再进入 Monitor 去竞争对象了

偏向锁的操作不用直接捅到操作系统,不涉及用户到内核转换,不必要直接升级为最高级

8.2.3、案例

以一个 account 对象的“对象头”为例

假如有一个线程执行到 synchronized 代码块的时候,JVM 使用 CAS 操作把线程指针 ID 记录到 Mark Word 当中,并修改标偏向标示,标示当前线程就获得该锁。锁对象变成偏向锁(通过 CAS 修改对象头里的锁标志位)。执行完同步代码块后,线程并不会主动释放偏向锁

这时线程获得了锁,可以执行同步代码块。当该线程第二次到达同步代码块时会判断此时持有锁的线程是否还是自己(持有锁的线程 ID 也在对象头里),JVM 通过 account 对象的 Mark Word 判断:当前线程 ID 还在,说明还持有着这个对象的锁,就可以继续进入临界区工作。由于之前没有释放锁,这里也就不需要重新加锁。

如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高

结论:JVM 不用和操作系统协商设置 Mutex(争取内核),它只需要记录下线程 ID 就标示自己获得了当前锁,不用操作系统接入。上述就是偏向锁:在没有其他线程竞争的时候,一直偏向偏心当前线程,当前线程可以一直执行

8.2.4、偏向锁的撤销

偏向锁的撤销:偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行:

  • 第一个线程正在执行 synchronized 方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级为轻量级锁。此时,轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁
  • 第一个线程执行完成 synchronized 方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向

8.3、轻量级锁

轻量级锁是为了在线程近乎交替执行同步块时提高性能(竞争不激烈),在没有多线程竞争的前提下,通过 CAS 减少重量级锁使用操作系统互斥量产生的性能消耗【先自旋再阻塞】

升级时机:当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁

假如线程 A 已经拿到锁,这时线程 B 又来抢该对象的锁,由于该对象的锁已经被线程 A 拿到,当前该锁已是偏向锁了。而线程 B 在争抢时发现对象头 Mark Word 中的线程 ID 不是线程 B 自己的线程 ID(而是线程 A),那线程 B 就会进行 CAS 操作希望能获得锁。

此时线程 B 操作中有两种情况:如果锁获取成功,直接替换 Mark Word 中的线程 ID 为 B 自己的 ID (A → B),重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程"被"释放了锁),该锁会保持偏向锁状态,A 线程 Over,B 线程上位;如果锁获取失败,则偏向锁升级为轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程 B 会进入自旋等待获得该轻量级锁

8.4、重量级锁

有大量的线程参与锁的竞争,冲突性很高,自旋到达一定次数还是没有获取锁成功,这时候轻量级锁就会膨胀为重量级锁,当锁膨胀为重量锁时,就不能再退回到轻量级锁

9、自旋锁

自旋锁在 JDK 1.4 中已经引入,在 JDK 1.6 中默认开启。

在程序中,Java 虚拟机的开发工程师们在分析过大量数据后发现:共享数据的锁定状态一般只会持续很短的一段时间,为了这段时间去挂起和恢复线程其实并不值得。

如果物理机上有多个处理器,可以让多个线程同时执行的话。我们就可以让后面来的线程“稍微等一下”,但是并不放弃处理器的执行时间,看看持有锁的线程会不会很快释放锁。这个“稍微等一下”的过程就是自旋

自旋锁和阻塞锁最大的区别就是:到底要不要放弃处理器的执行时间?

阻塞锁和自旋锁来都是要等待获得共享资源。但是阻塞锁是放弃了 CPU 时间,进入了等待区,等待被唤醒;自旋锁是一直“自旋”在那里,时刻的检查共享资源是否可以被访问

自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用 CPU 时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。

如果锁被占用很长时间的话,自旋的线程等待的时间也会变长,白白浪费掉处理器资源。因此在JDK中,自旋操作默认 10 次,我们可以通过参数 “-XX:PreBlockSpin” 来设置,当超过来此参数的值,则会使用传统的线程挂起方式来等待锁释放

10、自适应自旋锁

JDK 1.6 的时候,又出现了一个“自适应自旋锁”。它的出现使得自旋操作变得聪明起来,不再跟之前一样死板。所谓的“自适应”意味着对于同一个锁对象,线程的自旋时间是根据上一个持有该锁的线程的自旋时间以及状态来确定的。

例如对于 A 锁对象来说,如果一个线程刚刚通过自旋获得到了锁,并且该线程也在运行中,那么 JVM 会认为此次自旋操作也是有很大的机会可以拿到锁,因此它会让自旋的时间相对延长。但是如果对于 B 锁对象自旋操作很少成功的话,JVM 甚至可能直接忽略自旋操作。因此,自适应自旋锁是一个更加智能,对我们的业务性能更加友好的一个锁

11、锁消除

除了自旋锁之后,JDK 中还有一种锁的优化被称之为锁消除 (JIT编译器对内部锁的具体实现所做的一种优化)

在动态编译同步块的时候,JIT 编译器可以借助一种被称为逃逸分析(Escape Analysis)的技术来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程
如果同步块所使用的锁对象通过这种分析被证实只能够被一个线程访问,那么 JIT 编译器在编译这个同步块的时候就会取消对这部分代码的同步

如:

public void f() {
   Object obj = new Object();
   synchronized(obj) {
       System.out.println(obj);
   }
}

代码中对 obj 这个对象进行加锁,但是 obj 对象的生命周期只在 f() 方法中,并不会被其他线程所访问到,所以在 JIT 编译阶段就会被优化掉。优化成:

public void f() {
   Object obj = new Object();
   System.out.println(obj);
}

其实,一般有经验的程序员是有能力判断是否需要加锁的,像这段代码完全没必要加锁。但是还是有可能会疏忽。如:经常在代码中使用StringBuffer 作为局部变量,而 StringBuffer 中的 append() 是线程安全的,有 synchronized修饰的,这种情况开发者可能会忽略。这时候,JIT 就可以帮忙优化,进行锁消除

总之,在使用 synchronized 的时候,如果 JIT 经过逃逸分析之后发现并无线程安全问题的话,就会做锁消除

12、锁粗化

在代码中,需要加锁的时候,我们提倡尽量减小锁的粒度,这样可以避免不必要的阻塞。这也是很多人原因是用同步代码块来代替同步方法的原因,因为往往它的粒度会更小一些。

如果在一段代码中连续的对同一个对象反复加锁解锁,其实是相对耗费资源的,这种情况可以适当放宽加锁的范围,减少性能消耗

当 JIT 发现一系列连续的操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中的时候,会将加锁同步的范围扩散(粗化)到整个操作序列的外部

如:

for (int i = 0;i < 100000; i++) {  
	synchronized(this) {
		// TODO
		do();
	}
}

会被粗化成:

synchronized(this) {
	for (int i = 0;i < 100000; i++) {  
		// TODO
		do();
	}
}

【参考资料】
synchronized
轻松搞懂Java中的自旋锁

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/531750.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

UDS(ISO14229) ———— 0x10(DiagnosticSessionControl)

前言 在UDS协议中,我们首先接触到的是诊断和通信管理功能单元(Diagnostic and communication management functional unit)模块。在这个模块里面,DiagnosticSessionControl是我们第一个需要掌握的内容。 按照ISO 14229上面的划分,我们可以将诊断会话模式分为两大类; 一类…

Pytest精通指南(02)对比Unittest的差异

文章目录 前言用例编写规则不同用例前置与后置条件不同断言功能不同测试报告失败重跑机制参数化用例分类执行Unittest 前后置示例Pytest 前后置示例总结 前言 在Python中&#xff0c;unittest和pytest是两个主流的测试框架&#xff1b; 它们都旨在支持自动化测试、使用断言验证…

通信分类3G,4G,5G,通信专用名词

Generation: 2G: GSM全名为&#xff1a;Global System for Mobile Communications&#xff0c;中文为全球移动通信系统&#xff0c;俗称"全球通"&#xff0c;是一种起源于欧洲的移动通信技术标准&#xff0c;是第二代移动通信技术 3G&#xff1a;WCDMA 4G&#xff1a…

C++奇迹之旅:探索类对象模型内存的存储猜想

文章目录 &#x1f4dd;前言&#x1f320; 类的实例化&#x1f309;类对象模型 &#x1f320; 如何计算类对象的大小&#x1f309;类对象的存储方式猜想&#x1f320;猜想一&#xff1a;对象中包含类的各个成员&#x1f309;猜想二&#xff1a;代码只保存一份&#xff0c;在对象…

特征融合篇 | RTDETR引入基于内容引导的特征融合方法 | IEEE TIP 2024

本改进已集成到 RT-DETR-Magic 框架。 摘要—单幅图像去雾是一个具有挑战性的不适定问题,它从观察到的雾化图像中估计潜在的无雾图像。一些现有的基于深度学习的方法致力于通过增加卷积的深度或宽度来改善模型性能。卷积神经网络(CNN)结构的学习能力仍然未被充分探索。本文提…

Prometheus+grafana监控nacos和spring-boot服务(增加自定义指标)(七)

前面记录了项目中常用的各种中间件的指标采集器的用法及搭建方式 &#xff0c; 由于所有组件写一篇幅过长&#xff0c;所以每个组件分一篇方便查看&#xff0c;前六篇链接如下 Prometheusgrafana环境搭建方法及流程两种方式(docker和源码包)(一)-CSDN博客 Prometheusgrafana…

【3GPP】【核心网】核心网/蜂窝网络重点知识面试题二(超详细)

1. 欢迎大家订阅和关注&#xff0c;3GPP通信协议精讲&#xff08;2G/3G/4G/5G/IMS&#xff09;知识点&#xff0c;专栏会持续更新中.....敬请期待&#xff01; 目录 1. 对于主要的LTE核心网接口&#xff0c;给出运行在该接口上数据的协议栈&#xff0c;并给出协议特征 2. 通常…

电压继电器SRMUVS-220VAC-2H2D 导轨安装 JOSEF约瑟

系列型号&#xff1a; SRMUVS-58VAC-2H欠电压监视继电器&#xff1b;SRMUVS-100VAC-2H欠电压监视继电器&#xff1b; SRMUVS-110VAC-2H欠电压监视继电器&#xff1b;SRMUVS-220VAC-2H欠电压监视继电器&#xff1b; SRMUVS-58VAC-2H2D欠电压监视继电器&#xff1b;SRMUVS-100…

python爬虫 爬取网页图片

http://t.csdnimg.cn/iQgHw //爬虫爬取图片其实是很简单的&#xff0c;但是大多数同学&#xff0c;可能对 url的设置一直有困惑&#xff08;这点本人也在研究&#xff09;&#xff0c;而本篇文章&#xff0c;对于想要爬取图片的小白简直是福利。你只需要将文章代码运行即可&am…

自动化测试面试题及答案大全

&#x1f525; 交流讨论&#xff1a;欢迎加入我们一起学习&#xff01; &#x1f525; 资源分享&#xff1a;耗时200小时精选的「软件测试」资料包 &#x1f525; 教程推荐&#xff1a;火遍全网的《软件测试》教程 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1…

【YOLOv9】完胜V8的SOTA模型Yolov9(论文阅读笔记)

官方论文地址: 论文地址点击即可跳转 官方代码地址: GitCode - 开发者的代码家园 官方代码地址点击即可跳转 1 总述 当输入数据经过各层的特征提取和变换的时候,都会丢失一定的信息。针对这一问题:

顺序表讲解

一.数据结构 在学习顺序表之前&#xff0c;我们先需要了解什么是数据结构。 1.什么是数据结构呢&#xff1f; 数据结构是由“数据”和结构两词组合而来。 什么是数据呢&#xff1f; 你的游戏账号&#xff0c;身份信息&#xff0c;网页里的信息&#xff08;文字&#xff0c…

onSaveInstanceState()与onRestoreInstanceState()

目录 1.二者作用 2.onSaveInstanceState调用时机 2.1 五种情况 前4种情况Activity生命周期&#xff1a; 2.2 注意事项&#xff1a;确定会被系统回收并销毁&#xff0c;不会调用此方法 两个例子 3.onRestoreInstanceState调用时机 3.1实例——屏幕切换生命周期 3.2 极端…

Python实现读取dxf文件的所有字符

Python实现读取dxf文件的所有字符 import ezdxfdef read_dxf_and_print_text(filename):# 加载DXF文件doc ezdxf.readfile(filename)# 遍历所有的实体for entity in doc.entities:# 检查实体是否是TEXT、MTEXT或DIMENSIONif isinstance(entity, ezdxf.entities.Text):print(f…

初识--数据结构

什么是数据结构&#xff1f;我们为什么要学习数据结构呢....一系列的问题就促使我们不得不了解数据结构。我们不禁要问了&#xff0c;学习C语言不就够了吗&#xff1f;为什么还要学习数据结构呢&#xff1f;这是因为&#xff1a;数据结构能够解决C语言解决不了的问题&#xff0…

Unity多线程简单示例

using UnityEngine; using System.Threading;public class texxxst : MonoBehaviour {Thread thread;void Start(){// 创建一个新的线程&#xff0c;并传入要执行的方法thread new Thread(new ThreadStart(DoWork));// 启动线程thread.Start();}void DoWork(){for (int i 0; …

数据降维方法-主成分分析(PCA)

目录 一、前言 二、向量的表示及基变换 三、基变换 四、协方差矩阵 五、协方差 六、优化目标 一、前言 主成分分析(Principal Component Analysis) 用途&#xff1a;降维中的常用手段 目标&#xff1a;提取最有价值的信息&#xff08;基于方差&#xff09; 问题&#x…

【项目精讲】RESTful简洁描述

RESTful是什么 是一种架构风格/API设计规范将一切数据视为资源利用HTTP请求方式 POST、GET、PUT、DELETE&#xff0c;描述对资源的操作 GET 获取资源POST 新建资源PUT 更新资源DELETE 删除资源 通过HTTP响应状态码&#xff0c;描述对资源的操作结果请求数据和英大数据均为JSO…

YOLOv8模型剪枝实战:DepGraph(依赖图)方法

课程链接&#xff1a;YOLOv8模型剪枝实战&#xff1a;DepGraph(依赖图)方法_在线视频教程-CSDN程序员研修院 YOLOv8是一个当前非常流行的目标检测器&#xff0c;本课程使用DepGraph&#xff08;依赖图&#xff09;剪枝方法对YOLOv8进行网络剪枝&#xff0c;使其更加轻量和实用…

SL4010 低压升压恒压芯片 2.7-24V输入 输出30V/10A 300W功率

SL4010是一款高效能、宽电压范围的低压升压恒压芯片&#xff0c;其卓越的性能和广泛的应用领域使其在市场上备受瞩目。该芯片支持2.7-24V的宽输入电压范围&#xff0c;能够提供稳定的30V/10A输出&#xff0c;最大输出功率高达300W&#xff0c;为各种电子设备提供稳定可靠的电源…