第十七节 多线程(二)购票经典案例+单例设计模式

1. 多线程案例

  1. 这是一个经典的多线程案例,火车票售票案例,
    • 需求:假设一共100张火车票,多个窗口去售卖 。
  2. 想一想,在现实中是如果买票的,需要注意哪些事项 ?

1.1 窗口售票

1.1.1 方式一:继承 Thread 类

  1. 第一种继承Thread方式看看有什么效果!? 会出现什么样的问题!?
public class Test_Ticket {
    public static void main(String[] args) {
        TicketThread t = new TicketThread();
        TicketThread t1 = new TicketThread();
        TicketThread t2 = new TicketThread(); //在sleep()时增加一个窗口。
        t.start();
        t1.start();
        t2.start();//增加售卖窗口开始买票

    }
}
/*
    自定义类
 */

class TicketThread extends Thread{
    //int num_ticket =100;//1.定义总票数;
    static int  num_ticket =100;//使用static 全局变量定义总票数;
    //重写run() 方法
    @Override
    public void run() {
        //设置循环保证有票就要卖
        while (true){
            //2.TODO 注意:如果让线程休眠了(其目的就是为了让线程出错,增加了线程切换频率)。还能保证正确性么?
            try {
                Thread.sleep(10);//单位毫秒。
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //打印卖票信息
            System.out.println(Thread.currentThread().getName()+" : "+num_ticket--);
            if (num_ticket<=0)break;//设置循环出口当没有票时就跳出。
        }
        //窗口提示消息
        if (num_ticket==0){
            System.out.println("票已经售完");
        }
    }
}
  1. 经过休眠之后产生的问题:
    • 产生第一个问题:出现了重复买票现象;

      在这里插入图片描述

    • 产生的第二个问题:出现了超卖的现象;
      在这里插入图片描述

1.1.2 方式二:实现Runnable接口

  1. 实现接口能解决相应问题么!?
//业务场景同Thread一样。
public class Test_Ticket2 {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable(); 
        Thread t = new Thread(r);
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        Thread t3 = new Thread(r);

        t.start();
        t1.start();
        t2.start();
        t3.start();

    }
}

//自定义类实现接口
class MyRunnable implements Runnable{
    int num_Ticket =100;

    //重写run()方法
    @Override
    public void run() {

        //循环一直卖票
        while (true){
            if (num_Ticket>0) { //假设我让票大于0在开始卖呢!?还会发生那样的问题么!?
            
            //TODO :制造睡眠,让线程产生频换切换,出现错误。
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
                //打印 输出当前线程名称 + 票号
                System.out.println(Thread.currentThread().getName() + "= " + num_Ticket--);
            }
            //循环出口,当没有余票时,停止。
            if (num_Ticket<=0) break;
        }
    }
}

1.2 售票案例—同步锁

  1. 我们在上述 “售票案例” 中发现了多线程由于线程睡眠(产生现象线程的频换切换)导致了出现了问题: 超卖 和 重卖。
  2. 如何判断线程中有没有安全问题!?参考以下3点:
    • 这个程序时不是多线程!?
    • 在代码执行中有没有共享数据!?
    • 有没有多条语句去操作共享数据!?
  3. 以上的问题用什么解决方式: 到底该从哪方面入手呢!? 能不能把有问题的代码上一把锁,谁用谁开!?
    • 解决问题,就需要把有问题的代码,“全部包裹起来”,一次只让一个线程去执行,给上一把锁。
    • 让多线程对操作的共享数据,做到一个排队现象。如生活中的案例: 上厕所。

1.2.1 Synchronized

  1. 就是使用Synchronized将会引起线程安全问题的逻辑代码,用锁的形式,“锁起来”,只有持有这个锁的钥匙才能访问,并且,同一时刻,只有这一个线程持有这把锁的钥匙,从而确保线程安全。

    • 同步需要两个或者两个以上的线程。
    • 多个线程之间必须使用同一个锁。
    	sychronized(锁对象){ // 这里的对象,相当于多线程的锁。线程相当于从该对象拿到了一个锁。
    		容易出问题的代码(需要共享的代码,涉及到共享操作的)
    	}
    
    
  2. 使用位置,锁的对象必须是唯一

    • 可以修饰 方法称为同步方法,会自动分配,使用的锁对象是this
    • 可以修饰代码块称为同步代码块,锁对象可以任意,但是必须唯一。
  3. 使用同步锁的特点:

    • 同步的缺点是会降低程序的执行效率, 对执行流程可控,但是为了保证线程安全,必须牺牲性能。
    • 要控制锁的范围。不需要给所有代码都上锁,例如:去卫生间给整个商场都上锁,就是不对滴。

