多线程_02

1.卖票案例的思考

**需求:**某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票

思路:
1.定义一个类SellTicket实现Runnable接口,里面定义一个成员变量:private int tickets = 100;

2.在SellTicket类中重写run()方法实现卖票,代码步骤如下:
A:判断票数大于0,就卖票,并告知是哪个窗口卖的
B:卖了票之后,总票数要减1
C:票没有了,也可能有人来问,所以这里用死循环让卖票的动作一直执行

3.定义一个测试类SellTicketDemo,里面有main方法,代码步骤如下:
A:创建SellTicket类的对象
B:创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
C:启动线程

SellTicket.java

public class SellTicket implements Runnable {
    
    
    //定义一个类SellTicket实现Runnable接口,里面定义一个成员变量:private int tickets = 100;
    private int tickets = 100;

    @Override
    public void run() {
    
    
        //A:判断票数大于0,就卖票,并告知是哪个窗口卖的
        //B:卖了票之后,总票数要减1
        //C:票没有了,也可能有人来问,所以这里用死循环让卖票的动作一直执行
        while (true){
    
    
            if (tickets > 0) {
    
    
                System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                tickets--;
            }
        }
    }
}

SellTicketDemo.java

public class SellTicketDemo {
    
    
    public static void main(String[] args) {
    
    
        //创建SellTicket类的对象
        SellTicket st = new SellTicket();
        //创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
        Thread t1 = new Thread(st, "窗口1");//st作为构造方法的参数传入,成为一个线程
        Thread t2 = new Thread(st, "窗口2");
        Thread t3 = new Thread(st, "窗口3");

        //启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

实现结果:

窗口1正在出售第100张票
窗口1正在出售第99张票
窗口1正在出售第98张票
窗口1正在出售第97张票
窗口1正在出售第96张票
窗口1正在出售第95张票
窗口1正在出售第94张票
窗口1正在出售第93张票
窗口1正在出售第92张票
窗口1正在出售第91张票
窗口1正在出售第90张票
窗口1正在出售第89张票
窗口1正在出售第88张票
窗口1正在出售第87张票
窗口1正在出售第86张票
窗口1正在出售第85张票
窗口1正在出售第84张票
窗口1正在出售第83张票
窗口1正在出售第82张票
窗口1正在出售第81张票
窗口1正在出售第80张票
窗口3正在出售第100张票
窗口1正在出售第79张票
窗口3正在出售第78张票
窗口1正在出售第77张票
窗口3正在出售第76张票
窗口1正在出售第75张票
窗口3正在出售第74张票
窗口1正在出售第73张票
窗口3正在出售第72张票
窗口2正在出售第72张票
窗口3正在出售第70张票
窗口1正在出售第71张票
窗口1正在出售第67张票
窗口1正在出售第66张票
窗口1正在出售第65张票
窗口1正在出售第64张票
窗口1正在出售第63张票
窗口1正在出售第62张票
窗口1正在出售第61张票
窗口1正在出售第60张票
窗口3正在出售第68张票
窗口2正在出售第69张票
窗口3正在出售第58张票
窗口3正在出售第56张票
窗口3正在出售第55张票
窗口3正在出售第54张票
窗口3正在出售第53张票
窗口3正在出售第52张票
窗口3正在出售第51张票
窗口3正在出售第50张票
窗口3正在出售第49张票
窗口3正在出售第48张票
窗口1正在出售第59张票
窗口3正在出售第47张票
窗口3正在出售第45张票
窗口3正在出售第44张票
窗口2正在出售第57张票
窗口2正在出售第42张票
窗口2正在出售第41张票
窗口2正在出售第40张票
窗口2正在出售第39张票
窗口2正在出售第38张票
窗口2正在出售第37张票
窗口3正在出售第43张票
窗口3正在出售第35张票
窗口1正在出售第46张票
窗口1正在出售第33张票
窗口1正在出售第32张票
窗口1正在出售第31张票
窗口1正在出售第30张票
窗口1正在出售第29张票
窗口1正在出售第28张票
窗口1正在出售第27张票
窗口1正在出售第26张票
窗口1正在出售第25张票
窗口1正在出售第24张票
窗口1正在出售第23张票
窗口1正在出售第22张票
窗口1正在出售第21张票
窗口1正在出售第20张票
窗口1正在出售第19张票
窗口1正在出售第18张票
窗口3正在出售第34张票
窗口2正在出售第36张票
窗口2正在出售第15张票
窗口2正在出售第14张票
窗口2正在出售第13张票
窗口2正在出售第12张票
窗口2正在出售第11张票
窗口2正在出售第10张票
窗口2正在出售第9张票
窗口2正在出售第8张票
窗口2正在出售第7张票
窗口2正在出售第6张票
窗口2正在出售第5张票
窗口2正在出售第4张票
窗口2正在出售第3张票
窗口2正在出售第2张票
窗口2正在出售第1张票
窗口3正在出售第16张票
窗口1正在出售第17张票

卖票案例的反思

实际生活中,售票时出票也是需要时间的,在出售一张票的时候,需要一点时间的延迟:
每次出票时间100毫秒,用sleep()方法实现改进:
SellTicketDemo.java
不变
SellTicket.java

public class SellTicket implements Runnable {
    
    
    //定义一个类SellTicket实现Runnable接口,里面定义一个成员变量:private int tickets = 100;
    private int tickets = 100;

    @Override
    public void run() {
    
    
      //相同的票出现了多次
        while (true){
    
    
            //tickets=100
            //t1,t2,t3
            //假设t1线程抢到CPU的执行权
            if (tickets > 0) {
    
    
                //通过sleep()方法来模拟出票时间
                try {
    
    
                    Thread.sleep(100);
                    //t1线程休息了100毫秒
                    //t2线程抢到了CPU的执行权,t2线程就开始执行,执行到这里的时候,t2线程休息100毫秒
                    //t3线程抢到了CPU的执行权,t3线程就开始执行,执行到这里的时候,t3线程休息100毫秒
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                //假设线程按照顺序醒过来
                //t1抢到CPU的执行权,在控制台输出:窗口1正在出售第100张票
                System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                //t2抢到CPU的执行权,在控制台输出:窗口2正在出售第100张票
                //t3抢到CPU的执行权,在控制台输出:窗口3正在出售第100张票
                tickets--;
                //如果这三个线程还是按照顺序来,这里就执行了3次--的操作,最终变成了97
                //按照此逻辑,有可能会出现负数
            }
        }
    }
}

结果输出:

窗口1正在出售第100张票
窗口2正在出售第99张票
窗口3正在出售第98张票
窗口1正在出售第97张票
窗口3正在出售第96张票
窗口2正在出售第96张票
窗口1正在出售第94张票
窗口2正在出售第93张票
窗口3正在出售第93张票
窗口1正在出售第91张票
窗口3正在出售第90张票
窗口2正在出售第90张票
窗口1正在出售第88张票
窗口2正在出售第87张票
窗口3正在出售第87张票
窗口1正在出售第85张票
窗口2正在出售第84张票
窗口3正在出售第84张票
窗口1正在出售第82张票
窗口2正在出售第81张票
窗口3正在出售第80张票
窗口1正在出售第79张票
窗口2正在出售第78张票
窗口3正在出售第77张票
窗口1正在出售第76张票
窗口2正在出售第75张票
窗口3正在出售第74张票
窗口1正在出售第73张票
窗口2正在出售第72张票
窗口3正在出售第71张票
窗口1正在出售第70张票
窗口2正在出售第69张票
窗口3正在出售第68张票
窗口1正在出售第67张票
窗口2正在出售第66张票
窗口3正在出售第65张票
窗口1正在出售第64张票
窗口2正在出售第63张票
窗口3正在出售第62张票
窗口1正在出售第61张票
窗口2正在出售第60张票
窗口3正在出售第59张票
窗口1正在出售第58张票
窗口2正在出售第57张票
窗口3正在出售第56张票
窗口1正在出售第55张票
窗口2正在出售第54张票
窗口3正在出售第53张票
窗口1正在出售第52张票
窗口2正在出售第51张票
窗口3正在出售第50张票
窗口1正在出售第49张票
窗口2正在出售第48张票
窗口3正在出售第47张票
窗口1正在出售第46张票
窗口2正在出售第45张票
窗口3正在出售第44张票
窗口1正在出售第43张票
窗口2正在出售第42张票
窗口3正在出售第41张票
窗口1正在出售第40张票
窗口2正在出售第39张票
窗口3正在出售第38张票
窗口1正在出售第37张票
窗口2正在出售第36张票
窗口3正在出售第35张票
窗口1正在出售第34张票
窗口2正在出售第33张票
窗口3正在出售第32张票
窗口1正在出售第31张票
窗口2正在出售第30张票
窗口3正在出售第29张票
窗口1正在出售第28张票
窗口2正在出售第27张票
窗口3正在出售第26张票
窗口1正在出售第25张票
窗口2正在出售第24张票
窗口3正在出售第23张票
窗口1正在出售第22张票
窗口2正在出售第21张票
窗口3正在出售第20张票
窗口1正在出售第19张票
窗口2正在出售第18张票
窗口3正在出售第17张票
窗口1正在出售第16张票
窗口2正在出售第15张票
窗口3正在出售第14张票
窗口1正在出售第13张票
窗口2正在出售第12张票
窗口3正在出售第11张票
窗口1正在出售第10张票
窗口2正在出售第9张票
窗口3正在出售第8张票
窗口1正在出售第7张票
窗口2正在出售第6张票
窗口3正在出售第5张票
窗口1正在出售第4张票
窗口2正在出售第3张票
窗口3正在出售第2张票
窗口1正在出售第1张票
窗口2正在出售第0张票
窗口3正在出售第-1张票

卖票出现了问题
相同的票出现了多次
出现了负数的票

问题原因:
线程执行的随机性导致的

2.卖票案例数据安全问题的解决

判断多线程程序出现数据安全问题的标准:

  1. 是否是多线程环境
  2. 是否有共享数据
  3. 是否有多条语句操作共享数据

如何解决多线程安全问题呢?
基本思想:让程序没有安全问题的环境

怎么实现呢?
多条语句操作共享数据的代码起来,让任意时刻只能有一个线程执行即可
Java提供了同步代码块的方式来解决

3.同步代码块

锁多条语句操作共享数据,可以使用同步代码块实现
格式:
synchronized(任意对象) {
多条语句操作共享数据的代码
}

synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁。
SellTicketDemo.java
不变
SellTicket.java

public class SellTicket implements Runnable {
    
    
    //定义一个类SellTicket实现Runnable接口,里面定义一个成员变量:private int tickets = 100;
    private int tickets = 100;
    private Object obj = new Object();

    @Override
    public void run() {
    
    
        while (true) {
    
    
            //tickets=100
            //t1,t2,t3
            //假设t1先线程抢到CPU的执行权
            //假设t2后线程抢到CPU的执行权
            synchronized (obj) {
    
    //只上一把锁
                if (tickets > 0) {
    
    
                    //通过sleep()方法来模拟出票时间
                    try {
    
    
                        Thread.sleep(100);
                        //t1线程休息了100毫秒
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    //t1抢到CPU的执行权,在控制台输出:窗口1正在出售第100张票
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                    tickets--;
                }
            }
            //t1出来了,这段代码的锁就被释放了
        }
    }
}

结果输出:

窗口1正在出售第100张票
窗口1正在出售第99张票
窗口1正在出售第98张票
窗口1正在出售第97张票
窗口1正在出售第96张票
窗口1正在出售第95张票
窗口2正在出售第94张票
窗口2正在出售第93张票
窗口2正在出售第92张票
窗口2正在出售第91张票
窗口2正在出售第90张票
窗口2正在出售第89张票
窗口2正在出售第88张票
窗口2正在出售第87张票
窗口2正在出售第86张票
窗口3正在出售第85张票
窗口3正在出售第84张票
窗口3正在出售第83张票
窗口3正在出售第82张票
窗口3正在出售第81张票
窗口3正在出售第80张票
窗口3正在出售第79张票
窗口3正在出售第78张票
窗口3正在出售第77张票
窗口3正在出售第76张票
窗口3正在出售第75张票
窗口3正在出售第74张票
窗口3正在出售第73张票
窗口3正在出售第72张票
窗口3正在出售第71张票
窗口3正在出售第70张票
窗口3正在出售第69张票
窗口3正在出售第68张票
窗口3正在出售第67张票
窗口3正在出售第66张票
窗口3正在出售第65张票
窗口3正在出售第64张票
窗口3正在出售第63张票
窗口3正在出售第62张票
窗口3正在出售第61张票
窗口3正在出售第60张票
窗口3正在出售第59张票
窗口3正在出售第58张票
窗口3正在出售第57张票
窗口3正在出售第56张票
窗口3正在出售第55张票
窗口3正在出售第54张票
窗口3正在出售第53张票
窗口3正在出售第52张票
窗口3正在出售第51张票
窗口3正在出售第50张票
窗口3正在出售第49张票
窗口3正在出售第48张票
窗口3正在出售第47张票
窗口3正在出售第46张票
窗口3正在出售第45张票
窗口3正在出售第44张票
窗口3正在出售第43张票
窗口3正在出售第42张票
窗口3正在出售第41张票
窗口3正在出售第40张票
窗口3正在出售第39张票
窗口3正在出售第38张票
窗口3正在出售第37张票
窗口3正在出售第36张票
窗口3正在出售第35张票
窗口3正在出售第34张票
窗口3正在出售第33张票
窗口3正在出售第32张票
窗口3正在出售第31张票
窗口3正在出售第30张票
窗口3正在出售第29张票
窗口3正在出售第28张票
窗口3正在出售第27张票
窗口3正在出售第26张票
窗口3正在出售第25张票
窗口3正在出售第24张票
窗口3正在出售第23张票
窗口3正在出售第22张票
窗口3正在出售第21张票
窗口3正在出售第20张票
窗口3正在出售第19张票
窗口3正在出售第18张票
窗口3正在出售第17张票
窗口3正在出售第16张票
窗口2正在出售第15张票
窗口2正在出售第14张票
窗口2正在出售第13张票
窗口2正在出售第12张票
窗口2正在出售第11张票
窗口2正在出售第10张票
窗口2正在出售第9张票
窗口2正在出售第8张票
窗口2正在出售第7张票
窗口2正在出售第6张票
窗口2正在出售第5张票
窗口2正在出售第4张票
窗口2正在出售第3张票
窗口2正在出售第2张票
窗口2正在出售第1张票

同步的好处和弊端
好处:解决了多线程的数据安全问题
弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

4.同步方法bian

同步方法:就是把synchronized关键字加到方法上
格式:

修饰符 synchronized 返回值类型 方法名(方法参数) {

   }

同步方法的锁对象:this
同步普通方法,相当于对对象进行同步,方法中有一个可以代表本来对象的关键字this。所以用this就可以了。

同步静态方法:就是把synchronized关键字加到静态方法上
格式:

修饰符 static synchronized 返回值类型 方法名(方法参数) {    }

同步静态方法的锁对象是:类名.class

同步静态方法,静态属于类,所以,相当于对类进行同步,而类只有一个字节码文件,所以,这里使用的是类的字节码文件对象作为锁的。格式是:类名.class

SellTicketDemo.java
不变
SellTicket.java

public class SellTicket implements Runnable {
    
    
    //定义一个类SellTicket实现Runnable接口,里面定义一个成员变量:private int tickets = 100;
    //private int tickets = 100;//适用于变体1,2,3
    private static int tickets = 100;//变体4修改
    private Object obj = new Object();
    int x = 0;

    @Override
    public void run() {
    
    
        while (true) {
    
    
            if (x % 2 == 0) {
    
    
                // synchronized (obj) {//只上一把锁       适用于变体1,2,3
                synchronized (SellTicket.class) {
    
    //变体4修改
                    if (tickets > 0) {
    
    
                        //通过sleep()方法来模拟出票时间
                        try {
    
    
                            Thread.sleep(100);
                            //t1线程休息了100毫秒
                        } catch (InterruptedException e) {
    
    
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                        tickets--;
                    }
                }
            } else {
    
    
                //变体1,没问题,能正常卖票
//                synchronized (obj) {//只上一把锁
//                    if (tickets > 0) {
    
    
//                        //通过sleep()方法来模拟出票时间
//                        try {
    
    
//                            Thread.sleep(100);
//                            //t1线程休息了100毫秒
//                        } catch (InterruptedException e) {
    
    
//                            e.printStackTrace();
//                        }
//                        System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
//                        tickets--;
//                    }
//                }
                //变体2调用
                // setTicket();
            }
            x++;

        }
    }
    //变体2,没问题,能正常卖票
//    private void setTicket(){
    
    
//                        synchronized (obj) {//只上一把锁
//                    if (tickets > 0) {
    
    
//                        //通过sleep()方法来模拟出票时间
//                        try {
    
    
//                            Thread.sleep(100);
//                            //t1线程休息了100毫秒
//                        } catch (InterruptedException e) {
    
    
//                            e.printStackTrace();
//                        }
//                        System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
//                        tickets--;
//                    }
//                }
//    }
    //变体3,就是把synchronized关键字加到方法上。出现重复票数,因为第一把锁所是obj;第二把锁是this(代表本类)
    //把第一把锁改成this,打印卖票正常
//        private synchronized void setTicket(){
    
    
//
//                    if (tickets > 0) {
    
    
//                        //通过sleep()方法来模拟出票时间
//                        try {
    
    
//                            Thread.sleep(100);
//                            //t1线程休息了100毫秒
//                        } catch (InterruptedException e) {
    
    
//                            e.printStackTrace();
//                        }
//                        System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
//                        tickets--;
//                    }
//                }


    //变体4,同步静态方法:就是把synchronized关键字加到静态方法上。出现重复
    //第一把锁是'obj',第二把锁是'类名.class'。把第一把锁obj改成SellTicket.class
    private static synchronized void setTicket() {
    
    

        if (tickets > 0) {
    
    
            //通过sleep()方法来模拟出票时间
            try {
    
    
                Thread.sleep(100);
                //t1线程休息了100毫秒
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
            tickets--;
        }
    }
}

输出结果:
正常100到1

5.线程安全的类

  • StringBuffer
    [1] 线程安全,可变的字符序列
    [2] 从版本JDK 5开始,被StringBuilder 替代。 通常应该使用StringBuilder类,因为它支持所有相同的操作,但它更快,因为它不执行同步

  • Vector
    从Java 2平台v1.2开始,该类改进了List接口,使其成为Java Collections Framework的成员。 与新的集合实现不同, Vector被同步。 如果不需要线程安全的实现,建议使用ArrayList代替Vector

  • Hashtable

[1] 该类实现了一个哈希表,它将键映射到值。 任何非null对象都可以用作或者
[2] 从Java 2平台v1.2开始,该类进行了改进,实现了Map接口,使其成为Java Collections Framework的成员。 与新的集合实现不同, Hashtable被同步。 如果不需要线程安全的实现,建议使用HashMap代替Hashtable

6.Lock锁

虽然我们可以理解同步代码块同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作,Lock中提供了获得锁和释放锁的方法。

  • void lock():获得锁
  • void unlock():释放锁

Lock接口 不能直接实例化,这里采用它的实现类ReentrantLock来实例化

SellTicketDemo.java
不变
SellTicket.java

public class SellTicket implements Runnable {
    
    

    //定义一个类SellTicket实现Runnable接口,里面定义一个成员变量:private int tickets = 100;
    private int tickets = 100;
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
    
    
        while (true) {
    
    
            try {
    
    //中间的代码容易出问题,加try保险
                lock.lock();//上一把锁
                if (tickets > 0) {
    
    
                    //通过sleep()方法来模拟出票时间
                    try {
    
    
                        Thread.sleep(100);
                        //t1线程休息了100毫秒
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                    tickets--;
            }

            }finally {
    
    //即使中间的代码处问题,最后也能释放锁
                lock.unlock();//释放锁
            }

        }
    }
}

结果输出:
正常

猜你喜欢

转载自blog.csdn.net/gy99csdn/article/details/112951974