java内置锁synchronized的可重入性

本文详细解释了Java内置锁`synchronized`的可重入性原理及其实现方式,包括如何在同一个类中重用`synchronized`方法,以及不同线程对同一对象锁的竞争情况。

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

http://m.oschina.net/blog/107642


java内置锁synchronized的可重入性

Mr&Cheng 发布于 1年前,共有 0 条评论

    当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞.

    我们来看看synchronized,它拥有强制原子性的内置锁机制,是一个重入锁,所以在使用synchronized时,当一个线程请求得到一个对象锁后再次请求此对象锁,可以再次得到该对象锁,就是说在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以拿到锁,如下:

public class Child extends Father {
	public static void main(String[] args) {
		Child child = new Child();
		child.doSomething();
	}

	public synchronized void doSomething() {
		System.out.println("child.doSomething()");
		doAnotherThing(); // 调用自己类中其他的synchronized方法
		
	}

	private synchronized void doAnotherThing() {
		super.doSomething(); // 调用父类的synchronized方法
		System.out.println("child.doAnotherThing()");
	}
}

class Father {
	public synchronized void doSomething() {
		System.out.println("father.doSomething()");
	}
}

运行结果:

child.doSomething()
father.doSomething()
child.doAnotherThing()

   这里的对象锁只有一个,就是child对象的锁,当执行child.doSomething时,该线程获得child对象的锁,在doSomething方法内执行doAnotherThing时再次请求child对象的锁,因为synchronized是重入锁,所以可以得到该锁,继续在doAnotherThing里执行父类的doSomething方法时第三次请求child对象的锁,同理可得到,如果不是重入锁的话,那这后面这两次请求锁将会被一直阻塞,从而导致死锁。

    所以在java内部,同一线程在调用自己类中其他synchronized方法/块或调用父类的synchronized方法/块都不会阻碍该线程的执行,就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入。因为java线程是基于“每线程(per-thread)”,而不是基于“每调用(per-invocation)”的(java中线程获得对象锁的操作是以每线程为粒度的,per-invocation互斥体获得对象锁的操作是以每调用作为粒度的)

  我们再来看看重入锁是怎么实现可重入性的,其实现方法是为每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁。

   我们再来看看下例子加深一下理解:

public class Child extends Father {
	public static void main(String[] args) {
		Child child = new Child();
		child.doSomething();
	}

	public  void doSomething() {
		while(i>0){
			System.out.println("child.doSomething(),i="+i--);
			super.doSomething();
			doSomething();
		}
	}
}

class Father {
	int i = 6;
	public synchronized void doSomething() {
		System.out.println("father.doSomething(),i="+i--);
	}
}

运行结果:

child.doSomething(),i=6 
father.doSomething(),i=5 
child.doSomething(),i=4 
father.doSomething(),i=3 
child.doSomething(),i=2 
father.doSomething(),i=1

 

再看:

import java.util.ArrayList;

public class ReentrancyTest {
	static public int i = 1;
	public int count ;

	public ReentrancyTest() {
		super();
	}

	public static void main(String[] args) {
		int threadNum = 10; // 设置10个线程同时执行
		// 每个线程都关联同一个ReentrancyTest对象
		ReentrancyTest reentrancyTest = new ReentrancyTest(); 
		ArrayList<MyThread> threadList = new ArrayList<MyThread>();
		 // 为10个线程赋值同一个ReentrancyTest对象的引用
		for (int i = 0; i < threadNum; i++)
		{
			MyThread myThread = new MyThread();
			myThread.reentrancyTest = reentrancyTest;
			threadList.add(myThread);
		}
		 // 启动10个线程
		for (int i = 0; i < threadNum; i++) {
			new Thread((MyThread) threadList.get(i)).start();
		}
	}

	public void doSomething() { 
		//随机产生一个睡眠时间
		int sleep=(int)(Math.random()*500);     
		try {
			Thread.sleep(sleep); 
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		count = i++;
		System.out.println("第"+count+"个线程:"+Thread.currentThread().getName()
				+ "进入到doSomething()执行代码--睡眠"+sleep+"毫秒");
		try {
			Thread.sleep(sleep);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+ "线程执行doSomething()完毕,睡眠时间:"
				+ sleep+",已进入doSomething执行代码的线程总数为:"+count);
	}
}

class MyThread extends Thread {
	public ReentrancyTest reentrancyTest;

	public MyThread() {
		super();
	}

