多线程常见案例

多线程常见案例

实现一个线程安全的单例模式

  1. 单例模式的概念就是代码中的某个类只有有一个实例,不能有多个

  2. 实际开发中常见且有用,且有些概念天生就应该是单例的,比如jdbc编程中的数据源DataSource就应该只有一个.

  3. 实际的单例模式有两个

    1. 饿汉模式(相对较为着急的就把单例给构造好了)
    2. 懒汉模式(只有在必要的时候才会把这个单例进行构造)

    具体:

饿汉(自身已经线程安全):

class MySingleton1{
    //1.加载类时就已经将单例进行构造,所以说比较着急,所以叫饿汉
    private static MySingleton1 instance = new MySingleton1();
    
    //2.提供一个私有构造方法,让类的调用者无法进行构造,进而避免了单例模式被破坏
    private MySingleton1(){};
    
    //3.提供一个获取该单例的接口
    public static MySingleton1 getInstance(){
        return instance;
    }
    
}
public class Demo6 {
    public static void main(String[] args) {
        MySingleton1 instance=MySingleton1.getInstance();//此处的instance就是单例的引用啦
        //MySingleton1 instance1=new MySingleton1();编译报错,无法自行构造单例
    }
}

懒汉:

(版本1)

class MySingleton2{
    //1.默认是null,且不着急进行构造
    private static MySingleton2 instance;
    
    //2.与饿汉类似,提供一个私有构造方法,让类的调用者无法自行构造单例(反射除外)
    private MySingleton2(){}
    
    //3.提供一个获取到单例的接口
    public MySingleton2 getInstance(){
        if(instance==null){
            instance=new MySingleton2();
        }
        return instance;
    }
}

不足:多线程并发时,如果若干个线程同时读到了为null的instance,那各个线程都会用getInstance获取到一个对象,单例模式此时被破坏,或者说此时已经出现了线程安全问题,那就不符合一个线程安全问题的单例模式的要求了.进而需考虑线程安全问题,即要使用synchronized

(版本2)

class MySingleton2{
    //1.默认是null,且不着急进行构造
    private static MySingleton2 instance;

    //2.与饿汉类似,提供一个私有构造方法,让类的调用者无法自行构造单例(反射除外)
    private MySingleton2(){}

    //3.提供一个获取到单例的接口
    public synchronized MySingleton2 getInstance(){
        if(instance==null){
            instance=new MySingleton2();
        }
        return instance;
    }
}

不足:获取单例的接口被synchronized修饰,表明多线程并发执行getInstance时,只有一个线程或率先获取到锁,那么这个线程将对instance进行构造初始化,而后续线程再获取到锁时,面对的已经是构造好的instance了,此时接口直接返回非null的已经构造好的instance.可见,线程安全问题得以解决,但是这里还是有问题:明显可以看出,我们只需要在instance进行构造初始化的时候价格锁就可以了,后续线程不需要进行构造了,只会涉及读的操作,只读是不会有线程安全问题的,那就不用加锁了,所以考虑仅在尝试构造时才进行加锁.

(版本3)

class MySingleton2{
    //1.默认是null,且不着急进行构造
    private static MySingleton2 instance;

    //2.与饿汉类似,提供一个私有构造方法,让类的调用者无法自行构造单例(反射除外)
    private MySingleton2(){}

    //提供一个专门上锁的对象
    Object locker=new Object();
    //3.提供一个获取到单例的接口
    public  MySingleton2 getInstance(){
        if(instance==null){
            synchronized (locker){//当然你给类对象上锁也是可以的,无所谓
                if(instance==null){
                    instance=new MySingleton2();
                }
            }
        }
        return instance;
    }
}

