多线程案例

本文介绍了Java中的单例模式(包括饿汉模式和懒汉模式)、阻塞队列(生产者-消费者模型)的原理与应用,以及定时器和线程池的创建和实现,探讨了保证线程安全的关键思路。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

1.单例模式

1.1 饿汉模式

1.2 懒汉模式

1.3 比较

2.阻塞队列

2.1 概念

2.2 生产者-消费者模型

2.2.1 生产者-消费者模型的好处

2.2.2 实现

2.2.3 判断队列满

3.定时器

3.1 概念

3.2 实现

4.线程池

4.1 概念

4.2 创建

4.3 简单代码实现

5.总结


1.单例模式

单例模式是一个非常经典的设计模式,也是最经常考的设计模式

单例就是单个实例(对象)

有些场景中,我们希望有的类,只能有一个对象,不能有多个,在这样的场景下,就可以使用单例模式。

单例模式,需要两步走:

1.在类的内部,提供一个现成的实例

2.把构造方法设为private,避免其他代码能够创建出实例

通过上述方法,就强制了其他程序员在使用这个类的时候,就不会创建出多个对象了。

使用getInstance方法可以获得这个对象

//希望这个类能够有唯一实例
class Singleton {
   
    private static Singleton instance = new Singleton();
    //通过这个方法来获取刚才的实例
    //后续如果相使用这个类,都通过getInstance方法获取
    public static Singleton getInstance() {
        return instance;
    }

    //把构造方法设置为私有,此时类外面的其他代码,就无法new出这个类的对象了
    private Singleton() {

    }
}
public class Demo21 {
    public static void main(String[] args) {
        //此处又有一个实例,这就不是单例了
//        Singleton s1 = new Singleton();
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        //此时s1和s2是同一个对象
        System.out.println(s1 == s2);    //true
    }
}

单例模式具体的实现方式有很多,最常见的就是“懒汉模式”和“饿汉模式”

1.1 饿汉模式

类加载的同时,创建实例.

1.2 懒汉模式

首次调用getInstancce的时候,才会创建出真正的实例。

“懒”其实是一个褒义词,非必要就不做,减少一些工作量,提高效率。

class SingletonLazy {
    private static volatile SingletonLazy instance = null;
    public static SingletonLazy getInstance() {
      if(instance == null) {      //同样的代码写两次,十分有意义
          synchronized (SingletonLazy.class) {    //一旦加锁,就可能产生阻塞,可能会阻塞很久很久,
                        // 进一步导致这个阻塞的过程中Instance就可能改变了(其他线程把Instance给new出来了)
              if(instance == null) {
                  instance = new SingletonLazy();
              }
          }
      }
        return instance;
    }
    private SingletonLazy() {

    }
}

理解双重if判定/volatile:

加锁/解锁是⼀件开销⽐较⾼的事情.⽽懒汉模式的线程不安全只是发⽣在⾸次创建实例的时候.因此后续使⽤的时候,不必再进⾏加锁了。

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

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

当多线程⾸次调⽤getInstance,⼤家可能都发现instance为null,于是⼜继续往下执⾏来竞争锁,其中竞争成功的线程,再完成创建实例的操作。

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

1.3 比较

饿汉模式,懒汉模式,两种写法,是否线程安全?(如果多个线程,同时调用getInstance是否会出现问题?)

如果多个线程,同时修改同一个变量,此时就可能出现线程安全问题

如果多个线程,同时读取同一个变量,这时候没事~不会出现线程安全问题

饿汉方式,getInstance,只是进行读取!!不是修改!!所以是安全的

而懒汉模式,既涉及到读取,也涉及到修改,就可能存在问题

那如何保证懒汉模式是安全的呢?

1.加锁 

把if和new包裹在一起

这样写后,后续每次调用getInstance,都需要先加锁

但实际上,懒汉模式,线程安全问题,只是出现在最开始的时候(对象还没new的时候)

2.双重if

一旦对象new出来了,后续多线程调用getInstance,就只有读操作,就不会线程不安全了,此时就可以不加锁了

第一个if判定是否需要加锁

第二个if用来判定是否需要new对象

3.volatile - 指令重排序

编译器优化,编译器为了执行效率,可能会调整原有代码的执行顺序,调整的前提是保持逻辑不变

java当中的new操作可能触发指令重排序

1.申请内存空间           买房子

2.在内存空间上构造对象(构造方法)      装修

3.把内存地址,赋值给instance引用           交钥匙

可以1 2 3执行  精装房     也可以1 3 2执行  毛坯房

执行完13后instance就非空了 但是指向的是一个还没初始化的非法对象

此时2开始执行了,判定instance==null,条件不成立,于是t2直接return instance.

进一步t2的线程代码就可能访问instance里面的属性和方法了。

针对上述问题,解决办法,仍然是volatile

