Java 用多线程卖票
Java Selling Tickets with Multithreading
我有两个卖票的线程
public class MyThread {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread thread1 = new Thread(()->{
for (int i = 0; i < 30; i++) {
ticket.sell();
} }, "A");
thread1.start();
Thread thread2 = new Thread(()->{
for (int i = 0; i < 30; i++) {
ticket.sell();
} }, "B");
thread2.start();
}
}
class Ticket {
private Integer num = 20 ;
private Object obj = new Object();
public void sell() {
// why shouldn't I use "num" as a monitor object ?
// I thought "num" is unique among two threads.
synchronized ( num ) {
if (this.num >= 0) {
System.out.println(Thread.currentThread().getName() + " sells " + this.num + "th ticket");
this.num--;
}
}
}
}
如果我使用num
作为监控对象,输出将是错误的。
但是如果我使用obj
作为监控对象,输出将是正确的。
使用 num
和使用 obj
有什么区别?
============================================= ==
为什么我用(Object)num
作为监控对象还是不行?
class Ticket {
private int num = 20 ;
private Object obj = new Object();
public void sell() {
// Can I use (Object)num as a monitor object ?
synchronized ( (Object)num ) {
if (this.num >= 0) {
System.out.println(Thread.currentThread().getName() + " sells " + this.num + "th ticket");
this.num--;
}
}
}
}
Integer
是一个装箱值。它包含一个原语 int
,编译器处理 autoboxing/autounboxing 即 int
。因此,语句 this.num--
实际上是:
num=Integer.valueOf(num.intValue()-1)
也就是说,一旦您执行该更新,包含锁的 num
实例就会丢失。
这里的根本问题是synchronizing on a non-final value。
了解 Java 内存模型最重要的事情 - 即线程在执行 Java 程序时看到的值 - 是 happens-before relationship.
在同步块的特定情况下,在退出同步块之前在一个线程中完成的操作发生在另一个线程在同步块中完成的操作之前 - 所以,如果第一个线程在该同步块内递增一个变量,第二个线程看到更新后的值。
这超越了众所周知的事实,即同步块一次只能由一个线程进入:一次只能有一个线程和你得到看看上一个线程做了什么。
// Thread 1 // Thread 2
synchronized (monitor) {
num = 1
} // Exiting monitor
// *happens before*
// entering monitor
synchronized (monitor) {
int n = num; // Guaranteed to see n = 1 (provided no other thread has entered a block synchronized on monitor and changed it first).
}
这个保证有一个非常重要的警告:只有当同步块的两个执行使用同一个监视器时它才成立。这与 变量 不同,它是堆上相同的实际具体对象(变量没有监视器,它们只是指向堆中值的指针)。
因此,如果您在同步块内重新分配监视器:
synchronized (num) {
if (num > 0) {
num--; // This is the same as `num = Integer.valueOf(num.intValue() - 1);`
}
}
然后你破坏了发生前保证,因为到达那个同步块的下一个线程正在进入另一个对象的监视器(*)。
一旦你这样做了,你的程序的行为就是不明确的:如果你幸运的话,它会以明显的方式失败;如果您非常不幸,它 似乎 可以工作,然后在稍后的某个日期开始神秘地失败。
您的代码刚刚损坏。
这也不是 Integer
特有的东西:这段代码会有同样的问题。
// Assume `Object someObject = new Object();` is defined as a field.
synchronized (someObject) {
someObject = new Object();
}
(*) 实际上,对于新对象,你仍然会得到一个发生在前面的关系:它不是针对这个同步块内的事情,而是针对在使用该对象作为对象的其他同步块中发生的事情监视器。从本质上讲,不可能推断出这意味着什么,因此您不妨将其视为“损坏”。
正确的方法是在您不能(不仅仅是不要)重新分配的字段上进行同步.您可以简单地在 this
上同步(无法重新分配):
synchronized (this) {
if (num > 0) {
num--; // This is the same as `num = Integer.valueOf(num.intValue() - 1);`
}
}
现在在块内重新分配 num
并不重要,因为您不再对其进行同步。您总是在同一件事上进行同步,从而获得先发生后发生的保证。
但是请注意,您必须 始终 从同步块内部访问 num
- 例如,如果您有一个 getter 来获取剩余票数,that 也必须在 this
上同步,以便获得 happens-before 保证 sell()
方法中更改的值在getter.
这行得通,但可能并不完全理想:有权访问您的 Ticket
实例的任何人也可以在其上进行同步。这意味着他们可能会死锁您的代码。
相反,通常的做法是引入一个纯粹用于锁定的私有字段:这就是 obj
字段为您提供的。您的代码的唯一修改应该是使其成为 final
(并给它一个比 obj
更好的名称):
private final Object obj = new Object();
无法在您的 class 外部访问,因此恶意客户端无法直接为您造成死锁。
同样,这不能在您的同步块(或其他任何地方)内重新分配,因此您不存在通过重新分配它来破坏 happens-before 保证的风险。
我有两个卖票的线程
public class MyThread {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread thread1 = new Thread(()->{
for (int i = 0; i < 30; i++) {
ticket.sell();
} }, "A");
thread1.start();
Thread thread2 = new Thread(()->{
for (int i = 0; i < 30; i++) {
ticket.sell();
} }, "B");
thread2.start();
}
}
class Ticket {
private Integer num = 20 ;
private Object obj = new Object();
public void sell() {
// why shouldn't I use "num" as a monitor object ?
// I thought "num" is unique among two threads.
synchronized ( num ) {
if (this.num >= 0) {
System.out.println(Thread.currentThread().getName() + " sells " + this.num + "th ticket");
this.num--;
}
}
}
}
如果我使用num
作为监控对象,输出将是错误的。
但是如果我使用obj
作为监控对象,输出将是正确的。
使用 num
和使用 obj
有什么区别?
============================================= ==
为什么我用(Object)num
作为监控对象还是不行?
class Ticket {
private int num = 20 ;
private Object obj = new Object();
public void sell() {
// Can I use (Object)num as a monitor object ?
synchronized ( (Object)num ) {
if (this.num >= 0) {
System.out.println(Thread.currentThread().getName() + " sells " + this.num + "th ticket");
this.num--;
}
}
}
}
Integer
是一个装箱值。它包含一个原语 int
,编译器处理 autoboxing/autounboxing 即 int
。因此,语句 this.num--
实际上是:
num=Integer.valueOf(num.intValue()-1)
也就是说,一旦您执行该更新,包含锁的 num
实例就会丢失。
这里的根本问题是synchronizing on a non-final value。
了解 Java 内存模型最重要的事情 - 即线程在执行 Java 程序时看到的值 - 是 happens-before relationship.
在同步块的特定情况下,在退出同步块之前在一个线程中完成的操作发生在另一个线程在同步块中完成的操作之前 - 所以,如果第一个线程在该同步块内递增一个变量,第二个线程看到更新后的值。
这超越了众所周知的事实,即同步块一次只能由一个线程进入:一次只能有一个线程和你得到看看上一个线程做了什么。
// Thread 1 // Thread 2
synchronized (monitor) {
num = 1
} // Exiting monitor
// *happens before*
// entering monitor
synchronized (monitor) {
int n = num; // Guaranteed to see n = 1 (provided no other thread has entered a block synchronized on monitor and changed it first).
}
这个保证有一个非常重要的警告:只有当同步块的两个执行使用同一个监视器时它才成立。这与 变量 不同,它是堆上相同的实际具体对象(变量没有监视器,它们只是指向堆中值的指针)。
因此,如果您在同步块内重新分配监视器:
synchronized (num) {
if (num > 0) {
num--; // This is the same as `num = Integer.valueOf(num.intValue() - 1);`
}
}
然后你破坏了发生前保证,因为到达那个同步块的下一个线程正在进入另一个对象的监视器(*)。
一旦你这样做了,你的程序的行为就是不明确的:如果你幸运的话,它会以明显的方式失败;如果您非常不幸,它 似乎 可以工作,然后在稍后的某个日期开始神秘地失败。
您的代码刚刚损坏。
这也不是 Integer
特有的东西:这段代码会有同样的问题。
// Assume `Object someObject = new Object();` is defined as a field.
synchronized (someObject) {
someObject = new Object();
}
(*) 实际上,对于新对象,你仍然会得到一个发生在前面的关系:它不是针对这个同步块内的事情,而是针对在使用该对象作为对象的其他同步块中发生的事情监视器。从本质上讲,不可能推断出这意味着什么,因此您不妨将其视为“损坏”。
正确的方法是在您不能(不仅仅是不要)重新分配的字段上进行同步.您可以简单地在 this
上同步(无法重新分配):
synchronized (this) {
if (num > 0) {
num--; // This is the same as `num = Integer.valueOf(num.intValue() - 1);`
}
}
现在在块内重新分配 num
并不重要,因为您不再对其进行同步。您总是在同一件事上进行同步,从而获得先发生后发生的保证。
但是请注意,您必须 始终 从同步块内部访问 num
- 例如,如果您有一个 getter 来获取剩余票数,that 也必须在 this
上同步,以便获得 happens-before 保证 sell()
方法中更改的值在getter.
这行得通,但可能并不完全理想:有权访问您的 Ticket
实例的任何人也可以在其上进行同步。这意味着他们可能会死锁您的代码。
相反,通常的做法是引入一个纯粹用于锁定的私有字段:这就是 obj
字段为您提供的。您的代码的唯一修改应该是使其成为 final
(并给它一个比 obj
更好的名称):
private final Object obj = new Object();
无法在您的 class 外部访问,因此恶意客户端无法直接为您造成死锁。
同样,这不能在您的同步块(或其他任何地方)内重新分配,因此您不存在通过重新分配它来破坏 happens-before 保证的风险。