【JavaSE】多线程常见案例

目录

一、单例模式

1.1 饿汉模式

1.2 懒汉模式

 1.3 多线程版懒汉模式

二、阻塞队列

2.1 特点:

2.2 作用:

2.3 标准库中的阻塞队列

2.4 自己实现阻塞队列

三、定时器

3.1 概念

3.2 标准库中的定时器

3.3 自己实现定时器

3.3.1 分析

3.3.2 具体实现

四、线程池

4.1 内核态和用户态

4.2 标准库中的线程池

4.2.1 ThreadPoolExecutor

4.2.2 Executors(ThreadPoolExecutor的封装)

4.2.3 面试题:如果按照实际场景显示的指定相关线程的参数,程序运行会更可控。那线程个数指定为几比较合适呢?

4.3 实现一个简单的线程池


一、单例模式

单例模式就能保证某个类在程序中只存在唯一一份实例,而不会创建出多个实例。这一点,在很多场景都得到了应用,如JDBC中的DataSource实例就只需要一个。

1.1 饿汉模式

class SingletonDataSource1{
//此处的static类成员的初始化时机是在“类加载的时候”,即程序一启动用到这个类,就会立刻加载,实例创建的时机比较早(比较着急)
    private static SingletonDataSource1 instance = new SingletonDataSource1();
    // 构造方法也是private,不能在内外被获取
    private SingletonDataSource1(){
    }
    public static SingletonDataSource1 getInstance(){
        return instance;
    }
}
public class try1 {
    public static void main(String[] args) {
        // 无论在代码中的哪个地方调用getInstance,得到的都是同一个实例
        SingletonDataSource1 dataSource1 = SingletonDataSource1.getInstance();
    }
}

1.2 懒汉模式

class SingletonDataSource3{
    private static SingletonDataSource3 instance = null;
    private SingletonDataSource3(){
    
    }
    public static SingletonDataSource3 getInstance(){
        if(instance == null){
            instance = new SingletonDataSource3();
        }
        return instance;
    }
}

饿汉模式相比,主要区别在于实例创建的时机不同了,不再是类加载的时候立即创建实例。而是在首次调用getInstance的时候,才会真正创建实例。

以上两个版本的代码,在多线程环境下是否会存在多线程安全问题呢?

对于饿汉模式来说,多线程调用getInstance,只是针对同一个变量来读。因此是线程安全的。

对于懒汉模式来说,多线程调用getInstance,大部分情况是读,但是也可能会修改。这个修改发生在未初始化之前,多个线程同时调用getInstance就可能导致多线程同时修改。因此,线程不安全。

 1.3 多线程版懒汉模式

将读和写打包成一个原子的操作,即instance == null和instance = new SingletonDataSource3();打包成一个原子的操作。

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

对于懒汉模式来说,线程不安全只是出现在未初始化的时候,一旦初始化成功,后续调用getInstance就变成了两次读操作,线程安全问题也就没有了。

但如果按照上述代码这样写,每次调用getInstance,都会触发这里的锁竞争,就是是在线程安全的情况下,仍然会触发锁竞争。

现在要想办法让这个加锁操作不要执行的太过频繁:在实例化之前加个锁,实例化之后,不再加锁

class SingletonDataSource3{
    private static SingletonDataSource3 instance = null;
    private SingletonDataSource3(){ }
    public static SingletonDataSource3 getInstance(){
        if(instance == null){ //这里是为了判断是否加锁
            synchronized (SingletonDataSource3.class){
                if(instance == null){ // 这里是判断是否要创建实例
                    instance = new SingletonDataSource3();
                }
            }
        }
        return instance;
    }
}

又有了一个新问题,如果当前有很多个线程,同时调用getInstance,就会涉及到很多个线程都去读instance的内存值,相当于CPU读取了内存很多次,就可能会触发编译器的优化。后续的读内存操作可能就不读了,而是直接读CPU寄存器了。因此,加上volatile,可以避免这种情况。

class SingletonDataSource3{
    private static volatile SingletonDataSource3 instance = null;
    private SingletonDataSource3(){ }
    public static SingletonDataSource3 getInstance(){
        if(instance == null){ //这里是为了判断是否加锁
            synchronized (SingletonDataSource3.class){
                if(instance == null){ // 这里是判断是否要创建实例
                    instance = new SingletonDataSource3();
                }
            }
        }
        return instance;
    }
}

二、阻塞队列

是一种比较特殊的队列,遵守先进先出,也是线程安全的队列。

2.1 特点:

带有阻塞功能,如果队列为空,尝试出队列就会阻塞,一直阻塞到队列不空。

如果队列满了,尝试进入队列,就会阻塞,一直阻塞队列不满为止。

基于阻塞队列,就可以实现生产者消费者模型。

2.2 作用:

在开发中起到服务器之间解耦合的作用

在请求突然暴增的时候,起到削峰填谷的效果。

