多线程案例

多线程案例

单例模式

啥是设计模式? 设计模式好比象棋中的 “棋谱”. 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有一些固定的套路.
按照套路来走局势就不会吃亏. 软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路.
按照这个套路来实现代码, 也不会吃亏.

单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.
这一点在很多场景上都需要. 比如 JDBC 中的 DataSource 实例就只需要一个.
单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种.

饿汉模式

只涉及到读操作是线程安全的
类加载的同时, 创建实例.

class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
   }
}

懒汉模式-单线程版

类加载的时候不创建实例. 第一次使用的时候才创建实例.

class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
       }
        return instance;
   }
}

懒汉模式-多线程版

上面的懒汉模式的实现是线程不安全的.
线程安全问题发生在首次创建实例时(涉及到多个线程同时修改一个变量的操作). 如果在多个线程中同时调用 getInstance 方法, 就可能导致创建出多个实例.
一旦实例已经创建好了, 后面再多线程环境调用 getInstance 就不再有线程安全问题了(不再修改 instance 了)

class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
       }
        return instance;
   }
}

懒汉模式-多线程版(改进)

以下代码在加锁的基础上, 做出了进一步改动:
使用双重 if 判定, 降低锁竞争的频率.
给 instance 加上了 volatile.

class Singleton {
    private static volatile Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
           if (instance == null) {
               instance = new Singleton();
               }
           }
       }
        return instance;
   }
}

解释两个if语句: 第一个if是用来判定是否需要加锁 单例模式的代码只在首次使用的时候会涉及到线程不安全的问题,需要加锁
第二个if是用来判断是否需要new对象 只不过凑巧,这两个条件写法一样
一旦加锁就可能会产生阻塞,在这个阻塞过程中,其他线程可能先获取了锁对象,已经创建出instance ,
所以两侧if的执时机可能差异很大,这两个if的执行结果也可能截然相反

volatile 防止指令重排序造成的错误
指令重排序和前面提到内存可见性都是编译优化问题,可能会调整原有代码的执行顺序,调整的前提是保证逻辑不变
通常情况下,单线程情况下指令重排序能够保证逻辑不变的前提下,八程序的执行效率大幅度提高,但在多线程的情况下可能会判断失误

new的操作可能会触发指令重排序 new的操作分为三步
1.申请内存空间
2.在内存空间上构造对象
3.把内存地址赋值给instance引用 可以按照1 2 3 来执行 也可以 13 2来执行 哪种顺序执行多线程都无所谓,但是在多线程的情况下 1 3 2的顺序就可能出现问题

在这里插入图片描述
假如现在有t1 t2 两个线程,t2线程按照1 3 2的顺序执行,此时正在执行3,与此同时 t1 执行到第一个if语句,判断instance不为空,直接返回instance, 但此时instance对应的内存空间还没有构造对象,此时instance指向的是一个没有初始化的非法对象。进一步 t2 线程的代码还可能会访问到instance里面的属性和方法,这个时候就很容易出现bug
针对上述问题,解决方案,仍然是volatile,让volatile修饰instance,此时就可以保证instance在修改的过程中就不会出现那指令重排序的现象。

阻塞式队列

塞队列是什么

阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.
阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:
当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.
阻塞队列的一个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型.

生产者消费者模型

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.

1) 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力.

比如在 “秒杀” 场景下, 服务器同一时刻可能会收到大量的支付请求. 如果直接处理这些支付请求,
服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程). 这个时候就可以把这些请求都放到一个阻塞队列中,
然后再由消费者线程慢慢的来处理每个支付请求. 这样做可以有效进行 “削峰”, 防止服务器被突然到来的一波请求直接冲垮.

2) 阻塞队列也能使生产者和消费者之间 解耦.

比如过年一家人一起包饺子. 一般都是有明确分工, 比如一个人负责擀饺子皮, 其他人负责包. 擀饺子皮的人就是 “生产者”, 包饺子的人就是
“消费者”. 擀饺子皮的人不关心包饺子的人是谁(能包就行, 无论是手工包, 借助工具, 还是机器包),
包饺子的人也不关心擀饺子皮的人是谁(有饺子皮就行, 无论是用擀面杖擀的, 还是拿罐头瓶擀, 还是直接从超市买的)