不足:首先说明双层if的原因:12行代码和14行代码的执行时间间隔可能已经经历了沧海桑田,意思就是说:线程1执行完12,且判断语句成立,即进入if语句体,与此同时线程2也完成了同样的操作进入了if,但是线程2率先给locker进行上锁,所以线程1陷入阻塞等待,此时线程而就对instance进行了构造初始化啦~!构造结束,线程2获取到单例,线程1接着执行13行代码,给locker上锁,然后检查instance不是null,if语句执行结束,然后获取到同样的单例.前述锁描述的过程就说明了为什么要加两层if语句,为的就是上一步读到的为null的instance下一步就已经被别的线程构造好了.到这里,已经解决了后续线程再通过接口获取单例时无脑上锁的不足.但是:synchronized修饰的代码块,并不能保证指令不被重排,此处就是15行代码,构造一个对象涉及到三步:在内存申请空间->构造一个对象->instance指向刚才申请的内存地址.姑且前三部编号为1->2->3,那指令重排有可能是:1->3->2,在3->2之间,还是前述的两个线程,其中的t1线程后续为locker上锁检查instance是否为空的时候,此时instance指向的是一块刚申请的内存地址呢(但是instance并没有构造好),所以instance此时是不为null的,那线程1就直接return了,那此时线程1返回的就是一个错误的单例,c语言中也提到,指针类型,一个对象都没构造后,光有一个引用指向一个空间,都没说这个引用是代表哪种对象,显然是不合理的.为此,instance应当加上volatile修饰,防止对instance进行修改(此处就是对instance进行构造)时发生指令重排.

(版本4,完整版本)

class MySingleton2{
    //1.默认是null,且不着急进行构造
    private static volatile MySingleton2 instance;

    //2.与饿汉类似,提供一个私有构造方法,让类的调用者无法自行构造单例(反射除外)
    private MySingleton2(){}

    //提供一个专门上锁的对象
    Object locker=new Object();
    //3.提供一个获取到单例的接口
    public  MySingleton2 getInstance(){
        if(instance==null){
            synchronized (locker){//当然你给类对象上锁也是可以的,无所谓
                if(instance==null){
                    instance=new MySingleton2();
                }
            }
        }
        return instance;
    }
}

总结:

  1. 双重if
  2. synchronized
  3. volatile

实现一个阻塞队列

  1. 基于队列,源于队列(先进先出)
  2. 相比较传统队列有一些新的功能:带有阻塞功能,线程安全

阻塞功能:队列满了,还想往队列里放东西,就会阻塞,直到有空位给你放了,才能放的进去

​ 队列为空,还想从队列里拿出点啥,就会阻塞,直到队列里又有了元素.


本文基于循环队列来实现一个阻塞队列:

class MyBlockingQueue{
    private int[] elem;//先不构造了吧,让用户可以构造一个自己想要的大小
    private int size;//记录有效数据的个数(此处判定慢不慢使用的的方法是看有效元素个数和数组长度之间的关系)
    private int head;//记录队头下标
    private int tali;//记录队尾的下标

