JavaEE初阶--多线程案例

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录


前言

上节博客,我们讲解了线程安全是啥:一段代码,在多线程中,并发执行后,产生bug。问题产生的原因:

1)操作系统对于线程的调度是随机的,抢占式执行[根本]。

2)多个线程同时修改同一个变量。

3)修改操作不是原子性的

4)内存可见性->编译器优化

5)指令重排序。

解决方案:

1)加锁:synchronized(锁对象) {

需要加锁的代码

}//解锁。

2)volatile

3)等待通知wait/notify:

也可以通过wait/notify来解决线程饿死问题,Object提供的方法,搭配synchronized锁对象得和调用wait/notify得对象是一致得。使用一次notify唤醒多个线程的wait随机唤醒其中一个线程,如果像唤醒所有,需要多次调用notify也可以使用notifyAll。

这节我们就要结合前面所学来编写一些经典的多线程案例。

一、单例模式

单例模式是校招中最常考的设计模式之一。

设计模式是什么?

设计模式好比象棋中"棋谱"。红方当头炮,黑马来跳,针对红方的一些走法,黑方应招的时候有一些固定的套路,按照套路来走局势就不会吃亏。

单例模式能保证某个类在程序中只存在唯一一份实例,而不会创建出多个实例。

这一点在很多场景上都需要。比如JDBC中地DataSource实例就只需要一个。

既然讲到了数据库地JDBC操作,我们就回忆一下DataSource地过程:

1.创建DataSource->描述了数据库服务器在哪里(url,user,password)。

2。建立连接 dataSource.getConnection();

3.拼装SQL语句 Statement 或者PreparedStatement

4.执行SQL execute方法/executeQuery/executeUpdate

5.遍历结果集合

RrsultSet,迭代器遍历

6.关闭资源

1)RrsultSet,

2) Statement

3)Connection

单例模式具体地实现方式有很多,最常见地是"饿汉"和"懒汉"两种。

饿汉模式

类加载地同时,创建实例。

java 复制代码
class  Singleton {
    private  static  Singleton instance = new Singleton();
    private  Singleton() {

    }
    public  static  Singleton getInstance(){
        return  instance;
    }
}

懒汉模式

单线程版

类加载地时候不创建实例,第一次使用地时候才创建实例。

