第四讲 多线程的应用(1)
一、由一个实例引出的问题
假设我们有这样一个需求:某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。
1、继承Thread类实现
代码实现如下:
package cn.itcast_01;
public class SellTickets extends Thread {
public SellTickets() {
super();
}
public SellTickets(String name) {
super(name);
}
@Override
public void run() {
int ticket=100;
while(true){
if(ticket>0){
System.out.println(getName()+"正在出售第"+(ticket--)+"张票");
}
}
}
}
package cn.itcast_01;
public class SellTicketsDemo {
public static void main(String[] args) {
SellTickets st1=new SellTickets("窗口1");
SellTickets st2=new SellTickets("窗口1");
SellTickets st3=new SellTickets("窗口1");
st1.start();
st2.start();
st3.start();
}
}
通过查看程序运行发现同一张票被出售了多次,这是为什么呢?
- 由于ticket这个变量被定义在了run()方法中,每一个线程都会执行这条语句,相当于买着各自的100张票
改进:将ticket这个变量定义为成员变量。
- 通过运行发现还是会出现不同的窗口卖同一张票,原因是每一个线程在新建线程对象是都会加载这个成员标量
改进:将ticket这个变量定义为静态成员变量。
- 通过运行发现上述问题出现的概率小了,但并不是完全杜绝了
通过阅读代码我们发现这100张票是三个窗口共有的,将ticket定义为静态的显然不合适,创建三个卖票的线程也不合适,所以我们用第二种方法改进。
2、实现Runnable接口实现
代码实现如下:
package cn.itcast_01;
public class SellTicketsImpl implements Runnable {
private int ticket=100;
@Override
public void run() {
while(true){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"正在出售第"+(ticket--)+"张票");
}
}
}
}
package cn.itcast_01;
public class SellTicketsImplDemo {
public static void main(String[] args) {
SellTicketsImpl sti=new SellTicketsImpl();
Thread t1=new Thread(sti,"窗口1");
Thread t2=new Thread(sti,"窗口2");
Thread t3=new Thread(sti,"窗口3");
t1.start();
t2.start();
t3.start();
}
}
通过这种方法我们实现了电影院售票程序,从表面上看不出什么问题,但是在真实场景中,售票时网络是不能实时传输的,总是存在延迟的情况,在出售一张票以后需要一点时间的延迟。所以我们改进以上代码,实现出售完一张票后延迟100毫秒,再出售下一张。
代码实现如下:
package cn.itcast_02;
public class SellTicketsImpl implements Runnable {
private int ticket=100;
@Override
public void run() {
while(true){
if(ticket>0){
//为了模拟更真实的应用场景,我们延时100毫秒
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在出售第"+(ticket--)+"张票");
}
}
}
}
package cn.itcast_02;
public class SellTicketsImplDemo {
public static void main(String[] args) {
SellTicketsImpl sti=new SellTicketsImpl();
Thread t1=new Thread(sti,"窗口1");
Thread t2=new Thread(sti,"窗口2");
Thread t3=new Thread(sti,"窗口3");
t1.start();
t2.start();
t3.start();
}
}
通过运行发现,加入延迟后出现了两个问题:
- 相同的票卖了多次
- 出现了0票和负数票
出现多个窗口卖同一张票的原因是CPU的每一次操作都是一个原子性(最简单、最基本)的操作,而ticket–这个操作可以分为以下几个步骤:a.记录以前的值。b.ticket减1操作。c:将以前的值输出。d.ticket变成99。而在这几个步骤中间如果其他线程抢到了CPU的执行权就会出现多个窗口卖同一张票的情况。
而出现出现了0票和负数票的原因是随机性和延迟性。三个窗口最坏情况是卖到-1张票,四个窗口最坏情况是卖到-2张票。
以上两个问题就是我们所说的线程安全问题。线程安全问题在理想状态下是不容易出现的,但是一旦出现对软件的影响是非常大的。
二、线程安全问题的解决
1、线程安全问题的判断标准
- 是否是多线程环境。
- 是否有共享数据。
- 是否有多条语句操作共享数据。
而上述实例中同时具备了以上三个标准,所以会出现线程安全问题。而前两个标准必然是符合的,所以我们只能限制第三个标准来解决线程安全问题。
2、线程安全问题的解决方法
通过限制第三个标准来解决线程安全问题的思路是,将操作共享数据的代码包成一个整体,让某个线程执行这个整体时其他线程不能执行。Java提供了线程同步机制来实现上述思路。
- 同步代码块
格式:synchronized(对象){需要同步的代码;}
对象:任意对象,但多个线程必须使用同一个对象,即保证是“同一把锁”。同步能解决线程安全问题的关键就在于这个对象。
同步代码:操作共享数据的多条语句。
代码实现如下:
package cn.itcast_03;
public class SellTicketsImpl implements Runnable {
private int ticket = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (ticket > 0) {
// 为了模拟更真实的应用场景,我们延时100毫秒
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (ticket--) + "张票");
}
}
}
}
}
package cn.itcast_03;
public class SellTicketsImplDemo {
public static void main(String[] args) {
SellTicketsImpl sti=new SellTicketsImpl();
Thread t1=new Thread(sti,"窗口1");
Thread t2=new Thread(sti,"窗口2");
Thread t3=new Thread(sti,"窗口3");
t1.start();
t2.start();
t3.start();
}
}
- 同步方法及锁对象代
同步方法的锁对象是this,代码实现如下:
package cn.itcast_04;
public class SellTicketsImpl implements Runnable {
private int ticket = 100;
private int i = 0;
@Override
public void run() {
while (true) {
if (i % 2 == 0) {
//同步方法的锁对象是this
synchronized (this) {
if (ticket > 0) {
// 为了模拟更真实的应用场景,我们延时100毫秒
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (ticket--) + "张票");
}
}
} else {
sellTickets();
}
i++;
}
}
private synchronized void sellTickets() {
if (ticket > 0) {
// 为了模拟更真实的应用场景,我们延时100毫秒
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (ticket--) + "张票");
}
}
}
- 静态方法及锁对象
静态方法的锁对象是当前类的class文件对象,代码实现如下:
package cn.itcast_05;
public class SellTicketsImpl implements Runnable {
private static int ticket = 100;
private int i = 0;
@Override
public void run() {
while (true) {
if (i % 2 == 0) {
//静态方法的锁对象是当前类的class文件对象
synchronized (SellTicketsImpl.class) {
if (ticket > 0) {
// 为了模拟更真实的应用场景,我们延时100毫秒
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (ticket--) + "张票");
}
}
} else {
sellTickets();
}
i++;
}
}
private static synchronized void sellTickets() {
if (ticket > 0) {
// 为了模拟更真实的应用场景,我们延时100毫秒
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (ticket--) + "张票");
}
}
}
3、同步线程的特点
- 线程同步的前提
a.多线程
b.多个线程使用的是同一个所对象 - 线程同步的好处
同步的出现解决了多线程的安全问题 - 线程同步的弊端
当线程相对较多时,因为每个线程都回去判断同步上的锁对象,这是很耗费资源的,无形中会降低程序的执行效率。