多线程案例(单例模式(饿汉-懒汉),阻塞队列,定时器,线程池)

目录

  • 单例模式
      • 饿汉模式
      • 懒汉模式
  • 阻塞队列
  • 定时器
  • 线程池
  • 总结

单例模式

饿汉模式

饿汉模式:在类加载的时候就创建实例.

class SingleTon{
	//在类加载的时候就创建了单一实例
    private static SingleTon instance = new SingleTon();
    //构造器设置成私有防止用户创建别的实例
    private SingleTon(){
    }
    //设置成static方便调用该实例
    public static SingleTon getInstance(){
        return instance;
    }
}

懒汉模式

懒汉模式:类加载的时候不创建实例,在第一次调用的时候才创建实例

class SingleTon1{
	//类加载额时候instance 为null
    private static SingleTon1 instance = null;
    private SingleTon1(){}
    public static SingleTon1 getInstance(){
    	//第一次调用时创建实例
        if(instance == null){
            instance = new SingleTon1();
        }
        return instance;
    }
}

上述懒汉模式的代码在单线程的情况下是可以正常运行的.但是当多个线程并发调用懒汉模式时,上述代码就会因为并发冲突产生一些问题.
出现问题的原因是因为当多个线程同时调用getInstance()方法时,判断instance是否为null和给instance创建实例这两个操作不是原子性的.
多线程案例(单例模式(饿汉-懒汉),阻塞队列,定时器,线程池)_第1张图片
因此在多线程环境下,我们需要给getInstance方法加锁.

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

加锁之后,多个线程在调用getInstance方法时就不会产生线程冲突.但此时又引发了一个问题:效率问题.我们知道单例模式只创建1个实例对象.因此当第1个线程抢占到锁并为instance创建实例之后,剩下的线程再调用getInstance方法就直接return就好.因此对于除第一个线程之外的线程加锁其实上是在浪费资源.因此我么可以在锁外面在加上一层判断语句.

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

这样我们就可以节省不必要的锁开销.但尽管如此,当线程的数量变多之后,最外层的判断语句就会被大量执行,这时就会引发内存可见性问题:即原本应该从内存中读取的instance被编译器自动优化到了从寄存器中读取.这时如果我们对instance进行了其他的操作导致instance重新变为null那么也不会触发创建实例的操作.因此我们需要给instance设置volatile属性来抑制内存可见性.

class SingleTon1{
    private static volatile SingleTon1 instance = null;
    private static Object locker = new Object();
    private SingleTon1(){}
    public static SingleTon1 getInstance(){
        if(instance == null) {
            synchronized (locker) {
                if (instance == null) {
                    instance = new SingleTon1();
                }
            }
        }
        return instance;
    }
}

至此,一个简单完整的懒汉模式则创建好了.懒汉模式相比较与饿汉模式而言效率更高,但需要注意的问题也更多.

阻塞队列

  1. 概念:阻塞队列是一种特殊的队列,它属于一种线程安全结构,当队列满时,有元素入队列就会处于阻塞状态;当队列空时,继续出队列也会产生阻塞.
  2. 应用场景:适用于"生产者消费者模型".社会中存在大量的消费者和生产者.生产者生产产品,消费者通过消费来获取生产者生产的产品.但如果仅存在生产者和消费者,就会出现很多问题,比如说供不应求,供大于求.因此"生产者消费者模型"产生.它主要是通过一个阻塞队列来充当缓冲区,生产者生产的产品会放入阻塞队列中,消费者消费时从阻塞队列中获取产品.阻塞队列可以平衡生产者和消费者的处理能力:当生产者短时间内生产了大量产品时而产生供大于求的问题时可以将大量产品放入阻塞队列中,然后等待消费者慢慢消费;同时阻塞队列也降低了生产者和消费者之间的耦合性.也就是把二者的一部分功能单独提现出来由阻塞队列实现.生产者只需要进行生产,消费者只需要进行消费即可.
  3. Java标准库中的阻塞队列
 public static void main(String[] args) throws InterruptedException {
        BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
        blockingQueue.put(10);
        int ans1 = blockingQueue.take();
        //ans2在取元素时会因为队列空而阻塞.
        int ans2 = blockingQueue.take();
    }
  1. 阻塞队列的简单实现
class MyBlockingQueue{
    private int[] item = new int[1000];
    private int front = 0;
    private int rear = 0;
    private int size = 0;
    synchronized public void put(int num) throws InterruptedException {
        if(size >= item.length){
            this.wait();
        }
        item[rear] = num;
        //模拟循环队列,当队尾指针指向队列的最后一个元素时,将其指向0
        if(rear == item.length-1){
            rear = 0;
        }else {
            rear++;
        }
        this.notify();
        size++;
    }
    synchronized public Integer take() throws InterruptedException {
        if(size == 0){
            this.wait();
        }
        int ans = item[front];
        //模拟循环队列,当队首指针指向队列的最后一个元素时,将其指向0
        if(front == item.length-1) {
            front = 0;
        }else{
            front++;
        }
        this.notify();
        size--;
        return ans;
    }
    public int getSize(){
        return size;
    }
}

对于阻塞队列处于阻塞状态,我们可以通过Thread类的wait()和notify()方法实现阻塞和唤醒的状态.

