多线程安全问题案例
线程在执行的时候,cpu的执行权有可能随时被抢走;
1、所以如果是在多线程的环境下,会出现一些安全的问题。 以下面的需求为例:
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口买票,请设计一个程序模拟该电影院买票。
这里面的安全保证得是:每一张票是由某个窗口卖出去的,而且卖了就没有了。这都得用同步机制才能实现。 如果不用锁机制实现线程同步,上面的安全问题就无法得到保障。
例如下面的代码:
java
public class UnsafeTicketSale extends Thread {
// 共享资源:100张票
private static int ticket = 0;
@Override
public void run() {
while (true) {
if (ticket < 100) {
try {
// 模拟网络延迟,放大线程安全问题
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + " 卖出第 " + ticket + " 张票");
} else {
break;
}
}
}
public static void main(String[] args) {
UnsafeTicketSale window1 = new UnsafeTicketSale();
UnsafeTicketSale window2 = new UnsafeTicketSale();
UnsafeTicketSale window3 = new UnsafeTicketSale();
window1.setName("窗口1");
window2.setName("窗口2");
window3.setName("窗口3");
window1.start();
window2.start();
window3.start();
}
}
部分结果是:
窗口1 卖出第 2 张票
窗口2 卖出第 2 张票
窗口3 卖出第 3 张票
可以看到,第 2 张票被两个窗口同时卖出,出现了线程安全问题。
使用同步代码块解决
我们可以 使用同步代码块解决线程安全问题 即可以使用一个锁对象 SafeTicketSaleWithBlock.class(类对象),保证三个线程共用同一把锁。 当一个线程进入同步代码块时,其他线程必须等待锁释放,确保 ticket++ 操作的原子性。 从而运行结果中不会出现重复卖票或超卖的情况。
java
public class SafeTicketSaleWithBlock extends Thread {
private static int ticket = 0;
@Override
public void run() {
while (true) {
// 同步代码块:锁对象为类对象,保证所有线程共用同一把锁
synchronized (SafeTicketSaleWithBlock.class) {
if (ticket < 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(getName() + " 卖出第 " + ticket + " 张票");
} else {
break;
}
}
}
}
使用同步方法解决
同步方法的核心就是在方法声明中加上 synchronized 关键字,它会自动对整个方法体进行加锁。
-
非静态同步方法
java修饰符 synchronized 返回值类型 方法名(参数列表) { // 线程安全的代码 } 锁对象:默认是 this,也就是调用该方法的对象本身。
-
静态同步方法
java修饰符 static synchronized 返回值类型 方法名(参数列表) { // 线程安全的代码 }锁对象:默认是当前类的字节码对象(类名.class),这个对象在全局是唯一的。
解决示例:
java
public class SafeTicketSaleWithMethod implements Runnable {
private int ticket = 0;
// 同步方法:锁对象为 this(当前对象)
private synchronized void sellTicket() {
if (ticket < 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName() + " 卖出第 " + ticket + " 张票");
}
}
@Override
public void run() {
while (ticket < 100) {
sellTicket();
}
}
public static void main(String[] args) {
SafeTicketSaleWithMethod task = new SafeTicketSaleWithMethod();
Thread window1 = new Thread(task, "窗口1");
Thread window2 = new Thread(task, "窗口2");
Thread window3 = new Thread(task, "窗口3");
window1.start();
window2.start();
window3.start();
}
}
讨论
1、可以看到synchronized (SafeTicketSaleWithBlock.class) 使用的锁对象是字节码文件;这个类可以是任意的类;因为无论哪个类的字节码文件都是唯一的,而我们只需要保证锁这个资源是唯一的就行,无其他要求
2、为什么类的字节码对象(XXX.class)是唯一的?
当一个类被 JVM 加载完成后,会在内存中生成一个对应的「Class 对象」(也就是你说的字节码文件对象),这个对象全局唯一,无论你创建多少个该类的实例,对应的 Class 对象只有一个。
3、 锁的范围
同步方法的锁范围是整个方法体,无法像同步代码块那样精确控制锁的范围。
如果方法体中只有一小部分代码需要同步,使用同步代码块性能更高。
| 特性 | 同步方法 | 同步代码块 |
|---|---|---|
| 语法 | 简单,只需加 synchronized 关键字 |
需显式指定锁对象 synchronized(锁对象){...} |
| 锁对象 | 固定(this 或 类名.class) |
可以自定义,更灵活 |
| 锁范围 | 整个方法体 | 可以精确控制到代码块 |
| 性能 | 略低(锁范围大) | 更高(锁范围小) |
| 可读性 | 高(一眼看出是同步方法) | 稍低(需看锁对象) |
s