• 多线程案例


    多线程

    Thread类及常见方法

    线程的状态

    线程安全问题



    多线程案例

    多线程案例实际就是通过一些具体的例子来更好地理解多线程及多线程之间的并发编程。

    单例模式

    单例模式是一种常见的设计模式,而所谓的设计模式就好比是一副棋谱,包含了对一些情况的固定套路和应对方法;而这里的设计模式就是软件开发中针对一些特定的问题场景所总结出来的一套“棋谱”。
    那么什么又是单例模式呢?
    前面说到,单例模式就是创建类型的一种常用的软件设计模式。一般使用单例模式的目的就是保证某个类在程序中只有一个实例,单例模式有2种具体的实现方式,即懒汉模式和饿汉模式。

    饿汉模式

    什么是饿汉模式?即程序一旦启动,就会立即创建实例。
    如何理解饿汉模式呢?就以其字面的叫法来理解,就好比如果一个人饿了很久,那么必然在见到食物的一瞬间,就会立即开始摄入食物。
    下面是其具体的代码实现:

    //饿汉模式
     class Singleton {
        //类加载的同时就创建实例
        private static Singleton instance =new Singleton();
        //一个私有的构造方法
        private Singleton(){
    
        }
        public static Singleton getInstance(){
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    懒汉模式

    与饿汉模式不同,懒汉模式在程序启动的同时,并不会创建实例,直到第一次使用时才真正创建实例。
    可以近似的理解为,一个懒惰的人在并没有那么饿的情况下想吃东西,但不会真正去吃什么,直到它真正饿的时候才去找食物吃(只是为了理解这种程序启动但不会立即创建实例的模式,不要较真哦~~~)

    //懒汉模式
    public class SingletonLazy {
        //类加载,但暂时不创建实例
        private static SingletonLazy instance=null;
        private SingletonLazy(){
        }
        public static SingletonLazy getInstance(){
            if (instance==null){
                //在第一次使用时,才创建实例
                instance=new SingletonLazy();
            }
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    很明显,对于懒汉模式而言,如果有多个线程同时调用getInstance()方法,就可能会创建出多个实例,而创建多个实例对于单例模式而言自然是不被允许的,自然也就会产生线程安全问题,如何解决这种线程安全问题呢?
    下面是线程安全版本的懒汉模式:

    class SingletonLazy{
        //对于涉及到读写操作的变量加上volatile
        private static volatile SingletonLazy instance=null;
        private SingletonLazy(){}
        public static SingletonLazy getInstance(){
            //首先判断是否已经创建了实例
            if (instance==null){
                synchronized (SingletonLazy.class){
    
                    if (instance==null){
                        instance=new SingletonLazy();
                    }
                }
            }
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    与上面的单线程版本的懒汉模式相比,这里线程安全版本的懒汉模式主要是做了这样几处改动:

    • 加锁
      由于加锁再进行解锁是一种开销极大的操作,同时懒汉模式的线程不安全问题也只是存在于首次创建实例,因此后续使用就不必进行加锁了;
    • 使用双重if判断
      外层if主要是判定当前是否已经创建了instance实例;
    • 给instance加上volatile
      由于对变量进行的读写操作,为了避免“内存可见性”带来的线程安全问题,加上关键字volatile;

    阻塞式队列

    阻塞式队列首先是一个队列,因此满足“先进先出”的基本原则;而阻塞队列与普通队列的不同主要体现在:

    • 当阻塞队列队满,如果继续入队就会使队列阻塞,直到有元素出队;
    • 当阻塞队列为空,尝试出队就会使队列阻塞,直到有元素入队;

    下面生产者消费者模型就是阻塞队列的一个典型模型:

    生产者消费者模型

    import java.util.Random;
    import java.util.concurrent.BlockingQueue;
    import java.util.concurrent.LinkedBlockingQueue;
    
    public class Demo {
        public static void main(String[] args) throws InterruptedException {
            //使用阻塞队列作为一个数据缓冲区
            BlockingQueue<Integer> blockingQueue=new LinkedBlockingQueue<>();
            //消费者线程
            Thread customer =new Thread(()->{
                while(true){
                    try {
                        //从数据缓冲区中取到数据
                        int value=blockingQueue.take();
                        System.out.println("消费元素:"+value);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"消费者");
            customer.start();
    
            //生产者线程
            Thread producer=new Thread(()->{
                while(true){
                    //一个随机函数
                    Random random=new Random();
                    while(true){
    
                        try {
                            //生产的元素用随机函数来生成
                            int num=random.nextInt(1000);
                            System.out.println("生产元素:"+num);
                            blockingQueue.put(num);     //元素放入数据缓冲区
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            },"生产者");
            producer.start();
            customer.join();
            producer.join();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    在这里的阻塞队列就相当于一个存放元素的数据缓冲区或者是一个放置元素的容器;通过这样一个容器,避免了生产者线程和消费者线程的直接交互,降低了两者之间的耦合性;
    另外,使用阻塞队列来充当这里的数据缓冲区,也是由于阻塞队列是线程安全的,即不会出现在两个线程并发地操作阻塞队列的过程中出现数据不一致的情况,也不会出现在多个线程并发更改共享数据而造成脏数据的情况;

    阻塞队列的实现

    这里阻塞队列的模拟实现是以循环队列的思想完成的:

    public class MyBlockQueue {
        //使用一个数组来实现队列
        private int [] items=new int[1000];
        private volatile int size=0;    //队列元素个数
        private int head=0;     //队列的头部元素
        private int tail=0;     //队列的尾巴元素
    
        //添加元素
        public void put(int value) throws InterruptedException {
            synchronized (this){
                //队列为满时阻塞等待
                while (size==items.length){
                    wait();
                }
                
                items[tail]=value;
                tail=(tail+1)&items.length;
                size++;
                notifyAll();
            }
        }
    
        //取出元素
        public int take() throws InterruptedException {
            int ret=0;
            synchronized (this){
                //队列为空,阻塞等待
                while (size==0){
                    wait();
                }
                ret=items[head];
                head=(head+1)%items.length;
                size--;
                notifyAll();
            }
            return ret;
        }
        
        public synchronized int size(){
            return size;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    需要保证其线程安全性,在必要的位置进行加锁操作或对需要读写的变量加volatile 进行修饰;

    定时器

    所谓定时器是开发中比较常见的一个组件,通过使用定时器,可是使代码在一段规定的时间之后再被执行;而实现定时器就涉及到了多线程;
    下面代码就是标准库中定时器的使用:

    import java.util.Timer;
    import java.util.TimerTask;
    
    public class Demo1 {
        public static void main(String[] args) throws InterruptedException {
            //Timer即标准库中提供的实现定时器的类
            Timer timer=new Timer();
            //schedule()是Timer类中的一个主要方法,用来安排需要执行的任务
            timer.schedule(new TimerTask() {
                @Override
                public void run() {
                    System.out.println("执行任务");
                }
            },1000);    //delay是schedule方法的第二个参数,表示安排的任务将在这个时间之后被执行
    
            while (true){
                System.out.println("main");
                Thread.sleep(1000);
    
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    运行结果:
    在这里插入图片描述看到在定时器中的任务等待执行的时间里,线程并不是在干等,其他的线程是可以正常执行的;而这之后程序没有自己停止的原因其实也正是定时器涉及到多线程的一个表现,由于Timer类中是自己开启了一个线程去执行任务的,但在任务执行完毕以后这个线程却并没有被销毁,因此整个的进程没有结束;

    定时器的模拟实现

    import java.util.concurrent.PriorityBlockingQueue;
    
    
    //需要执行的任务
    class MyTask implements Comparable<MyTask>{
        //任务内容
        private Runnable command;
        //任务的执行时间
        private long time;
        //一个执行任务的构造方法
        public MyTask(Runnable command,long after){
            this.command=command;
            //执行的时间需要是一个绝对的时间,而不是在多久之后执行的相对时间,使用时间戳来实现
            this.time=System.currentTimeMillis()+after;
        }
        //调用Runnable的run方法来执行任务
        public void run(){
            command.run();
        }
        //获取时间
        public long getTime(){
            return time;
        }
    
        @Override
        public int compareTo(MyTask o) {
            //时间小的先执行,时间大的后执行
            return (int) (this.time=o.time);
        }
    }
    //手动实现的定时器类
    
    class MyTimer{
        //用来阻塞等待的锁对象
        private Object locker=new Object();
        //定时器中的任务根据时间顺序来执行,因此选择优先级队列来完成
        private PriorityBlockingQueue<MyTask>queue=new PriorityBlockingQueue<>();
        //对执行的任务进行安排
        public void schedule(Runnable command,long after){
            MyTask myTask=new MyTask(command,after);
            synchronized (locker){
                //把任务放入队列中等待执行
                queue.put(myTask);
                locker.notify();
            }
        }
    
        public MyTimer(){
            //启动一个线程来执行任务
            Thread t=new Thread(()->{
                while (true){
                    try {
                        synchronized (locker){
                            //队列为空,没有任务可以执行,等待
                            if (queue.isEmpty()){
                                locker.wait();
                            }
                            //从队列中取出一个任务
                            MyTask myTask=queue.take();
                            //记录当前的时间
                            long curTimer=System.currentTimeMillis();
                            //如果获取到的时间大于当前时间,表示需要等待
                            if (myTask.getTime()>curTimer){
                                queue.put(myTask);
                                locker.wait(myTask.getTime()-curTimer);
                            }else{
                                //直接执行
                                myTask.run();
                            }
                        }
    
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
    
                }
            });
            t.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81

    使用模拟实现的定时器:

    public class Demo2 {
        public static void main(String[] args) {
            MyTimer myTimer=new MyTimer();
            myTimer.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println("任务1");
                }
            },3000);
            myTimer.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println("任务2");
                }
            },2000);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这里插入图片描述
    定时器只是使当前的一个任务在一定时间之后再执行,在此期间,其他线程是可以进行正常执行的;

    over!

  • 相关阅读:
    Webpack5 Asset Module 使用小结
    基于Python的深度优先搜索----解决迷宫最短路径问题
    Spring是如何整合JUnit的?JUnit源码关联延伸阅读
    节流&防抖
    JavaScript入门--数组
    vue v-for 渲染大量数据卡顿的优化方案
    艾美捷RPMI-1640培养基L-谷氨酰胺的参数和配方
    详解虚拟机!京东大佬出品 HotSpot VM 源码剖析笔记(附完整源码)
    Biwen.Settings添加对IConfiguration&IOptions的集成支持
    C++官网 Tutorials C++ Language Program structure:Statements and flow control
  • 原文地址:https://blog.csdn.net/weixin_54175406/article/details/126678340