java 复制代码
class  Singleton {
    private  static  Singleton instance =null;
    private  Singleton() {

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

刚才编写的两份代码(饿汉,懒汉),是否是线程安全?该怎么办?(非常经典的面试题)

饿汉模式是线程安全的,而懒汉模式是线程不安全的。因为懒汉模式

涉及到了多线程的修改操作,如果在多个线程中同时调用getinstance方法,就可能导致创建出多个实例。

一旦实例已经创建好了,后面再多线程环境调用getinstance就不再有线程安全问题了(不再修改instance了)

加上synchronized可以改善这里的线程安全问题。

java 复制代码
class  Singleton {
    private  static  Singleton instance =null;

    private  Singleton() {

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

但是加锁又引入了新的问题?

当把实例创建好了之后,后续再调用getinstance,此时都是直接执行return,如果只是进行if判定 + return,纯粹的读操作了,读操作不涉及到线程安全问题。

但是每次调用上述的方法,都会触发一次加锁操作,虽然不涉及线程安全问题了,多线程情况下,这里的加锁就会相互阻塞,影响程序的执行效率。

以下代码在加锁的基础上,做出了进一步改动:

使用双重if判定,降低锁竞争的频率。

java 复制代码
class  Singleton {
    private  static  Singleton instance =null;

    private  Singleton() {

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

但是,这样改进还是会有问题,是否会存在"内存课间休息"问题呢?

可能存在,编译器优化这个事情,非常复杂的,但是,为了稳妥起见,可以给instance直接加上一个volatile从根本上杜绝,内存可见性问题。

java 复制代码
class  Singleton {
    private  static volatile Singleton  instance =null;
    private static Object locker = new Object();
    private  Singleton() {

    }
    public  static  Singleton getInstance(){

        if (instance==null){
            synchronized (locker){
                if (instance == null){
                    instance = new Singleton();
                }
            }
        }
        return  instance;
    }
}

理解双重if判定/vola:

加锁/解锁是一件开销比较高的事情,而懒汉模式的线程不安全只是发生在首次创建实例的时候,因此后续使用的时候,不必再进行加锁了。

外层的if就是判定下看当前是否已经把instance实例创建出来了。

同时为了避免"内存可见性"导致读取的instance出现偏差,于是补充上volatile。

当多线程首次调用getinstance,大家可能都发现instance为null,于是又继续往下执行来竞争锁,其中竞争成功的线程,再完成创建实例的操作。

当这个实例创建完了之后,其他竞争到锁的线程就被里层if挡住了,也就不会继续创建其他实例。

1.又三个线程,开始执行getinstance,通过外层的if(instance == null) 知道了实例还没有创建的消息,于是开始竞争同一把锁。

2.其中线程1率先获取到锁,此时线程1通过里层的if(instance == null)进一步确认实例是否已经创建,如果没创建,就把这个实例创建出来。

3.当线程1释放锁之后,线程2和线程3也拿到锁,也通过里层的if(instance == null)来确认实例是否已经创建,发现实例已经创建出来了,就不再创建了。

4.后续的线程,不必加锁,直接就通过外层if(instance == null) 就知道实例已经创建了从而不再尝试获取锁了。降低了开销。

二、阻塞队列

1.阻塞队列是什么

阻塞队列是一种特殊的队列,也遵守"先进先出"的原则。

阻塞队列是一种线程安全的数据结构,并且具有以下特性:

  • 当队列满的时候,继续入队列就会阻塞,直到有其他线程从队列中取走元素。
  • 当队列空的时候,继续出队列也会阻塞,直到有其他线程往队列中插入元素。

阻塞队列的一个典型应用场景"生产者消费者模型"。这是一种非常典型的开发模型。

2.生产者消费者模型

生产者消费者模型就是通过一个容器来解决生产者和消费设的强耦合问题。

生产者和消费者彼此之间不直接通讯,而是通过阻塞队列来进行通讯,所以生产者生产数据之后不再等待消费者处理,而是直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取。

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

比如在"秒杀"的购物场景下,服务器同一时刻可能会收到大量的支付请求。如果直接处理这些支付请求,服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程)。这个时候就可以把这些请求都放到一个阻塞队列中,然后再由消费者线程慢慢的处理每个支付请求。

这样做可以有效进行"削峰",防止服务器被突然到来的一波请求直接冲垮。

一般来说A这种上游的服务器,尤其是入口的服务器,干的活更简单,单个请求消耗的资源数少。

像B这种下游的服务器,通常承担更重的任务量,复杂的计算/存储工作,单个请求消耗的资源数更多。日常工作中,确实是会给B这样角色的服务器分配更好的机器,即便如此,也很难保证B承担的访问量能够比A更高。

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

举个栗子:比如过年一家人一起包饺子。一般都是由明确分工,比如我负责擀饺子皮,我妈妈和我妹妹负责包。我这个擀饺子皮的人不关心包饺子的人是谁(能包就行,无论是手工包,借助工具,还是机器包),包饺子的人也不关心擀饺子皮的人是谁(有饺子皮就行,无论是用擀面杖擀得,还是拿罐头瓶擀,还是直接从超市买的)。

在Java标准库中提供了现成的阻塞队列.如果我们需要在一些程序中使用阻塞队列,直接使用标准库中的即可。

  • BlockingQueue是一个接口。真正实现的类是LinkedBlockingQueue。
  • put方法用于阻塞式的如对流,take用于阻塞式的出队列。
  • BlockingQueue也有offer,poll,peek等方法,但是这些方法不带有阻塞特性。
java 复制代码
public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> queue = new LinkedBlockingQueue<>();
        //入队列
        queue.put("passion");
        //出队列,如果没有put直接take,就会阻塞
        String elem = queue.take();
        System.out.println(elem);
    }

生产者消费者模型

java 复制代码
class demo{
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<Integer> queue = new LinkedBlockingDeque<>();
        Thread producer = new Thread(()->{
            Random random = new Random();
            while (true){
                int num = random.nextInt(1000);
                System.out.println("生产者开始生产元素"+num);
                try {
                    queue.put(num);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"生产者");
        Thread cosumer = new Thread(()->{
            try {
                int value = queue.take();
                System.out.println("消费者开始消费元素"+value);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        },"消费者");
        producer.start();
        cosumer.start();
        producer.join();
        cosumer.join();
    }
}

下面是我模拟实现的阻塞队列的代码:

java 复制代码
//模拟实现阻塞队列
class MyBlockingQueue{
    private String[] data = null;
    //队首
    private  int head =0;
    //队尾
    private  int tail = 0;
    //元素个数
    private int size =0;
    public  MyBlockingQueue(int capacity){
        data = new String[capacity];
    }
    public void put(String elem){
        synchronized (this){
            while (size>=data.length){
                //队列满了,需要阻塞
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            data[tail] = elem;
            tail++;
            if (tail >=data.length){
                tail =0;
            }
            size++;
            this.notify();
        }
    }
    public String take() throws InterruptedException {
        synchronized (this){
            while (size==0){
                //队列空了,需要阻塞
                this.wait();
            }
            String ret = data[head];
            head++;
            if (head>=data.length){
                head=0;
            }
            size--;
            this.notify();
            return ret;
        }
    }
}
public class demo17 {
    public static void main(String[] args) throws InterruptedException {
        MyBlockingQueue queue = new MyBlockingQueue(1000);
        Thread producer = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            while (true){
                System.out.println("请输入要生产的产品");
                String elem = scanner.next();
                System.out.println("生产者开始生产元素"+elem);
                queue.put(elem);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"生产者");
        Thread consumer = new Thread(()->{
           while (true){
               try {
                   String value = queue.take();
                   System.out.println("消费者开始消费"+value);
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }

           }
        });
        producer.start();
        consumer.start();
        producer.join();
        consumer.join();
    }
}

三、定时器

定时器是什么

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

定时器是一种时间开发中非常常用的组件。

比如网络通信中,如果对方500ms内没有返回数据,则断开连接尝试重连。

比如一个Map,希望里面的某个key在3s之后过期(自动删除)。

类似于这样的场景就需要用到定时器。

标准库中的定时器

  • 标准库中提供了一个Timer类,Timer类的核心方法为schedule。
  • schedule包含两个参数。第一个参数指定即将要执行的任务代码,第二个参数指定多长时间之后执行(单位为毫秒)。
java 复制代码
public class demo18 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("passion");
            }
        },1000);
    }
}

模拟实现一个定时器

定时器的构成:

  • 一个带优先级队列(不要使⽤ PriorityBlockingQueue,容易死锁!)
  • 队列中的每个元素是一个Task对象。
  • Task中带有一个时间属性,队首元素就是即将要执行的任务。
  • 同时有一个worker现藏一直扫描队首元素,看队首元素是否需要执行.

1.创建一个类,表示一个任务。

2.定时器中,能够管理多个任务的,必须使用一些集合类(优先级队列)把这多个任务给管理起来。

3.实现schedule方法,把任务添加到队列中即可

4.额外创建一个线程,负责执行队列中的任务。

java 复制代码
//创建一个类,表示任务
class  MyTimerTask implements  Comparable<MyTimerTask>{
    private  Runnable task;
    //记录任务要执行的时刻
    private  long time;
    public  MyTimerTask(Runnable task,long time){
        this.task = task;
        this.time = time;
    }
    @Override
    public int compareTo(MyTimerTask o) {
        return  (int) (this.time-o.time);
    }
    public long getTime(){
        return time;
    }
    public void run(){
        task.run();
    }
}
class MyTimer {
    //使用优先级队列把多个任务管理起来
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    //直接使用this作为锁对象,当然也是可以的。
    private Object locker = new Object();

    //实现schedule方法,把任务添加到队列中
    public void schedule(Runnable task, long delay) {
        synchronized (locker) {
            //以入队列这个时刻作为时间基准
            MyTimerTask timerTask = new MyTimerTask(task, System.currentTimeMillis() + delay);
            queue.offer(timerTask);
            locker.notify();
        }
    }

    public MyTimer() {
        //创建一个线程,负责执行队列中的任务
        Thread t = new Thread(() -> {
            try {
                while (true) {
                    synchronized (locker) {
                        //取出队首元素
                        //还是需要加上while
                        while (queue.isEmpty()) {
                            //这里的sleep时间不好设定
                            locker.wait();
                        }
                        MyTimerTask task = queue.peek();
                        if (System.currentTimeMillis() < task.getTime()) {
                            //当前任务时间,如果比系统时间大,说明任务执行的时间未到
                            locker.wait(task.getTime() - System.currentTimeMillis());
                        } else {
                            //时间到了,执行任务
                            task.run();
                            queue.poll();
                        }
                    }
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        t.start();
    }
}
public class demo19 {
    public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("keep passion 1000");
            }
        },1000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("keep passion 2000");
            }
        },2000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("keep passion 3000");
            }
        },3000);
        //Executors.newScheduledThreadPool(4);
    }
}