让volatile修饰Instance,此时就可以保证Instance在修改的过程中就不会出现指令重排序的现象了

这个代码中,涉及到三个要点

1.正确进行加锁

2.要进行两重if判断

3.用volatile修饰

2.阻塞队列

2.1 概念

多线程代码中常用到的一种数据结构。

阻塞队列是什么?

阻塞队列是⼀种特殊的队列.也遵守"先进先出"的原则.阻塞队列能是⼀种线程安全的数据结构,并且具有以下特性:

• 当队列满的时候,继续⼊队列就会阻塞,直到有其他线程从队列中取⾛元素。

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

阻塞队列的⼀个典型应⽤场景就是"⽣产者消费者模型".这是⼀种⾮常典型的开发模型。
 

2.2 生产者-消费者模型

擀饺子皮  - 生产者

包饺子,消耗饺子皮  - 消费者

生产出来的饺子皮,设置一个专门的地方放置饺子皮   -  阻塞队列

2.2.1 生产者-消费者模型的好处

1.解耦合

两个模块联系越紧密,耦合越高,反之则越低

尤其对分布式系统来说,更加有意义

2.削峰填谷

峰:短时间内,请求量比较多

谷:短时间内,请求量比较少

A收到了较大的请求量,A会把对应的请求写入到队列中。    削峰

B仍然可以按照之前的节奏,来处理请求。

峰值情况,一般不会持续存在,只会短时间出现,过了A峰值后,A的请求量就恢复正常了,B就可以逐渐的把积压的数据给处理掉了。   填谷

有了这样的机制后,就可以保证在突发情况来临后,系统可以正常运行。

2.2.2 实现

在java标准库里,已经提供了现成的阻塞队列,供程序员使用

标准库里,针对BlockingQueue提供了两种最重要的实现方式:

1.基于数组

2.基于链表

import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

public class Demo23 {
    public static void main(String[] args) throws InterruptedException {
        BlockingDeque<String> queue = new LinkedBlockingDeque<>();   //基于链表的BlockingDeque
        queue.put("111");
        queue.put("222");
        queue.put("333");
        queue.put("444");
        String elem = queue.take();
        System.out.println(elem);        //111
        elem = queue.take();
        System.out.println(elem);        //222
        elem = queue.take();
        System.out.println(elem);        //333
        elem = queue.take();
        System.out.println(elem);        //444
        elem = queue.take();
        System.out.println(elem);     //没有任何打印
    }
}

2.2.3 判断队列满

1.浪费一个格子,让tail指向head的前一个位置,就算满了

2.专门搞一个变量,size,来表示元素个数,size为0就是空,为数组最大值,就是满

class MyBlockingQueue {

    // 此处这里的最大长度,也可以指定构造方法,由构造方法的参数来指定
    private String[] data = new String[1000];
    // 队列的起始位置
    private volatile int head = 0;
    // 队列的结束位置的下一个位置
    private volatile int tail = 0;
    // 队列中有效元素的个数
    private volatile int size = 0;

//    private final Object locker = new Object();

    //提供核心方法入队列和出队列
    public void put(String elem) throws InterruptedException {
        synchronized (this) {
            while (size == data.length) {
                // 队列满了
                // 如果是队列满,继续插入元素,就会阻塞
                this.wait();
            }
            //队列没满,真正往里面添加元素
            data[tail] = elem;
            tail++;
            //如果tail++后到达数组末尾,这个时候就需要让它回到开头
            if(tail == data.length) {
                tail = 0;
            }
            size++;
            // 这个notify用来唤醒take中的wait
            this.notify();
        }
    }
    public String take() throws InterruptedException {
        synchronized (this) {
            while (size == 0) {
                //队列空了
                this.wait();
            }
            //队列不空,就可以把队首元素(head 位置的元素)删除掉,并进行打印
            String ret = data[head];
            head++;
            if(head == data.length) {
                head = 0;
            }
            size--;
            this.notify();    //唤醒
            return ret;
        }
    }
}
public class Demo24 {
    public static void main(String[] args) {
        //生产者,消费者,分别使用一个线程表示(也可以使用多个线程)
        MyBlockingQueue queue = new MyBlockingQueue();
        // 消费者
        Thread t1 = new Thread(() -> {
            while (true) {
                try {
                    String result = queue.take();
                    System.out.println("消费元素: " + result);

                    //暂时先不sleep
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        // 生产者
        Thread t2 = new Thread(() -> {
            int number = 1;
            while (true) {
                try {
                    queue.put(number + "");
                    System.out.println("生产元素: " + number);
                    number++;
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t1.start();
        t2.start();
    }
}

3.定时器

3.1 概念

日常开发常用组件

主线程执行schedule方法的时候,就是把这个任务给放到timer对象中

与此同时,timer里头也包含一个线程,这个线程就叫做“扫描线程“,一旦时间到,扫描线程就会执行刚才安排的任务了。

在Timer里,是可以安排多个任务的。

import java.util.Timer;
import java.util.TimerTask;

//定时器
public class Demo25 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("3000");
            }
        },3000);
        timer.schedule(new TimerTask() {   //使用匿名内部类的方法,继承了TimerTask,并且创造出一个实例
            @Override
            public void run() {         //目的也是为了重写run 通过run描述任务的详细情况
                System.out.println("2000");
            }
        },2000);   //当前安排的任务,啥时候执行,此处填写的时间,就是从当前时刻为基准,往后推迟
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("1000");
            }
        },1000);
        System.out.println("程序启动!");
    }
}