    public MyBlockingQueue(int len){
        this.elem=new int[len];
    }
    //写一个专用于上锁的对象
    Object locker=new Object();
    //主要实现两个功能,拿出来和放进去
    //1.put(),对照标准库中BlockingQueue中带有的put()起的同名方法
    public void put(int data){
        synchronized (locker){
            if(this.size==this.elem.length){
                //应当设置成阻塞
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //可以正常放了
            this.elem[tali]=data;
            tali=(tali+1)%this.elem.length;//循环公式
            this.size++;
            //每次放进去一个元素最好都能提醒一下,能拿啦!~
            locker.notify();
        }
    }

    //2.take(),拿出队头元素
    public int take(){
        synchronized (locker){
            if(this.size==0){
                //应当陷入阻塞
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //有东西拿
            int front=this.elem[head];
            head=(head+1)%this.elem.length;//循环公式
            this.size--;
            //每次拿走一个元素,就提醒一下能放啦!~
            locker.notify();
            return front;
        }
    }
}

基于阻塞队列,实现一个生产者消费者模型⌚️

因为阻塞队列的存在,就像一个大坝,雨水比较多的季节,就把水蓄起来,等待干旱的季节,就可以把水放出来进行灌溉,起到了一个**“削峰填谷”**的目的.阻塞队列也起到了类似的功能,这里的生产者消费者模型描述的就是一个类似的过程,商品得要先生产出来,消费者才能去购买,反过来,只有消费者的消费能力足够的强,生产方才能继续生产更多的商品,否则就会出现货物囤积的现象了.

具体:(消费水平高)

class MyBlockingQueue{
    private int[] elem;//先不构造了吧,让用户可以构造一个自己想要的大小
    private int size;//记录有效数据的个数(此处判定慢不慢使用的的方法是看有效元素个数和数组长度之间的关系)
    private int head;//记录队头下标
    private int tali;//记录队尾的下标

    public MyBlockingQueue(int len){
        this.elem=new int[len];
    }
    //写一个专用于上锁的对象
    Object locker=new Object();
    //主要实现两个功能,拿出来和放进去
    //1.put(),对照标准库中BlockingQueue中带有的put()起的同名方法
    public void put(int data){
        synchronized (locker){
            if(this.size==this.elem.length){
                //应当设置成阻塞
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //可以正常放了
            this.elem[tali]=data;
            tali=(tali+1)%this.elem.length;//循环公式
            this.size++;
            //每次放进去一个元素最好都能提醒一下,能拿啦!~
            locker.notify();
        }
    }

    //2.take(),拿出队头元素
    public int take(){
        synchronized (locker){
            if(this.size==0){
                //应当陷入阻塞
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //有东西拿
            int front=this.elem[head];
            head=(head+1)%this.elem.length;//循环公式
            this.size--;
            //每次拿走一个元素,就提醒一下能放啦!~
            locker.notify();
            return front;
        }
    }
}
public class Demo8 {
    public static void main(String[] args) {
        MyBlockingQueue queue=new MyBlockingQueue(10);
        Thread producer=new Thread(()->{
            int num=0;
            while(true){
                queue.put(num);
                System.out.println("生产了"+num);
                num++;
                try {
                    Thread.sleep(1000);//生产一个商品就睡一秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();

        Thread consumer =new Thread(()->{
            while(true){
                int front=queue.take();
                System.out.println("消费了"+front);//有一个商品就消费一个商品,消费速度非常的快
            }
        });
        consumer.start();
    }
}
//打印:(商品一出来就被消费咯)
生产了0
消费了0
生产了1
消费了1
生产了2
消费了2
生产了3
消费了3
生产了4
消费了4

(生产水平高)

class MyBlockingQueue{
    private int[] elem;//先不构造了吧,让用户可以构造一个自己想要的大小
    private int size;//记录有效数据的个数(此处判定慢不慢使用的的方法是看有效元素个数和数组长度之间的关系)
    private int head;//记录队头下标
    private int tali;//记录队尾的下标

    public MyBlockingQueue(int len){
        this.elem=new int[len];
    }
    //写一个专用于上锁的对象
    Object locker=new Object();
    //主要实现两个功能,拿出来和放进去
    //1.put(),对照标准库中BlockingQueue中带有的put()起的同名方法
    public void put(int data){
        synchronized (locker){
            if(this.size==this.elem.length){
                //应当设置成阻塞
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //可以正常放了
            this.elem[tali]=data;
            tali=(tali+1)%this.elem.length;//循环公式
            this.size++;
            //每次放进去一个元素最好都能提醒一下,能拿啦!~
            locker.notify();
        }
    }

    //2.take(),拿出队头元素
    public int take(){
        synchronized (locker){
            if(this.size==0){
                //应当陷入阻塞
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //有东西拿
            int front=this.elem[head];
            head=(head+1)%this.elem.length;//循环公式
            this.size--;
            //每次拿走一个元素,就提醒一下能放啦!~
            locker.notify();
            return front;
        }
    }
}
public class Demo8 {
    public static void main(String[] args) {
        MyBlockingQueue queue=new MyBlockingQueue(10);
        Thread producer=new Thread(()->{
            int num=0;
            while(true){
                queue.put(num);
                System.out.println("生产了"+num);
                num++;
            }
        });
        producer.start();

        Thread consumer =new Thread(()->{
            while(true){
                int front=queue.take();
                System.out.println("消费了"+front);//有一个商品就消费一个商品,消费速度非常的快
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        consumer.start();
    }
}
//打印:(生产速度明显比消费速度要快很多呀)
生产了0
生产了1
消费了0
生产了2
生产了3
生产了4
生产了5
生产了6
生产了7
生产了8
生产了9
生产了10
生产了11
消费了1
消费了2
生产了12
消费了3
生产了13

实现一个定时器

标准库的定时器用法

public class Demo9 {
    public static void main(String[] args) {
        Timer timer=new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("我是定时器执行的任务");
            }
        },3000);//定时器启动之时开始计时,3后执行定时器的任务,任务执行完毕,定时器并没有关闭
    }
}

所以实现一个定时器,最主要的是设计一个容器:把任务和时间做存储,其次要提供一个布置任务的schedule方法.

实现一个定时器

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();//多态式调用,因为后续任务将是实现Runnable接口的实现类
    }

    public long getTime(){
        return this.time;
    }

    public int compareTo(MyTask o){
        return (int)(this.getTime()-o.getTime());
    }
}
class Mytimer{
    //1.安排一个容器能装任务,标准库中的线程安全的带有优先级的阻塞队列
    private PriorityBlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();

    //2.提供一个schedule方法
    public void schedule(Runnable runnable,long delay){
        MyTask task=new MyTask(runnable,delay);
        queue.put(task);
        synchronized (locker){
            locker.notify();//往堆里丢一个任务,就会让扫描线程去检查堆顶元素是不是快执行了
        }
    }
    Object locker=new Object();
    //3.在构造时开一线程执行里头的任务,也叫扫描线程
    public Mytimer(){
        Thread t=new Thread(()->{
            while(true){
                try {
                    MyTask front=queue.take();//若队列为空,会陷入阻塞
                    long curTime=System.currentTimeMillis();
                    if(curTime<front.getTime()){
                        queue.put(front);
                        //防止忙等
                        synchronized (locker){
                            locker.wait(front.getTime()-curTime);
                        }
                    }else{
                        front.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }

}
public class Demo10 {
    public static void main(String[] args) {
        Mytimer mytimer=new Mytimer();
        mytimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("恭喜你完成了定时器哦");
            }
        },3000);
    }
}

注意事项:

  1. 存放任务的容器是带有阻塞功能的优先级队列,再学堆的时候就已经说了,往堆里放自定义类型的时候,要么将自定义类实现Comparable接口,要么就专门给自定义类型写一个比较器,在构造优先级队列的时候,把这个比较器的实例丢进去进行构造,但是查看带有阻塞功能的优先级队列,发现没有只带一个比较器的构造方法,所以只能采用自定义类型实现Comparable接口的方法.
  2. 构造定时器的时候就开启扫描线程,这里我们要专门处理一个忙等问题,即将wait设置一个最长等待时间,就是当前线程扫描的到堆顶元素的执行时间(绝对时间戳)还差多少,每当队列中新加入了元素,扫描线程就需要重现检查堆顶的任务是不是要执行了,所以搭配着使用了wait和notify方法

实现一个线程池

线程池出现的原因:多线程并发时,也会涉及到线程的频繁创建与销毁,这也会带来一定的时间开销,那此时就产生了线程池的并发处理方式,其原理就是把内核态的创建线程改成了纯用户态的创建方式,内核态创建一个线程在时间等方面不可控,而纯用户态会立马执行,各方面都可控.所以采用线程池的方式并发,可以较调用start(),效率来的更高.

标准库中的基础线程池的用法

public class Demo5 {
    private static int num=0;
    private static Object locker=new Object();
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);//指定线程池里开10个线程
        for(int i=0;i<10;i++){
            synchronized (locker){//防止num++不是原子的
                executorService.submit(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("我是任务 "+num++);
                    }
                });
            }
        }
    }
}

自己实现一个线程池

class MyThreadPool{
    //1.线程池里要能描述一个任务,直接使用Runnable即可
    //2.组织任务,可以使用一个阻塞队列
    private static BlockingQueue<Runnable> queue=new LinkedBlockingDeque<>();
    //3.描述一个线程
    static class worker extends Thread{
        //1.获取到刚才阻塞队列里的任务
        private BlockingQueue<Runnable> queue;
        public worker(BlockingQueue<Runnable> queue){
            this.queue=queue;
        }

        @Override
        public void run() {
            while(true){
                try {
                    Runnable runnable=queue.take();
                    runnable.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    //4.组织线程
    private ArrayList<worker> workers=new ArrayList<>();

    //线程池的构造器
    public MyThreadPool(int n){
        for(int i=0;i<n;i++){
            MyThreadPool.worker worker=new MyThreadPool.worker(queue);
            worker.start();//非常容易忘记呀
            workers.add(worker);
        }
    }

    //5.提供一个注册任务的接口
    public void submit(Runnable runnable){
        try {
            this.queue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Conclusion

  1. 线程安全的单例模式
  2. 线程安全的阻塞队列的实现
  3. 基于上述阻塞队列实现一个生产者-消费者模型
  4. 实现一个定时器
  5. 实现一个线程池

你可能感兴趣的:(JavaEE初阶,单例模式,阻塞队列,定时器,线程池,生产者消费者模型)