	@Override
	public void run() {
		reentrancyTest.doSomething();
		super.run();
	}
}

 

运行结果:

第1个线程:Thread-16进入到doSomething()执行代码--睡眠7毫秒
Thread-16线程执行doSomething()完毕,睡眠时间:7,已进入doSomething执行代码的线程总数为:1
第2个线程:Thread-10进入到doSomething()执行代码--睡眠154毫秒
第3个线程:Thread-15进入到doSomething()执行代码--睡眠221毫秒
第4个线程:Thread-17进入到doSomething()执行代码--睡眠222毫秒
第5个线程:Thread-14进入到doSomething()执行代码--睡眠276毫秒
Thread-10线程执行doSomething()完毕,睡眠时间:154,已进入doSomething执行代码的线程总数为:5
第6个线程:Thread-19进入到doSomething()执行代码--睡眠340毫秒
第7个线程:Thread-13进入到doSomething()执行代码--睡眠367毫秒
第8个线程:Thread-12进入到doSomething()执行代码--睡眠404毫秒
Thread-15线程执行doSomething()完毕,睡眠时间:221,已进入doSomething执行代码的线程总数为:8
Thread-17线程执行doSomething()完毕,睡眠时间:222,已进入doSomething执行代码的线程总数为:8
第10个线程:Thread-11进入到doSomething()执行代码--睡眠451毫秒
第10个线程:Thread-18进入到doSomething()执行代码--睡眠451毫秒
Thread-14线程执行doSomething()完毕,睡眠时间:276,已进入doSomething执行代码的线程总数为:10
Thread-19线程执行doSomething()完毕,睡眠时间:340,已进入doSomething执行代码的线程总数为:10
Thread-13线程执行doSomething()完毕,睡眠时间:367,已进入doSomething执行代码的线程总数为:10
Thread-12线程执行doSomething()完毕,睡眠时间:404,已进入doSomething执行代码的线程总数为:10
Thread-11线程执行doSomething()完毕,睡眠时间:451,已进入doSomething执行代码的线程总数为:10
Thread-18线程执行doSomething()完毕,睡眠时间:451,已进入doSomething执行代码的线程总数为:10

    由此可见多线程在操作同一对象时,如果对象中的函数不是同步的,多线程可以并发执行此函数

    如果把doSomething方法加上synchronized同步后的结果变为:

第1个线程:Thread-10进入到doSomething()执行代码--睡眠208毫秒
Thread-10线程执行doSomething()完毕,睡眠时间:208,已进入doSomething执行代码的线程总数为:1
第2个线程:Thread-19进入到doSomething()执行代码--睡眠124毫秒
Thread-19线程执行doSomething()完毕,睡眠时间:124,已进入doSomething执行代码的线程总数为:2
第3个线程:Thread-17进入到doSomething()执行代码--睡眠176毫秒
Thread-17线程执行doSomething()完毕,睡眠时间:176,已进入doSomething执行代码的线程总数为:3
第4个线程:Thread-18进入到doSomething()执行代码--睡眠43毫秒
Thread-18线程执行doSomething()完毕,睡眠时间:43,已进入doSomething执行代码的线程总数为:4
第5个线程:Thread-15进入到doSomething()执行代码--睡眠98毫秒
Thread-15线程执行doSomething()完毕,睡眠时间:98,已进入doSomething执行代码的线程总数为:5
第6个线程:Thread-16进入到doSomething()执行代码--睡眠360毫秒
Thread-16线程执行doSomething()完毕,睡眠时间:360,已进入doSomething执行代码的线程总数为:6
第7个线程:Thread-13进入到doSomething()执行代码--睡眠286毫秒
Thread-13线程执行doSomething()完毕,睡眠时间:286,已进入doSomething执行代码的线程总数为:7
第8个线程:Thread-14进入到doSomething()执行代码--睡眠442毫秒
Thread-14线程执行doSomething()完毕,睡眠时间:442,已进入doSomething执行代码的线程总数为:8
第9个线程:Thread-11进入到doSomething()执行代码--睡眠483毫秒
Thread-11线程执行doSomething()完毕,睡眠时间:483,已进入doSomething执行代码的线程总数为:9
第10个线程:Thread-12进入到doSomething()执行代码--睡眠447毫秒
Thread-12线程执行doSomething()完毕,睡眠时间:447,已进入doSomething执行代码的线程总数为:10

 

