卖票案例
需求:某电影院共有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)启动线程
卖票案例的练习
xxxxxxxxxx
package 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--;
}
}
}
}
xxxxxxxxxx
package 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语句
所有问题的原因:线程执行的随机性导致的
卖票案例思考的练习
xxxxxxxxxx
package 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--;
}
}
}
}
xxxxxxxxxx
package 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] 使同步;同速运行
xxxxxxxxxx
synchronized(任意对象){
多条语句操作共享数据的代码
}
synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁 注意要让多个线程去使用同一把锁,具体代码在"a_12_1SellTicket"类,把if语句锁起来
给if语句加锁之后,只能有一个线程在if语句里面执行,当这个线程执行完毕之后,if锁就会被释放,t2或t3线程就会进入if循环,...
同步的好处和弊端: 好处:解决了多线程的数据安全问题 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
卖票_同步代码块解决数据安全的练习
xxxxxxxxxx
package 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--;
}
}
}
}
}
xxxxxxxxxx
package 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即可
}
xxxxxxxxxx
package 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();
}
}