1.3 使用同步锁改造售票案例

1.3.1 改造Thread 和 Runnable

  1. 锁的位置需要考虑,小了控制不住,大了又牺牲了性能。
    • 如何判断线程中有没有安全问题!?
public class Test_Ticket {
    public static void main(String[] args) {



        //创建线程
        MyThread t = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        MyThread t4 = new MyThread();

        t.start();
        t2.start();
        t3.start();
        t4.start();

    }
}

class MyThread extends Thread{
      static int num_Ticket=100; //票数(共享数据)
      //static Object o = new Object(); 这个也可以 也是全局唯一。
       Object o = new Object(); //创建唯一对象。 
    /*1.确定多线程隐患:3点:  是不是存在多个线程中?  存在共享数据!? 是否有多条语句操作该共享数据!?
      2.确定范围:范围太大,影响性能,范围太小,没有作用。                            
     */
    //重写run()方法
    @Override
    public void run() {
        while (true) {

            //TODO 制造困难 睡上一会。
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //synchronized (new Object()){ // new Object()也是任意对象。 运行结果!? 没用!为什么?
            //synchronized (o){ // 不管你怎么调用我的o对象引用只有一次。 运行结果!? 还不行!? Why!?
            synchronized (MyThread.class){ //使用本类。类对象锁定!!相当于锁住了本类。 运行结果: ok
            //TODO 售票的业务处理
            if (num_Ticket > 0) {
                System.out.println(getName() + " = " + num_Ticket--);
            }
            if (num_Ticket <= 0)break;//break,只能在循环里使用。

             }
        }

    }
}
  • 改造Runnable
public class Test_Runnable {
    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();
        Thread t = new Thread(r);
        Thread t2 = new Thread(r);
        Thread t3 = new Thread(r);
        Thread t4= new Thread(r);

        t.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

class MyRunnable implements Runnable{
    int num_Ticket =100; //票总数
    Object o =  new Object();
    @Override
    //synchronized public void run() { 可以直接在方法上加锁
         public void run() {
        //TODO 制造困难  睡觉10
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        while (true) {
          // synchroized(new Object()) // 对象不唯一。 
            synchronized (this) { //使用本类对象,指的是this  或者 // synchronized(o)也行
                if (num_Ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + "=" + num_Ticket--);
                }
                if (num_Ticket <= 0) break;//循环出口。
            }
        }
    }
}
  • 还记得之前学过的StringBuffer 和StringBuilder:有看过它们底层实现么!!!?

2. 线程池

2.1 概述

  1. “池化技术的思想” 主要是为了减少每次获取资源的消耗,提高对资源的利用率。其目的是限制和管理资源,如:使用线程池可以进行统一的分配,调优和监控。
    • 降低资源消耗。
    • 提高相应速度。
    • 提高线程的可管理性。

2.2 线程池创建方式

2.2.1 ExecutorService/Executors

  1. ExecutorService 属于接口,用来存储线程的池子,把新建线程/启动线程/关闭线程的任务都交给池来管理。
           execute(Runnable任务对象) 把任务丢到线程池。
    
  2. Executors属于类,提供了工厂方法用来创建不同类型的线程池。
    		newFixedThreadPool(int nThreads) 最多n个线程的线程池
    		newCachedThreadPool() 足够多的线程,使任务不必等待
    		newSingleThreadExecutor() 只有一个线程的线程池
    

2.2.2 售票案例:用线程池创建线程

  • 通过线程池技术,改造售票案例 Runnable 接口。

    public class Test_Runnable {
        public static void main(String[] args) {
            MyRunnable r = new MyRunnable();
            // 使用线程池改造
          /*
            Thread t = new Thread(r);
            Thread t2 = new Thread(r);
            Thread t3 = new Thread(r);
            Thread t4= new Thread(r);
    
            t.start();
            t2.start();
            t3.start();
            t4.start();
           */
            //Todo 不想那么麻烦使用Thread 的start()方法去启动线程。 使用线程池技术来管理资源的启动。
            ExecutorService pool = Executors.newFixedThreadPool(3);//相当于启动3个线程。
           // pool.execute(r); //这只是取了一根线程。多线程怎么使用!? 通过循环。
            // 池子里有3个但是循环取了5个!? 相当于3个线程循环执行,提高使用率。如果循环少于线程数,相当于闲着线程。
            for (int i = 0; i <6 ; i++) {
                pool.execute(r);
            }
    
    
        }
    }
    
    class MyRunnable implements Runnable{
        int num_Ticket =100; //票总数
        Object o =  new Object();
        @Override
        //synchronized public void run() { 可以直接在方法上加锁
             public void run() {
            //TODO 制造困难  睡觉10
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            while (true) {
                synchronized (this) { //使用本类对象,指的是this  或者 // synchronized(o)也行
                    if (num_Ticket > 0) {
                        System.out.println(Thread.currentThread().getName() + "=" + num_Ticket--);
                    }
                    if (num_Ticket <= 0) break;//循环出口。
                }
            }
        }
    }
    

