从程序分析线程获得的是对象锁还是对象的方法锁?

本文探讨了Java中同步方法(synchronized)的工作原理。通过示例代码解释了当一个线程调用对象的同步方法时,它获得的是对象锁而非仅方法锁。这意味着其他线程不能访问同一对象上的所有同步方法,但仍可以访问非同步方法。

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

这篇文章需要探究的问题是当一个线程调用一个对象的同步方法(synchronized修饰)时,其获得的是对象锁(其他线程无法访问该对象的所有方法),还是获得的是对象方法的锁(其他线程只是无法访问该方法而已).

也就是说,有一个类有三个方法,其中两个为同步方法.另一个为非同步方法.
当有两个线程Thread0和Thread1,Thread0在调用fun1()时,可以确定的是Thread1也是无法调用fun1()的.
但是Thread1能不能够调用fun2和fun3呢?
这就是本文需要探究的问题.

class  test{

    public synchronized void fun1()
    {}

    public  void fun2()
    {}

    public synchronized void fun3()
    {}
}
package myThread;
public class TestThread {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        ValueTest ValueC = new ValueTest();

        MyThread  thread0 = new MyThread(ValueC);
        MyThread  thread1 = new MyThread(ValueC);

        thread0.start();        
        thread0.valclass.setValue(100);
        thread1.start();

    }

}

class MyThread extends Thread{

    ValueTest valclass;

    publicyThread(ValueTest valclass) {
        this.valclass = valclass;
    }
    public void run() {     
        System.out.println("the thread name is :" +    Thread.currentThread().getName() 
                           + "\t the value is :" + this.valclass.getValue());
    }
}

class ValueTest{

    private  int value;

    public synchronized void setValue(int val) {
        this.value = val;
        try {
            Thread.sleep(1000);
        }
        catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
        this.value += val;      
    }   
    public   int getValue() {
        return this.value;
    }

}

类ValueTest创建了一个同步方法setValue(int val) 和一个非同步方法getValue().
在setValue(int val)中,先根据传入的参数设置value值,然后睡眠1000ms,之后再累加传入的值.因此设置了两次.

public synchronized void setValue(int val) {
        this.value = val;
        try {
            Thread.sleep(1000);
        }
        catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
}

主函数中创建了两个线程,并且线程1调用setValue(100)设在value值.

ValueTest ValueC = new ValueTest();

MyThread  thread0 = new MyThread(ValueC);
MyThread  thread1 = new MyThread(ValueC);

thread0.start();
thread1.start();        
thread0.valclass.setValue(100);

线程执行程序run输出当前线程的名称和value值.

    public void run() {     
        System.out.println("the thread name is :" +    Thread.currentThread().getName() 
                           + "\t the value is :" + this.valclass.getValue());
    }

输出为:

the thread name is :Thread-0     the value is :100
the thread name is :Thread-1     the value is :100

当将getValue设置为同步方法后

public  synchronized  int getValue() {
        return this.value;
    }

输出为,此时线程0的value更改为200.

the thread name is :Thread-0     the value is :200
the thread name is :Thread-1     the value is :200

分析程序
第一种情况下,getValue() 不是同步方法,当Thread-0调用setValue(100)后,value值设置为100.之后线程1进入休眠状态1000ms.Thread-1获得运行权,执行run最后两者都打印value的值100;

第二种情况getValue() 是同步方法,当Thread-0调用setValue(100)后,
value值设置为100.两个线程的run中都等待setValue(100)执行完后才能执行getValue.所以两个线程打印的值都为200.

