前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >【JavaEE初阶】春风若有怜花意,可否许我再少年?- (重点)线程

【JavaEE初阶】春风若有怜花意,可否许我再少年?- (重点)线程

作者头像
用户11369350
发布2024-12-24 11:09:54
发布2024-12-24 11:09:54
4100
代码可运行
举报
运行总次数:0
代码可运行

1. 多线程案例

1.1 单例模式

单例模式是校招中最常考的设计模式之⼀.

什么是设计模式? 设计模式好比象棋中的 “棋谱”. 红方当头炮, 黑方马来跳. 针对红方的⼀些走法, 黑方应招的时候有⼀些固定的套路. 按照套路来走局势就不会吃亏. 软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了⼀些固定的套路. 按照这个套路来实现代码, 也不会吃亏.

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

单例模式具体的实现方式有很多. 最常见的是 “饿汉” 和 “懒汉” 两种.

1.1.1 饿汉模式

类加载的同时, 创建实例.

代码语言:javascript
代码运行次数:0
复制
 //饿汉模式, 在类加载的时候就创建了对象, 时机较早, 所以叫饿汉
    //
class Singleton{
    private static Singleton instance = new Singleton();

    //通过该方法来获取实例.
    public static Singleton getInstance() {
        return instance;
    }
    //把构造方法设为私有,防止类外的代码有new了一个Singleton对象出来破坏单例模式.
    private Singleton() {

    }
}
public class Demo21 {
    public static void main(String[] args) {
        //Singleton singleton = new Singleton();
        Singleton s1 = Singleton.getInstance();//getInstance是静态方法可以通过类名的方式直接调用.
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);//s1和s2是同个对象.
    }
}
1.1.2 懒汉模式(单线程)

类加载的时候不创建实例. 第一次使用的时候才创建实例.

代码语言:javascript
代码运行次数:0
复制
//懒汉模式 单线程版
class SingleLazy {
    private SingleLazy instance = null;

    public SingleLazy getInstance() {
        if(instance == null) {
            return new SingleLazy();
        }
        return instance;
    }
    private SingleLazy(){

    }
}

public class SingleLazyTest {
    public static void main(String[] args) {

    }
}
1.1.3 懒汉模式(多线程)

上面的懒汉模式的实现是线程不安全的. 线程安全问题发生在首次创建实例时. 如果在多个线程中同时调用 getInstance 方法, 就可能导致创建出多个实例.

方案: 1. 使用synchronized 改善线程安全问题. 2. 外层再来一个if 语句, 降低锁竞争的频率. 3. 涉及到修改操作的成员变量用 volatile , 避免 “内存可见性” 导致读取的 instance 出现偏差.

详细说明两层 if 各自的作用:

当多线程首次调用 getInstance, 大家可能都发现 instance 为 null, 于是又继续往下执行来竞争锁, 其中竞争成功的线程, 再完成创建实例的操作. 当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例. 后续的线程, 不必加锁, 直接就通过外层 if (instance == null) 就知道实例已经创建了,从而不再尝试获取锁了. 降低了开销.

代码语言:javascript
代码运行次数:0
复制
//懒汉模式
    //要使用的时候才创建, 所以叫懒汉
class SingletonLazy {
    //加上volatile修饰instance避免多线程情况下指令重排序引起线程不安全.
    private static volatile SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
        if(instance == null) {
            synchronized(SingletonLazy.class) {
                //加锁把整个if 都锁上才有效.
                if(instance == null) {
                    return new SingletonLazy();
                }
            }
        }

        return instance;
    }
    private SingletonLazy(){ }
}
public class Demo22 {
    public static void main(String[] args) {
        //...
    }
}

2. 阻塞队列

2.1 什么是阻塞队列?

阻塞队列是⼀种特殊的队列. 也遵守 “先进先出” 的原则. 阻塞队列能是⼀种线程安全的数据结构, 并且具有以下特性: • 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素. • 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素. 阻塞队列的一个典型应用场景就是 “生产者消费者模型”. 这是⼀种非常典型的开发模型.

2.2 生产消费模型

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。 生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取. 1. 阻塞队列就相当于⼀个缓冲区,平衡了生产者和消费者的处理能力. (削峰填谷) 比如在 “秒杀” 场景下, 服务器同⼀时刻可能会收到大量的支付请求. 如果直接处理这些支付请求, 服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程). 这个时候就可以把这些请求都放到⼀个 阻塞队列中, 然后再由消费者线程慢慢的来处理每个支付请求. 这样做可以有效进行 “削峰”, 防止服务器被突然到来的一波请求直接冲垮. 2. 阻塞队列也能使生产者和消费者之间 解耦合.

如果不用阻塞队列, A 与 B直接交互, 如果B 出问题, A也会受影响. 想再加一个服务器C, A的代码也需要修改.