四、线程池

线程池是什么

虽然创建线程/销毁线程的开销:

想象一个场景:

在学校附近开了一家快递店,老板很精明,想到一个与众不同的办法来经营。店里没有雇人,而是每次有业务来了,就现场找一名同学过来把快递送了,然后解雇同学。这个类比我们平时来一个任务,起一个线程进行处理的模式。

很快老板发现问题来了,每次招聘+解雇同学的成本还是非常高的。老板还是很善于变通的,直到了为什么大家都要雇人了,所以指定了一个目标,公司业务人员会扩张到3个人,但还是随着业务逐步雇人。于是再有业务来了,老板就看,如果现在公司还没3个人,就雇一个人去送快递,否则只是把业务放到一个本本上,等着3个快递人员空闲的时候去处理。这个就是我们要带出的线程池的模式。

线程池最大的好处就是减少每次启动、销毁线程的损耗。

标准库中的线程池

  • 使用Executors.newFixedThread(10)能创建出固定包含10个线程的线程池。
  • 返回值类型为ExecutorService
  • 通过ExecutorService.submit可以注册一个任务到线程池中。
java 复制代码
class demo{
  public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);
        pool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("passion");
            }
        });
    }
}

上面的代码中ThreadPoolExecutor类的核心方法,submit(Runnable)

