题目描述
某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。
方式一:继承Thread类
public class SellTicket extends Thread {
// 定义100张票,为了让多个线程共享这100张票,所以应该用静态修饰
private static int tickets = 100;
@Override
public void run() {
while (tickets > 0) {
System.out.println(getName() + "正在出售第" + (100 - tickets-- + 1) + "张票");
}
}
}
public static void main(String[] args) {
SellTicket st1 = new SellTicket();
SellTicket st2 = new SellTicket();
SellTicket st3 = new SellTicket();
st1.setName("窗口1");
st2.setName("窗口2");
st3.setName("窗口3");
st1.start();
st2.start();
st3.start();
}
运行结果
方式二:实现Runnable接口
推荐使用方式二来实现多线程。
public class SellTicketInter implements Runnable {
private int tickets = 100;
@Override
public void run() {
while (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "正在出售第" + (101 - tickets--) + "张票");
}
}
}
public static void main(String[] args) {
SellTicketInter sti = new SellTicketInter();
Thread t1 = new Thread(sti, "窗口1");
Thread t2 = new Thread(sti, "窗口2");
Thread t3 = new Thread(sti, "窗口3");
t1.start();
t2.start();
t3.start();
}
改进:为了模拟真实情况,加入延时
public class SellTicketInter implements Runnable {
private int tickets = 100;
@Override
public void run() {
while (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + (101 - tickets--) + "张票");
}
}
}
出现负数
数据出现重复
加入延迟后,就产生了两个问题:
A:相同的票卖了多次
原因:CPU的一次操作必须是原子性的。
由于在SellTicketInter中需要执行tickets--这一项,计算机先记录tickets的原始值,然后tickets--,然后输出原始值,然后更新tickets值,CPU一共做了四步。但是在线程休眠那一行,可能有多个线程都处于休眠状态,当第一个线程醒过来的时候,正好做到输出tickets的值的时候,第二个线程也醒了,在更新tickets值之前醒过来了,所以第二个线程输出的也是原始值,所以这就导致了多个线程输出一样的数字,即相同的票卖了多次。
B:出现了负数票
随机性和延迟导致的。在还剩最后一张票的时候,多个线程进来了,然后处于休眠状态,然后第一个线程醒过来,输出正在卖第1张票,更新tickets值后,第二个线程醒过来,输出正在卖第0张票,更新tickets值后,第三个线程醒了过来,输出正在卖第-1张票。这就导致了出现负数的票。
解决方法
- 要想解决问题,就要知道哪些原因会导致出问题:(而且这些原因也是以后我们判断一个程序会有线程安全问题的标准)
A:是否是多线程环境
B:是否有共享数据
C:是否有多条语句操作共享数据 - 上面的程序满足这三个原因,但是因为A和B的问题改变不了,所以只能改变原因C。
- 思想:把多条语句操作共享数据的代码给包成一个整体,让某个线程在执行的时候,别人不能来执行即可。利用Java提供的同步机制。
同步代码块
格式:
synchronized(对象){需要同步的代码;}
注意:同步可以解决安全问题的根本原因就在那个对象上,该对象如同锁的功能。多个线程必须是同一把锁。
public class SellTicketInter implements Runnable {
private int tickets = 100;
//创建锁对象
private Object obj = new Object();
@Override
public void run() {
synchronized (obj) {
while (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
}
}
}
}
同步的特点
- 同步的前提
多个线程
多个线程使用的是同一个锁对象 - 同步的好处
同步的出现解决了多线程的安全问题。 - 同步的弊端
当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
注意
- 同步代码块的锁对象是谁?
任意对象 - 同步方法的格式及锁对象问题?
把同步关键字加在方法上,该方法就变成了同步方法。
private synchronized void sellTicket(){
...
}
问题:那么同步方法的锁对象是谁呢?
答:this
- 静态同步方法及锁对象问题?
静态方法的锁对象根本不可能是this,因为静态方法随着类的加载而加载,这个时候this根本就不存在。所以静态方法的锁对象是类的字节码文件对象,即.class文件对象。(字节码的相关知识请参考“反射”)
线程安全的类
StringBuffer sb = new StringBuffer();
Vector<String> v = new Vector<String>();
Hashtable<String, String> h = new Hashtable<String, String>();
Vector是线程安全的时候才去考虑用的,但实际上还是很少用,那么用谁呢(如何把一个线程不安全的集合类变成一个线程安全的集合类呢)?
//public static <T> List<T> synchronizedList (List<T> list)
List<String> list1 = new ArrayList<String>();//线程不安全
List<String> list2 = Collections.synchronizedList(new ArrayList<String>());//线程安全
网友评论