在A 与 B 之间加上阻塞队列, A B之间的耦合性就会降低, B出了问题, A 大概率不会有问题.

2.3 Java标准库中的阻塞队列

在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.

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

代码语言:javascript
代码运行次数:0
复制
public class Demo23 {
    public static void main(String[] args) throws InterruptedException {
        //BlockingQueue<String> queue = new ArrayBlockingQueue<>();
        BlockingQueue<String> queue = new LinkedBlockingQueue<>();
        queue.put("111");
        queue.put("222");
        queue.put("333");
        queue.put("444");
        String elem = queue.take();
        System.out.println(elem);
        elem = queue.take();
        System.out.println(elem);
        elem = queue.take();
        System.out.println(elem);
        elem = queue.take();
        System.out.println(elem);
        
        System.out.println("第五次:");
        elem = queue.take();
        System.out.println(elem);
    }
}

2.4 自实现阻塞队列

• 通过 “循环队列” 的方式来实现. • 使用 synchronized 进行加锁控制. • put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进 行 wait. 被唤醒时不⼀定队列就不满了, 因为同时可能是唤醒了多个线程). • take 取出元素的时候, 判定如果队列为空, 就进空 wait. (也是循环 wait)

循环队列分析图:

这里是引用
这里是引用
代码语言:javascript
代码运行次数:0
复制
//自实现阻塞队列
    //用数组实现循环阻塞队列.
class MyBlockingQueue2 {
    private String[] data = new String[1000];
    private Object locker = new Object();
    private volatile int head = 0;//指向队头.
    private volatile int tail = 0;//指向最后一个有效元素的下一个位置.
    private volatile int size = 0;//记录有效元素的个数.

    public void put(String elem) throws InterruptedException {
        synchronized(locker) { // wait()方法需要配合while使用.
            while(size == data.length) {
                //一直阻塞等待,直到有其他线程调用take方法.
                // 使用wait等待,需要 synchroninzed加锁.
                locker.wait();
            }
            data[tail] = elem;
            size++;
            tail++;
            if(tail == data.length-1) {//当tail 指向最后一个位置时,继续++会越界, 所以令其指回队头.
                tail = 0;
            }
            locker.notify();
        }
    }

    public String take() throws InterruptedException {
        synchronized(locker) {
            while(size == 0) {// wait()方法需要配合while使用.
                //一直阻塞等待, 直到有其他线程调用put方法
                //使用wait等待,需要 synchronized 加锁.
                locker.wait();
            }
            String result = data[head];
            size--;
            head++;
            if(head == data.length-1) {//同tail
                head = 0;
            }
            locker.notify();
            return result;
        }
    }
}

2.5 实现生产消费模型

代码语言:javascript
代码运行次数:0
复制
//利用上述自实现的阻塞队列, 实现一个简单的生产消费模型.
public class test12 {
    static MyBlockingQueue2 queue2 = new MyBlockingQueue2();
    public static void main(String[] args) {
        //消费者
        Thread t1 = new Thread(() -> {
            while(true) {
                try {
                    String result = queue2.take();
                    System.out.println("消费元素: " + result);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

            }
        });
        //生产者
        Thread t2 = new Thread(() -> {
            int num = 0;
            while(true) {
                try {
                    queue2.put(num+"");
                    System.out.println("生产元素: " + num);
                    num++;
                    Thread.sleep(500);//生产慢一点.
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t1.start();
        t2.start();
    }
}

3. 定时器

3.1 什么是定时器

定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码.

定时器是一种实际开发中非常常用的组件. 比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连. 比如一个 Map, 希望里面的某个 key 在 3s 之后过期(自动删除). 类似于这样的场景就需要用到定时器.

3.2 标准库中的定时器

• 标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule . • schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后 执行 (单位为毫秒).

代码语言:javascript
代码运行次数:0
复制
//定时器
public class Demo25 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        //给定时器安排一个任务, 预定再xxx时间去执行.
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("3000");
            }
        },3000); // schedule()方法是给定时器安排一个任务,并规定 xxx 时间后执行


        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("2000");
            }
        },2000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("1000");
            }
        },1000);
        System.out.println("程序启动");
    }
}

3.3 自实现定时器

1. Timer线程中需要有一个扫描线程, 扫描任务是否到达执行时间. 2. 需要有一个数据结构, 把所有的任务都存起来. 3. 还需要创建一个类, 通过类描述一个任务, 包括任务的执行时间和任务的内容. 这里的数据结构使用优先级队列因为它可以使拿到最小时间的操作为O(1)时间复杂度. 如果用ArrayList 还要去遍历, 用PriorityQueue就行, 不要使用 PriorityBlockingQueue, 容易死锁! , 因为阻塞优先级队列不方便处理两处wait.

