多线程的一些案例

单例模式

这是一种常见的“设计模式”。

“设计模式”类似于“棋谱”。

场景:代码中的有些概念不应该存在多个实例,此时应该使用单例模式来解决

两种典型的方式实现单例模式:

1、饿汉模式:“饿”代表只要类被加载,就会立刻实例化 Singleton 实例,后续无论怎么操作,只要永远不使用 getlnstance,就不会出现其他的实例。

2、懒汉模式

类加载的时候,没有立刻实例化,第一次调用 getInstance 的时候才会真正实例化,如果要是代码一整场都没有调用getInstance 此时实例化的过程也就被省略了

 

那么单例模式和线程有什么关系呢?

刚才两种单例模式的实现方式中,饿汉是线程安全的,懒汉是线程不安全的。

原因:

首先回顾一下导致线程不安全的原因:1.线程的调度抢占式执行;2.修改操作不是原子的;3.多线程同时修改同一个变量;4.内存可见性;5.指令重排序。对于饿汉来说,多线程同时调用 getInstance,由于 getInstance 里只做了一件事:读取 instance 实例的地址,这就代表着多个线程在同时读取同一个变量,并不是修改,所以饿汉是线程安全的。

对于懒汉模式来说,多线程同时调用 getInstance ,getInstance中做了四件事:1.读取 instance 的内容;2.判断 instance 是否为 null;3.如果 instance 为 null,就 new 实例;4.返回实例的地址。在第二步操作中 new 实例会修改 instance 的值。所以是线程不安全的。

 

用一个时间轴来展示懒汉模式:

如何改进懒汉模式,让代码变成线程安全的?

第一种优化方式:加锁

下面展示一个错误的修改方式:

这样写,此时读取判断,操作和 new 修改操作让不是原子的,下面的操作为正确的解决办法

这两种写法都是正确的,认为上面的写法锁的粒度更小,下面的锁的粒度更大,(锁中包含的代码越多就认为“粒度”越大),一般代码的粒度越小越好。

另一种优化方式:在锁上方再加一个 if 后这样可以提高效率:

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

 

单例模式为了保证线程安全,涉及到三个要点:

  1. 加锁保证线程安全
  2. 双重 if 保证效率
  3. volatile 避免内存可见性引来的问题

阻塞队列

是并发编程中的一个重要基础组件,帮助我们实现“生产者-消费者模型”(这是一种典型的处理并发编程的模式)

“生产者-消费者模型”:

举一个生活中的例子,就是如果甲、乙、丙三个人需要一个桌子

他们需要的操作步骤就是:

1.用斧子砍树 2.把木头通过小车车运输 3.拼接成桌子

那么就会有两种情况:第一种情况是三个人每个人都操作一遍两个步骤;第二种情况就是甲砍树,乙运输,丙个人拼接桌子

第一种情况对于斧子(锁)的需求太高,而第二种情况就比较常见,其中第一个人就是生产者,另外两个人就是消费者

还有一个问题就是甲砍树太快,小车车放不下;或者乙运输的太快,丙不能很快拼接完成。

阻塞队列的特点也是如此,他是一个先进先出的队列:入队列的时候如果发现队列满了就会阻塞,直到有其他线程调用出队列操作让队列中有空位之后,才能继续入队列;如果出队列操作太快,队列空了额,继续出队列,也会阻塞,一直阻塞到有其他线程生产了元素,才能继续出队列


队列的基本操作:

  1. 入队列
  2. 出队列
  3. 取队首元素

阻塞队里只提供前两个操作,不支持取队首元素

        //阻塞版本的入队列
        public void put(int value) throws InterruptedException {

            synchronized (this) {
                if (size == array.length){
                    wait();
                }
                array[tail] = value;
                tail++;
                if (tail == array.length){
                    tail = 0;
                }
                size++;
                notify();
            }
        }

        //阻塞版本的出队列
        public int take() throws InterruptedException {
            int ret = -1;
            synchronized (this){
                if (size == 0){
                    wait();
                }
                ret = array[head];
                head = 0;
                if (head == array.length) {
                    head = 0;
                }
                size--;
                notify();
            }
            return ret;
        }
    }
}

体会上面的两个wait 操作,一个在队列满的时候阻塞,一个在队列空的时候阻塞,两个操作永远不会冲突

 

假设两个线程入队列,一个线程入队列,一个线程出队列,此时如果队列已经满了,两个入队列线程就会线程就阻塞了,此时如果出队列操作

如果多个线程 wait notify 的时候唤醒哪个线程由操作系统调度器说了算(程序员的角度理解就是随机的)