2.3 标准库中的阻塞队列

public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> queue = new LinkedBlockingQueue<>();
        queue.put("hello");
        String s = queue.take();
    }

只有put和take这两个方法带有阻塞作用

2.4 自己实现阻塞队列

①通过循环队列的方式来实现

②使用synchronized关键字进行加锁控制

③put时,如果队列满了,就需要阻塞等待。等take一个元素时,队列就不满了,这时就可以唤醒这个线程。

④take时,如果队列为空,就需要阻塞等待。等put一个元素时,队列不空,就可以唤醒该线程了。

class MyBlockingQueue1{
    private int[] items = new int[1000]; //队列的初始大小
    private int size = 0;  //有效元素的个数
    private int head = 0;
    private int tail = 0;
    private Object locker = new Object(); // 定义一个锁对象
    // 入队列
    public void put(int value) throws InterruptedException {
        synchronized (locker){
            if(size == items.length){
                // 如果队列满了,就触发阻塞操作
                locker.wait();
                //当take后,队列不满,就会被唤醒
            }
            items[tail] = value;
            tail++;
            if(tail >=items.length){
                tail = 0;
            }
            size++;
            // 唤醒take的阻塞操作
            locker.notify();
        }

    }
    // 出队列
    public Integer take() throws InterruptedException {
        synchronized (locker){
            if(size == 0){
                // 阻塞等待,当队列不为空的时候就唤醒
                locker.wait();
            };
            int ret = items[head];
            head++;
            if(head>=items.length){
                head = 0;
            }
            size--;
            // 唤醒put的阻塞操作
            locker.notify();
            return ret;
        }
    }

}
public class try1 {
    // 使用该队列作为交易场所
    private static MyBlockingQueue1 queue1= new MyBlockingQueue1();