定时器

  1. 概念:定时器相当于一个"闹钟",通过给每个待执行的代码预先设定好执行时间使代码能够按照需求在指定时刻运行.
  2. Java标准库中的定时器:Java标准库中提供了一个Timer类,其核心方法就是schedule(runnable,time),第一个参数是具体需要执行的任务,第二个参数是指该任务从当前时刻开始推迟执行的时间
   public static void main(String[] args) throws InterruptedException {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("执行任务");
            }
        },2000);
    }
  1. 定时器的简单实现:根据Java标准库中的Timer,我们可以手动实现一个简单的定时器.首先我们需要思考用什么数据结构去描述定时器.我们知道定时器的功能是让多个待执行的任务按照其执行时间的先后一次执行.比较容易的想到用一个队列来实现.但考虑到其中各个任务在队列中的顺序,我们需要实现的是让其中最先到达执行时刻的任务放在队首.所以我们可以用一个优先级队列来实现,也就是堆.考虑到线程安全,我们可以用阻塞优先级队列来实现.
@SuppressWarnings("all")
class MyTask implements Comparable<MyTask>{
    //创建任务类:该类对象在传入的时候需要传入该任务的执行时刻和具体的任务.
    //因为任务类需要存储到堆中,所以要指定任务类在堆中的排列规则
    private Runnable runnable;
    private long delay;
    public MyTask(Runnable r,long d){
        runnable = r;
        delay = System.currentTimeMillis()+d;
    }
    public void run(){
        runnable.run();
    }
    @Override
    public int compareTo(MyTask t) {
        return (int)(this.delay - t.getDelay());
    }
    public long getDelay(){
        return this.delay;
    }
}
@SuppressWarnings("all")
class MyTimer{
    //创建优先级队列
    private PriorityBlockingQueue<MyTask> p = new PriorityBlockingQueue<>();
    private Object locker = new Object();
    public void MyTimer(){
        Worker worker = new Worker();
        worker.start();
    }
    //需要创建一个线程来持续不断的读取堆中的任务
    class Worker extends Thread{
        @Override
        public void run() {
            while(true){
                long time = System.currentTimeMillis();
                MyTask task = p.peek();
                //获取到当前堆中的首元素的待执行时刻和当前时刻进行比较
                if(task.getDelay() <= time){
                    task.run();
                    p.poll();
                }else{
                    //代码执行到这里说明任务还没有到达执行的时刻,所以需要等待.
                    synchronized (locker){
                        try {
                            locker.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
    public void schedule(Runnable runnable,long delay) {
        MyTask task = new MyTask(runnable,delay);
        p.offer(task);
        //考虑到在任务等待执行时刻到来的过程中仍有可能有任务入队列,且该任务的执行时刻要早于堆中的首任务,所以在
        //每次安排任务的时候都需要唤醒一次等待.
        synchronized (locker){
            locker.notify();
        }
    }

}

线程池

  1. 概念:将多个线程存储到一个"池子"当中,减少线程建立和销毁的损耗.当然线程池的开辟也需要消耗一部分资源,但这部分资源的消耗要小于每次创建线程,销毁线程的消耗.线程池就好比雇马.比如我是一个马商,为了节省在雇马市场租赁摊位的钱,我选择当有人需要雇马时,我就回仓库选取一匹马然后回到雇马市场把马雇给别人,当别人把马还回来之后我在把马牵回仓库.这就类比需要一个任务就创建一个线程的模式;但我发现这种模式其实并不能节约成本,所以我还是决定租赁摊位,将仓库中的一部分马放到摊位上,当别人雇马时直接从摊位上雇,雇完以后直接放回摊位里.如果生意异常火爆,摊位里的马全被雇走了,我再回仓库去牵其他的马.后一种模式中的摊位其实就是线程池.
  2. Java标准库中的线程池:Java标准库中提供了Executors创建线程,其有多种创建线程池的方式.
    1. newFixedThreadPool:创建固定线程数的线程池
    2. newCachedThreadPool:创建线程数目动态增长的线程池
    3. newSingleThreadExecutor:创建包含单个线程的线程池
    4. newScheduleThreadPool:设定延迟时间执行命令.
  3. 线程池的简单实现
class ThreadPool{
	//用一个阻塞队列来存储任务,阻塞队列可以保证线程安全
    private BlockingQueue<Runnable> p = new LinkedBlockingQueue<>();
	
	//创建工作线程
    static class Worker extends Thread{
        BlockingQueue<Runnable> q;
        public Worker(BlockingQueue q){
            this.q = q;
        }
        
        @Override
        public void run() {
            while(true){
                try {
                    Runnable r = q.take();
                    r.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    //用一个顺序表来模拟线程池存储多个工作线程
    private List<Worker> list = new ArrayList<>();
    public ThreadPool(int n){
        for(int i = 0; i < n; i++){
            Worker worker = new Worker(p);
            worker.start();
            list.add(worker);
        }
    }
    public List<Worker> getList(){
        return list;
    }
    //将任务呈递到阻塞队列中
    public void submit(Runnable r){
        p.offer(r);
    }
}

总结

本篇文章主要介绍了多线程的一些常见案例:单例模式,阻塞队列,定时器以及线程池并根据其原理进行了简单的实现.

你可能感兴趣的:(多线程,多线程,单例模式,阻塞队列,定时器,线程池)