Java中的多线程(下)

作者:~小明学编程 

文章专栏:JavaEE

格言:热爱编程的,终将被编程所厚爱。
在这里插入图片描述

目录

多线程案例

单例模式

饿汉模式

懒汉模式

阻塞式队列

为什么要引入阻塞队列

Java中的阻塞队列

模拟实现阻塞队列

定时器

标准库中的定时器

模拟实现定时器

MyTask类

MyTimer

线程池

为什么需要线程池

标准库中的线程池

模拟实现线程池


多线程案例

单例模式

单例模式下会保证我们的实例只有一份,单例模式分为两种一种是饿汉模式,一种是懒汉模式,下面我们对两种模式分别的进行介绍。

饿汉模式

//实现单例模式-饿汉模式
class Singleton{
    //在类里面就创建一个实例,该实例是唯一的。
    private static Singleton instance = new Singleton();
    //我们将构造方法设为私有这样就保证我们无法再创建实例了
    private Singleton() {}
    //提供公开的一个方法使得我们获取到这个唯一的实例
    public static Singleton getInstance() {
        return instance;
    }
}

前面说到要保证只有一份实例所以我们的构造方法必须是私有的这样才能保证不会再new新的对象,然后我们还得在类里面创建一个静态的实例,因为是静态的所以能保证其唯一性,最后再通过一个静态的方法来获取我们那个唯一的实例。

所谓的饿汉模式就是很着急,我们刚开始无论你用不用到都会创建一个实例,这样就会有一个好处,那就是我们不用担心线程的安全,因为我们只需要读不用更改,我们这个类加载完之后实例就创建好了。

懒汉模式

class Singleton1 {
    private static Singleton1 instance = null;
    private Singleton1(){}

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

懒汉模式就是我们在刚开始的时候不去创建实例,等我们第一次用的时候我们再去创建实例,好处就是节省了资源的开销,但是这样写会有问题,当我们多个线程同时去竞争第一次创建的时候就会产生线程安全问题,会同时的创建多个实例,所以我们就得加锁。

class Singleton1 {
    private static volatile Singleton1 instance = null;
    private Singleton1(){}
    public static Singleton1 getInstance() {
        if (instance==null) {
            synchronized (Singleton1.class) {
                if (instance==null) {
                    instance = new Singleton1();
                }
            }
        }
        return instance;
    }
}

首先我们先说说下面这两个if()判断,第一个判断是判断我们当前是不是要加锁如果我们已经有实例对象了就不用再去加锁浪费资源了,如果我们判断确实是第一次使用,这个时候可能会有很多个线程都通过了这个if()语句,然后大家开始开始竞争这把锁,竞争到了之后再次判断一下我们是不是第一次使用,如果是第一次的话我们要new()这个实例对象,如果我们没有这个if()语句那就是多个线程轮流来new()这个实例了。

此外我们要对instance的实例加上一个volatile的关键词来防止指令的重排序,防止编译器的优化导致其它的线程可能读到一个非空的instance。

阻塞式队列

阻塞式队列和普通的队列一样都是具有先进先出的特点不同的是阻塞队列是属于线程安全的,当我们的队列为空的时候我们还想要出的时候就会进入阻塞状态直到队列里面有元素才能出,当我们的队列满的时候我们还想要入队列的时候此时也会进入阻塞的状态,直到我们出了一个元素才会结束阻塞的状态。

为什么要引入阻塞队列

要想知道为什么我们要引入阻塞队列我们首先就要了解生产者与消费者的模式,所谓的生产者与消费者的模型就是一个人负责生产,一个人负责消费,如果其中的一个人生产的过多了,我们的消费者的压力就会很大,因为都要进行消费,消费量一下子就大了起来。

这样我们的生产者就影响到了我们的消费者,这就不遵循我们代码的规范我们要尽可能的保证高内聚,低耦合,显然刚刚耦合过高了,这个时候我们就又想了一个办法那就是引用一个队列,我们把生产者所有生产的东西都放到这个队列里面去,然后消费者稳定消费就行了,这样各干各的互不影响。

Java中的阻塞队列

BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性