代码语言:javascript
代码运行次数:0
复制
//这个类用来记录任务内容和执行时间.
class MyTimerTask2 {
    private Runnable runnable;//任务内容
    private long time;//绝对时间
    public MyTimerTask2(Runnable runnable,long delay) {//delay为相对时间,加上当前时间即为绝对时间.
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }
    public long getTime() {
        return time;
    }
    public Runnable getRunnable() {
        return runnable;
    }
}
//重写比较器
class Intmp implements Comparator<MyTimerTask2> {
    @Override
    public int compare(MyTimerTask2 o1, MyTimerTask2 o2) {
        return (int) (o1.getTime() - o2.getTime());
    }
}

//定时器
class MyTimer2 {
    //利用优先级队列来存放任务.
    private PriorityQueue<MyTimerTask2> queue = new PriorityQueue<>(new Intmp());

    private Object locker = new Object();
    public void schedule2(Runnable runnable,long delay) {
        synchronized (locker) {
            queue.offer(new MyTimerTask2(runnable,delay)) ;
            locker.notify();
        }

    }
    public MyTimer2() {
        Thread t2 = new Thread(() -> {
            while(true) {
                try {
                    synchronized (locker) {
                        //如果队列为空,就令其等待.直到有其他线程调用schedule方法.
                        while(queue.isEmpty()) { //wait配合 while 使用
                            //调用wait方法等待必然是在 加锁情况下的, 那么给 此线程和 调用schedule的线程加锁.
                            locker.wait();//由schedule中的notify唤醒.
                        }
                        MyTimerTask2 myTimerTask2 = queue.peek();
                        long curTime = System.currentTimeMillis();
                        if(curTime >= myTimerTask2.getTime()) {
                            myTimerTask2.getRunnable().run();
                            queue.poll();
                        }else {
                            //如果时间还没到会循环很多次直到时间到,不如直接让线程等待到执行时间
                            //其实就是时间还没到就让线程休息,减少消耗cpu的资源.
                            locker.wait(myTimerTask2.getTime()-curTime);//由schedule中的notify唤醒.
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        });
        t2.start();
    }

}
public class test13 {
    public static void main(String[] args) {
        MyTimer2 timer2 = new MyTimer2();
        timer2.schedule2(new Runnable() {
            @Override
            public void run() {
                System.out.println("200");
            }
        },2000);
        timer2.schedule2(new Runnable() {
            @Override
            public void run() {
                System.out.println("100");
            }
        },1000);
        timer2.schedule2(new Runnable() {
            @Override
            public void run() {
                System.out.println("300");
            }
        },3000);
        System.out.println("程序开始执行!");
    }
}

调试多线程程序一般使用打印日志(println), 虽然使用调试器,可以调试多线程程序,但是一般不太建议使用调试器来调试,调试器的断点,就会影响到某些线程的执行顺序 调试时候,执行的效果, 和不调试的时候,执行的效果,不一定一样, (比如: 正常运行时有 bug, 调试的时候 bug 就不能重现了) , 这有点类似于量子力学中的"测不准原理", 观察手段影响到了结果.

4. 线程池

4.1 什么是线程池

想象这么一个场景: 在学校附近新开了一家快递店,老板很精明,想到一个与众不同的办法来经营。店里没有雇人,而是每次有业务来了,就现场找⼀名同学过来把快递送了,然后解雇同学。这个类比我们平时来⼀个任务,起一个线程进行处理的模式。 很快老板发现问题来了,每次招聘 + 解雇同学的成本还是非常高的。老板还是很善于变通的,知道 了为什么大家都要雇人了,所以指定了一个指标,公司业务人员会扩张到 3 个人,但还是随着业务 逐步雇人。于是再有业务来了,老板就看,如果现在公司还没 3 个人,就雇一个人去送快递,否则 只是把业务放到一个本本上,等着 3 个快递人员空闲的时候去处理。这个就是我们要带出的线程池 的模式。

为什么从线程池中取比创建新线程的效率更高呢? 如果一段程序在系统内核中执行,就称为"内核态", 如果不在, 则称为"用户态". 从线程池中取这个动作是纯粹的用户态操作,创建新的线程这个动作则需要用户态和内核态相互配合着完成的, 但是系统内核中还有其他的动作需要执行, 一般不会第一时间就把线程创建出来, 内核第一时间在执行哪个动作是不可控的.\

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

4.2 标准库中的线程池

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

代码语言:javascript
代码运行次数:0
复制
public class Demo28 {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);
        service.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        });
    }
}

Executors 创建线程池的几种方式. • newFixedThreadPool: 创建固定线程数的线程池 • newCachedThreadPool: 创建线程数目动态增长的线程池. • newSingleThreadExecutor: 创建只包含单个线程的线程池. • newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer. 相当于有多个扫描线程的定时器. Executors 本质上是 ThreadPoolExecutor 类的封装.