3. 线程锁

3.1 悲观锁和乐观锁

  1. 悲观锁:总是假设最坏的情况出现,用悲观的态度防止 并发间线程出现的冲突。
    • 在进行读写操作时,在释放锁之前,任何人都不能对其数据进行操作,直到前面释放锁之后,后一个对数据加锁在进行操作。同sychronized一样。
  2. 乐观锁:总是朝着最好的结果看待事情,理想的认为每次数据不会被修改,所以不需要持锁操作。
    • 但是在更新的时候会通过版本号机制或者算法(CAS,Compare And Swap),去判断一下是否发生过数据更改。

3.2 常见的锁(拓展知识)

  1. sychronized 互斥锁, (悲观锁)

    • 采用synchronized修饰符实现的同步机制叫做互斥锁机制。每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源。如果没有这个锁标记,任何一个对象系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,因此叫做互斥锁。
  2. ReentrantLock 排他锁(悲观锁)

    • ReentrantLock是排他锁,又称独占锁。排他锁在同一时刻仅有一个线程可以进行访问,实际上是一种相对比较保守的锁策略。
    • 在这种情况下任何“读/读”、“读/写”、“写/写”操作都不能同时发生,这在一定程度上降低了吞吐量。例子:若操作者S 对数据对象A加上独占锁,则只允许操作者S读取和修改A,其他任何事务都不能再对A加任何类型的锁,直到释S放A上的锁。这就保证了S在释放A上的锁之前不能再读取和修改A。(然而读操作之间不存在数据竞争问题,如果”读/读”操作能够以共享锁的方式进行,那会进一步提升性能。) 所以需要读写锁。
  3. ReentrantReadWriteLock 读写锁(乐观锁)

    • 因此引入了ReentrantReadWriteLock顾名思义,ReentrantReadWriteLock是Reentrant(可重入,可重新在进入的意思)Read(读)Write(写)Lock(锁),我们下面称它为读写锁。读写锁内部又分为读锁和写锁, 读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。
    • 读锁和写锁分离从而提升程序性能,读写锁主要应用于读多写少的场景。

3.3 用读写锁改造售票案例

  1. 同悲观锁sychronized, 属于jvm底层级别,将读写都锁住了,其实多线程的 并不影响数据的完整性,主要是修改。所以使用ReentrantReadWriteLock 将写的过程控制,适用于读多写少的场景。

  2. ReentrantReadWriteLock分公平锁和非公平锁,公平锁即,排队顺序拿锁,先排先得,非公平不看排队顺序,可以插队。

    • 非公平锁性能比公平锁高5~10倍, 因为公平锁需要频繁唤醒队列中的线程,比较消耗资源。但是非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
public class Test_Reentrant {
    public static void main(String[] args) {
        MyRunnable_Ticket r = new MyRunnable_Ticket();
        Thread t = new Thread(r);
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);

        t.start();
        t1.start();
        t2.start();

    }
}

class MyRunnable_Ticket implements Runnable{
    int num_ticket =100; //总票数

    //1. 创建读写锁,static 修饰保证全局唯一
    static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);//默认false 非公平模式,容易死锁,性能高。

    @Override
    public void run() {

        while (true) {
            /*
               2. 在操作资源前面上锁
             */
            lock.writeLock().lock();//上了写锁
            // Todo 制造睡眠,切换线程
            try {
                Thread.sleep(10);

                // 售票
                if (num_ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + "=" + num_ticket--);
                }

                //不满足条件就跳出while循环
                if (num_ticket <= 0) break;

            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
      
                lock.writeLock().unlock();//3. 不会像sychronized自动放锁,它需要手动释放。
            }
        }
    }
}

  1. 拓展知识: sychronized 也属于非公平锁,因为它获取锁的方式也是随机不排队的。
    • 二者区别在于:
      synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象, 因此使用 Lock 时需要在 finally 块中释放锁。