    public static void main(String[] args) throws InterruptedException {
        BlockingDeque<Integer> blockingDeque = new LinkedBlockingDeque<>();
        blockingDeque.put(5);
        System.out.println(blockingDeque.take());//5

    }

我们通常采用put()和take()方法,其它的方法不具有阻塞性质。

模拟实现阻塞队列

class MyBlockingDeque {
    int[] queue = new int[100];
    int head = 0;//队列头
    int tail = 0;//队列尾
    int size = 0;//已经存在的元素个数
    private Object object = new Object();
    public void put(int val) throws InterruptedException {
        synchronized (object) {
            if (size==queue.length) {//如果队列已经满了,那就阻塞
                object.wait();
            }
            queue[tail] = val;
            tail++;
            if (tail>= queue.length) {
                tail = 0;
            }
            size++;
            object.notify();//唤醒阻塞
        }

    }
    public int take() throws InterruptedException {
        synchronized (object) {
            if (size==0) {//想要拿而且还为空那就阻塞等待里面有元素
                object.wait();
            }
            int ret = queue[head];
            head++;
            if (head>= queue.length) {
                head = 0;
            }
            size--;
            object.notify();//唤醒阻塞
            return ret;
        }
    }

}
public class Demo9 {
    private static MyBlockingDeque myBlockingDeque = new MyBlockingDeque();

    public static void main1(String[] args) throws InterruptedException {
        myBlockingDeque.put(1);
        myBlockingDeque.put(2);
        System.out.println(myBlockingDeque.take());
    }
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                int num = 0;
                while (true) {
                    System.out.println("入队"+num);
                    num++;
                    try {
                        myBlockingDeque.put(num);
//                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread.start();
        Thread thread1 = new Thread(()->{
            while (true) {
                try {
                    Thread.sleep(500);
                    System.out.println("出队"+myBlockingDeque.take());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread1.start();
    }

}

这里我们对队列进行了测试,发现当我们阻塞的时候就会停下来然后等待唤醒重新继续运行程序。

定时器

所谓的定时器就像我们的闹钟一样,到了特定的时间就去执行某项工作,我们Java中也提供了类似的功能,也就是到达指定的时间再去执行。

标准库中的定时器

1.标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule。

2.schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后
执行 (单位为毫秒)。

    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("3000");
            }
        },3000);
        System.out.println("main");

    }

这样看上去我们还是不够了解其具体的情况,然后我们试着来实现这样一个类,方便我们更加深刻的了解其原理。

模拟实现定时器

MyTask类

Mytask类的功能主要就是描述我们的任务单位,我们每个任务包含了哪些属性,比如说最重要的两点我们的任务要有我们执行的代码,还要有我们的时间(啥时候执行),这都是中古要的信息,现在我们要把他们都放在一起然后统一描述。

class MyTask implements Comparable<MyTask>{
    private Runnable runnable;//定义一个接口
    private long time;//记录时间
    //创建任务的时候就传入相应的接口和时间
    public MyTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis()+delay;//时间是将要执行代码的时刻
    }
    public void run() {
        runnable.run();
    }
    public long getTime() {
        return time;
    }

