JavaSE之多线程(四)

今天要讲的还是多线程,但在此之前先做个练习,用多线程模拟电影院售票,问题:某电影院目前正在上映贺岁大片(红高粱,少林寺传奇藏经阁),共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。

我们知道实现线程有两种方式,我们先来看看第一种方式:继承Thread类实现。具体代码如下:

public class SellTicket extends Thread {
    
    //定义100张票
    //private int tickets=100;
    // 为了让多个线程对象共享这100张票,我们其实应该用静态修饰
    private static int tickets=100;
        
    public SellTicket() {
        super();
    }
    
    public SellTicket(String name){
        super(name);
    }
    @Override
    public void run() {
        
        //每个线程进来都会走这里,这样的话,每个线程对象相当于买的是自己的那100张票,这不合理,所以应该定义到外面
        //int tickets=100;//定义100张票
        
        //模拟影院一直有票在出售
        while(true){
        if (tickets>0) {
            System.out.println(getName()+"正在出售第"+(tickets--)+"张票");
        }
        }
    }
}


/*
 * 方式1: 继承Thread类来实现。
 */
public class SellTicketDemo {
    public static void main(String[] args) {
        //创建三个线程对象
        SellTicket st1=new SellTicket("窗口1");
        SellTicket st2=new SellTicket("窗口2");
        SellTicket st3=new SellTicket("窗口3");
        
        //启动线程
        st1.start();
        st2.start();
        st3.start();

    }

}

 方式一运行结果:

通过对代码的分析,发现合情合理,将票数的tickets声明为静态的让三个线程对象共享,可运行结果上还是出现了,最后一张票居然卖了两次的问题。所以这是个问题,稍后再说这个问题,现在再用第二种方式实现一下:实现Runnable接口。代码如下:

public class SellTicket implements Runnable {
    //定义100张票
    private int tickets=100;

    @Override
    public void run() {
        // 模拟影院一直有票在出售
        while (true) {
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + "正在出售第"
                        + (tickets--) + "张票");
            }
        }

    }

}

/**
 * 
 * 方式二:实现Runnable接口创建线程
 *
 */
public class SellTicketDemo {
    public static void main(String[] args) {
        //创建资源对象
        SellTicket st =new SellTicket();
        //创建三个线程对象
        Thread t1=new Thread(st,"窗口1");
        Thread t2=new Thread(st,"窗口2");
        Thread t3=new Thread(st,"窗口3");
        
        //启动线程
        t1.start();
        t2.start();
        t3.start();
        

    }

}

方式二运行结果:

 之前说过两种方式的区别,这里能够清晰的发现第二种方式是比较好的,强烈推荐使用。方式二仅仅创建了一个资源对象,那就是100张票,不会出现同一张票被出售多次的奇葩情况。

上面的代码,实际上并不能完全的模拟售票,因为售票时的网络会出现适当的延迟,下面改进下方式二的代码模拟出现网络延迟的情形。改进后的代码如下:

public class SellTicket implements Runnable {
    //定义100张票
    private int tickets=100;

    @Override
    public void run() {
        // 模拟影院一直有票在出售
        while (true) {
            if (tickets > 0) {
                //为了模拟更真实的场景,需要加入延迟
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在出售第"
                        + (tickets--) + "张票");
            }
        }

    }

}


/**
 * 
 * 改进方式二:实现Runnable接口创建线程
 *
 */
public class SellTicketDemo {
    public static void main(String[] args) {
        //创建资源对象
        SellTicket st =new SellTicket();
        //创建三个线程对象
        Thread t1=new Thread(st,"窗口1");
        Thread t2=new Thread(st,"窗口2");
        Thread t3=new Thread(st,"窗口3");
        
        //启动线程
        t1.start();
        t2.start();
        t3.start();
        

    }

}

改进后的运行结果:

通过对改进后的运行结果的分析,发现两个问题:A:相同的票卖了多次;B:出现了负数票

先说说为什么会出现相同的票,这时因为CPU的一次操作必须是原子性的(原子性就是指最简单最基本的操作,不可再细分的操作)。分析如下:

在理想状态下,当线程t1跑起来到sleep时休息,此时线程t2抢到CPU的执行权,也跑到sleep时开始休息,此时t1又开始跑,打印出“正在出售第100张票”之后,做自减操作完毕后,t2也休息完了开始跑,就会打印“正在出售第99张票”,但是实际情况往往不同,就是会出现当t1打印完“正在出售第100张票”还没来得及做自减操作时,t2又跑起来了,所以就会打印相同的票,即是相同的票卖了多次。

再说说为什么会出现负数票,这是因为随机性和延迟导致的。分析如下:

由于随机性,可能会出现三个线程在票数为最后一张的时候相继进入到while循环里的sleep处,并且均休息了,然后,t1打印后自减,t2打印后自减,t3打印后自减,也就是说:

窗口1正在出售第1张票,tickets=0
窗口2正在出售第0张票,tickets=-1
窗口3正在出售第-1张票,tickets=-2

综上所述,就会出现出现了负数票。

一、线程安全

1.线程安全问题出现的原因(也是判断一个程序是否存在线程安全问题的标准):

A:是否是多线程环境
 B:是否有共享数据
 C:是否有多条语句操作共享数据

而其实,我前面的那个练习,就是满足了这三个条件,导致出现了线程安全问题,那么怎么解决线程安全问题呢?

2.解决线程安全问题的方法:

解决思想:把多条语句操作共享数据的代码给包成一个整体,让某个线程在执行的时候,别人不能来执行(Java的同步机制)

解决方案一:同步代码块

格式:

synchronized(对象){
//需要同步的代码;(多条语句操作共享数据的代码部分)
}

注意:

1.同步代码块能够解决线程安全问题的根本原因在于那个对象,该对象就如同一把,将其他的非正在运行的线程锁在门外。

2.多个线程必须是同一把锁

下面是通过同步代码块的方式解决线程安全问题的代码:

public class SellTicket implements Runnable {
    //定义100张票
    private int tickets=100;
    //创建锁对象
    private Object obj=new Object();
    

    @Override
    public void run() {
        // 模拟影院一直有票在出售
        while (true) {
            synchronized (obj) {
                if (tickets > 0) {
                    //为了模拟更真实的场景,需要加入延迟
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第"
                            + (tickets--) + "张票");
                }
            }
            
        }

    }

}


/**
 * 
 * 同步代码块解决线程安全问题
 *
 */
public class SellTicketDemo {
    public static void main(String[] args) {
        //创建资源对象
        SellTicket st =new SellTicket();
        //创建三个线程对象
        Thread t1=new Thread(st,"窗口1");
        Thread t2=new Thread(st,"窗口2");
        Thread t3=new Thread(st,"窗口3");
        
        //启动线程
        t1.start();
        t2.start();
        t3.start();
        

    }

}

对同步代码块的解释:如下图,这个图是我自己画的,不是特别好,当能稍微解释。

总结下有关同步的几点:

同步的特点:
前提:多个线程
解决问题的时候要注意:多个线程使用的是同一个锁对象
同步的好处 :同步的出现解决了多线程的安全问题。
同步的弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
文章到此结束。

 

猜你喜欢

转载自www.cnblogs.com/wholovewangjie/p/9078638.html