    public static void main(String[] args) {
        // 两个线程,一个作为生产者,一个作为消费者(这两种搞多个也是可以的)
        Thread producer = new Thread(() -> {
            int a = 1;
            while(true){
                try {
                    System.out.println("生产者生产了:"+a);
                    queue1.put(a);
                    a++;
                    // 如果给生产者加个sleep,生产慢,消费快,队列在这种情况下大部分是空的,消费者很多时间都在阻塞等待
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();
        Thread customer = new Thread(() -> {
            while (true){
                try {
                    int a = queue1.take();
                    System.out.println("消费者消费了"+a);
                    // 消费者加个sleep,生产的快,消费慢,队列不部分情况是满的,没消费掉一个元素,才能生产一个元素
//                Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        customer.start();
    }
}

三、定时器

3.1 概念

定时器是软件开发中的一个重要组件。达到一个设定的时间之后,就执行某个指定好的代码。

3.2 标准库中的定时器

标准库中提供了一个Timer类,Timer类的核心方法是schedule。schedule包含两个参数。

第一个参数表示任务,任务就是TimerTask类,类似于Runnable接口,也是重写run方法

第二个参数指定多长时间之后执行(单位:毫秒).

    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                // 在这描述具体的任务
                System.out.println("hello");
            }
        },5000);
    }

3.3 自己实现定时器

3.3.1 分析

定时器的schedule操作可以加入多个任务。因此,需要对多个任务进行描述和组织。

描述:需要说清楚这个任务是具体内容,以及这个任务多久后执行。可以自定义一个Task类来实现。

组织:不仅能新增新的任务,还要从所有任务中找出最快要到执行时间的任务。有的任务10s后执行,有的任务1min后执行。当然,得先执行10s后要执行得任务。可以使用优先级队列将其组织,采用小根堆。这样就能保证堆顶的元素是最小的。

单独的扫描线程:需要不停的扫描堆中的最小元素,判断其是否到执行时间了。如果时间到了,就执行这个任务的代码。

3.3.2 具体实现

class MyTimer2{
    // 使用Task2这个内部类表示当前的一个任务
    static class Task2 implements Comparable<Task2>{
        // 要执行的任务
        private Runnable runnable;
        // 多久后执行
        private long time;
        // 构造方法
        public Task2(Runnable runnable,long after){
            this.runnable = runnable;
            this.time = System.currentTimeMillis()+after;
        }
        public void run(){
            runnable.run();
        }
        @Override
        public int compareTo(Task2 o) {
            return (int)(this.time - o.time);
        }
    }
    // 准备一个堆,存放所有的任务
    // PriorityQueue 这个队列本身是不安全的,需要自己手动加锁
    // PriorityBlockingQueue带有优先级的阻塞队列,是线程安全的,put插入元素,take取元素
    private PriorityBlockingQueue<Task2> tasks = new PriorityBlockingQueue<>();
    // 通过这个方法,往定时器中添加任务
    public void schedule(Runnable runnable,long time){
        Task2 task = new Task2(runnable,time);
        tasks.put(task);
    }
    private Object locker = new Object();
    // 创建一个扫描线程,让该线程不断的取堆顶元素,判断该任务是否可以执行
    // 在MyTimer2实例化的时候,就创建该线程
    public MyTimer2(){
        Thread t = new Thread(() -> {
           while (true){
               try {
                   // 取堆顶元素
                   Task2 task = tasks.take();
                   long curTime = System.currentTimeMillis();
                   // 判断执行时间是否到了
                   if(curTime < task.time){
                       // 没到,放回去
                       tasks.put(task);
                       // 此处的locker存在的目的是为了能够进行等待,而不是为了保证互斥
                       synchronized (locker){
                           locker.wait(task.time - curTime);
                       }
                   }else{
                       task.run();
//                       tasks.poll();
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        t.start();
    }
}
public class try5 {
    public static void main(String[] args) {
        MyTimer2 myTimer2 = new MyTimer2();
        myTimer2.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("sleep"+System.currentTimeMillis());
            }
        },1000);
        myTimer2.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("work"+System.currentTimeMillis());
            }
        },3000);
        System.out.println("main"+System.currentTimeMillis());
    }
}

四、线程池

4.1 内核态和用户态

线程池最大的好处:减少每次启动、销毁线程的损耗。

4.2 标准库中的线程池

4.2.1 ThreadPoolExecutor

 此处这个ThreadPoolExecutor使用比较复杂,光构造方法就需要很多操作。标准库中也提供了封装版本,提供了更简单的接口,直接使用。

4.2.2 Executors(ThreadPoolExecutor的封装)

newFixedThreadPool: 创建固定线程数的线程池;

newCachedThreadPool: 创建线程数目动态增长的线程池.

newSingleThreadExecutor: 创建只包含单个线程的线程池.

newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.

4.2.3 面试题:如果按照实际场景显示的指定相关线程的参数,程序运行会更可控。那线程个数指定为几比较合适呢?

网上的回答:假设机器CPU是10核心,线程池线程个数:1 * 核心数、1.2*核心数、1.5*核心数、2*核心数·······

只要回答中有具体的数字,答案都是错的

假设CPU是10核心,

极端情况1:线程的任务100%的时间都在使用CPU,此时线程数量不应该超过10;

极端情况2:线程的任务1%的时间都在使用CPU,99%的时间都在阻塞,此时线程数量不应该超过1000;

但是,在每个任务的实际执行过程中,有多少时间在使用CPU,多少时间在阻塞,是不好量化的。

在实际中,是通过测试的方式,来找到一个更合适的线程个数来设定。在测试的过程中,观察不同线程数下的CPU的使用情况。要保证CPU既不会特别空闲,也不会特别紧张。根据性能测试,找到一个合适的数值,来保证效率和可靠性之间的平衡点。

为什么不能让CPU特别空闲?

线程数设置的少,CPU就很空闲,整个任务的执行时间就更长。

为什么不能让CPU特别紧张?

线程数设置的多,CPU就很繁忙,整个任务的执行时间就更快。当然也不能让CPU太忙,要考虑到冗余,要能够预防突发情况

4.3 实现一个简单的线程池

相当于是一个newFixedThreadPool,即固定线程数的线程池。

package thread;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;


class MyThreadPool2{
    // 1.描述任务,直接使用Runnable
    // 2.组织任务,使用一个阻塞队列存放若干个任务
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
    // 3.还需要描述一个工作线程是那样的
    // 在实际使用中,工作线程不止一个,这多个工作线程共享一个任务队列
    // 先描述一个工作线程长啥样
    static class Worker extends Thread{
        private BlockingQueue<Runnable> queue = null;
        // 通过这里的构造方法,把上面建好的任务队列给传到线程里,方便线程去取任务
        public Worker(BlockingQueue<Runnable> queue){
            this.queue = queue;
        }
        // 描述一个线程要做的工作,即反复的从队列中读取任务,然后执行任务
        @Override
        public void run() {
            while (true){
                try {
                    // 如果任务队列不为空,此时就能立即取出一个任务并执行
                    // 如果任务队列为空,就会产生阻塞,阻塞到直到有人加入新的任务进去
                    Runnable task = queue.take();
                    task.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    // 4.需要组织若干个任务线程
    private List<Worker> workerList = new ArrayList<>();
    // 5.搞一个构造方法,指定一下有多少个线程在线程池中
    public MyThreadPool2(int n){
        for(int i=0;i<n;i++){
            Worker worker = new Worker(queue);
            // 创建新的线程,先让他跑起来,再保存到数组中
            worker.start();
            workerList.add(worker);
        }
    }
    // 6.实现一个submit来注册任务到线程池中
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }
}
public class try6 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool2 myThreadPool2 = new MyThreadPool2(10);
        for (int i = 0; i < 100; i++) {
            myThreadPool2.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello");
                }
            });
        }
    }
}

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘减减

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值