我正在努力理解java中的内部锁。我有一个程序,我在其中启动两个线程,它们将循环通过并调用同一对象上的同步方法。我希望两个线程并行执行,但看起来它是按顺序执行的。
如果我在循环中引入睡眠,那么它们按随机顺序执行[正如我所预期的]
public class Synchronized {
private int valueM;
public Synchronized( int value) {
valueM = value;
}
synchronized
public void one() throws InterruptedException
{
System.out.println("Object[" + valueM + "] executing one");
Thread.sleep(100); //For case 2: comment it out
System.out.println("Object[" + valueM + "] completed one");
}
synchronized
public void two() throws InterruptedException
{
System.out.println("Object[" + valueM + "] executing two");
Thread.sleep(100); //For case 2: comment it out
System.out.println("Object[" + valueM + "] completed two");
}
}
测试代码:
@org.junit.jupiter.api.Test
void test_sync() throws InterruptedException
{
Synchronized obj = new Synchronized(1);
Runnable task_one = new Runnable() {
public void run() {
for (int i=0 ; i<10; i++)
{
try {
obj.one();
//Thread.sleep(100); //For case 2: uncomment it out
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
};
Runnable task_two = new Runnable() {
public void run() {
for (int i=0 ; i<10; i++)
{
try {
obj.two();
//Thread.sleep(100); //For case 2: uncomment it out
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
};
Thread t1 = new Thread(task_one);
Thread t2 = new Thread(task_two);
t1.start();
t2.start();
t1.join();
t2.join();
}
输出:
Case 1: output:
Object[1] executing one
Object[1] completed one
...10times
Object[1] executing two
Object[1] completed two
...10times
Case 2: output: random order
Object[1] executing one
Object[1] completed one
Object[1] executing two
Object[1] completed two
...
更新:原始问题已修复。。即使在情况1中,看起来它也是随机的,但我只有在加载更多迭代(30K)时才能看到它。。
所以线程切换在没有睡眠的for循环中发生得更少?Java JVM试图让for循环作为"一种"原子(不是完全的,而是尽可能多的)来执行它,这有什么特别之处吗?
intrinsic
锁(synchronized
关键字)被认为是"不公平的",这意味着无法保证在竞争线程之间锁的获取率相同。
众所周知,释放锁的线程通常更有可能再次获取锁,从而导致您遇到的问题。
如果你希望你的线程具有类似的获取可能性(公平性),你可以使用像ReentrantLock这样的显式锁,确保使用可选的boolean
参数将其设置为真正的
ReentrantLock(boolean fair)
然后你可以用这种方式
class X {
private final ReentrantLock lock = new ReentrantLock(true);
public void m() {
lock.lock();
try {
// method body
} finally {
lock.unlock()
}
}
}
您已将方法one
和two
标记为synchronized
。这意味着,在线程可以进入其中任何一个之前,它必须获取obj
上的锁。如果另一个线程持有锁,则线程无法获取锁。当线程退出one
/two
时,锁被释放,两个线程再次争夺锁。有时第一个线程成功,有时第二个线程成功——这就是为什么您看到调用的随机顺序,但从不混合。
所以这是经过设计的。实际上,您已经告诉JVM,您不希望两个线程同时运行。
让我们试着理解你的问题,然后试着看看预期的结果。
-
在同一对象(类型为
Synchronized
的当前对象)上有两个方法是synchronized
。 -
共有2个线程。执行路径中的每个线程都试图多次调用其中一个同步方法。
-
有两种情况,情况1没有在线程上调用
sleep
方法,情况2有在当前执行的线程上调用的sleep
方法。
现在从第3点开始。sleep
不解除锁定。它是对已获取锁的对象调用的wait
方法,用于释放其他线程的锁。因此,在您的情况下,基本上睡眠只会使执行速度变慢,而不是其他任何事情。
线程调度程序决定线程的执行顺序以及处理器周期共享。它不保证任何顺序,也不保证任何随机性,它可能是随机的,也可能不是随机的。
现在,正如我所说,当sleep
不释放锁时,我们有时如何获得随机执行顺序?答案是:一旦其中一个同步方法的执行被两个线程中的一个线程结束,锁就会被释放,线程调度程序就会决定给哪个线程下一次执行的机会。
Thread#start是一个相对来说非常慢的方法。计算到10(或计算到1000)并不需要计算机很长时间。第一个线程早在操作系统完成第二个线程实际执行的工作之前就完成了计数。如果你想"同时"启动两个线程,你需要使用一个锁存器。
根据您的执行环境,系统控制台编写器本身可能是一个同步的有争议的资源(或者相反,它可能无法保证以与访问它的线程顺序一致的时间点刷新和写入),这一事实也让您的测试感到困惑因为暂停获取控制台写入程序通常会隐藏它们的内存一致性错误。
public static CountDownLatch latch = new CountDownLatch(1);
public static class Thing implements Runnable {
@Override
public void run() {
try {
latch.await();
//doStuff
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) throws Exception {
Thing thing1 = new Thing();
Thing thing2 = new Thing();
new Thread(thing1).start();
new Thread(thing2).start();
latch.countDown();
}