4. 设计模式

4.1 怎么理解设计模式!?

  1. 设计模式是 针对面向对象中反复出现问题的解决方案,是一套反复被开发者使用,并且多数人知晓的,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。 设计模式一共有23种之多,都是针对不同场景的解决方案。

在这里插入图片描述

4.2 单例设计模式(Singleton)

  1. 单例设计(singleton):一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点它最主要的特点就是保证在内存中该类只有一个实例对象。 这样做的好处,反复创建对象或者销毁,引起的资源浪费。
  2. 我们先看一个典型的单例模式设计的类 java.lang.Runtime ,分析下单例如何创建!?

4.2.1 单例模式一:饿汉式

  1. 啥是饿汉式!?是一种设计思想,举一个生活例子:一个很饿的人,它很勤快就怕自己饿着,提前准备食物,什么时候想吃,拿过来就吃。
    • 即饿汉式在 开始类加载的时候就已经实例化,并且创建单例对象,用的时候拿过来就用。
/**
     饿汉式
 */
public class Test_Singleton {
    public static void main(String[] args) {

        //4.测试
        MySingLeton s = MySingLeton.getSingLeton();
        System.out.println(s);

        //4.1 怎么测试创建成功,地址值相同即可! s==s1。
        MySingLeton s1 = MySingLeton.getSingLeton();
        System.out.println(s1);

    }
}

class MySingLeton{
    //1.私有化 无参构造方法,无参通过构造方法创建对象。
    private MySingLeton(){}

    //2.在类的内部,创建本类对象,私有化该对象
    //2.1 当提供的对外访问方法用static 修饰后 创建本类对象也需要用static修饰,静态只能调用静态
    // private MySingLeton singLeton = new MySingLeton();
    static private MySingLeton singLeton = new MySingLeton();

    //3.提供对外访问方法。
    //3.1 无法创建对象,怎么访问该方法!? 所以需要static 修饰,通过类名.方法调用!
    // public MySingLeton getSingLeton() {
   static public MySingLeton getSingLeton() {
        return singLeton; // 为什么!?会报错,因为静态只能调用静态
    }
}

4.2.2 单例模式二: 懒汉式(面试知识点)

  1. 啥是懒汉式!?生活中的例子:因为它比较懒么!?什么时候饿了,什么时候在想办法搞点食物!
    • 即懒汉式: 开始不会实例化什么时候用就什么时候new,才进行实例化 需要时在使用,相当于 延迟加载。
/**
 * 懒汉式:面试重点知识,同饿汉式创建方式大体相似,但是有细微不同!
 *  设计到两个层面,1,什么是延迟加载!?
 *                2,如何解决线程安全!?在多线程的情况下。
 *
 */
public class Test_SingLeton2 {
    public static void main(String[] args) {

        MySingLeton2 mySingLeton = MySingLeton2.getMySingLeton();
        System.out.println(mySingLeton);
        MySingLeton2 mySingLeton2 = MySingLeton2.getMySingLeton();
        System.out.println(mySingLeton2==mySingLeton);//判断是否一致。
    }
}
class MySingLeton2{

   static Object obj =  new Object();
    //1.私有化无参构造方法,目的:防止任意创建对象。
    private MySingLeton2(){}

    //2.在本类中,创建本类的对象。并私有化。
    static private MySingLeton2  mySingLeton ; //2.1懒汉式,相当于什么时候需要什么时候在创建!--延迟加载!

    //3.对外提供访问方法
     static public MySingLeton2 getMySingLeton(){
        //4.关键点,判断什么时候需要在创建!
        //4.1 问题:该模式存在多个数据操作! 存在线程安全隐患!?怎么解决!?
        synchronized (obj) { // 这样相当于将整个内容全部包裹进来!也可以是用类锁
            if (mySingLeton == null) {
                mySingLeton = new MySingLeton2();
            }
            return mySingLeton;
        }
    }
}

4.2.3 单例模式:懒汉式和饿汉式的区别

  1. 线程安全方面:
    • 饿汉式: 线程不存在多数据操作,线程安全。
    • 懒汉式:线程不安全,需要锁机制处理。
  2. 效率和内存空间:
    • 饿汉式: 因为没有锁机制, 执行效率高,但是一开始就会加载对象,不管是否使用都会占用内存。
    • 懒汉式:有锁机制,效率比饿汉式差点,但是不占用内存空间,因为什么时候使用什么时候创建。
  • 16
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吴琼老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值