是时候来唠一唠synchronized关键字了,Java多线程的必问考点!

简介: 本文简要介绍了Java中的`synchronized`关键字,它是用于保证多线程环境下的同步,解决原子性、可见性和顺序性问题。从JDK1.6开始,synchronized进行了优化,性能得到提升,现在仍可在项目中使用。synchronized有三种用法:修饰实例方法、静态方法和代码块。文章还讨论了synchronized修饰代码块的锁对象、静态与非静态方法调用的互斥性,以及构造方法不能被同步修饰。此外,通过反汇编展示了`synchronized`在方法和代码块上的底层实现,涉及ObjectMonitor和monitorenter/monitorexit指令。

写在开头

在之前的博文中,我们介绍了volatile关键字,Java中的锁以及锁的分类,今天我们花5分钟时间,一起学习一下另一个关键字:synchronized

synchronized是什么?

首先synchronized是Java中的一个关键字,所谓关键字,就是Java中根据底层封装所赋予的一种具有特殊语义的单词,而synchronized译为同步之意,可保证在同一时刻,被它修饰的方法或代码块只能有一个线程执行,它的使用解决了并发多线程中的三大问题:原子性、可见性、顺序性

很多小伙伴在过往的书籍中可能会看到说synchronized是一种重量级锁,性能差,不建议在代码中使用,其实这是早期的synchronized特点,自JDK1.6之后,synchronized 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让 synchronized 锁的效率提升了很多。因此, synchronized 还是可以在实际项目中使用的,像 JDK 源码、很多开源框架都大量使用了 synchronized 。

synchronized的使用

synchronized在Java中主要的3种使用方式:

  1. 修饰实例方法: 为当前对象实例加锁,进入同步方法需要先获取对象锁;
  2. 修饰静态方法: 为当前类加锁,锁定的是Class对象,进入同步方法需要先获取类锁;
  3. 修饰代码块: 为指定对象加锁,进入同步方法需要先获取指定对象的锁。

样例:

//修饰实例方法,为当前实例加锁
synchronized void method() {
   
   
    //业务代码
}
//修饰静态方法,锁为当前Class对象
synchronized static void method() {
   
   
    //业务代码
}
//修饰代码块,锁为括号里面的对象
synchronized(this) {
   
   
    //业务代码
}

写到这里,突然想到了3个面试可能会考的知识点,列举一下!

问题1:synchronized修饰代码块可以给类加锁吗?

当然可以!我们前面说了修饰代码块时,是给代码中的对象加锁,这里面的对象既可以是实例也可以是类。

问题2:静态 synchronized 方法和非静态 synchronized 方法之间的调用互斥么?

不互斥!如果线程A调用一个实例对象的非静态synchronized方法,线程B同时去调用这个实例对象所属类的静态synchronized方法并不会发生互斥,因为线程A此时拿到的是实例对象锁,而线程B拿到的是当前类的锁。

问题3:构造方法可以用 synchronized 修饰么?

不可以!构造方法本身就是线程安全的,在Java开发规范里也明确告诉我们

构造方法不能是抽象的(abstract)、静态的(static)、最终的(final)、同步的(synchronized)。

synchronized的底层原理

在synchronized的底层(JVM层面),针对方法与代码块的实现逻辑是不同的,因此我们在分析底层原理是也要分别来看。

1、当synchronized修饰方法时

public class Test {
   
   
    public synchronized void method() {
   
   
        System.out.println("synchronized 方法");
    }
}

我们通过对编译后的class文件进行反编译后,分析其底层实现。

知识点扩展:
我们通过javap命令进行反编译,javap是Java class文件分解器,可以反编译,也可以查看java编译器生成的字节码等。
javap参数如下:

image.png

使用方式,既可以在电脑的命令行提示符中使用,也可以通过idea的terminal终端使用,我这里采用idea中进行反汇编操作。参考命令:javap -c -v Test.class

【反汇编结果】

image.png

由上图可看出同步方法通过加 ACC_SYNCHRONIZED 标识实现线程的执行权的控制,如果修饰的是实例方法,JVM会获取对象锁,如果修饰的是静态方法,JVM会获取当前类锁。

2、当synchronized修饰代码块时

public class Test {
   
   
    public void method() {
   
   
        synchronized (this) {
   
   
            System.out.println("synchronized");
        }
    }
}

【反汇编结果】

image.png

与同步方法不同,同步代码块中使用了monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置,并且monitorexit标识有2个,以保证在正常执行和异常情况下均可释放锁。

在命令执行到monitorenter时,线程会去尝试获取对象得锁,这里也可称之为对象所对应的monitor所有权。写到这里,我们又要做一个知识点扩展啦。

知识点扩展:

在JVM中monitor的底层基于C++实现,被称之为对象监视器,每个对象都会内置一个ObjectMonitor与之关联,关联的起始地址存于对象头的MarkWord中。

image.png

ObjectMonitor几个关键属性:

  • _owner:指向持有ObjectMonitor对象的线程
  • _WaitSet:存放处于wait状态的线程队列
  • EntryList:存放处于等待锁block状态的线程队列
  • recursions:锁的重入次数
  • _count:用来记录该线程获取锁的次数

当多个线程同时访问同步代码时,会被放入EntryList中,根据线程优先级尝试获取对象锁,如果锁的计数器为 0 则表示锁可以被获取,获取到锁的线程进入owner区域,count加1,这里其实还有之前说的object中的wait/notify/notifyall的组合,也依赖monitor,所以他们才必须用在同步方法或代码块中。
对象锁的的拥有者线程才可以执行 monitorexit 指令来释放锁。在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。

总结

关于synchronized的介绍其实远没有结束,还有很多细节可以值得学习,我们会在后面的文章中逐渐补充,避免文章过长,读者失去阅读的耐心!

结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!

目录
相关文章
|
2天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
24 6
|
15天前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
10天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
10天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
30 3
|
11天前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
15天前
|
监控 Java 数据库连接
Java线程管理:守护线程与用户线程的区分与应用
在Java多线程编程中,线程可以分为守护线程(Daemon Thread)和用户线程(User Thread)。这两种线程在行为和用途上有着明显的区别,了解它们的差异对于编写高效、稳定的并发程序至关重要。
24 2
|
15天前
|
监控 Java 开发者
Java线程管理:守护线程与本地线程的深入剖析
在Java编程语言中,线程是程序执行的最小单元,它们可以并行执行以提高程序的效率和响应性。Java提供了两种特殊的线程类型:守护线程和本地线程。本文将深入探讨这两种线程的区别,并探讨它们在实际开发中的应用。
20 1
|
16天前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
52 1
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
52 1
C++ 多线程之初识多线程
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
26 3