多线程模拟售票及线程同步与死锁


1.线程同步
写一个模拟现实售票的例子。
首先,定义一个售票类Tickets。
//售票类
class Tickets extends Thread {
	
	//定义总票数为100,因为多个窗口售票,所以定义为静态的,即类成员共享
	private static int tickets = 100;
	
	public Tickets(String name) {
		super(name);
	}
	
	//重写run方法,封装运行代码
	public void run() {
		//循环售票
		while(true) {
			//如果票数大于0,则可以继续出售。每售出一张,票数减一
			if(tickets > 0) {
				System.out.println(Thread.currentThread().getName() + "售出第" + (tickets--) + "号票");
			}
		}
	}	
}
接下来,开启四个线程模拟四个售票窗口
public class TicketDemo {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		//创建四个售票窗口
		Tickets t1 = new Tickets("1号窗口");
		Tickets t2 = new Tickets("2号窗口");
		Tickets t3 = new Tickets("3号窗口");
		Tickets t4 = new Tickets("4号窗口");
		//开始售票
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}
运行程序,可以看到四个窗口开始售票,直到票售完为止。



但是,这个程序有没有安全隐患呢?
我们把售票类的循环体内代码稍微改动一下。
//售票类
class Tickets extends Thread {
	
	//定义总票数为100,因为多个窗口售票,所以定义为静态的,即类成员共享
	private static int tickets = 100;
	
	public Tickets(String name) {
		super(name);
	}
	
	//重写run方法,封装运行代码
	public void run() {
		//循环售票
		while(true) {
			//如果票数大于0,则可以继续出售。每售出一张,票数减一
			if(tickets > 0) {
				//让线程执行到此,休眠10毫秒
				//多线程操作时,会出现安全问题,可能出现0,-1,-2号票
				try{
					Thread.sleep(10);
				}catch(InterruptedException e){
					e.printStackTrace();
				}
				
				System.out.println(Thread.currentThread().getName() + "售出第" + (tickets--) + "号票");
			}
		}
	}	
}
再次运行程序,会出现如下运行结果:

我们看到,售票售出了0、-1、-2号票,当然,每次运行的结果可能不同,但是可能会出现与现实情况不符的现象。

这是为什么呢?

这就是多线程同步的安全问题。出现问题的原因是:多个线程(4个窗口)操作同一共享数据(票)时,一条线程只执行了一部分,还没执行完,而另一条线程参与进来执行,导致共享数据错误。就如上例所示,假使票卖到只剩1张时,一个窗口将要出售最后一张票。当其判断票数大于0成立后,还没执行下一句,另一条线程又抢夺到cpu的执行权,同样判断票数大于0成立。于是,下面无论这两条线程哪一条抢夺到cpu的执行权,这两条线程都会先后执行,就会出现票为-1的现象。

如何解决多线程安全问题呢?

对多条线程操作共享数据时,只能让一个线程全都执行完。在执行过程中,其他线程不可以参与执行。
Java对于多线程安全问题,提供了专业的解决方式:使用同步代码块。

synchronized(对象)
{
          要被同步的代码;
}

对象如同锁,持有锁的线程可以在同步中执行。没有锁的线程,即便获取了cpu的执行权,也无法执行。

我们把售票类的代码加上同步代码块
//售票类
class Tickets extends Thread {
	
	//定义总票数为100,因为多个窗口售票,所以定义为静态的,即类成员共享
	private static int tickets = 100;
	
	public Tickets(String name) {
		super(name);
	}
	
	//重写run方法,封装运行代码
	public void run() {
		//循环售票
		while(true) {
			//同步代码块,使用本类字节码对象作为锁
			synchronized(Tickets.class) {
				//如果票数大于0,则可以继续出售。每售出一张,票数减一
				if(tickets > 0) {
					//让线程执行到此,休眠10毫秒
					//多线程操作时,会出现安全问题,可能出现0,-1,-2号票
					try{
						Thread.sleep(10);
					}catch(InterruptedException e){
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + "售出第" + (tickets--) + "号票");
				}
			}
		}
	}	
}
此时再次运行程序,将会看到不会在出现0、-1、-2号票。