标准库中的阻塞队列

在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.
BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.

BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// 入队列
queue.put("abc");
// 出队列. 如果没有 put 直接 take, 就会阻塞. 
String elem = queue.take();

生产者消费者模型实现

public static void main(String[] args) throws InterruptedException {
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
        Thread custer = new Thread(()->{
            while(true){
                try {
                   int value = queue.take();
                    System.out.println("消费元素"+value);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"消费者");

        custer.start();

        Thread producter = new Thread(()->{
            Random random = new Random();
            while (true){
                try {

                    int value = random.nextInt(100);

                    System.out.println("生产元素"+value);
                    queue.put(value);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"消费者");

        producter.start();
        
        custer.join();
        producter.join();

    }

阻塞队列实现

通过 “循环队列” 的方式来实现.

使用 synchronized 进行加锁控制.
put 插入元素的时候, 判定如果队列满了, 就进行 wait.
注意这里的wait操作,往往使用while作为条件判断的方式,目的是为了让wait唤醒之后还能再次确认依次,是否条件仍然满足。这里唤醒wait的不一定是notify,还有可能是通过调用Interrupt方法导致wait抛出InterrupttedException唤醒的
take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)

public class BlockingQueue {
    private int[] items = new int[1000];
    private volatile int size = 0;
    private int head = 0;
    private int tail = 0;
    public void put(int value) throws InterruptedException {
        synchronized (this) {
            // 此处最好使用 while.

            // 否则 notifyAll 的时候, 该线程从 wait 中被唤醒,

            // 但是紧接着并未抢占到锁. 当锁被抢占的时候, 可能又已经队列满了

            // 就只能继续等待

            while (size == items.length) {
                wait();
           }
            items[tail] = value;
            tail = (tail + 1) % items.length;
            size++;
            notifyAll();
       }
   }
    public int take() throws InterruptedException {
        int ret = 0;
        synchronized (this) {
            while (size == 0) {
             wait();
           }
            ret = items[head];
            head = (head + 1) % items.length;
            size--;
            notifyAll();
       }
        return ret;
   }
    public synchronized int size() {
        return size;
   }
    // 测试代码

    public static void main(String[] args) throws InterruptedException {
        BlockingQueue blockingQueue = new BlockingQueue();
        Thread customer = new Thread(() -> {
            while (true) {
                try {
                    int value = blockingQueue.take();
                    System.out.println(value);
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
           }
       }, "消费者");
        customer.start();
        Thread producer = new Thread(() -> {
            Random random = new Random();
            while (true) {
                try {
                    blockingQueue.put(random.nextInt(10000));
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
           }
       }, "生产者");
        producer.start();
        customer.join();
        producer.join();
   }
}

定时器

定时器是什么

定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码.

定时器是一种实际开发中非常常用的组件. 比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连. 比如一个
Map, 希望里面的某个 key 在 3s 之后过期(自动删除). 类似于这样的场景就需要用到定时器.

标准库中的定时器

标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .
schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).

Timer timer = new Timer();

timer.schedule(new TimerTask() {
    @Override

    public void run() {
        System.out.println("hello");
   }
}, 3000)

主线程执行schedule方法的时候,就是把这个任务给放到timer对象中了.
于此同时,timer里头也包含一个线程,这个线程叫做“扫描线程”,一旦时间到,扫描线程就会执行刚才安排的任务了.
仔细观察,可以发现,整个进程其实没有结束!!就是因为Timer内部的线程,阻止了进程结束.
Timer里,是可以安排多个任务的.

实现定时器

定时器的构成:
1.Timer 中需要有一个线程,扫描任务是否到时间,可以执行了
2.一个带优先级队列把所有的任务都保存起来.
为啥要带优先级呢?

具体使用啥样的数据结构比较好呢?假设使用数组(ArrayList),此时,扫描线程,就需要不停的遍历数组中的每个任务判定每个任务是否都到达执行时间。
上述这样的遍历过程就比较低效。

使用优先级队列,是更好的办法,给Timer中添加的这些任务,都是带有一个“时间”
一定是时间小的先执行。最先执行的就是时间最小的任务!!如果时间最小的任务,还没到时间呢,其他任务更不会到时间了!优先级队列,可以使用O(1)时间,来获取到时间最小的任务的!!

3.还需要创建一个类,通过类的对象来描述一个任务.(至少要包含任务内容和时间)

//通过这个类描述一个任务
class MyTimerTask implements Comparable<MyTimerTask>{
    //要有一个要执行的任务
   private Runnable runnable;
    private long time;
    public MyTimerTask(Runnable runnable,long delay){
        this.runnable = runnable;
        this.time = System.currentTimeMillis()+delay;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        //队首元素最小(
        return (int)(this.time-o.time);
    }

    public long getTime(){
        return time;
    }
public Runnable getRunnable(){
        return runnable;
}
}

class MyTimer{
    private Object locker = new Object();
    public PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    public void  schedule(Runnable runnable,long delay){
        synchronized (locker){
            queue.offer(new MyTimerTask(runnable,delay));
            locker.notify();
        }
    }

    //扫描线程
    public MyTimer(){
        Thread t = new Thread(()->{
            //扫描线程,需要不停的扫描队首元素,看是否到达时间
            while (true){
                try{
                    synchronized (locker){
                        //使用while作为循环,避免虚假唤醒
                        while (queue.isEmpty()){
                            //使用wait等待
                            //添加新的任务换新
                            locker.wait();
                        }

                        MyTimerTask task = queue.peek();
                        long currentTime = System.currentTimeMillis();
                        if(currentTime >= task.getTime()){
                            task.getRunnable().run();
                            queue.poll();
                        }else {
                //避免忙等,消耗cup资源
                //在任务还没有到的时候,就wait阻塞(线程不会在cpu上调度,也就把cpu资源让出来给别人)
                            locker.wait(task.getTime()-currentTime);
                        }

                    }
                }catch (InterruptedException e){
                    e.printStackTrace();
                }

            }
        });
        t.start();

    }
}

优先级队列要求元素是可以比较的,这里MyTimerTask实现Comparable接口
代码中涉及一个线程(比如主线程)给队列添加任务,扫描线程是另外一个线程,也需要操作队列。不同的线程都要操作同一个队列就可能有线程安全问题,加锁是必要的

public void  schedule(Runnable runnable,long delay){
        synchronized (locker){
            queue.offer(new MyTimerTask(runnable,delay));
            locker.notify();
        }

这里的notify唤醒两处wait
1.队列从空变为不同
2.当最新任务执行时间还没到,进入阻塞,新的任务来了之后就唤醒一下,然后重新判断一下最早执行的任务以及更新时间。

  //使用while作为循环,避免虚假唤醒
                        while (queue.isEmpty()){
                            //使用wait等待
                            //添加新的任务换新
                            locker.wait();
                        }

这里用if判断,和上面阻塞队列一样

线程池

线程池最大的好处就是减少每次启动、销毁线程的损耗。
线程诞生的意义,是因为进程的创建/销毁,太重量了(比较慢)

有对比,才有伤害.和进程比,线程,是要快了,但是如果进一步提高创建销毁的频率,线程的开销也不能忽视了!

两种典型的办法进一步提髙效率:
1.**协程(轻量级线程)**相比于线程,把系统调度的过程,给省略了.(程序猿手工调度)当下,一种比较流行的并发编程的手段,但是在Java圈子里,协程还不够流行.
使用协程更多的是Go(Go诞生的时候主打的卖点,协程就是一个)和Python(Python的线程是废柴,后来有了协程之后,相比于之前的线程香太多了)
2.线程池
这个方案,减少每次启动、销毁线程的损耗,使线程也不至于很慢
这个词,其实是计算机中一种比较重要的思想方法,很多地方都涉及到。例如:线程池,进程池,内存池,连接池……
线程池:在使用第一个线程的时候,提前把2 3 4 5 …线程创建好
后续如果想使用新的线程,不必重新创建了,直接拿过来就能用(此时创建线程的开销就被降低了)

为啥,从池子取 的效率比创建线程,效率更高?
从池子取,这个动作,是纯粹用户态的操作
创建新的线程,这个动作,则是需要 用户态 + 内核态 相互配合,完成的操作

如果一段程序,是在系统内核中执行,此时就称为“内核态”,如果不是,则称为“用户态”
操作系统,是由_内核+配套的应用程序_构成的
内核系统最核心的部分。
创建线程操作,就需要调用系统api,进入到内核中,按照内核态的方式来完成一系列动作

例如:我现在去银行办卡,柜台小姐姐说需要提供省份证复印件。现在有两种选择
1.把省份证给小姐姐,她帮我复印
2.我自己到大厅的自助复印机自行复印 1的过程就相当于内核态的操作,此时我i也不知道柜员小时之后都做了什么,什么时候帮我复印,什么时候回来。不可控!!
柜员相当于系统内核,是要给所有进程提供服务,当你要创建线程的时候,人家内核会帮你做,但做的过程中难免也要做一些其他的事物,我们没办法控制。
2的过程相当于纯用户态,我可以立即去复印,复印完立即回来不拖泥带水。非常可控!!

标准库中的线程池

使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
返回值类型为 ExecutorService
通过 ExecutorService.submit 可以注册一个任务到线程池中.

ExecutorService pool = Executors.newFixedThreadPool(10);

pool.submit(new Runnable() {
    @Override

    public void run() {
        System.out.println("hello");
   }
});
ExecutorService service = Executors.newCachedThreadPool();

由于上述代码可知 线程池对象不是咱们new的而是通过一个专门的方法,返回一个线程池对象。上述代码体现了工厂模式

工厂模式是给构造方法,填坑的
通常创建对象,使用new. new关键字就会触发类的构造方法. 但是,构造方法,存在一定的局限性.

考虑有个类

class Point{
public Point(double x,double y){}
public Point(double r,double a){}
}

上述代码中第一个构造方法期望使用笛卡尔坐标系,来构造对象,第二个构造方法期望使用极坐标构造对象。很多时候 构造一个对象 希望有多种构造方式, 多种方式 就需要使用多个版本的构造方法来分别实现
但是构造方法要求方法的签名必须是类名 不同的构造方法就只能通过 重载 的方式来区分了.(重载->参数类型/个数不同)
上述代码中这两方法,没有构成重载,就会编译失败!!
使用工厂设计模式,就能解决这个问题 使用普通的方法,代替构造方法完成初始化工作 普通方法就可以使用方法的名字来区分了.也就不再收到重载的规则制约了.
实践中,一般单独搞一个类,给这个类搞一些静态方法,由这样的静态方法负责构造出对象
例如:

//使用笛卡尔坐标系,来构造对象
class PointFactory {
 public static Point makePointByXY(double x , double y ) { 
 Point p = new Point(); 
 p.setX(x);
  p.setY(y); 
  return p;
 } 
 //使用极坐标构造对象
public static Point makePointByRA(double r , double a ) { 
 Point p = new Point(); 
 p.setX(r);
  p.setY(a); 
  return p;
 } 
 }

Executors 创建线程池的几种方式

**newFixedThreadPool**: 创建固定线程数的线程池
**newCachedThreadPool**: 创建线程数目动态增长的线程池.
cache缓存. 用过之后不着急释放,先留着以备下次使用…
此时构造出的线程池对象,有一个基本的特点
线程数目是能够动态适应的.  随着往线程池里添加任务,这个线程池中的线程会根据需要自动被创建出来.  创建出来之后也不会着急销毁,会在池子里保留一定的时间 ,以备随时再使用.

**newSingleThreadExecutor**: 创建只包含单个线程的线程池. 
**newScheduledThreadPool**: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.Timer有一个扫描线程,而这里有多个扫描线程执行任务)


Executors 本质上是 ThreadPoolExecutor 类的封装.       
上述这几个工厂方法生成的线程池,本质上都是对一个类进行的封装ThreadPoolExecutor这个类功能非常丰富,提供了很多参数,标准库上述的几个工厂方法其实就是给这个类填写了不同的参数用来构造线程池了.

在这里插入图片描述

int corePoolSize

核心线程数

int maximumPoolSize

最大线程数 这两个参数描述了线程池中,线程的数目. 这个线程池里线程的数目是可以动态变化的. 变化范围就是[corePoolSize,
maximumPoolSize] corePoolSize相当于正式员工,maximumPoolSize相当于正式员工+实习生
正式员工,允许摸鱼.不会因为摸鱼,被公司开除。
实习生,不允许摸鱼.如果这段时间任务多了,此时,就可以多搞几个实习生,来干活;如果过段时间任务少了,并且少的这个状态持续了一段时间空闲的实习生就可以裁掉了。这样做,既可以满足效率的要求又可以,避免过多的系统开销

long keepAliveTime, TimeUnit unit

keepAliveTim: 允许实习生摸鱼的时间,不是说,实习生一摸鱼就立即被开除。 unit : 时间单位,ms,s,min

BlockingQueue workQueue

workQueue:阻塞队列。用来存放线程池中的任务的。 可以根据需要灵活设置这里的队列是啥。
需要优先级,就可以设置PriorityBlockingQueue。
如果不需要优先级,并且任务数目是相对恒定的,可以使用ArrayBlockingQueue
如果不需要优先级,并且任务数目变动较大、LinkedBlockingQueue

ThreadFactory threadFactory

threadFactory:工厂模式的体现。 此处使用ThreadFactory作为工厂类由这个类负责创建线程、
使用工厂类创建线程,主要是为了在创建过程中,对线程的属性做出一些设置、
如果手动创建线程,就得手动设置这些属性,就比较麻烦,使用工厂方法封装一下

RejectedExecutionHandler handle

在这里插入图片描述

线程池的拒绝策略 一个线程池,能容纳的任务数量,有上限。 当持续往线程池里添加任务的时候,一旦已经达到上限了,继续再添加,会出现什么效果??
不同的拒绝策略,就有不同的效果 AbortPolicy 直接抛出异常 CallerRunsPolicy
新添加的任务,由添加任务的线程负责执行 DiscardOldestPolicy 丢弃队列中最老的任务 DiscardPolicy
丢弃当前新加的任务

注意:线程数目和拒绝策略面试必考

使用线程池,需要设置线程的数量、数目设置多少合适?
设cpu核心数(逻辑核心数)是N
假设一个线程的所有代码都是cpu密集型代码,这个时候,线程池的数量不应该超过N(设置N就是极限了),设置的比N更大,这个时候,也无法提高效率了。(cpu吃满了)此时更多的线程反而增加调度的开销。
假设一个线程的所有代码都是IO密集的,这个时候不吃CPU,此时设置的线程数,就可以是超过N较大的值。一个核心可以通过调度的方式,来并发执行嘛~
综上所述代码不同,线程池的线程数目设置就不同无法知道一个代码,具体多少内容是cpu密集,多少内容是IO密集正确做法:使用实验的方式,对程序进行性能测试测试过程中尝试修改不同的线程池的线程数目看哪种情况下,最符合你的要求通过实验的方式工程师!!!实验是非常重要的环节~
超线程:一个物理cpu核心可以相当于多个cpu 逻辑核心

实现简单的线程池

//实现简单的线程池

class  MyThreadPool{
    //任务队列,拒绝策略阻塞等待
    private BlockingQueue <Runnable> queue = new ArrayBlockingQueue<>(1000);
    //通过这个方法,把任务添加到队列里面
    public void submit(Runnable runnable) throws InterruptedException {

        queue.put(runnable);
    }
    public MyThreadPool(int n){

        for(int i = 0; i < n; i++){
            Thread t = new Thread(()->{
                try {
                    Runnable runnable = queue.take();
                    runnable.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            t.start();
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值