目录

一,单例模式

单例模式也就是一个进程中只允许有一个实例,是我们平时在开发中比较重要常见的一种模式,我们单例模式一般分为两种,一种是饿汉模式,一种是懒汉模式

饿汉模式:

//饿汉模式
class Singleton{
    //static保证了成员或者方法是属于类的,不属于实例,所以你不能通过new实例来调用它,刚刚好保证单例,因为new实例会创建多个对象
    private static Singleton instance=new Singleton();
    //单例模式最关键的
    private Singleton(){
    }
    public static Singleton getInstance(){
        return instance;
    }
}

在饿汉模式里面,我们调用的开始就直接创建了唯一实例,所以线程安全,因为没有判断等其他操作,只有读的操作

懒汉模式(原始):

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

在get方法里面判断了是否创建过实例之后再来操作,但是这样的懒汉模式线程不安全,当我们两个线程同时判定是否为空的时候,可能就会出现创建了两个instance实例,单例模式就失败了,并且new操作会因为指令重排序导致出现问题,指令重排序:编译器的一种底层优化,调整指令执行顺序,使代码效率更高。

(1)就比如new这个操作可以简化理解为三个操作:1:分配内存空间,2:针对空间进行初始化,3:内存空间首地址,赋值到引用变量中。按正常顺序,我们应该是123,但是经过指令重排序会导致变成132,就会传回去一个没有数据的值。

(2)并且外层还有if判断,加上new操作,就会使这个创建操作不是原子的,也会因为线程的随机调度导致线程安全问题,所以我们要在instance变量前面加上volatile修饰,防止指令重排序,并且在if和new的这个操作外层套上一把锁,将两个操作打包为一个整体。

(3)但是我们还需要思考一个问题,当我们第一次创建好实例之后,就不需要每次都使用锁然后判断了,这样会浪费时间和内存空间,所以我们外层需要再使用一层if判断

懒汉模式(改进):