如果没有 wait 执行了 notify 没有影响,有线程在 wait ,notify 就就唤醒一个线程,没有线程 wait 不会有任何负面影响

 

public static void main(String[] args) {
        BlockingQueue blockingQueue = new BlockingQueue();
        //第一次让消费者消费的快一些,生产者慢一些
        //此时就会消费者等待
        //第二次让消费者生产的快一些,消费者慢一些
        //此时就会预期看到,生产者线程刚开始的时候会快速插入元素,直到队列满的时候就会阻塞
        //此时就要消费了以后才能生产
        Thread producer = new Thread(){
            @Override
            public void run(){
                for (int i = 0; i < 10000; i++) {
                    try {
                        blockingQueue.put(i);
                        System.out.println("生产元素:" + i);
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        producer.start();
        Thread consumer = new Thread(){
            @Override
            public void run(){
               while (true){
                   try {
                       int ret = blockingQueue.take();
                       System.out.println("消费元素:" + ret);
                       //
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
            }
        };
        consumer.start();
    }
}

运行效果:即使消费的比生产的快,但是还是要等生产完成后才能消费

 

 public static void main(String[] args) {
        BlockingQueue blockingQueue = new BlockingQueue();
        //第一次让消费者消费的快一些,生产者慢一些
        //此时就会消费者等待
        //第二次让消费者生产的快一些,消费者慢一些
        //此时就会预期看到,生产者线程刚开始的时候会快速插入元素,直到队列满的时候就会阻塞
        //此时就要消费了以后才能生产
        Thread producer = new Thread(){
            @Override
            public void run(){
                for (int i = 0; i < 10000; i++) {
                    try {
                        blockingQueue.put(i);
                        System.out.println("生产元素:" + i);
                        
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        producer.start();
        Thread consumer = new Thread(){
            @Override
            public void run(){
               while (true){
                   try {
                       int ret = blockingQueue.take();
                       System.out.println("消费元素:" + ret);
                       Thread.sleep(500);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
            }
        };
        consumer.start();
    }
}

 

第二次运行效果:刚开始生产者比较快就快速生产,直到队列满了,就阻塞等待消费者消耗了以后才继续生产

                                           

 

当有两个消费者线程的时候

当两个消费者都触发 wait 操作后,接下来当我们调用 notifyAll 的时候,就把上面两个线程都唤醒了,于是两个线程就都去重新获取锁:

消费者1 ,先获取到锁,于是就执行下面出队列操作(执行完毕释放锁)

消费者2,后获取到锁,于是也会执行下面的出队列操作,但是注意:刚才生产者生产的一个元素,已经被消费者1 线程给取走了,当前实际是一个空队列,如果强行往下执行取队里取素操作,就会出现逻辑错误。

定时器

相当于一个闹钟,进行任务的管理。

定时器是多线程编程中的一个重要/常用组件,应用场景非常广泛,网络编程中特别常见。

定时器的构成:

  1. 使用一个类来描述“一段逻辑”(一个要执行的任务),同时也要记录这个任务在什么时间点执行
  2. 使用一个阻塞优先队列来组织若干个 Task。(使用优先队列是为了保证队首元素就是要被最早执行的任务)【阻塞队列既支持阻塞的特性,又支持优先级的“先进先出”,本质上是一个“堆”】
  3. 需要一个扫描线程,不停的扫描,判定队首是否时间到。(扫描线程要循环的检测,队首元素是否需要执行,如果需要执行的话,就执行这个任务。)
  4. 实现一个方法 schedule,给定时器内部安排一个任务。
  5. 为了避免忙等,还需要引入一个额外的对象,让扫描线程借助这个对象进行 wait 。(使用带超时时间版本的 wait)

随意一个对象都可以放入优先队列中么?

答:优先队里而需要知道对象之间的大小关系,才能把优先级排出来(才能保证队首元素是优先级最高的)

优先队列中的元素必须是可比较的
比较规则的指定主要是两种方式:1、让 Task 实现 Comparable 接口 2、让优先队列构造的时候,传入一个比较器对象(Comparator)

标准库中其实已经提供了阻塞队列,定时器等基本组件,实际工作中,可以直接运用,下面的代码是为了理解原理,也是为了加深对多线程的掌握。

import java.sql.Time;
import java.util.concurrent.PriorityBlockingQueue;

public class ThreadDemo1 {
    //优先队列中的元素必须是可比较的
    //比较规则的指定主要是两种方式
    static class Task implements Comparable<Task> {
        //Runnable 中有一个 run 方法,就可以借助这个 run 方法来描述要执行的具体任务是什么
        private Runnable command;
        //time 表示什么时候来执行 command,是一个绝对时间(ms级别的时间戳)
        private long time;

        //构造方法的 after 参数表示:after 秒后执行(是一个相对时间)
        //这个相对时间的参数是为了而用起来方便
        public Task(Runnable command, long after) {
            this.command = command;
            this.time = System.currentTimeMillis() + after;
        }

        //执行具体的逻辑
        public void run(){
            command.run();
        }

        @Override
        public int compareTo(Task o) {
            return (int) (this.time - o.time);
        }
    }

    static class Worker extends Thread{
        private PriorityBlockingQueue<Task> queue = null;

        public Worker(PriorityBlockingQueue<Task> queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            //实现具体的线程执行内容
            while (true){

                try {
                    //1.取出队首元素,检查时间是否到了
                    Task task = queue.take();
                    //2.检查当前任务时间是否到了
                    long curTime = System.currentTimeMillis();
                    if (task.time > curTime){
                        //时间还没到,就把任务再放回队列中
                        queue.put(task);

                    }else {
                        task.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        }
    }

    static class Timer{
        //1.用一个 Task 类来描述任务
        //2.用一个阻塞队队列来组织若昂的任务,队首元素就是时间最早的任务
        private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
        //3.用一个线程循环扫描挡墙的阻塞队列队首元素,如果时间到,就执行任务
        public Timer(){
            //创建线程
            Worker worker = new Worker(queue);
            worker.start();
        }
        //4.还需要提供一个方法,让调用者能把任务安排进来
        public void schedule(Runnable command,long after){  //安排任务
            Task task = new Task(command,after);
            queue.put(task);
        }
    }

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

我设置的任务为运行5s 后输出一个“hehe”

 

为了更直观的看到效果,可以把主函数的内容改成每隔 2s 执行一次输出

运行10s 后的结果

忙等

举个例子:我们定一个上课的闹钟时间为 9:00,小明现在看了一下时间为 8:00,又过了一会儿又看了一下时间8:01,又过了一会儿又看了一下时间8:02,剩下的时间还有将近一个小时,还可以做的事情有很多,但是小明一直在看时间,等待上课,这种频繁的盯着表的行为就叫作忙等。

我们的线程就可能会出现这种问题,扫描线程极快的运行 while  循环,有可能会大量的资源浪费 CPU 资源进行比较时间和入队列出队列操作。为了解决这个问题,我们就要借助 wait / notify 来解决。有下面几种情况

  • wait() 死等,一直等到 notify 的通知过来
  • wait(time),等待是有上限的,如果有 notify 就被提前唤醒,如果没有 notify,时间到了也一样可以被唤醒。

代码阻塞在 wait 处,避免了频繁占用 CPU

解决忙等问题部分的代码:

    static class Worker extends Thread{
        private PriorityBlockingQueue<Task> queue = null;
        private Object mailBox = null;
        
        public Worker(PriorityBlockingQueue<Task> queue,Object mailBox) {
            this.queue = queue;
            this.mailBox = mailBox;
        }

        @Override
        public void run() {
            //实现具体的线程执行内容
            while (true){

                try {
                    //1.取出队首元素,检查时间是否到了
                    Task task = queue.take();
                    //2.检查当前任务时间是否到了
                    long curTime = System.currentTimeMillis();
                    if (task.time > curTime){
                        //时间还没到,就把任务再放回队列中
                        queue.put(task);
                        synchronized (mailBox){
                            mailBox.wait(task.time - curTime);
                        }
                    }else {
                        task.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        }
    }

    static class Timer{
        //为了避免忙等,需要使用 wait 方法
        //使用一个单独的对象来辅助进行 wait
        private Object mailBox = new Object();
        
        
        //1.用一个 Task 类来描述任务
        //2.用一个阻塞队队列来组织任务,队首元素就是时间最早的任务
        private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
        //3.用一个线程循环扫描挡墙的阻塞队列队首元素,如果时间到,就执行任务
        public Timer(){
            //创建线程
            Worker worker = new Worker(queue,mailBox);
            worker.start();
        }
        //4.还需要提供一个方法,让调用者能把任务安排进来
        public void schedule(Runnable command,long after){  //安排任务
            Task task = new Task(command,after);
            queue.put(task);
            synchronized (mailBox){
                mailBox.notify();
            }
        }
    }

在扫描线程内部加上 wait

在安排任务方法内部加上 notify 

 

线程池

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值