    所以不同线程对同一对象锁是要竞争的,是同步阻塞模式,不能像同一线程对同一对象锁是可重入的!


<think>嗯,用户现在想了解Javasynchronized锁的可重入及其原理。根据之前的对话历史,用户已经接触过Seata协议的相关内容,但现在转向了Java并发的问题。需要确保回答准确且符合用户提供的引用内容。 首先,用户的问题很明确:synchronized是否可重入,以及原理是什么。引用中的资料提到synchronized可重入的,并且解释了可重入锁的实现原理,比如通过记录持有线程和持有数量。比如引用[3]和引用[5]都提到这一点。 我需要先确认用户可能已经知道什么。用户可能已经了解synchronized的基本用法,比如修饰代码块和方法,但可能对可重入的具体机制不太清楚。用户提供的引用中有示例代码,比如引用[2]中的ReentrantLockExample,展示了方法A和方法B都使用同一个锁的情况,说明同一线程可以多次获取锁,这就是可重入的体现。 接下来,需要组织回答的结构。根据用户之前的回答风格,分点回答会比较清晰。例如,先回答是否可重入,再解释原理,接着与ReentrantLock的对比,最后总结应用场景。同时要引用用户提供的引用资料来支持观点。 需要注意用户提供的引用中有一些关于Reentrant Lock的对比,比如引用[4]提到synchronized是隐式获取释放锁,而ReentrantLock需要显式调用。但在回答synchronized可重入时,可能不需要深入对比,但可以简要提及,以帮助理解不同锁的实现差异。 另外,用户提供的引用[5]详细说明了synchronized的实现原理,包括锁对象头中的计数器和持有线程信息。这部分需要转化为更易懂的语言,避免过于技术化的术语,但保持准确。 可能存在的疑问点:用户是否了解对象头、Monitor等底层概念?需要适当解释,但不要过于深入,以免混淆。例如,可以提到JVM通过对象头中的计数器来跟踪锁的重入次数,而不必展开对象头的具体结构。 还要检查是否有遗漏的信息。例如,synchronized可重入是否有限制?根据资料,同一线程可以多次获取同一锁,没有限制次数,但需要正确释放,即每次进入同步块计数器加一,退出时减一,直到为零才释放锁。这点需要明确说明。 最后,生成相关问题部分,需要考虑用户可能进一步的问题,比如与ReentrantLock的区别,或者实际应用中的注意事项。根据引用中的内容,可以提出关于实现原理对比、应用场景选择等问题,帮助用户深入理解。</think>### Javasynchronized锁的可重入及原理分析 #### 一、synchronized是否可重入? **是的,synchronized锁是可重入的**。 在Java中,同一线程可以多次获取同一个对象锁,而不会因锁已被自身持有而阻塞。例如以下场景均可正常执行: 1. 同一线程递归调用`synchronized`方法。 2. 同一线程在`synchronized`代码块内调用另一个`synchronized`方法(锁对象相同)。 3. 子类通过`synchronized`方法调用父类的`synchronized`方法(锁对象为实例本身)[^3]。 **示例**(基于引用[2]的代码扩展): ```java public class ReentrantDemo { public synchronized void methodA() { methodB(); // 同一线程可重入锁 } public synchronized void methodB() { System.out.println("Reentrant call"); } } ``` 上述代码中,线程调用`methodA`时会自动获取实例锁,并在调用`methodB`时再次获取同一锁而不会阻塞。 --- #### 二、synchronized可重入的实现原理 ##### 1. **锁的持有者与计数器** - **对象头标记**:每个Java对象都与一个监视器(Monitor)关联,对象头中存储了锁状态、持有线程和重入计数器[^5]。 - **计数器机制**: - 当线程首次获取锁时,计数器置为1。 - 每次重入(同一线程再次获取锁),计数器**递增**。 - 退出同步代码块时,计数器**递减**,直至归零时释放锁[^5]。 ##### 2. **源码级逻辑** ```java // 伪代码示意 if (当前线程 == 锁持有线程) { 计数器++; } else { 竞争锁或加入等待队列; } ``` ##### 3. **关键设计特点** - **粒度控制**:锁的获取以**线程**为粒度,而非方法调用(即“per-thread”而非“per-invocation”)[^3]。 - **避免死锁**:可重入避免了线程因重复请求自身已持有的锁而导致的死锁。 --- #### 三、与ReentrantLock的对比 虽然两者均支持可重入,但实现方式不同: | 特 | synchronized | ReentrantLock | |--------------------|----------------------------------|------------------------------| | **锁获取/释放** | 自动(进入/退出代码块时触发)[^4] | 手动(`lock()`/`unlock()`)[^4] | | **实现机制** | JVM内置锁,通过对象头实现 | 基于AQS(AbstractQueuedSynchronizer) | | **灵活** | 有限 | 支持公平锁、条件变量等 | --- #### 四、应用场景与注意事项 1. **适用场景**: - 方法递归调用。 - 多层同步代码块的嵌套执行(如调用链中的多个同步方法)。 2. **风险提示**: - **锁泄漏**:若重入次数未正确释放(如异常跳过`monitorexit`),可能导致锁无法释放。 - **能影响**:过深的锁重入可能增加竞争概率,需合理设计同步范围。 --- ### 相关问题 1. **synchronized可重入与ReentrantLock的实现机制有何本质区别?** 2. **若synchronized的计数器溢出会发生什么?Java如何避免这种情况?** 3. **在分布式系统中,synchronized可重入是否仍然有效?为什么?** 4. **如何通过JVM工具(如jstack)观察synchronized锁的重入次数?**
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值