使用多线程模拟卖票功能:
要求:在电影院有三个窗口同时卖电影票(50张)。启动3条线程,每1条线程代表一个售票窗口。
1)定义卖票的线程。
class SaleTicketThread extends Thread {
static int num = 50; //多个线程共享着这些电影票
public SaleTicketThread(String name) {
super(name);
}
@Override
public void run() {
while (true) {
if (num > 0) {
System.out.println(this.getName() + "卖出了一张电影票,还剩下" + (num - 1) + "张电影票.");
num--;
} else {
System.out.println("票已售罄!");
break;
}
}
}
}
因为多个窗口共享着这50张票,所以设置num成员属性为静态static。
2)启动三条线程同时进行卖票。
public class Demo {
public static void main(String[] args) {
//模拟三个卖票的窗口
SaleTicketThread t1 = new SaleTicketThread("窗口1");
SaleTicketThread t2 = new SaleTicketThread("窗口2");
SaleTicketThread t3 = new SaleTicketThread("窗口3");
//启动线程开始卖票
t1.start();
t2.start();
t3.start();
}
}
运行上面程序发现存在以下问题:
1)一张电影票被卖出了多张;
2)票卖光后,还可以继续卖;
存现上面问题的原因,是因为多线程的安全问题导致的。如果多线线程同时共享着同一个资源,而且访问资源的任务代码有多行的时候,就有可能会出现线程安全问题。上面例子就是因为多个线程共享同一个num变量,而且执行卖票的代码有多行,所以就出现了线程安全问题。如果出现了线程安全问题,那么就会导致共享资源的数据发生错乱,从而出现结果不一致的情况。
解决线程安全问题的办法:
1. 使用同步代码块。
同步代码块可以保证在同一个时间片内只有一个线程执行同步代码块中的代码。
- 定义格式:
synchronized (锁对象) {
// 需要同步的代码…
}
锁对象可以是任意对象。但是,必须要保证多个线程可以共享同一个锁对象。所以,只要保证多个线程共享同一个对象,那么该对象就可以作为锁。
synchronized ("锁") {
if (num > 0) {
System.out.println(this.getName() + "卖出了一张电影票,还剩下" + (num - 1) + "张票.");
num--;
} else {
System.out.println("票已卖光!");
break;
}
}
}
上面程序使用了字符串常量作为锁对象。
2. 使用同步函数。
- 定义格式:
访问修饰符 synchronized 返回值类型 函数名(形参列表) {
}
同步函数就是在函数返回值类型前面加上synchronized关键字即可。
注意:同步函数也有锁对象。而同步函数的锁对象就是this。
@Override
public synchronized void run() {
while (true) {
if (num > 0) {
System.out.println(this.getName() + "卖出了一张电影票,还剩下" + (num - 1) + "张票.");
num--;
} else {
System.out.println("票已卖光!");
break;
}
}
}
运行上面程序发现,线程安全问题仍然存在,问题没有被解决(见下图)。
因为同步函数的锁对象是this,也就是当前的线程对象。也就是说,如果启用多条线程进行卖票的时候,它们并不是共享同一个锁对象。那么,是不是就不能够使用同步函数解决上面问题呢?答案是可以的。但是需要使用另外一种创建线程的方式。
实现线程的另外一种方式:
第一步:定义一个类,实现Runnable接口;
第二步:重写Runnable接口的run方法, 把需要实现的任务代码定义在该方法中;
第三步:创建该类对象;
第四步:创建Thread对象,然后把上面创建的对象传入到构造函数中;
第五步:调用Thread对象的start方法启动线程;
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class Demo1 {
public static void main(String[] args) {
//创建Runnable实现类的对象
MyRunnable task = new MyRunnable();
//创建Thread对象,然后把Runnable实现类对象传入
Thread t = new Thread(task);
//启动线程
t.start();
}
}
注意:实现Runnable接口的对象不是线程对象。之所以在创建Thread对象时候传入一个Runnable对象,是因为Thread对象需要借用Runnable对象的run方法执行任务代码。
使用同步函数改造卖票的例子:
class SaleTicketTask implements Runnable {
static int num = 50; //多个线程共享着这些电影票
@Override
public synchronized void run() {
while (true) {
if (num > 0) {
System.out.println(Thread.currentThread().getName()
+ "卖出了一张电影票,还剩下" + (num - 1) + "张票.");
num--;
} else {
System.out.println("票已卖光!");
break;
}
}
}
}
public class Demo {
public static void main(String[] args) {
//模拟三个卖票的窗口
SaleTicketTask task = new SaleTicketTask();
Thread t1 = new Thread(task, "窗口1");
Thread t2 = new Thread(task, "窗口2");
Thread t3 = new Thread(task, "窗口3");
//启动线程开始卖票
t1.start();
t2.start();
t3.start();
}
}
运行上面程序发现,之前出现的同步安全问题解决了。
上面程序之所以能够在run方法上使用synchronized,是因为同步函数的锁对象是this,即Runnable对象,t1、t2、t3线程都是共享同一个Runnable对象。