3.2 实现

定时器的构成

• ⼀个带优先级队列(不要使⽤PriorityBlockingQueue,容易死锁!)

• 队列中的每个元素是⼀个Task对象.

• Task中带有⼀个时间属性,队⾸元素就是即将要执⾏的任务

• 同时有⼀个worker线程⼀直扫描队⾸元素,看队⾸元素是否需要执⾏

import java.util.PriorityQueue;

class MyTimerTask implements Comparable<MyTimerTask> {
    //要有一个要执行的任务
    private Runnable runnable;
    //还要有一个执行任务的时间
    private long time;
    //此处的delay就是schedule方法传入的”相对时间“
    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);
        //这样的写法,就是让队首元素是最大时间的值
//        return o.time - this.time;
    }

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

// 自己的定时器
class MyTimer {
    // 使用一个数据结构,保存所有要安排的任务
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    private Object locker = new Object();
    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) {
                        // 不要使用if作为wait的判定条件
                        while (queue.isEmpty()) {
                            // 使用wait进行等待,但是要想使用,需要搭配synchronized,不能单独使用
                            // 需要由另外的线程唤醒
                            //添加了新的任务,就应该唤醒
                            locker.wait();
                        }
                        MyTimerTask task = queue.peek();
                        // 比较一下队首元素是否可以执行了
                        long curTime = System.currentTimeMillis();
                        if(curTime >= task.getTime()) {
                            // 当前时间已经达到了任务时间,就可以执行任务了
                            task.getRunnable().run();
                            queue.poll();    //任务执行完了,就可以从队列删除了
                        } else  {
                            // 当前时间还没到任务时间,暂时不执行任务
                            // 暂时 啥也不管,等下一轮判定
                            locker.wait(task.getTime() - curTime);  //当任务时间没到的时候,就wait阻塞
                                                                //线程不会在cpu上调度,也就把cpu资源让出来给别人了
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}
public class Demo26 {
    public static void main(String[] args) {
        MyTimer timer = new MyTimer();

        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("3000");
            }
        },3000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("2000");
            }
        },2000);
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("1000");
            }
        },1000);
        System.out.println("程序开始执行");
    }

}

4.线程池

4.1 概念

优化频繁创建销毁线程的场景

线程诞生的意义,是因为进程的创建/销毁,太重量了(比较慢)

但是线程如果进一步提高创建销毁的频率,那么线程的开销也不能忽视了。

于是出现了线程池,线程池最⼤的好处就是减少每次启动、销毁线程的损耗。

核心思想是:在使用第一个线程的时候,提前把2,3,4,5…线程创建好,后续如果需要使用,直接取就行,不需要消耗额外的资源。

4.2 创建

Executors创建线程池的⼏种⽅式

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

• newCachedThreadPool:创建线程数⽬动态增⻓的线程池.

• newSingleThreadExecutor:创建只包含单个线程的线程池.

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

Executors本质上是ThreadPoolExecutor类的封装.

public class Demo27 {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(4);
        service.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        });
    }
}

4.3 简单代码实现

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

class MyThreadPool {
    //任务队列
    private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);
    //通过这个方法,把任务加到队列中
    public void submit(Runnable runnable) throws InterruptedException {
        // 此处的拒绝策略,相当于是第五种策略,阻塞等待~~(下策)
        queue.put(runnable);
    }

    public MyThreadPool(int n) {
        //创建出n个线程,负责执行上述队列中的任务
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                //让这个线程,从队列中消费任务,并进行执行
                try {
                    Runnable runnable = queue.take();
                    runnable.run();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            t.start();
        }
    }
}
public class Demo28 {
    public static void main(String[] args) throws InterruptedException {

        MyThreadPool myThreadPool = new MyThreadPool(4);
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            myThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("执行任务:" + finalI);
                }
            });
        }
    }
}

5.总结

保证线程安全的思路:

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

2. 适⽤共享资源只读,不写的模型

        a. 不需要写共享资源的模型

        b. 使⽤不可变对象

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

        a. 保证原⼦性

        b. 保证顺序性

        c. 保证可⻅性

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值