总结:
  当线程A调用对象加入synchronized关键字的X方法时,A线程就获得了方法锁,更准确的讲是获得了对象锁,所以其他线程必须等待线程A执行完该同步的X方法后才能执行该X方法.但其他线程可以调用非synchronized的方法.但是也不能调用其他synchronized的方法.
  为什么要这么处理呢?
  假如同步方法X和Y都对变量num进行操作,线程A访问方法X,方法X未执行完,线程B访问方法Y,两个都对num进行处理,这可能会出现问题!就失去了synchronized存在的意义.

 public synchronized void  X(){ // do something for num }
 public synchronized void  Y(){ // do something for num }
<think>我们正在讨论Java中条件等待线程被唤醒后的竞争行为。根据引用[1]和引用[2]的内容,我们可以明确以下关键点:1.当线程调用`await()`(或`wait()`)方法,它会释放并进入该条件的等待集(WaitSet)。2.当另一个线程调用同一条件上的`signal()`(或`notify()`)方法,会从等待集中选择一个线程唤醒。3.被唤醒的线程并不会立即运行,而是需要重新尝试获取(即重新进入EntrySet竞争)。现在,用户的问题是:条件等待线程被唤醒后是否会与同步阻塞线程一起竞争对象?根据引用[1]:“当可用,该线程(被唤醒的线程)不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的signalAll方法为止。”这里需要澄清:实际上,在Java中,被唤醒的线程会从等待集(WaitSet)转移到入口集(EntrySet),然后与入口集中其他阻塞线程一起竞争。引用[2]也明确指出:在`notify()`方法后,被唤醒的线程需要重新获取对象对象锁。如果有多个线程等待(包括被唤醒的线程和原本在EntrySet中等待线程),则由线程调度器随机挑选。因此,答案是:**条件等待线程被唤醒后,会转移到的入口集(EntrySet)中,与所有在该上同步阻塞的线程一起竞争对象。**我们可以用以下步骤描述这一过:1.**初始状态**:-对象有两个关联队列:-EntrySet(同步阻塞队列):存放因尝试获取而失败的线程。-WaitSet(条件等待队列):存放调用`wait()`方法而释放线程。2.**唤醒操作**:-当某个线程调用`notify()`,从WaitSet中随机选择一个线程,将其移出WaitSet,放入EntrySet。3.**竞争**:-当持有线程释放(退出同步块),JVM会从EntrySet中随机选择一个线程(具体取决于JVM实现,可能是非公平的,也可能是公平的)来获取。-此,EntrySet中可能包含:-原本就在EntrySet中等待线程(同步阻塞线程)-刚从WaitSet转移过来的线程(被唤醒的条件等待线程)4.**重新获取**:-被唤醒的线程在EntrySet中与其他线程一起竞争。一旦成功获取,它将从之前调用`wait()`的地方继续执行。注意:引用[3]中提到的线程状态转换:-被唤醒的线程从`Waiting`(无限等待)状态变为`Blocked`(阻塞)状态(即进入EntrySet),然后再变为`Runnable`(可运行)状态。**总结**:-条件等待线程被唤醒后,会进入的EntrySet,与所有同步阻塞线程一起竞争。-获取是非确定的(由线程调度器决定),被唤醒的线程并不具有优先级优势。引用[5]强调了死风险:如果没有其他线程调用`signalAll`(或`notify`)来唤醒等待线程程序可能会死。同样,如果被唤醒的线程在EntrySet中竞争不到,它也会继续阻塞。###示例场景假设有一个对象`lock`,当前有三个线程:-线程A:持有,正在执行同步块。-线程B:因尝试获取失败而阻塞在EntrySet。-线程C:因调用`lock.wait()`而进入WaitSet。现在,线程A执行了`lock.notify()`,线程C被唤醒并移动到EntrySet。然后线程A继续执行,直到退出同步块并释放。此,EntrySet中有两个线程线程B(原本就在EntrySet)和线程C(刚从WaitSet转移过来)。当释放线程B和线程C将一起竞争线程调度器可能会选择线程B或线程C来获取。###引用支持-[^1]:等待获得线程(EntrySet)和调用`await`(或`wait`)方法线程(WaitSet)本质不同。被唤醒的线程需要重新进入竞争。-[^2]:`notify()`唤醒等待状态的线程(从WaitSet移出),然后这些线程需要重新获取对象锁(进入EntrySet竞争)。-[^3]:线程状态转换:从`Waiting`(无限等待)状态变为`Blocked`(阻塞)状态,即进入EntrySet。-[^4]:`wait`释放并将线程放入等待集;`notify`唤醒等待集上的一个线程,该线程需要重新获取。-[^5]:被唤醒的线程依赖于其他线程释放,否则可能死。###相关问题1.为什么被唤醒的线程需要重新竞争?2.如何避免被唤醒的线程竞争中被“饿死”?3.`notify()`和`notifyAll()`在唤醒线程后的竞争行为有何不同?4.在竞争过中,被唤醒的线程是否有优先级?5.什么是“虚假唤醒”(spuriouswakeup)?它如何影响竞争?</think>### 条件等待线程被唤醒后的竞争行为分析 #### 核心结论 **条件等待线程被唤醒后,会与同步阻塞线程一起进入竞争队列(Entry Set),共同竞争对象的所有权**。这是Java线程同步机制的关键设计原则[^1][^2]。 #### 竞争过详解 ```mermaid graph TD A[线程调用wait()] --> B[释放并进入Wait Set] C[其他线程调用notify()] --> D[从Wait Set唤醒一个线程] D --> E[被唤醒线程进入Entry Set] F[同步阻塞线程] --> E E --> G[所有线程共同竞争] G --> H[获胜线程获得执行] ``` 1. **唤醒后的状态转移** - 当条件等待线程被`notify()`唤醒后,会从**Waiting状态**转换为**Blocked状态**[^3] - 此它会被移出条件等待队列(Wait Set),进入竞争队列(Entry Set) - 在Entry Set中,它与所有因`synchronized`阻塞的线程地位平等 2. **竞争机制** ```java // 示例场景 synchronized(lock) { lock.wait(); // 线程A进入Wait Set } // 另一个线程 synchronized(lock) { lock.notify(); // 唤醒线程A // 此处线程A已转移到Entry Set } // 释放,Entry Set中所有线程开始竞争 ``` - 当持有线程退出同步块,JVM会触发竞争 - Entry Set中的所有线程(包括被唤醒线程和新阻塞线程)平等参与竞争 - 分配遵循**非公平策略**,没有先来后到顺序[^2] 3. **关键特性** - **无唤醒优先级**:被唤醒线程不享有竞争优势[^2] - **双重阻塞**:被唤醒线程可能经历两次阻塞(Wait Set → Entry Set → 运行)[^3] - **竞争不确定性**:最终哪个线程获得由JVM线程调度器决定[^2] #### 实验验证 通过线程状态跟踪可观察到: ```java Thread awakenedThread = new Thread(() -> { synchronized(lock) { lock.wait(); // 进入WAITING // 唤醒后进入BLOCKED,直到获得 } }); Thread blockedThread = new Thread(() -> { synchronized(lock) { // 直接进入BLOCKED } }); // 唤醒后使用jstack观察: // 两个线程都会显示为"java.lang.Thread.State: BLOCKED (on object monitor)" ``` #### 设计原理 1. **状态分离机制** - Wait Set:主动放弃线程(条件不满足) - Entry Set:被动等待线程(资源竞争) - 唤醒操作本质是**线程迁移**(Wait Set → Entry Set)[^1] 2. **安全保证** - 被唤醒线程必须重新验证条件(避免虚假唤醒)[^5] ```java synchronized(lock) { while(!condition) { // 必须用while循环检查 lock.wait(); } // 执行操作 } ``` 3. **死预防** - 如果被唤醒线程永远竞争不到,会导致**唤醒丢失**[^5] - 解决方案:优先使用`notifyAll()`+条件重验[^5] #### 性能影响 | 线程类型 | 唤醒后状态 | 竞争优势 | 典型等待间 | |----------------|-------------|----------|-------------| | 条件等待线程 | BLOCKED | 无 | 不确定 | | 同步阻塞线程 | BLOCKED | 无 | 不确定 | > ⚠️ **风险提示**:高频唤醒+激烈竞争可能导致**线程饥饿**,被唤醒线程长期无法获得[^5]。 ### 引用支持 [^1]: 条件等待线程本质不同,被唤醒后仍需等待可用[^1] [^2]: 被唤醒线程需重新获取对象锁,与其他等待线程平等竞争[^2] [^3]: 唤醒后线程状态从WAITING转变为BLOCKED[^3] [^4]: 唤醒操作本质是线程迁移(Wait Set → Entry Set)[^4] [^5]: 被唤醒线程竞争失败可能导致程序挂起[^5] --- ### 相关问题 1. 为什么被唤醒的线程需要重新竞争而不是直接运行? 2. 如何避免被唤醒线程竞争中"饿死"? 3. `notify()`和`notifyAll()`在竞争行为上有何区别? 4. Java的竞争机制是公平还是非公平的? 5. 虚假唤醒(spurious wakeup)如何影响竞争行为?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值