通过Runnable描述一段要执行的任务。通过submit任务放到线程池中,此时线程池里的线程就会执行这样的任务。构造这个类的时候,构造方法,比较麻烦(参数有点多)。

Executors创建线程池的几种方式

  • newFixedThreadPool:创建固定线程数的线程池。

  • newCachedThreadPool:创建线程数目动态增长的线程池。

  • newSingleThreadExecutor:创建单个线程的线程池。

  • newScheduledThreadPool:设定延迟时间后执行命令,或者定期执行命令。是进阶版的Timer。

Executors本质上是ThreadPoolExecutor类的封装。
ThreadPoolExecutor提供了更多的可选参数,可以进一步细化线程池行为的设定。

  • int corePoolSize:核心线程数,线程池一创建,这些线程也要随之创建,直到整个线程池销毁,这些线程才会销毁。就把这个核心线程数比作公司的正式员工的数量(正式员工,一旦录用,永不辞退,除非公司破产)。
  • int maximunPoolSize:最大线程数,核心线程+非核心线程(自适应),不繁忙就销毁,繁忙就创建。把这比作正式员工+实习生的数目。(实习生:一段时间不干活,就被辞退。)
  • long keepAliveTime:非核心线程允许空闲的最大时间。例如,允许实习生摸鱼的时间,如果实习生连续一个月都没有啥活,就可以考虑优化掉。
  • TimeUnit unit:keepaliveTime的时间单位,是秒,分钟,还是其他值。
  • BlockingQueue<Runnable> workQueue:传递任务的阻塞队列(选择使用数组/链表,指定capacity和指定是否要带有优先级/比较规则)线程池本质上也是生产者消费者模型,调用submit就是在生产任务,线程池里的线程就是在消费任务。
  • threadFactory:创建线程的工厂,参与具体的创建线程工作,通过不同线程工厂创建出的线程相当于对一些属性进行了不同的初始化设置。
  • RejectedExecutionHandler:拒绝策略,如果任务量超出公司的负荷了接下来怎么处理。
  1. AbortPolicy():超过负荷,直接抛出异常,
  2. CallerRunsPolicy():调用者负责处理多出来的任务。
  3. DiscardOldestPolicy():丢弃对流中最老的任务。
  4. DiscardPolicy():丢弃新来的任务。

