一、背景代码
public class SynchronizedTest {
public static void main(String[] args) {
Thread why = new Thread(new TicketConsumer(10), "why");
Thread mx = new Thread(new TicketConsumer(10), "mx");
why.start();
mx.start();
}
}
class TicketConsumer implements Runnable {
private volatile static Integer ticket;
public TicketConsumer(int ticket) {
this.ticket = ticket;
}
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "开始抢第" + ticket + "张票,对象加锁之前:" + System.identityHashCode(ticket));
synchronized (ticket) {
System.out.println(Thread.currentThread().getName() + "抢到第" + ticket + "张票,成功锁到的对象:" + System.identityHashCode(ticket));
if (ticket > 0) {
try {
//模拟抢票延迟
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket-- + "张票,票数减一");
} else {
return;
}
}
}
}
}
二、分析原因
这段代码确实存在多个问题,主要集中在并发操作和同步机制方面。让我们逐一分析这些问题:
- 锁对象的变化问题
由于 ticket 是一个 Integer 对象,而 Integer 在进行自增或自减操作后,会生成一个新的对象。因此,使用 synchronized(ticket) 锁定对象实际上锁定的是一个不断变化的对象,失去了同步的意义。
synchronized (ticket) {
// 执行代码块
}
每次 ticket 的减一操作都会生成一个新的 Integer 对象,因此 synchronized 的锁对象在每次循环中可能都是不同的,这样会导致多个线程并发地访问这块代码,无法达到同步的目的。
-
静态变量的实例化问题
在 TicketConsumer 构造函数中,你将 ticket 赋值为构造参数,但由于 ticket 是一个静态变量,所有实例共享同一个 ticket。这意味着在你创建多个 TicketConsumer 实例时,最后一个创建的实例会覆盖之前所有实例的 ticket 值。 -
volatile 修饰的问题
volatile 只能保证变量的可见性,即当一个线程修改了这个变量,其他线程可以立即看到它的变化,但不能保证复合操作(如自减操作)的原子性。这意味着在 ticket-- 操作中,多个线程可能会看到相同的 ticket 值,从而导致重复处理的问题。
代码修正
为了修正上述问题,可以采取以下措施:
使用一个单独的锁对象来同步,而不是使用 ticket。
确保 ticket 的原子性操作。
以下是修改后的代码:
import java.util.concurrent.TimeUnit;
public class SynchronizedTest {
public static void main(String[] args) {
TicketConsumer ticketConsumer = new TicketConsumer(10); // 共享同一个 TicketConsumer 实例
Thread why = new Thread(ticketConsumer, "why");
Thread mx = new Thread(ticketConsumer, "mx");
why.start();
mx.start();
}
}
class TicketConsumer implements Runnable {
private static Integer ticket;
private static final Object lock = new Object(); // 单独的锁对象
public TicketConsumer(int ticket) {
TicketConsumer.ticket = ticket;
}
@Override
public void run() {
while (true) {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "开始抢第" + ticket + "张票,对象加锁之前:" + System.identityHashCode(lock));
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "抢到第" + ticket + "张票,成功锁到的对象:" + System.identityHashCode(lock));
try {
// 模拟抢票延迟
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket-- + "张票,票数减一");
} else {
return;
}
}
}
}
}
修正后的解释
锁对象:
定义了一个单独的 lock 对象,确保所有线程都在同一个对象上进行同步。
共享 TicketConsumer 实例:
两个线程共享同一个 TicketConsumer 实例,确保它们都操作同一个 ticket。
同步:
使用 synchronized (lock) 确保多个线程在操作 ticket 时是互斥的,防止出现并发问题。
通过这些修改,可以确保在多线程环境下对 ticket 的操作是线程安全的,并避免了前述的问题