Java单例模式、阻塞队列、定时器、线程池

1. 单例模式

单例模式是一种经典的设计模式(软件开发中的固定套路)。
设计模式有很多种,目前主要考察:①单例模式 ②工厂模式。
单例——>单个实例(instance)对象,在一个程序中,某个类,只创建出一个实例(一个对象),不能创建多个对象。
Java中的单例模式,借助java语法,保证某个类只能创建出一个实例,而不能new多次。
java中实现单例模式有很多种写法,主要总结以下2种:
①饿汉模式(急迫)
②懒汉模式(从容)
通常认为,懒汉模式更好,效率更高。(非必要不去做)

计算机种的例子: 打开一个硬盘上的文件,读取文件内容,并显示出来
饿汉:把文件所有内容都读到内存中,并显示。(文件非常大会卡)
懒汉:只把文件读一小部分,把当前屏幕填充上,如果用户翻页了,再读其他文件内容,如果不翻页,就省下了。

1.1 饿汉模式实现单例

核心思想:早早就把实例创建好了

//单例模式
class Singleton {
    //通过静态成员变量的模式
    private static Singleton singleton = new Singleton();
    //饿汉模式,不足:类的实例的加载太早了,后面可能没有用到这个类的实例
    //获取到实例的方法
    public static Singleton getSingleton() {
        return singleton;
    }
    //构造方法私有化,禁止外部new实例
    private Singleton() {
    }
}
public class ThreadDemo3 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getSingleton();
        Singleton s2 = Singleton.getSingleton();
        //此时s1和s2是同一个对象
        System.out.println(s1 == s2);
//        Singleton s3 = new Singleton();
    }
}

分析: 在get方法中,只是单纯的读操作,没有修改,多个线程读同一个变量不会引起线程不安全的问题。
饿汉模式,认为式线程安全的,只是在读数据。

1.2 懒汉模式实现单例

核心思想:非必要,不创建,真正在使用的时候才去创建实例

class SingletonLazy {
    //懒汉模式
    private static SingletonLazy singletonLazy = null;
    //真正在使用的时候才去创建实例
    public static SingletonLazy getSingletonLazy() {
        if (singletonLazy == null) {
            singletonLazy = new SingletonLazy();
        }
        return singletonLazy;
    }

    private SingletonLazy() {
    }
}
public class ThreadDemo4 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getSingletonLazy();
        SingletonLazy s2 = SingletonLazy.getSingletonLazy();
        System.out.println(s1 == s2);
    }
}

分析:多线程下,可能无法保证懒汉模式创建对象的唯一性。

1.2.1 加锁实现懒汉模式线程安全

下面代码if和new不是原子的,因此需要加锁保证线程安全。

        if (singletonLazy == null) {
            singletonLazy = new SingletonLazy();
        }
        return singletonLazy;

则给if判定和new操作加锁:

        synchronized (SingletonLazy.class) {
            if (singletonLazy == null) {
                singletonLazy = new SingletonLazy();
            }
        }
        return singletonLazy;

但是加锁其实是一个比较低效的操作,因为加锁可能涉及到阻塞等待
上面代码,在任何时候调用getSingletonLazy()都会触发锁的竞争。
其实,此处的线程不安全,只出现在首次创建对象的时候,一旦对象new好了,后续调用getSingletonLazy(),就只是单纯的读操作,就没有线程安全的问题,这个时候就没必要再加锁了。
因此,非必要,不加锁。

    public static SingletonLazy getSingletonLazy() {
        if (singletonLazy == null) { //是否要加锁
            synchronized (SingletonLazy.class) {
                if (singletonLazy == null) { //是否要创建对象
                    singletonLazy = new SingletonLazy();
                }
            }   
        }
        return singletonLazy;
    }

判断是否要加锁,如果对象已经有了,就不必枷锁了,此时本身就是线程安全的。
注意:此处虽然2个if判断条件相同,但是如果调用的时间间隔长了,结果也可能不同,加锁可能导致阻塞,啥时候接触阻塞,沧海桑田!!!

1.2.2 volatile实现懒汉模式线程安全
                    singletonLazy = new SingletonLazy();

这个操作,可能会触发指令重排序:①创建内存(买房子)②调用构造方法(装修)③把内存地址赋给引用(拿到钥匙),其中②和③可以调换顺序。
如果t1执行了①和③,此时系统调度给t2,再去判断条件,发现条件不成立,非空,直接返回实例的引用,接下来t2继续调用,就会引发线程不安全。解决方法:

    volatile private static SingletonLazy singletonLazy = null;

