卖票案例
需求:某电影院共有100张票,共有3个窗口买票,请设置一个程序模拟该电影院卖票
思路: 1、定义一个SellTicket类实现Runnable接口,里面定义一个成员变量:private int tickets = 100 2、在SellTicket类中重写run()方法实现卖票,代码步骤如下 (1)判断票数大于0,就卖票,并告知是哪个窗口卖的 (2)卖了票之后,总票数要减1 (2)票卖完了,也可能有人来问,所以这里用死循环让卖票的动作一直执行 3、定义一个测试类,里面有一个main方法,代码步骤如下 (1)创建SellTicket类的对象 (2)创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称 (3)启动线程
卖票案例的练习
xxxxxxxxxxpackage ch21;
public class a_10_1SellTicket implements Runnable{
private int tickets = 100;
public void run() { //死循环,让卖票的动作一直执行 while (true) { //判断票数大于0,就卖票,并告知是哪个窗口卖的 if (tickets > 0) { System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");//拿到当前线程名称 //卖了票之后,总票数要减1 tickets--; } } }}xxxxxxxxxxpackage ch21;
//不足:第100张票被卖了3次,会在后面的学习中就行优化public class a_10_2测试 {
public static void main(String[] args) { //创建SellTicket类的对象 a_10_1SellTicket st = new a_10_1SellTicket(); //创建三个Thread类的对象,把a_10_1SellTicket对象作为构造方法的参数,并给出对应的窗口名称 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_11_1SellTicket"每次出票时间间隔设置为100毫秒,用sleep()方法实现
还存在如下问题,并给出原因分析: 第一个问题:相同的票出现了多次 第二个问题:出现了负数的票 原因:t1线程抢到CPU执行权时,进入了100毫秒的休眠,此时t2线程也抢到了执行权,然后t2也会进入休眠,t3同理。 假设3个线程按照顺序醒过来,那么t1会再次抢到CPU的执行权并在控制台输出"窗口1正在出售第100张票" 在t1将要执行减减操作时,如果t2已经抢到CPU执行权,那t2就会在t1执行减减操作之前在控制台输出"窗口2正在出售第100张票" 在t1和t2将要执行减减操作时,如果t3已经抢到CPU执行权,那t3就会在t1和t2执行减减操作之前在控制台输出"窗口3正在出售第100张票"。 就出现了相同票数卖了3次的情况。 注意最后减减操作被3个线程都执行了一次,所以就会导致多执行2次,于是就多减了2次,到票卖完时原来预期是会减到1,此时就会导致减到-1
上面那两个问题解决方法的分析如下 在线程按照顺序醒过来之后,t1抢到了CPU的执行权,输出"窗口1正在出售第100张票" 且假设t1继续拥有CPU的执行权,就会执行tickets减减操作,此时tickets就从1变成了0,0不满足if循环,所以线程1在tickets减减之后不会再进入if语句 此时t2线程抢到了CPU的执行权,在控制台输出"窗口2正在出售第0张票" 且假设t2继续拥有CPU的执行权,就会执行tickets减减操作,此时tickets就从0变成了-1,-1不满足if循环,所以线程2在tickets减减之后不会再进入if语句 然后t3线程抢到了CPU的执行权,在控制台输出"窗口3正在出售第-1张票" 且假设t3继续拥有CPU的执行权,就会执行tickets减减操作,此时tickets就从-1变成了-2,-2不满足if循环,所以线程3在tickets减减之后不会再进入if语句
所有问题的原因:线程执行的随机性导致的
卖票案例思考的练习
xxxxxxxxxxpackage ch21;
public class a_11_1SellTicket implements Runnable{
private int tickets = 100;
public void run() { while (true) { //tickets=1 if (tickets > 0) { //设置出票时间间隔 try { Thread.sleep(100);//sleep报红线,选中sleep,Alt+Enter,选try...catch捕获异常 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");//拿到当前线程名称 tickets--; } } }}xxxxxxxxxxpackage ch21;
public class a_11_2测试 {
public static void main(String[] args) {
a_11_1SellTicket st = new a_11_1SellTicket();
Thread t1 = new Thread(st,"窗口1"); Thread t2 = new Thread(st,"窗口2"); Thread t3 = new Thread(st,"窗口3");
t1.start(); t2.start(); t3.start();
}}
卖票_同步代码块解决数据安全
为什么出现问题?(这也是我们判断多线程程序是否会有数据安全问题的标准) 1、是否是多线程环境 2、是否有共享数据 3、是否有'多条语句操作共享数据' 只有同时满足上面3个条件才会出现数据安全问题
如何解决数据安全问题,如下 基本思想:让程序没有安全问题的环境,即破坏掉上面3个条件中的其中一个即可。前面2个条件我们破坏不了,只能破坏第3个条件
怎么破坏第3个条件,如下 1、把'多条语句操作共享数据'的代码给锁起来,让任意时刻只能有一个线程执行即可。即把if语句锁起来 2、Java提供了同步代码块的方式来解决 注意:多线程只是在操作临界资源时是单线程
什么是同步代码块,格式如下。synchronized[ˈsɪŋkrənaɪzd] 使同步;同速运行
xxxxxxxxxxsynchronized(任意对象){ 多条语句操作共享数据的代码}synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁 注意要让多个线程去使用同一把锁,具体代码在"a_12_1SellTicket"类,把if语句锁起来
给if语句加锁之后,只能有一个线程在if语句里面执行,当这个线程执行完毕之后,if锁就会被释放,t2或t3线程就会进入if循环,...
同步的好处和弊端: 好处:解决了多线程的数据安全问题 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
卖票_同步代码块解决数据安全的练习
xxxxxxxxxxpackage ch21;
public class a_12_1SellTicket implements Runnable{
private int tickets = 100;//共享数据 private Object obj = new Object();//定义new Object()对象
public void run() { while (true) {//多条语句对共享数据进行操作 try { Thread.sleep(10); //可以把sleep()写在下面的if语句里面,即把try..catch写在if里面。但是不如写在目前这个位置 } catch (InterruptedException e) { e.printStackTrace(); } synchronized(obj){ //synchronized (new Object()) { //把if语句锁起来。 //注意上面那行的synchronized()里面的任意对象不能直接写new Object()。不然我们的3个线程有了3把锁,问题是没有被解决的 //如何使3个线程用的是同一把锁呢,解决:把new Object()对象定义在run方法的外面。定义好之后,如下 if (tickets > 0) { System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");//拿到当前线程名称 tickets--; } } } }}xxxxxxxxxxpackage ch21;
public class a_12_2测试 {
public static void main(String[] args) {
a_12_1SellTicket st = new a_12_1SellTicket();
//多线程程序 Thread t1 = new Thread(st,"窗口1"); Thread t2 = new Thread(st,"窗口2"); Thread t3 = new Thread(st,"窗口3");
t1.start(); t2.start(); t3.start();
}}
卖票_同步方法解决数据安全
同步方法:就是把synchronized关键字加到方法上,格式如下 修饰符 synchronized 返回值类型 方法名(方法参数){}
同步对象的锁对象是:this
同步静态方法:就是把synchronized关键字加到静态方法上。格式如下
修饰符 static synchronized 返回值类型 方法名(方法参数){}同步静态方法的锁对象是:对应类名.class
a_13_1SellTicket.class
卖票_同步方法解决数据安全的练习
package ch21;
public class a_13_1SellTicket implements Runnable{
//private int tickets = 100;//非静态 private static int tickets = 100;//静态 private Object obj = new Object(); //添加一步操作 private int x = 0;
//优化:双通道,即if里面有两通道,一条是if里面的,另一条是else里面的。之前上一节课是大家排队走一条路,现在有两条路,提高了效率 public void run() { while (true) {
//添加一步操作,把下面的代码用if包住 if(x%2==0) {//对2取余。注意:为了方便验证下面的几个优化写法,这行的2可以改成其他离谱的数,目的是不让 //这里面的代码执行,而是去下面的else里面执行,因为else才是调用了我们写的几个优化方法,这里的只是最原始的写法 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } //synchronized (obj) { //synchronized (this) { synchronized (a_13_1SellTicket.class) { if (tickets > 0) { System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票"); tickets--; } } } else{ //else里面的代码可以用我们写好的sellTicket()方法,我先注释原本else里面的代码,把else里面的代码换成sellTicket()方法 /*try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (obj) { if (tickets > 0) { System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票"); tickets--; } }*/
//sellTicket(); //注释掉,方便检验sellTicket2()方法 //sellTicket2(); //注释掉,方便检验sellTicket3()方法 sellTicket3();//静态方法 } x++; } }
//---------------------------------------------------------------------------------------------------------------- //继续再优化一下,把try...catch,以及synchronized(obj){}里面的内容封装成一个方法sellTicket(),方便使用。如下 private void sellTicket(){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (obj) { if (tickets > 0) { System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票"); tickets--; } } }
//---------------------------------------------------------------------------------------------------------------- //继续再优化,如何把synchronized同步锁加在方法上,不加在方法里面的内容上。也就是我们这节课学的同步方法,如下 private synchronized void sellTicket2(){ if (tickets > 0) { try {//用这种优化方法的话,建议把try...catch写在if里面,因为此时里面跟外面效果是一样的,写在里面会显得代码稍微简洁一些 Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票"); tickets--; } } //把synchronized同步锁加在方法上会出现一个问题:"正在出售第n张票"会输出两遍。原因:需要搞清楚同步方法的锁对象是什么。非静态同步方法的锁对象是this //如何验证同步方法的锁对象是this,在24行把synchronized(obj){}的obj改为this,同样可以执行,且不出现"正在出售第n张票"输出2遍的问题 //如何解决输出两遍的问题,把上面的优化方法sellTicket2改为静态方法,注意修改后我会写成sellTicket3如下。且在最上面把tickets改成静态修饰的 private static synchronized void sellTicket3(){//静态。给这个静态的方法加一个synchronized同步锁。即就是我们这节课学的同步静态方法 if (tickets > 0) { try {//用这种优化方法的话,建议把try...catch写在if里面,因为此时里面跟外面效果是一样的,写在里面会显得代码稍微简洁一些 Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票"); tickets--; } } //注意:上面我们知道的是非静态同步方法的锁对象是this。那这里的静态同步方法的锁对象是什么呢,答案是a_13_1SellTicket.class。锁对象就是锁 //我们目前只需要了解SellTicket.class即可,后面的学习会逐步深入 //所以,把24行的synchronized(obj){}的obj改为a_13_1SellTicket.class即可}xxxxxxxxxxpackage ch21;
public class a_13_2测试 {
public static void main(String[] args) {
a_13_1SellTicket st = new a_13_1SellTicket();
//多线程程序 Thread t1 = new Thread(st,"窗口1"); Thread t2 = new Thread(st,"窗口2"); Thread t3 = new Thread(st,"窗口3");
t1.start(); t2.start(); t3.start();
}}