线程同步 - 线程何时释放对象上的锁

Posted

技术标签:

【中文标题】线程同步 - 线程何时释放对象上的锁【英文标题】:Thread synchronization- When does a thread release the lock on an object 【发布时间】:2014-12-01 23:28:04 【问题描述】:
public class MyStack2 
    private int[] values = new int[10];
    private int index = 0;

    public synchronized void push(int x) 
        if (index <= 9) 
            values[index] = x;
            Thread.yield();
            index++;
        
    

    public synchronized int pop() 
        if (index > 0) 
            index--;
            return values[index];
         else 
            return -1;
        
    

    public synchronized String toString() 
        String reply = "";
        for (int i = 0; i < values.length; i++) 
            reply += values[i] + " ";
        
        return reply;
    


public class Pusher extends Thread 
    private MyStack2 stack;

    public Pusher(MyStack2 stack) 
        this.stack = stack;
    

    public void run() 
        for (int i = 1; i <= 5; i++) 
            stack.push(i);
        
    


public class Test 
    public static void main(String args[]) 
        MyStack2 stack = new MyStack2();
        Pusher one = new Pusher(stack);
        Pusher two = new Pusher(stack);
        one.start();
        two.start();
        try 
            one.join();
            two.join();
         catch (InterruptedException e) 
        
        System.out.println(stack.toString());
    

由于MyStack2 类的方法是同步的,我期望输出为 1 2 3 4 5 1 2 3 4 5. 但是输出是不确定的。通常它会给出:1 1 2 2 3 3 4 4 5 5

根据我的理解,当线程一启动时,它会在push 方法上获得一个锁。在push() 内,线程一会产生一段时间。但是当yield() 被调用时它会释放锁吗?现在当线程二启动时,线程二会在线程一完成执行之前获得锁吗?谁能解释一下线程一何时释放堆栈对象上的锁?

【问题讨论】:

看看这个***.com/questions/18635616/… 【参考方案1】:

synchronized 方法只会在执行时阻止其他线程执行它。一旦它返回,其他线程就可以(并且通常会立即)获得访问权限。

获得1 1 2 2 ... 的情况可能是:

    线程 1 调用 push(1) 并被允许进入。 线程 2 调用 push(1) 并在线程 1 使用它时被阻塞。 线程 1 退出 push(1)。 线程 2 可以访问 push 并推送 1,但同时线程 1 调用 push(2)

结果1 1 2 - 你可以清楚地看到它是如何继续的。

【讨论】:

是的,但如果是这种情况,程序的输出不应该是:1 1 2 2 3 3 4 4 5 5。但通常这是输出。 @javaTech - 没有合同规定 Java 线程应该是公平的。几乎任何订单都可能是您的代码的结果。 1 1 2 2 ... 非常有效。【参考方案2】:

当你说:

据我了解,当线程一启动时,它会在 push 方法上获得锁。

这不太对,因为锁不只是在 push 方法上。 push 方法使用的锁位于调用 push 的 MyStack2 实例上。 pop 和 toString 方法使用与 push 相同的锁。当一个线程在一个对象上调用这些方法中的任何一个时,它必须等待直到它可以获取锁。正在调用 push 的线程将阻止另一个线程调用 pop。线程调用不同的方法来访问相同的数据结构,对所有访问该结构的方法使用相同的锁,防止线程同时访问数据结构。

一旦一个线程在退出同步方法时放弃了锁,调度程序就会决定哪个线程下一个获得锁。您的线程正在获取锁并多次释放它们,每次释放锁时,调度程序都会做出决定。你不能对哪个会被选中做出任何假设,它可以是其中任何一个。来自多个线程的输出通常是混乱的。

【讨论】:

"锁定防止数据结构被同时访问。"小心!新手可能不明白,保护数据的不是锁定本身。需要告诉他们(有时不止一次),保护数据的是更新或使用数据的每个代码块都锁定同一个锁。 @james:同意,重新措辞。【参考方案3】:

您似乎对 synchronized 和 yield 关键字的确切含义有些困惑。

同步意味着一次只有一个线程可以进入该代码块。把它想象成一扇门,你需要一把钥匙才能通过。每个线程在进入时获取唯一的密钥,并在完成后返回它。这允许下一个线程获取密钥并执行里面的代码。不管他们在同步方法中的时间有多长,一次只能进入一个线程。

Yield 向编译器建议(是的,它只是一个建议)当前线程可以放弃其分配的时间,而另一个线程可以开始执行。然而,情况并不总是这样。

在你的代码中,即使当前线程向编译器建议它可以放弃执行时间,但它仍然持有同步方法的关键,因此新线程无法进入。

不可预知的行为来自于 yield 没有像你预测的那样放弃执行时间。

希望有所帮助!

【讨论】:

“同步意味着一次只有一个线程可以进入那个代码块。”只有当代码每次进入块时总是试图锁定同一个对象时,这才是正确的。如果foo 不是常数,则可以有多个线程进入同一个synchronized(foo)... 块。 "Yield 建议...给编译器..." 不是给编译器。编译器不知道 Thread.yield() 和任何其他方法调用之间的区别。魔法(如果有的话)发生在运行时,很可能是当 JVM 实现中的本机线程进行某种“yield”系统调用时。 “不可预知的行为来自不放弃的收益...”我不这么认为,因为正如您所指出的,yield() 调用发生在同步块内。不管它是否做任何事情,在 yield() 返回之前,另一个线程都不会运行。不可预测性来自于有两个线程反复争夺同一个锁的事实。假设线程 A 拥有锁,而线程 B 正在等待锁。当线程 A 释放锁然后立即尝试再次锁定它时,哪个线程获得了锁? 是不可预测的。

以上是关于线程同步 - 线程何时释放对象上的锁的主要内容,如果未能解决你的问题,请参考以下文章

为什么 wait(), notify()和 notifyAll ()必须在同步方法或 者同步块中被调用?

Java线程:线程的同步与锁

java多线程

Java中的锁

java多线程并发系列之 (synchronized)同步与加锁机制

java线程从入门到精通