1.3 饿汉模式和懒汉模式小结(面试题)

  1. 饿汉模式:天然就是安全的,只有读操作
  2. 懒汉模式:不安全的,有读也有写
    • ①加锁,把if和new变成原子操作
    • ②双重if,减少不必要的加锁操作
    • ③使用volatile禁止指令重排序,保证后续线程肯定拿到的是完整的对象
class SingletonLazy {
    //懒汉模式
    volatile private static SingletonLazy singletonLazy = null;
    
    public static SingletonLazy getSingletonLazy() {
        if (singletonLazy == null) {
            synchronized (SingletonLazy.class) {
                if (singletonLazy == null) {
                    singletonLazy = new SingletonLazy();
                }
            }
        }
        return singletonLazy;0
    }

    private SingletonLazy() {
    }
}
public class ThreadDemo4 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getSingletonLazy();
        SingletonLazy s2 = SingletonLazy.getSingletonLazy();
        System.out.println(s1 == s2);
    }
}

2. 阻塞队列

阻塞队列是带有阻塞特性的队列,是线程安全的。

  1. 如果队列空,尝试出队列,就会阻塞等待,等待到队列不空为止;
  2. 如果队列满,尝试入队列,就会阻塞等待,等待到队列不满为止。

2.1 单线程下阻塞队列

import java.util.concurrent.*;
//阻塞队列
public class ThreadDemo5 {
    public static void main(String[] args) throws InterruptedException {
        //BlockingQueue是一个接口不能直接new,我们new他的实现类
        BlockingQueue<String> queue = new LinkedBlockingQueue<>();
        //阻塞队列的核心方法主要有2个,入队列和出队列
        queue.put("hello1");
        queue.put("hello2");
        queue.put("hello3");
        queue.put("hello4");
        queue.put("hello5");
        String ret = queue.take();
        System.out.println(ret);
        ret = queue.take();
        System.out.println(ret);
        ret = queue.take();
        System.out.println(ret);
        ret = queue.take();
        System.out.println(ret);
        ret = queue.take();
        System.out.println(ret);
        ret = queue.take();
        System.out.println(ret);
        ret = queue.take();
        System.out.println(ret);

    }
}

运行结果:
在这里插入图片描述
分析:JAVA标准库提供了阻塞队列的使用,在上述代码中,put了5次,take6次,前5次take都很顺利,第6次take就阻塞了。

2.2 多线程下阻塞队列——生产者消费者模型

生产者、消费者这样的角色是针对某个资源来说的。
生产者:生产某个资源;
消费者:消耗某个资源;
生产者和消费者之间交互数据需要用到一个交易场所(阻塞队列)。

生产者消费则最主要解决2方面问题:
1.可以让上下游模块之间,进行更好的“解耦合”
2.削峰填谷

