吉林网站建设哪家有,课程分销的网站怎么做,雅安市建设局网站,wordpress某个分类不在首页显示引言
在现代软件开发领域#xff0c;多线程并发编程已经成为提高系统性能、提升用户体验的重要手段。然而#xff0c;多线程环境下的数据同步与资源共享问题也随之而来#xff0c;处理不当可能导致数据不一致、死锁等各种并发问题。为此#xff0c;Java语言提供了一种内置…引言
在现代软件开发领域多线程并发编程已经成为提高系统性能、提升用户体验的重要手段。然而多线程环境下的数据同步与资源共享问题也随之而来处理不当可能导致数据不一致、死锁等各种并发问题。为此Java语言提供了一种内置的同步机制——synchronized关键字它能够有效地解决并发控制的问题确保共享资源在同一时间只能由一个线程访问从而维护程序的正确性与一致性。
synchronized作为Java并发编程的基础构建块其简洁易用的语法形式背后蕴含着复杂的底层实现原理和技术细节。深入理解synchronized的运行机制不仅有助于我们更好地利用这一特性编写出高效且安全的并发程序同时也有利于我们在面对复杂并发场景时做出更为明智的设计决策和优化策略。
本文将从synchronized的基本概念出发逐步剖析其内在的工作机制探讨诸如监视器Monitor等关键技术点并结合实际应用场景来展示synchronized的实际效果和最佳实践。通过对synchronized底层实现原理的深度解读旨在为大家揭示Java并发世界的一隅提升对并发编程的认知高度和实战能力。
synchronized是什么
synchronized是Java中实现线程同步的关键字主要用于保护共享资源的访问确保在多线程环境中同一时间只有一个线程能够访问特定的代码段或方法。它提供了互斥性和可见性两个重要特性确保了线程间操作的原子性和数据的一致性。
synchronized的特性
synchronized关键字具有三个基本特性分别是互斥性、可见性和有序性。
互斥性
synchronized关键字确保了在其控制范围内的代码在同一时间只能被一个线程执行实现了资源的互斥访问。当一个线程进入了synchronized代码块或方法时其他试图进入该同步区域的线程必须等待直至拥有锁的线程执行完毕并释放锁。
可见性
synchronized还确保了线程间的数据可见性。一旦一个线程在synchronized块中修改了共享变量的值其他随后进入同步区域的线程可以看到这个更改。这是因为synchronized的解锁过程包含了将工作内存中的最新值刷新回主内存的操作而加锁过程则会强制从主内存中重新加载变量的值。
有序性
synchronized提供的第三个特性是有序性它可以确保在多线程环境下对于同一个锁的解锁操作总是先行于随后对同一个锁的加锁操作。这就意味着通过synchronized建立起了线程之间的内存操作顺序关系有效地解决了由于编译器和处理器优化可能带来的指令重排序问题。
synchronized可以实现哪锁
有上述synchronized的特性我们可以知道synchronized可以实现这些锁
可重入锁Reentrant Locksynchronized 实现的锁是可重入的这意味着同一个线程可以多次获取同一个锁而不会被阻塞。这种锁机制允许线程在持有锁的情况下再次获取相同的锁避免了死锁的发生。排它锁/互斥锁/独占锁synchronized 实现的锁是互斥的也就是说在同一时间只有一个线程能够获取到锁其他线程必须等待该线程释放锁才能继续执行。这确保了同一时刻只有一个线程可以访问被锁定的代码块或方法从而保证了数据的一致性和完整性。悲观锁synchronized 实现的锁属于悲观锁因为它默认情况下假设会发生竞争并且会导致其他线程阻塞直到持有锁的线程释放锁。悲观锁的特点是对并发访问持保守态度认为会有其他线程来竞争共享资源因此在访问共享资源之前会先获取锁。非公平锁 synchronized在早期的Java版本中默认实现的是非公平锁也就是说线程获取锁的顺序并不一定按照它们请求锁的顺序来进行而是允许“插队”即已经在等待队列中的线程可能被后来请求锁的线程抢占。 有关Java中的锁的分类请参考阿里二面Java中锁的分类有哪些你能说全吗 synchronized使用方式
synchronized关键字可以修饰方法、代码块或静态方法用于确保同一时间只有一个线程可以访问被synchronized修饰的代码片段。
修饰实例方法
当synchronized修饰实例方法时锁住的是当前实例对象this。这意味着在同一时刻只能有一个线程访问此方法所有对该对象实例的其他同步方法调用将会被阻塞直到该线程释放锁。
public class SynchronizedInstanceMethod implements Runnable{private static int counter 0;// 修饰实例方法锁住的是当前实例对象private synchronized void add() {counter;}Overridepublic void run() {for (int i 0; i 1000; i) {add();}}public static void main(String[] args) throws Exception {SynchronizedInstanceMethod sim new SynchronizedInstanceMethod();Thread t1 new Thread(sim);Thread t2 new Thread(sim);t1.start();t2.start();t1.join();t2.join();System.out.println(Final counter value: counter);}
}像上述这个例子大家在接触多线程时一定会看过或者写过类似的代码i在多线程的情况下是线程不安全的所以我们使用synchronized作用在累加的方法上使其变成线程安全的。上述打印结果为
Final block counter value: 2000而对于synchronized作用于实例方法上时锁的是当前实例对象但是如果我们锁住的是不同的示例对象那么synchronized就不能保证线程安全了。如下代码
public class SynchronizedInstanceMethod implements Runnable{private static int counter 0;// 修饰实例方法锁住的是当前实例对象private synchronized void add() {counter;}Overridepublic void run() {for (int i 0; i 1000; i) {add();}}public static void main(String[] args) throws Exception {Thread t1 new Thread(new SynchronizedInstanceMethod());Thread t2 new Thread(new SynchronizedInstanceMethod());t1.start();t2.start();t1.join();t2.join();System.out.println(Final counter value: counter);}
}执行结果为
Final counter value: 1491修饰静态方法
若synchronized修饰的是静态方法那么锁住的是类的Class对象因此无论多少个该类的实例存在同一时刻也只有一个线程能够访问此静态同步方法。针对修饰实例方法的线程不安全的示例我们只需要在synchronized修饰的实例方法上加上static将其变成静态方法此时synchronized锁住的就是类的class对象。
public class SynchronizedStaticMethod implements Runnable{private static int counter 0;// 修饰实例方法锁住的是当前实例对象private static synchronized void add() {counter;}Overridepublic void run() {for (int i 0; i 1000; i) {add();}}public static void main(String[] args) throws Exception {Thread t1 new Thread(new SynchronizedStaticMethod());Thread t2 new Thread(new SynchronizedStaticMethod());t1.start();t2.start();t1.join();t2.join();System.out.println(Final counter value: counter);}
}执行结果为
Final counter value: 2000修饰代码块
通过指定对象作为锁可以更精确地控制同步范围。这种方式允许在一个方法内部对不同对象进行不同的同步控制。可以指定一个对象作为锁只有持有该对象锁的线程才能执行被synchronized修饰的代码块。
public class SynchronizedBlock implements Runnable{private static int counter 0;Overridepublic void run() {// 这个this还可以是SynchronizedBlock.class说明锁住的是class对象synchronized (this){for (int i 0; i 1000; i) {counter;}}}public static void main(String[] args) throws Exception {SynchronizedBlock block new SynchronizedBlock();Thread t1 new Thread(block);Thread t2 new Thread(block);t1.start();t2.start();t1.join();t2.join();System.out.println(Final counter value: counter);}
}
synchronized 内置锁作为一种对象级别的同步机制其作用在于确保临界资源的互斥访问实现线程安全。它本质上锁定的是对象的监视器(Object Monitor)而非具体的引用变量。这种锁具有可重入性即同一个线程在已经持有某对象锁的情况下仍能再次获取该对象的锁这显著增强了线程安全代码的编写便利性并在一定程度上有助于降低因线程交互引起的死锁风险。 关于如何避免死锁请参考阿里二面如何定位避免死锁连着两个面试问到了 synchronized的底层原理
在JDK 1.6之前synchronized关键字所实现的锁机制确实被认为是重量级锁。这是因为早期版本的Java中synchronized的实现依赖于操作系统的互斥量Mutexes来实现线程间的同步这涉及到了从用户态到内核态的切换以及线程上下文切换等相对昂贵的操作。一旦一个线程获得了锁其他试图获取相同锁的线程将会被阻塞这种阻塞操作会导致线程状态的改变和CPU资源的消耗因此在高并发、低锁竞争的情况下这种锁机制可能会成为性能瓶颈。
而在JDK 1.6中对synchronized进行了大量优化其中包括引入了偏向锁Biased Locking、轻量级锁Lightweight Locking的概念。接下来我们先说一下JDK1.6之前synchronized的原理。
对象的组成结构
在JDK1.6之前在Java虚拟机中Java对象的内存结构主要有对象头Object Header实例数据Instance Data对齐填充Padding 三个部分组成。 对象头Object Header 对象头主要包含了两部分信息Mark Word标记字段和指向类元数据Class Metadata的指针。Mark Word 包含了一些重要的标记信息比如对象是否被锁定、对象的哈希码、GC相关信息等。类元数据指针指向对象的类元数据用于确定对象的类型信息、方法信息等。 实例数据Instance Data 实例数据是对象的成员变量和实例方法所占用的内存空间它们按照声明的顺序依次存储在对象的实例数据区域中。实例数据包括对象的所有非静态成员变量和非静态方法。 填充Padding 在JDK 1.6及之前的版本中为了保证对象在内存中的存储地址是8字节的整数倍可能会在对象的实例数据之后添加一些填充字节。这些填充字节的目的是对齐内存地址提高内存访问效率。填充字节通常不包含任何实际数据只是用于占位。 对象头
在JDK 1.6之前的Java HotSpot虚拟机中对象头的基本组成依然包含Mark Word和类型指针Klass Pointer但当时对于锁的实现还没有引入偏向锁和轻量级锁的概念因此对象头中的Mark Word在处理锁状态时比较简单主要是用来存储锁的状态信息以及与垃圾收集相关的数据。在一个32位系统重对象头大小通常约为32位而在64位系统中大小通常为64位。 对象头组成部分
Mark Word标记字 在早期版本的HotSpot虚拟机中Mark Word主要存储的信息包括
对象的hashCode在没有锁定时。对象的分代年龄用于垃圾回收算法。锁状态信息如无锁、重量级锁状态在使用synchronized关键字时。对象的锁指针Monitor地址当对象被重量级锁锁定时存储的是指向重量级锁Monitor的指针。 对象头中的Mark Word是一个非固定的数据结构它会根据对象的状态复用自己的存储空间存储不同的数据。在Java HotSpot虚拟机中Mark Word会随着程序运行和对象状态的变化而存储不同的信息。其信息变化如下 从存储信息的变化可以看出
对象头的最后两位存储了锁的标志位01表示初始状态即未加锁。此时对象头内存储的是对象自身的哈希码。无锁和偏向锁的锁标志位都是01只是在前面的1bit区分了这是无锁状态还是偏向锁状态。当进入偏向锁阶段时对象头内的标志位变为01并且存储当前持有锁的线程ID。这意味着只有第一个获取锁的线程才能继续持有锁其他线程不能竞争同一把锁。在轻量级锁阶段标志位变为00对象头内存储的是指向线程栈中锁记录的指针。这种情况下多个线程可以通过比较锁记录的地址与对象头内的指针地址来确定自己是否拥有锁。
其中轻量级锁和偏向锁是Java 6 对 synchronized 锁进行优化后新增加的。重量级锁也就是通常说synchronized的对象锁锁标识位为10其中指针指向的是monitor对象也称为管程或监视器锁的起始地址。 类型指针Klass Pointer 或 Class Pointer 类型指针指向对象的类元数据Class Metadata即对象属于哪个类的类型信息用于确定对象的方法表和字段布局等。在一个32位系统重大小通常约为32位而在64位系统中大小通常为64位。 数组长度Array Length仅对数组对象适用 如果对象是一个数组对象头中会额外包含一个字段来存储数组的长度。在一个32位系统中大小通常约为32位而在64位系统中大小通常为64位。
监视器Monitor
在Java中每个对象都与一个Monitor关联Monitor是一种同步机制负责管理线程对共享资源的访问权限。当一个Monitor被线程持有时对象便处于锁定状态。Java的synchronized关键字在JVM层面上通过MonitorEnter和MonitorExit指令实现方法同步和代码块同步。MonitorEnter尝试获取对象的Monitor所有权即获取对象锁MonitorExit确保每个MonitorEnter操作都有对应的释放操作。
在HotSpot虚拟机中Monitor具体由ObjectMonitor实现其结构如下
ObjectMonitor() {_header NULL;_count 0; //锁计数器表示重入次数每当线程获取锁时加1释放时减1。_waiters 0, //等待线程总数不一定在实际的ObjectMonitor中有直接体现但在管理线程同步时是一个重要指标。_recursions 0; //与_count类似表示当前持有锁的线程对锁的重入次数。_object NULL; // 通常指向关联的Java对象即当前Monitor所保护的对象。_owner NULL; // 持有ObjectMonitor对象的线程地址即当前持有锁的线程。_WaitSet NULL; //存储那些调用过wait()方法并等待被唤醒的线程队列。_WaitSetLock 0 ; // 用于保护_WaitSet的锁。_Responsible NULL ;_succ NULL ;_cxq NULL ; //阻塞在EntryList上的单向线程列表可能用于表示自旋等待队列或轻量级锁的自旋链表。FreeNext NULL ; // 在对象Monitor池中可能用于链接空闲的ObjectMonitor对象。_EntryList NULL ; // 等待锁的线程队列当线程请求锁但发现锁已被持有时会被放置在此队列中等待。_SpinFreq 0 ;_SpinClock 0 ;OwnerIsThread 0 ; // 标志位可能用于标识_owner是否指向一个真实的线程对象。}其中最重要的就是_owner、_WaitSet、_EntryList和count几个字段他们之间的转换关系 _owner: 当一个线程首次成功执行synchronized代码块或方法时会尝试获取对象的Monitor即ObjectMonitor并将自身设置为_owner。该线程此刻拥有了对象的锁可以独占访问受保护的资源。 _EntryList → _owner: 当多个线程同时尝试获取锁时除第一个成功获取锁的线程外其余线程会进入_EntryList排队等待。一旦_owner线程释放锁_EntryList中的下一个线程将有机会获取锁并成为新的_owner。 _owner → _WaitSet: 当_owner线程在持有锁的情况下调用wait()方法时它会释放锁即_owner置为NULL并把自己从_owner转变为等待状态然后将自己添加到_WaitSet中。这时线程进入等待状态暂停执行等待其他线程通过notify()或notifyAll()唤醒。 _WaitSet → _EntryList: 当其他线程调用notify()或notifyAll()方法时会选择一个或全部在_WaitSet中的线程将它们从_WaitSet移除并重新加入到_EntryList中。这样这些线程就有机会再次尝试获取锁并成为新的_owner。
有上述转换关系我们可以发现当多线程访问同步代码时
线程首先尝试进入_EntryList竞争锁成功获取Monitor后将_owner设置为当前线程并将count递增。若线程调用wait()方法会释放Monitor、清空_owner并将线程移到_WaitSet中等待被唤醒。当线程执行完毕或调用notify()/notifyAll()唤醒等待线程后会释放Monitor使得其他线程有机会获取锁。
在Java对象的对象头Mark Word中存储了与锁相关的状态信息这使得任意Java对象都能作为锁来使用同时notify/notifyAll/wait等方法正是基于Monitor锁对象来实现的因此这些方法必须在synchronized代码块中调用。
我们查看上述同步代码块SynchronizedBlock的字节码文件 从上述字节码中可以看到同步代码块的实现是由monitorenter 和monitorexit指令完成的其中monitorenter指令所在的位置是同步代码块开始的位置第一个monitorexit指令是用于正常结束同步代码块的指令第二个monitorexit指令是用于异常结束时所执行的释放Monitor指令。 关于查看class文件的字节码文件有两种方式1、通过命令: javap -verbose class路径/class文件。2、IDEA中通过插件jclasslib Bytecode viewer。 我们再看一下作用于同步方法的字节码 我们可以看出同步方法上没有monitorenter 和 monitorexit 这两个指令了而在查看该方法的class文件的结构信息时发现了Access flags后边的synchronized标识该标识表明了该方法是一个同步方法。Java虚拟机通过该标识可以来辨别一个方法是否为同步方法如果有该标识线程将持有Monitor在执行方法最后释放Monitor。 总结
synchronized作用于同步代码块时的原理 Java虚拟机使用monitorenter和monitorexit指令实现同步块的同步。monitorenter指令在进入同步代码块时执行尝试获取对象的Monitor即锁monitorexit指令在退出同步代码块时执行释放Monitor。
而对于方法级别的同步的原理 Java虚拟机通过在方法的访问标志Access flags中设置ACC_SYNCHRONIZED标志来实现方法同步。当一个方法被声明为synchronized时编译器会在生成的字节码中插入monitorenter和monitorexit指令确保在方法执行前后正确地获取和释放对象的Monitor。
本文已收录于我的个人博客码农Academy的博客专注分享Java技术干货包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等。