为什么 java 线程在这种情况下不应该表现得如此不同?

Why java thread behave so differently if they shouldnt in this scenario?

我有一个线程休眠问题。在线程 运行 方法中,我有一个同步块和一个睡眠时间。 每个线程递增或递减shared class "value" 5个单位,然后休眠

public class borr {

    public static void main(String[] args) {

        int times=5;
        int sleeptime=1000;
        int initial=50;
        Shared shared = new Shared(initial);

        ThreadClass tIncrement = new ThreadClass(shared,times,sleeptime,true);
        ThreadClass tDecrement = new ThreadClass(shared,times,sleeptime,false);
        tIncrement.start();
        tDecrement.start();
    }
}

class Shared{

    int  value=0;

    public Shared(int value) {
        super();
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}

class ThreadClass extends Thread{

    Shared shared;
    int times=0;
    int sleeptime=0;
    boolean inc;

    public ThreadClass(Shared shared, int times, int sleeptime, boolean inc) {
        super();
        this.shared = shared;
        this.times = times;
        this.sleeptime = sleeptime;
        this.inc = inc;
    }

    public void run() {

        int aux;

        if(inc) {
            for(int i=0;i<times;i++) {
                synchronized(shared) {
                    aux=shared.getValue()+1;
                    shared.setValue(aux);
                    System.out.println("Increment, new value"+shared.getValue());

                    try {
                        Thread.sleep(sleeptime);
                    }catch(Exception e) {
                        e.printStackTrace();
                    }
                }
            }   
        }
        else {
            for(int i=0;i<times;i++) {
                synchronized(shared) {
                    aux=shared.getValue()-1;
                    shared.setValue(aux);
                    System.out.println("Decrement, new value"+shared.getValue());

                    try {
                        Thread.sleep(sleeptime);
                    }catch(Exception e) {
                        e.printStackTrace();
                    }
                }
            }   
        }
    }
}

但是如果我将 Thread.sleep 移出 synchronized 块,就像这样,输出是递增、递减、递增、递减。当它停止休眠并开始新的循环迭代时,其他线程不应该尝试进入吗?相反,它会继续循环直到该线程完成:

for(int i=0;i<times;i++) {
    synchronized(shared) {
        aux=shared.getValue()-1;
        shared.setValue(aux);
        System.out.println("Decrement, new value"+shared.getValue());
    }

    try {
        Thread.sleep(sleeptime);
    }catch(Exception e) {
        e.printStackTrace();
    }
}   

But if i move the Thread.sleep out of the synchronized block, like this, the output is increment, decrement, increment, decrement. The sleep is still inside each iteration of the loop so, shouldnt the result be the same in both cases?:

when it stops sleeping and starts a new iteration of the loop, shouldn't the other thread try to enter.

他们试图进入。

而另一个已经处于等待状态(即未主动 运行ning),因为它之前尝试进入。而刚刚释放锁的线程可以 运行 on 并立即取回现在无争议的锁。

这是一个竞争条件。当两个线程同时想要锁时,系统可以自由选择一个。似乎它选择了几个指令前刚刚发布的那个。也许您可以通过 yield()ing 来更改它。也许不吧。但无论哪种方式,它都不是 specified/deterministic/fair。如果您关心执行顺序,则需要自己明确安排事情。

在变体 A 中,您使用两个线程...

  • 重复5次
    • 输入一个同步块
      • 增量
      • 等1秒
  • 重复5次
    • 输入一个同步块
      • 递减
      • 等1秒

在变体 B 中,您使用两个线程...

  • 重复5次
    • 输入一个同步块
      • 增量
    • 等1秒
  • 重复5次
    • 输入一个同步块
      • 递减
    • 等1秒

在变体 A 中,两个线程始终处于活动状态(= 保持在同步块中)。

在变体 B 中,两个线程大部分时间都在休眠。

由于绝对不能保证接下来执行哪个线程,因此变体 A 和 B 的行为如此不同也就不足为奇了。虽然在 A 中,理论上两个线程都可以并行活动,但第二个线程没有太多机会活动,因为不在同步上下文中并不能保证在那一刻执行上下文切换(另一个线程是 运行).在变体 B 中完全不同:由于两个线程大部分时间都处于睡眠状态,因此 运行time 环境没有其他机会,因为 运行 在另一个线程处于睡眠状态时。当 VM 试图充分利用现有 CPU 资源时,睡眠将触发切换到另一个线程。

然而:两个线程 运行 之后的结果将完全相同。这是您唯一可以依赖的决定论。其他一切都取决于特定的实现细节,VM 将如何处理线程和同步块,甚至可以从 OS 到 OS 或一个 VM 的一个实现到另一个。

这很糟糕:

for(...) {
    synchronized(some_lock_object) {
        ...
    }
}

它不好的原因是,一旦某个线程 A 进入那个循环,那么每次它解锁锁时,接下来它所做的事情 就是锁定再次锁定。

如果循环体的执行时间很长,那么等待锁的任何其他线程 B 都会被操作系统置于等待状态。每次线程A释放锁,线程B都会开始唤醒,但是线程A可以在线程B获得机会之前重新获取。

这是starvation的经典例子。

解决该问题的一种方法是使用 ReentrantLock with a fair ordering policy 而不是 synchronized 块。当线程竞争公平锁时,获胜者总是等待时间最长的。

但是,公平锁的实现成本很高。一个更好的解决方案是始终保持任何 synchronized 块的主体尽可能短和尽可能甜。通常,线程保持锁定的时间不应超过在某个对象中分配少量字段所需的时间。