最终,我们去掉使线程休眠的代码,完成售票类的最终代码。
//售票类
class Tickets extends Thread {
	
	//定义总票数为100,因为多个窗口售票,所以定义为静态的,即类成员共享
	private static int tickets = 100;
	
	public Tickets(String name) {
		super(name);
	}
	
	//重写run方法,封装运行代码
	public void run() {
		//循环售票
		while(true) {
			//同步代码块,使用本类字节码对象作为锁
			synchronized(Tickets.class) {
				//如果票数大于0,则可以继续出售。每售出一张,票数减一
				if(tickets > 0) {
					System.out.println(Thread.currentThread().getName() + "售出第" + (tickets--) + "号票");
				}
			}
		}
	}	
}

注:能不能将while循环放在同步代码块中呢?
不能!否则会出现死循环!因为拿到锁的线程会一直将同步代码块都执行完才会释放锁,而while(true)会一直循环下去。

什么时候使用同步呢?
同步的前提:
1)必须要有两个或两个以上的线程
2)必须是多个线程使用同一个锁

如何判断哪些代码要加同步呢?
1)明确哪些代码是多线程运行代码
2)明确共享数据
3)明确多线程运行代码中哪些语句操作共享数据


2.死锁
使用线程同步,有时会出现死锁的情况。看下面程序:
package com.itheima;

/**
 * 死锁程序
 * @author YP
 * */

public class DeadLockDemo {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		//开启两个线程
		new Thread(new DeadLock(true)).start();
		new Thread(new DeadLock(false)).start();
	}

}

//提供锁的类
class MyLock
{
	//为了方便运用锁,将锁定义为静态的
	static Object locka = new Object();
	static Object lockb = new Object();
}

//实现implements接口创建线程类
class DeadLock implements Runnable {
	
	//判断执行何种语句的标志
	private boolean flag;
	
	DeadLock(boolean flag){
		this.flag = flag;
	}
	
	public void run() {
		if(flag){
			//先要获得a锁才可执行下面语句
			synchronized(MyLock.locka){
				System.out.println("if locka");
				//要继续执行下面语句,必须再获取b锁
				synchronized(MyLock.lockb) {
					System.out.println("if lockb");
				}
			}
		}else{
			//先要获得b锁才可执行下面语句
			synchronized(MyLock.lockb){
				System.out.println("else lockb");
				//要继续执行下面语句,必须再获取a锁
				synchronized(MyLock.locka) {
					System.out.println("else locka");
				}
			}
		}
	}
}
运行程序:

我们看到,出现了死锁。

死锁是如何出现的呢?
当一个线程拥有A锁,还要去拿B锁才可执行;而另一个线程拥有B锁,还要去拿A锁才可执行。两个线程互相僵持,致使死锁情况出现。
所以,我们写代码时要避免死锁情况的出现。给出如下建议:

1、在程序中尽量使用开放调用。依赖于开放调用的程序,相比于那些在持有锁的时候还调用外部方法的程序,更容易进行死锁自由度的分析。重新构建synchronized使开放调用更加安全。所谓开放调用是指调用的方法本身没有加锁,但是要以对方法操作的内容进行加锁。

2、如果你必须获得多个锁,那么锁的顺序必须是你设计工作的一部分:尽量减少潜在锁之间的交互数量,遵守并文档化该锁顺序协议。监测代码中死锁自由度的策略有:

1)识别什么地方会获取多个锁,并使锁数量尽可能少,保证它们的顺序在程序中一致。

2)在没有非开放调用的程序中,发现那些获得多重锁的实例是非常简单的。

3、尝试定时的锁,使用每个显式Lock类中定时tryLock特性,来替代使用内部锁机制。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值