基于阻塞队列,写个生产者消费者模型。

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class ThreadDemo6 {
    public static void main(String[] args) {
        BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();

        //消费者
        Thread t1 = new Thread(() -> {
            while (true) {
                try {
                    int value = blockingQueue.take();
                    System.out.println("消费元素" + value);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        //生产者
        Thread t2 = new Thread(() -> {
            int value = 0;
            while (true) {
                try {
                    System.out.println("生产元素" + value);
                    blockingQueue.put(value);
                    value++;
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t2.start();
    }
}

运行结果:
在这里插入图片描述

2.3 模拟写一个阻塞队列+生产者消费者

实现阻塞队列,分3步:
①先实现一个普通的队列
②加上线程安全(锁 + 内存可见性)
③加上阻塞功能(wait notify)

//阻塞队列
class MyBlockingQueue {
    volatile int rear; //队尾
    volatile int front; //队头
    volatile int size; //数量
    int[] array = new int[1000]; //数组

    //入队
    synchronized public void in(int x) throws InterruptedException {
        //判断队列是否满
        while (size == array.length) {
            //队列满了
            this.wait();
        }
        array[rear] = x;
        rear++;
        if (rear == array.length) {
            //达到末尾,就需要从头开始
            rear = 0;
        }
        size++;
        this.notify();
    }

    //出队
    synchronized public int out() throws InterruptedException {
        //判断队列是否为空
        while (size == 0) {
            this.wait();
        }
        int val = array[front];
        front++;
        if (front == array.length) {
            front = 0;
        }
        size--;
        this.notify();
        return val;
    }
}
public class Test {
    public static void main(String[] args) {
        MyBlockingQueue myBlockingQueue = new MyBlockingQueue();
        //生产者消费者模型
        Thread t1 = new Thread( () -> {
            //消费者
            while (true) {
                synchronized (myBlockingQueue) {
                    try {
                        System.out.println("消费了" + myBlockingQueue.out());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        Thread t2 = new Thread(() -> {
            int val = 0;
            //生产者
            while (true) {
                synchronized (myBlockingQueue) {
                    try {
                        myBlockingQueue.in(val);
                        System.out.println("生产了" + val);
                        Thread.sleep(1000);
                        val++;
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t1.start();
        t2.start();
    }
}

3. 定时器

3.1 定时器小例子

public class ThreadDemo {
    public static void main(String[] args) {
        //定时器
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello2");
            }
        },2000);
        System.out.println("hello1");
    }
}

运行结果:
在这里插入图片描述
分析:

  1. 标准库提供了一个Timer类,Timer类的核心方法尾schedule
  2. schedule包含2个参数,第1个参数是指定即将执行的任务代码,第二个参数是指定多次时间之后执行(单位是毫秒)。
  3. 可以发现上述代码在运行之后,进程并没有结束,这也说明Timer内置的线程是前台线程会阻止进程结束。

3.2 实现定时器——优先级阻塞队列

定时器内部管理的不仅仅是一个任务,可以管理很多任务。
虽然任务有很多,但是它们触发的时间是不同的,只需要有一个/一组工作线程,每次找到这些任务中最先到达时间的任务,一个线程先执行最早的任务,做完了之后再执行第二早的。(堆、优先级队列——阻塞优先级队列)

import java.util.concurrent.PriorityBlockingQueue;

//任务对象
class MyTask implements Comparable<MyTask>{
    public Runnable runnable;
    public long time;
    public MyTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        this.time = delay + System.currentTimeMillis();
    }

    @Override
    public int compareTo(MyTask o) {
        //这样意味着每次取出的是最小的元素
        return (int)(this.time - o.time);
    }
}

class MyTimer {
    private PriorityBlockingQueue<MyTask> priorityBlockingQueue = new PriorityBlockingQueue<>();

    public void schedule(Runnable runnable, Long delay) {
        MyTask myTask = new MyTask(runnable,delay);
        priorityBlockingQueue.put(myTask);
        synchronized (this) {
            this.notify();
        }
    }
    public MyTimer() {
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    synchronized (this) {
                        MyTask myTask = priorityBlockingQueue.take();
                        long curTime = System.currentTimeMillis();
                        if (myTask.time <= curTime) {
                            myTask.runnable.run();
                        }else {
                            //时间没到的话,就把刚刚取出来的任务塞回去
                            priorityBlockingQueue.put(myTask);
                            wait(myTask.time - curTime); //不忙等
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

public class ThreadDemo2 {
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello4");
            }
        },4000L);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello3");
            }
        },3000L);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello2");
            }
        },2000L);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello1");
            }
        },1000L);
        System.out.println("hello");
    }
}

运行结果:
在这里插入图片描述

4. 线程池

池的目的是为了提高效率,标准库提供了现场的线程池。

线程的创建,虽然比进程轻量,但是再频繁创建的情况下,开销也是不可忽略的,为啥从池子里拿线程比从系统创建线程更高效?
从线程池拿线程,是纯粹的用户态操作。 从系统创建线程,涉及到用户态和内核态之间的切换,真正的创建还是在内核态完成的。
结论:纯用户态操作时间是可控的,涉及到内核态操作时间就不太可控了。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadDemo3 {
    public static void main(String[] args) {
        //执行服务 线程池,此处并非是直接new ExecutorService对象,而是通过Executors类里面的静态方法完成的对象构造
        //new隐藏在方法内部的,创建对象不再直接new,工厂模式
        ExecutorService pool = Executors.newFixedThreadPool(10);
        //添加任务到线程池中
        pool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        });
    }
}

4.1 拒绝策略(4种)

标准库提供了4种拒绝策略

  1. 如果满了,继续添加任务,添加操作直接抛出异常
  2. 添加的线程自己负责执行这个任务
  3. 丢弃最老(最先安排)的任务
  4. 丢弃最新的任务

4.2 实现线程池

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class MyThreadPool {
    //阻塞队列用来存放任务
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
    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 {
                    while (true) {
                        Runnable runnable = queue.take();
                        runnable.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            t.start(); //启动线程
        }

    }
}
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool pool = new MyThreadPool(10);
        //自己实现线程池
        for (int i = 0; i < 1000; i++) {
            int n = i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello" + n);
                }
            });
        }
        Thread.sleep(1000);
    }
}

分析:此处可以看到,线程池中任务执行的顺序和添加顺序不一定相同,因为这10个线程是无序调度的。

当前代码,搞了10个线程的线程池,实际开发中,一个线程池的线程数量设计成多少比较合适?
答:线程不是越多越好,线程的执行本质是要在cpu上调度的,具体要通过测试的方式来确定。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

CodeKnightShuai

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

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

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

打赏作者

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

抵扣说明:

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

余额充值