class Singleton2 {
    private static volatile Singleton2 instance;
    private Singleton2() {
    }
    public static Singleton2 getInstance() {//双重检查
        if (instance == null) {
            synchronized (Singleton2.class) {
                if (instance == null) {
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }
}

总结一下单例模式注意线程安全需要注意的点1:正确的加锁,2:双重if判断,3:volatile

dataSource实现单例:

//dataSource实现单例模式
class DataSource {
    private static volatile DataSource dataSource;
    private DataSource() {
    }
    public static DataSource getInstance() {
        if (dataSource == null) {
            synchronized (DataSource.class) {
                if (dataSource == null) {
                    DataSource ds=new DataSource();
                    dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/java117?characterEncoding=utf8&useSSL=false&serverTimezone=UTC");
                    dataSource.setUser("root");       // 你的mysql账号
                    dataSource.setPassword("你的密码"); // 你的mysql密码
                    dataSource=ds;
                }
            }
        }
        return dataSource;
    }
}

这里有个注意点,就是我们用来一个ds这样的零时变量,最后再赋值回去,这样的作用是什么:因为有指令重排序这样的问题,所以如果没有这个零时变量,其他线程可能最坏情况下拿到还没初始化,并且还没有set的database对象,因为即使有锁,外线程依旧可以拿到里面的对象,因为DataSource是static修饰的,所有线程是共用的这一个

二,阻塞队列

队列特点:先进先出

阻塞队列的特点:1:线程安全,2:阻塞功能(阻塞队列为空的时候尝试出的时候,队列满尝试入的情况)

阻塞功能可以帮助实现生产者-消费者模型:1:降低资源的竞争,2:解决耦合(耦合就是代码功能的关联性,我们追求低耦合,关联性越低越好),3:削峰填谷(可以使请求高峰以及请求低谷的时候,服务器依旧可以按照自己的节奏来执行)

生产者-消费者的不足:1:单个请求的响应时间会受影响,2:使服务器结构更加复杂,3:这个模型更适合异步

阻塞队列的相关方法展示:

方便分为峰值和谷值来展示


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

//阻塞队列
public class demo8 {
    //生产快,但是消费慢,因为消费要有sleep,所以队列就产生了阻塞,每次保证队列只有两个元素
    //如果我不设置队列大小,不用阻塞队列,生产者就会疯狂的生成,然后占满内存,抛出异常
    public static void main1(String[] args) {
        BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<>(2);
        //生产者
        Thread t1=new Thread(()->{
            for (int i = 0; i < 500; i++) {
                try {
                    blockingQueue.put(i);
                    System.out.println("生产了"+i);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        //消费者
        Thread t2=new Thread(()->{
            for (int i = 0; i < 500; i++) {
                try {
                    Thread.sleep(500);
                    int take=blockingQueue.take();
                    System.out.println("消费了"+take);

                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t1.start();
        t2.start();
    }
    //消费快,生成慢
    public static void main(String[] args) {
        BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<>(2);
        //生产者
        Thread t1=new Thread(()->{
            for (int i = 0; i < 500; i++) {
                try {
                    Thread.sleep(500);
                    blockingQueue.put(i);
                    System.out.println("生产了"+i);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        //消费者
        Thread t2=new Thread(()->{
            for (int i = 0; i < 500; i++) {
                try {
                    int take=blockingQueue.take();
                    System.out.println("消费了"+take);

                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t1.start();
        t2.start();
    }
}

对于代码的解释:如果我们不使用阻塞队列进行阻塞的话,会出现

1:瞬时流量洪峰:生产者疯狂生成任务,消费处理不过来 → 队列满,生产者 put 阻塞(防止 OOM)。

什么是OOM:JVM 堆内存已经全部用光,还想继续创建新对象,JVM 直接抛OutOfMemoryError异常,程序直接崩溃。

2:流量低谷:没有新任务产生,队列全部消费完 → 队列空,消费者 take 阻塞,CPU 不轮询空转

什么是空转:线程一直在 CPU 循环做无效判断,啥业务活不干,但是一直占用 CPU,白白消耗 CPU 资源。

阻塞队列就很好地解决了以上的问题

自己实现简单的阻塞队列:

//自己创建的阻塞队列
class MyBlockingQueue{
    Object locker=new Object();
    private String[] array = new String[1000];
    private int head = 0;
    private int tail = 0;
    private int size = 0;
    public MyBlockingQueue(int capacity){
        array=new String[capacity];
    }
    public void put(String elm) throws InterruptedException {
        synchronized (locker) {
            //队列是满的
            while (size == array.length) {
                locker.wait();
            }
            //队列不满开始放东西
            array[tail] = elm;
            tail++;
            if (tail >= array.length) {
                tail = 0;
            }
            size++;
            locker.notify();
        }
    }
    public String take() throws InterruptedException {
        synchronized (locker) {
            //当队列为空的时候
            while (size==0){
               locker.wait();
            }
            //当队列不为空的时候
            String elm = array[head];
            head++;
            if (head >= array.length) {
                head = 0;
            }
            size--;
            locker.notify();
            return elm;
        }
    }
}

public class demo3 {
    public static void main(String[] args) {
        MyBlockingQueue myBlockingQueue=new MyBlockingQueue(2);
        //生产者
        Thread t1=new Thread(()->{
            for (int i = 0; i < 500; i++) {
                try {
                    Thread.sleep(500);
                    myBlockingQueue.put(i+"");
                    System.out.println("生产了"+i);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        //消费者
        Thread t2=new Thread(()->{
            for (int i = 0; i < 500; i++) {
                try {
                    String take=myBlockingQueue.take();
                    System.out.println("消费了"+take);

                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t1.start();
        t2.start();
    }
}

阻塞我们用到的是wait和notify的组合,因为我们需要达成某种条件才会阻塞,并且达到某种条件才会唤醒,不能单纯的sleep以及join,并且我们这里用到的循环是while,因为while可以有再次判断的机制

三,线程池

当我们刚引入线程的时候,我们会发现线程比进程好太多了,但是接下来我们还是会发现线程的使用开销也大了一些,所以我们再引入了线程池,也就是把线程提前创建好放在里面,这样就比操作系统创建的效率来的更高

这是一台电脑的基本组成,当我们调用start来创建线程的时候,我们会有从用户态到内核态再到用户态的切换,这样会消耗一些时间和内存,但是线程池是纯用户态的操作,会更加节省开销

线程池的构造

我们来介绍一下这里的参数

(1)corePoolSize(核心线程数),maximumPoolSize(最大线程数)

这里涉及到两类线程1:核心线程(在创建线程池当初就已经有了)2:非核心线程(因为任务太多额外创建的),两者相加就是最大线程数

(2)keepAliveTime (数值),unit(单位)

这里描述了非核心现场释放的时机

(3)workQueue(任务队列)

这里放要执行的任务,并且可以保证线程安全,可以让调用者手动指定队列

(4)threadFactory(线程工厂)

工厂设计模式:有一个工厂类,可以不依赖构造方法

可以自己提供工厂类,当然也可以用系统提供的工厂类(里面有两种)

(5)handler(拒绝策略)

任务队列到达上限,就会触发拒绝策略,主要有四种1:抛出异常,

2:调用submit这个线程自己负责来执行,

3:把任务队列中,最老的任务丢弃,把空的留给新的,

4:把任务队列中,最近添加的任务丢弃,把空的留给更加新的

线程池的创建

MyThreadFactory myThreadFactory=new MyThreadFactory("消息业务",false);
        ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(
                2,
                4,
                2,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(10),
                myThreadFactory,
                new ThreadPoolExecutor.AbortPolicy()
        );
package thread;

import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

public class MyThreadFactory implements ThreadFactory {
    //获取旧的编号,并且进行自增
    private final AtomicInteger threadIndex = new AtomicInteger(1);
    private final String threadName;
    private final boolean isDaemon;
    public MyThreadFactory(String threadName, boolean isDaemon) {
        this.threadName = threadName;
        this.isDaemon = isDaemon;
    }
    @Override
    public Thread newThread(Runnable r) {
        //先自定义名字
       String threadname= this.threadName + threadIndex.getAndIncrement();
       //创建线程
        Thread thread = new Thread(r,threadname);
        //设置守护线程
        thread.setDaemon(isDaemon);
        //设置未捕获异常处理器:线程运行抛出异常会进入这里
        thread.setUncaughtExceptionHandler((t, e) -> {
            System.err.println("【线程异常】线程名:" + t.getName() + ",异常信息:" + e.getMessage());
            e.printStackTrace();
        });
        return thread; // 返回给线程池使用
    }
}

这是我们自己创建了一个线程工厂然后传入进去,当然我们也可以用操作系统自带的

不过操作系统还提供了一个更加简单的构造方法,使用Excutor这个工厂类来创建线程池

前面两个是普通线程池,可以自动扩容,三四是指定了线程池的大小,六七十控制在未来的某个时间点执行,其他的可以自行去了解一下

并且我们往线程池中添加任务是使用submit关键字

自己实现固定数量的线程池

//自己创建线程池
class MyThreadPool{
    private BlockingQueue<Runnable> blockingQueue=new ArrayBlockingQueue<>(10);
    public void submit(Runnable runnable) throws InterruptedException {
        blockingQueue.put(runnable);
    }
    //添加构造方法
    public MyThreadPool(int size){
        for (int i = 0; i < size; i++) {
            Thread t=new Thread(()->{
                    try {
                        while (true) {
                        Runnable runnable = blockingQueue.take();
                        runnable.run();}
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
            });
            t.start();
            t.setDaemon(true);
        }

    }
}

四,定时器

定时器就和闹钟一样,在规定时间后进行执行

定时器的使用

public static void main(String[] args) throws InterruptedException {
        Timer timer=new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("定时任务执行了");
            }
        },1000);
        Thread.sleep(2000);
        timer.cancel();
    }

自己实现简易定时器

自己实现定时器的主要要点:(1)因为编译器的定时器需要我们传入任务然后时间,所以我们也需要创建一个任务类,里面定义了runnable以及时间(有时间间隔delay,也有任务时间time)

(2)我们用到优先级队列来存储任务,并且在任务类里面添加比较方法,让优先级队列知道是通过时间来进行排序的

(3)通过锁来使操作原子化,并且锁的位置也很重要

(4)通过wait/notify来进行阻塞等待以及唤醒,第一个wait是等待队列不为空,第二个等待是为了等待到达任务的执行时间,两个等待同时因为schedule方法里面的notify来唤醒,因为第一个等待本身就是等待不为空,schedule方法里面添加了方法所以就唤醒,第二个是因为可能新添加的任务时间更早,可能需要重新排序,但是在我们add方法执行后的瞬间就会重新进行排序了,所以同时在这里唤醒就是合理的

import java.util.PriorityQueue;
//自己实现定时器
//先得有一个任务类,定时器中的任务在这里编写以及初始化
class TimerTask implements Comparable<TimerTask>{
    private Runnable runnable;
    //目的是判定是否到达执行时间
    private long time;
    //delay是时间间隔
    public TimerTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }
    //在MyTimer里面通过run方法进行执行
    public void run(){
        runnable.run();
    }
    public long getTime() {
        return time;
    }

    @Override
    public int compareTo(TimerTask o) {
        return (int)(this.time - o.time);
    }
}
class MyTimer {
    Object locker =new Object();
    private PriorityQueue<TimerTask> queue = new PriorityQueue<>();

    public void schedule(Runnable runnable, long delay) {
        synchronized (locker){
            TimerTask timerTask = new TimerTask(runnable, delay);
            queue.add(timerTask);
            locker.notify();
        }
    }

    //去执行任务
    public MyTimer() {
        Thread thread = new Thread(() -> {
            while (true) {
                synchronized (locker) {
                    TimerTask timerTask = queue.peek();
                    try {
                        while (timerTask == null) {//队列为空
                            locker.wait();
                            timerTask = queue.peek();
                        }
                        long currentTime = System.currentTimeMillis();
                        if (currentTime >= timerTask.getTime()) {//到达执行时间
                            queue.poll().run();
                        } else {
                            locker.wait(timerTask.getTime() - currentTime);
                        }
                    }catch (InterruptedException e){
                        throw new RuntimeException("wait被终止");
                    }
                }
            }
        });
        thread.setDaemon(true);
        thread.start();
    }
}
public class demo12 {
    public static void main(String[] args) throws InterruptedException {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(() ->
            System.out.println("定时任务执行了1000"), 1000);
        myTimer.schedule(() ->
                        System.out.println("定时任务执行了2000"), 2000);
        myTimer.schedule(() ->
                        System.out.println("定时任务执行了3000"), 3000);
        Thread.sleep(5000);
    }
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