ThreadPoolExecutor 提供了更多的可选参数, 可以进⼀步细化线程池行为的设定. (面试常考) 链接: -> Java官方文档中的多线程包里面的线程池

看最后一个构造方法即可, 它的参数包括了上三种. • corePoolSize: 核心线程数. (就像正式员工, 一旦录用, 一般不会被辞退) • maximumPoolSize: 正式员工 + 临时工的数目. (临时工: 摸鱼太频繁, 就会被辞退). • keepAliveTime: 临时工允许的空闲时间. • unit: keepAliveTime 的时间单位, 是秒, 分钟, 还是其他值. • workQueue: 传递任务的阻塞队列. 可以灵活的设置这里的队列, 需要优先级,就设置PriorityBlockingQueue. 如果不需要优先级且任务数目相对恒定,可以使用ArrayBlockingQueue. 如果不需要优先级且任务数目变动较大 就使用LinkedBlockingQueue. • threadFactory: 创建线程的工厂, 参与具体的创建线程工作. 通过不同线程工厂创建出的线程相当于对一些线程属性进行了不同的初始化设置. • RejectedExecutionHandler: 拒绝策略, 如果任务量超出公司的负荷了接下来怎么处理.

◦ AbortPolicy(): 超过负荷, 直接抛出异常. ->直接不干了. ◦ CallerRunsPolicy(): 调用者负责处理多出来的任务. -> 谁添加谁负责. ◦ DiscardOldestPolicy(): 丢弃队列中最老的任务. -> 丢掉最老的. ◦ DiscardPolicy(): 丢弃新来的任务. -> 丢掉最新的.

上面的几个参数中拒绝策略和线程数重点掌握.

4.3 自实现线程池

• 核心操作为 submit, 将任务加⼊线程池中 • 使用 Runnable 描述⼀个任务. 最为submit方法的参数, 直接new 重写run方法执行任务. • 使用一个 BlockingQueue 组织所有的任务 • 不停的从 BlockingQueue 中取任务并执行. • 指定⼀下线程池中的最大线程数 maxWorkerCount; 当当前线程数超过这个最大值时, 就不再新增线程了.

代码语言:javascript
代码运行次数:0
复制
//自实现一个线程池
class MyThreadPool {
    //利用阻塞队列存放线程任务
    private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(100);

    //submit方法参数类型为Runnable. 作用是将任务加到队列中
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }

    //构造方法 利用for循环,构造 n 个线程
    public MyThreadPool(int n) {
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                try {
                    Runnable runnable = queue.take();
                    runnable.run();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            t.start();
        }
    }
}


public class test14 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myTHreadPool = new MyThreadPool(3);
        for (int i = 0; i < 3; i++) {
            int id = i;
            myTHreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("执行任务: " + id);
                }
            });
        }
    }
}

5. 总结-保证线程安全的思路

1. 使用没有共享资源的模型 2. 适用共享资源只读,不写的模型 a. 不需要写共享资源的模型 b. 使用不可变对象 3. 直面线程安全(重点) a. 保证原子性 b. 保证顺序性 c. 保证可见性

6. 对比线程和进程

6.1 线程的优点

1. 创建⼀个新线程的代价要比创建⼀个新进程小得多 2. 与进程之间的切换相比,线程之间的切换需要操作系统做的⼯作要少很多 3. 线程占用的资源要比进程少很多 4. 能充分利用多处理器的可并行数量 5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务 6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现 7. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

6.2 进程与线程的区别

1. 进程是系统进行资源分配和调度的⼀个独立单位,线程是程序执行的最小单位。 2. 进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈。 3. 由于同⼀进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信。 4. 线程的创建、切换及终止效率更高。

上述就是本篇博客的全部内容啦,本篇的结束意味着线程初阶的结束, 接下来就是线程进阶咯!! 感谢观看 ❤❤❤

🐎期待与你的下一次相遇

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-12-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 多线程案例
    • 1.1 单例模式
      • 1.1.1 饿汉模式
      • 1.1.2 懒汉模式(单线程)
      • 1.1.3 懒汉模式(多线程)
  • 2. 阻塞队列
    • 2.1 什么是阻塞队列?
    • 2.2 生产消费模型
    • 2.3 Java标准库中的阻塞队列
    • 2.4 自实现阻塞队列
    • 2.5 实现生产消费模型
  • 3. 定时器
    • 3.1 什么是定时器
    • 3.2 标准库中的定时器
    • 3.3 自实现定时器
  • 4. 线程池
    • 4.1 什么是线程池
    • 4.2 标准库中的线程池
    • 4.3 自实现线程池
  • 5. 总结-保证线程安全的思路
  • 6. 对比线程和进程
    • 6.1 线程的优点
    • 6.2 进程与线程的区别
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档