使用synchronized获取互斥锁的几点说明

本文介绍了并发编程中互斥锁的概念及其实现原理。详细解释了如何通过synchronized关键字来实现互斥锁,保障线程安全。同时,讨论了不同情况下锁的行为表现及其对线程的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转载请注明出处:http://blog.youkuaiyun.com/ns_code/article/details/17199201

 

    在并发编程中,多线程同时并发访问的资源叫做临界资源,当多个线程同时访问对象并要求操作相同资源时,分割了原子操作就有可能出现数据的不一致或数据不完整的情况,为避免这种情况的发生,我们会采取同步机制,以确保在某一时刻,方法内只允许有一个线程。

      采用synchronized修饰符实现的同步机制叫做互斥锁机制,它所获得的锁叫做互斥锁。每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池。任何一个对象系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,因此叫做互斥锁。

 

      这里就使用同步机制获取互斥锁的情况,进行几点说明:


      1、如果同一个方法内同时有两个或更多线程,则每个线程有自己的局部变量拷贝。


      2、类的每个实例都有自己的对象级别锁。当一个线程访问实例对象中的synchronized同步代码块或同步方法时,该线程便获取了该实例的对象级别锁,其他线程这时如果要访问synchronized同步代码块或同步方法,便需要阻塞等待,直到前面的线程从同步代码块或方法中退出,释放掉了该对象级别锁。


      3、访问同一个类的不同实例对象中的同步代码块,不存在阻塞等待获取对象锁的问题,因为它们获取的是各自实例的对象级别锁,相互之间没有影响。


      4、持有一个对象级别锁不会阻止该线程被交换出来,也不会阻塞其他线程访问同一示例对象中的非synchronized代码。当一个线程A持有一个对象级别锁(即进入了synchronized修饰的代码块或方法中)时,线程也有可能被交换出去,此时线程B有可能获取执行该对象中代码的时间,但它只能执行非同步代码(没有用synchronized修饰),当执行到同步代码时,便会被阻塞,此时可能线程规划器又让A线程运行,A线程继续持有对象级别锁,当A线程退出同步代码时(即释放了对象级别锁),如果B线程此时再运行,便会获得该对象级别锁,从而执行synchronized中的代码。


     5、持有对象级别锁的线程会让其他线程阻塞在所有的synchronized代码外。例如,在一个类中有三个synchronized方法a,b,c,当线程A正在执行一个实例对象M中的方法a时,它便获得了该对象级别锁,那么其他的线程在执行同一实例对象(即对象M)中的代码时,便会在所有的synchronized方法处阻塞,即在方法a,b,c处都要被阻塞,等线程A释放掉对象级别锁时,其他的线程才可以去执行方法a,b或者c中的代码,从而获得该对象级别锁。


     6、使用synchronized(obj)同步语句块,可以获取指定对象上的对象级别锁。obj为对象的引用,如果获取了obj对象上的对象级别锁,在并发访问obj对象时时,便会在其synchronized代码处阻塞等待,直到获取到该obj对象的对象级别锁。当obj为this时,便是获取当前对象的对象级别锁。


     7、类级别锁被特定类的所有示例共享,它用于控制对static成员变量以及static方法的并发访问。具体用法与对象级别锁相似。


    8、互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁,如果获得了锁,把锁的计数器加1,相应地,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁便被释放了。由于synchronized同步块对同一个线程是可重入的,因此一个线程可以多次获得同一个对象的互斥锁,同样,要释放相应次数的该互斥锁,才能最终释放掉该锁。

### 3.1 synchronized 的可重入性机制 `synchronized`是 Java 提供的一种隐式可重入锁,它允许同一个线程在持有锁的情况下多次进入被该锁保护的代码块,而不会导致死锁。其内部实现依赖于一个计数器,用于记录当前线程获取锁的次数。当线程第一次获取锁时,计数器加一;后续每次再次进入同步代码块时,计数器递增。只有当计数器减到零时,锁才会被完全释放,其他线程才能获得该锁[^3]。 这种机制确保了在外层方法获取锁之后,内层方法可以自动获取锁,无需额外等待或释放操作,从而避免了因重复请求锁而导致的阻塞问题。例如: ```java public class SyncReentrantDemo { public synchronized void outer() { System.out.println("Outer"); inner(); // 可以再次进入inner(),因为synchronized是可重入的 } public synchronized void inner() { System.out.println("Inner"); } public static void main(String[] args) { new SyncReentrantDemo().outer(); } } ``` 上述代码中,`outer()`方法调用`inner()`时,并不会因为`this`对象已经被锁定而阻塞,而是继续执行,这正是由于`synchronized`具备可重入特性[^3]。 ### 3.2 synchronized 实现原理与优化 在 JVM 层面,`synchronized`最初是通过操作系统提供的互斥量(mutex)来实现的,因此被称为“重量级锁”。然而,在 JDK 1.6 及以后版本中,为了提升性能并适应不同并发场景的需求,Java 对`synchronized`进行了多项优化,引入了锁升级机制:从偏向锁 → 轻量级锁 → 重量级锁的过程逐步演化[^4]。 - **偏向锁**:适用于只有一个线程访问同步块的情况,JVM 会将锁偏向给第一个访问它的线程,减少无竞争下的同步开销。 - **轻量级锁**:当多个线程交替访问同步块但不存在实际竞争时,使用 CAS 操作尝试获取锁,避免线程阻塞和唤醒带来的开销。 - **重量级锁**:当存在真正的线程竞争时,锁会膨胀为传统的基于操作系统的互斥量实现方式,此时线程会被挂起等待锁释放[^4]。 此外,JVM 还采用了自旋锁策略,即在尝试获取锁失败后,线程并不会立即进入阻塞状态,而是进行一定次数的空循环等待,期望在这段时间内锁能被释放,从而节省上下文切换的成本。 ### 3.3 使用建议与注意事项 尽管`synchronized`提供了简洁易用的同步语义,但在实际开发中仍需注意以下几点: - `synchronized`只能作用于方法或代码块,且必须指定一个对象作为锁。通常情况下,非静态方法默认使用`this`作为锁,而静态方法则使用类的`Class`对象作为锁。 - 为防止死锁,应尽量避免嵌套锁的使用,或者确保所有线程以相同的顺序获取多个锁。 - 在异常处理中,即使发生异常,JVM 也会自动释放由`synchronized`持有的锁,因此不需要像显式锁(如`ReentrantLock`)那样手动在`finally`块中释放锁[^2]。 ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值