    //重写比较方法,比较向后执行的顺序
    @Override
    public int compareTo(MyTask o) {
        return (int) (this.time - o.time);
    }
}

首先就是定义两个变量,runnable用于描述我们要执行的代码,接着就是time告诉我们啥时候要去执行这些代码。

接着是我们的构造方法,构造方法中要传入任务和时间,其中时间要加上我们当前的时间戳。

最后看到我们还要实现我们的comparable接口然后重写compareTo这个方法,因为我们的任务不止一个后面将会做比较哪个任务最近。

MyTimer

class MyTimer {
    //用优先级队列存放我们的多个任务
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    public void schedule(Runnable runnable,long relay) {
        MyTask myTask = new MyTask(runnable,relay);
        queue.put(myTask);
        synchronized (locker) {
            locker.notify();
        }
    }
    private Object locker = new Object();
    public MyTimer() {
        Thread thread = new Thread() {
            @Override
            public void run() {
                while (true) {
                      try {
                        MyTask task = queue.take();
                        long time = System.currentTimeMillis();//获取当前的时间
                        //如果当前的时间不到任务的时间
                        if (time<task.getTime()) {
                            queue.put(task);//放回去
                            synchronized (locker) {
                                locker.wait(task.getTime()-time);//控制等待时间避免cpu空转
                            }
                        } else {
                            task.run();
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            }
        };
        thread.start();
    }

}

首先我们要用一个容器来存放我们的多个任务,在多个数据结构的对比之下我们的优先级队列是最适合的,因为我们首先要找到我们的最小值,也就是距离我们执行任务最近的那个单位,堆刚好能满足并且调整的时间复杂度还很低。

先说说我们的schedule这个方法,该方法主要就是传达任务的,告诉我们任务的内容和时间,然后下面就是构造方法了,首先先开启一个线程,然后拿出我们的最前面的任务判断时间是否合理,如果不合理的话这个时候我们就将任务再次放回去并且开始阻塞,等待相应的时间再重新开始,方式无意义的循环,同时我们每放一个任务也会唤醒线程,重新找到最近时间的任务。

线程池

为什么需要线程池

就和字符串常量池一样,我们用线程池的核心思想也是为了节省资源,我们开辟和回收一个线程都是要消耗时间的,所以我们就想我们搞一个线程池,里面放着指定数量的线程,用到的时候直接就可以拿来用了,不用再去开辟了,大大的节省了时间,相应的也会消耗一定的资源,线程池最大的好处就是减少每次启动、销毁线程的损耗。

标准库中的线程池

使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
返回值类型为 ExecutorService
通过 ExecutorService.submit 可以注册一个任务到线程池中

    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000000; i++) {
            final int a = i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello"+a);
                }
            });
        }

    }

Executors 创建线程池的几种方式

newFixedThreadPool: 创建固定线程数的线程池
newCachedThreadPool: 创建线程数目动态增长的线程池.
newSingleThreadExecutor: 创建只包含单个线程的线程池.
newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer

模拟实现线程池

//模拟实现线程池类
class MyThreadPoll {
    //1.描述一个任务,用runnable来实现
    //2.用一个数据结构(阻塞队列)来存放我们的任务
    private LinkedBlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();
    //3.描述一个线程,工作线程的任务就是从队列中取出任务然后执行
    static class Worker extends Thread {
        private LinkedBlockingDeque<Runnable> queue = null;
        //从主类里面调用出我们的queue
        public Worker(LinkedBlockingDeque<Runnable> queue) {
            this.queue = queue;
        }

        //我们此线程的工作就是从队列里面拿任务然后执行
        @Override
        public void run() {
            while (true) {
                try {
                    Runnable runnable = queue.take();
                    runnable.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    private List<Thread> list = new ArrayList<>();

    public MyThreadPoll(int n) {//选择我们想要放的线程数量
        for (int i = 0; i < n; i++) {
            Worker worker = new Worker(queue);//new一个新线程
            worker.start();//开始线程
            list.add(worker);//将线程放入线程池list
        }
    }
    //将我们的任务加到任务队列里面去
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }
}

核心思想是一个放任务,然后我们的构造方法一直的处理任务。

首先我们用一个阻塞队列来存放我们的任务,然后我们用静态内部类的方式来构造组织一个我们的任务任务的目的就是执行里面的run()方法,然后我们在调用submit的时候就是往队列里面放任务,queue里面有任务了就不阻塞了构造方法已经开启了我们指定的线程,这些线程都堵塞在take()那里,所以一有任务就开始抢占执行。

猜你喜欢

转载自blog.csdn.net/m0_56911284/article/details/128356071