上线程池的参数threadFactory中提到了统一的构造并初始化线程这是一种设计模式,和单例模式是并列的关系-----称为**工厂模式--**用来弥补构造方法的缺陷的。

java 复制代码
class Point {
    public Point(double x,double y){

    }
    public Point(double r,double a){
        
    }
}

C++和Java共有的一个问题:构造方法的名字是固定的,要像提供不同的版本,就需要通过重载,有时候不一定能构成重载。

这个时候,就提出了工厂方法。工厂方法的核心通过静态方法,把构造对象new的过程,各种属性初始化的过程,封装起来了,提供多组静态方法,实现不同情况的问题。

java 复制代码
//平面上一个点
class Point {

}
class PointFactory {
    public  static  Point makePointByXY(double x,double y){
        Point p = new Point();
        //通过x和y 给p进行属性设置
        return p;
    }
    public  static  Point makePointByRA(double r,double a){
        Point p = new Point();
        //通过 r 和 a 给p进行属性设置。
        return p;
    }
}
class demo{
 public static void main(String[] args) {
        Point p = PointFactory.makePointByRA(9,9);
    }
}

实现线程池

  • 核心操作为submit,将任务加入线程池中
  • 使用Worker类描述一个工作线程,使用Runnable描述一个任务。
  • 使用一个BlockingQueue组织所有的任务
  • 每个worker线程要做的事情:不停的从BlockingQueue中取任务并执行。
  • 指定一下线程池中的最大线程数maxWorkerCount;当前线程数超过这个最大值时,就不再增加线程了。
java 复制代码
class  MyThreadPool{
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
    //通过这个方法,来把任务添加到线程池中。
    public  void  submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }
    //n表示线程池中有几个线程。
    //创建了一个固定数量的线程池
    public  MyThreadPool(int n){
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(()->{
                while (true){
                    try{
                        //取出任务
                        Runnable runnable = queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            t.start();
        }
    }
}
public class demo21 {
    //线程池
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool pool = new MyThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    //要执行的工作
                    System.out.println(Thread.currentThread().getName() +"passion");
                }
            });
        }
    }
}

五、对比线程和进程

5.1.线程的优点

  1. 创建一个新线程的代价要比创建一个新进程小得多。
  2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多,
  3. 线程占用的资源要比进程少很多。
  4. 能充分利用多处理器的可并行数量。
  5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
  6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
  7. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

5.2进程与线程的区别

  1. 进程是系统进行资源分配和调度的一个独立单位,线程时程序执行的最小单位。
  2. 进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈。
  3. 由于同一进程的个线程间共享内存和文件资源,可以不通过内核进行直接通信,
  4. 线程的创建、切换及终止效率更高。

总结

写到这里这个多线程初阶的内容就和大家共同一起学习完了。整个多线程中我们用了大量的方式去解决线程安全这个难点。就这个难点我总结了保证线程安全的思路:

1.使用没有共享资源的模型

2.使用共享资源只读,不写的模型

  • 不需要写共享资源的模型。
  • 使用不可变对象

3.直面线程安全 (重点)

  • 保证原子性
  • 保证顺序性
  • 保证可见性

到这里我们的多线程案例这张博客就结束了,下期预告---多线程进阶!!

相关推荐
小光学长2 小时前
基于ssm旅游管理系统的开发与设计z050cft7(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
java·数据库·旅游
用户3521802454752 小时前
🥯2025 年终极避坑指南:Spring Boot 2.7 + 3.2 混合集群的 Redis + OAuth2 序列化血泪史
java·后端·spring cloud
陌路物是人非2 小时前
记一个 @Resource BUG
java·开发语言·bug
晴天飛 雪2 小时前
Spring Boot 上传shp压缩包解析多少地块
java·spring boot
曹牧2 小时前
Java:Math.abs()‌
java·开发语言·算法
期待のcode2 小时前
Java的泛型
java·开发语言
FPGAI2 小时前
Java学习之计算机存储规则、数据类型、标识符、键盘录入、IDEA
java·学习
AC赳赳老秦2 小时前
pbootcms模板后台版权如何修改
java·开发语言·spring boot·postgresql·测试用例·pbootcms·建站
止水编程 water_proof2 小时前
SpringBoot快速上手
java·spring boot·后端