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

来源:互联网 发布:中国人工智能计算大会 编辑:程序博客网 时间:2024/06/06 19:02

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特性,来替代使用内部锁机制。


原创粉丝点击