最全多线程四大经典案例及java多线程的实现_java 多线程经典案例,2024年最新简直无敌

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

        return instance;
    }
}

}


我们将`Singleton`类对象加锁后,显然避免了刚刚的一些线程安全问题!但是出现了新的问题!


* `instance`初始化前  
 在初始化前,我们很好的将读写操作进行了原子封装,并不会造成线程不安全问题!
* `instance`初始化后  
 然而初始化后的每次读操作却并不好,当我们多个线程进行多操作时,很多线程就会造成线程阻塞,代码的运行效率极具下降!


我们如何保证,线程安全的情况下又保证读操作不会进行加锁,锁竞争呢?


我们可以间代码的两种情况分别处理!



//优化二
class Singleton2{
//懒汉模式, static 创建类时,并没有创建实例!
//private 保证这里的instance实例只有一份!!!
private static Singleton2 instance = null;
//私有的构造方法!保证该实例不能再创建
private Singleton2(){
}
//提供一个方法,外界可以获取到该实例!
public static Singleton2 getInstance() {
if(instancenull){//如果未初始化就进行加锁操作!
synchronized (Singleton.class){ //对读写操作进行加锁!
if(instance
null){//需要时再创建实例!
instance = new Singleton2();
}
}
}
//已经初始化后直接读!!!
return instance;
}
}


我们看到这里可能会有疑惑,咋为啥要套两个`if`啊,把里面的`if`删除不行吗!!!  
 我们来看删除后的效果:



//删除里层if
class Singleton2{
//懒汉模式, static 创建类时,并没有创建实例!
//private 保证这里的instance实例只有一份!!!
private static Singleton2 instance = null;
//私有的构造方法!保证该实例不能再创建
private Singleton2(){
}
//提供一个方法,外界可以获取到该实例!
public static Singleton2 getInstance() {
if(instance==null){//如果未初始化就进行加锁操作!
synchronized (Singleton.class){ //对读写操作进行加锁!
instance = new Singleton2();
}
}
//已经初始化后直接读!!!
return instance;
}
}


在删除里层的`if`后:  
 我们发现当有多个线程进行了第一个`if`判断后,进入的线程中有一个线程锁竞争拿到了锁!而其他线程就在这阻塞等待,直到该锁释放后,又有线程拿到了该锁,而这样也就多次创建了`instance`实例,显然不可!!!


所以这里的两个`if`都有自己的作用缺一不可!  
 第一个`if`:  
 判断是否要进行加锁初始化  
 第二个`if`:  
 判断该线程实例是否已经创建!



//最终优化版
class Singleton2{
//懒汉模式, static 创建类时,并没有创建实例!
//private 保证这里的instance实例只有一份!!!
//volatile 保证内存可见!!!避免编译器优化!!!
private static volatile Singleton2 instance = null;
//私有的构造方法!保证该实例不能再创建
private Singleton2(){
}
//提供一个方法,外界可以获取到该实例!
public static Singleton2 getInstance() {
if(instancenull){//如果未初始化就进行加锁操作!
synchronized (Singleton.class){ //对读写操作进行加锁!
if(instance
null){
instance = new Singleton2();
}
}
}
//已经初始化后直接读!!!
return instance;
}
}


而我们又发现了一个问题,我们的编译器是会对代码进行优化操作的!如果很多线程对第一个`if`进行判断,那`cpu`老是在内存中拿`instance`的值,就很慢,编译器就不开心了,它就优化直接将该值存在寄存器中,而此操作是否危险,如果有一个线程将该实例创建!那就会导致线程安全问题! 而`volatile`关键字保证了`instanse`内存可见性!!!


**总结懒汉模式**


* 双`if` 外层保证未初始化前加锁,创建实例. 里层`if`保证实例创建唯一一次
* `synchronized`加锁,保证读写原子性
* `volatile`保证内存可见性,避免编译器优化


## 阻塞队列


*什么是阻塞队列?*  
 顾名思义是队列的一种!  
 也符合先进先出的特点!  
 阻塞队列特点:



> 
> 当队列为空时,读操作阻塞  
>  当队列为满时,写操作阻塞
> 
> 
> 


阻塞队列一般用在多线程中!并且有很多的应用场景!  
 最典型的一个应用场景就是生产者消费者模型


## 生产者消费者模型


我们知道生产者和消费者有着供需关系!  
 而开发中很多场景都会有这样的供需关系!  
 比如有两个服务器`A`和`B`  
 `A`是入口服务器直接接受用户的网络请求  
 `B`应用服务器对`A`进行数据提供


在通常情况下如果一个网站的访问量不大,那么`A`和`B`服务器都能正常使用!  
 而我们知道,很多网站当很多用户进行同时访问时就可能挂!  
 我们知道,`A`入口服务器和`B`引用服务器此时耦合度较高!  
 当一个挂了,那么另一个服务器也会出现问题!  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/16b3d9d5e1cc4801b11386f68ee108ab.png)  
 而当我们使用生产者消费者模型就很好的解决了上述高度耦合问题!我们在他们中间加入一个阻塞队列即可!


![在这里插入图片描述](https://img-blog.csdnimg.cn/4cefe8b0eed14b97ae36a2dec22ea093.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYnVnIOmDrQ==,size_20,color_FFFFFF,t_70,g_se,x_16)  
 当增加就绪队列后,我们就不用担心`A`和`B`的耦合!  
 并且`A`和`B`进行更改都不会影响到对方! 甚至将改变服务器,对方也无法察觉!  
 而阻塞队列还保证了,服务器的访问速度,不管用户量多大! 这些数据都会先传入阻塞队列,而阻塞队列如果满,或者空,都会线程阻塞! 也就不存在服务器爆了的问题!!!  
 也就是起到了削峰填谷的作用!不管访问量一时间多大!就绪队列都可以保证服务器的速度!


### 标准库中的就绪队列


我们`java`中提供了一组就绪队列供我们使用!



> 
> `BlockingQueue`
> 
> 
> 


![在这里插入图片描述](https://img-blog.csdnimg.cn/c89cbfb44b5f4211bb3c747cd0e29f0e.png)  
 `BlockingQueue` 是一个接口. 真正实现的类是 `LinkedBlockingQueue`.  
 `put` 方法用于阻塞式的入队列,  
 `take` 用于阻塞式的出队列.  
 `BlockingQueue` 也有 `offer`, `poll`, `peek` 等方法, 但是这些方法不带有阻塞特性.



//生产着消费者模型
public class Test2 {
public static void main(String[] args) throws InterruptedException {
//创建一个阻塞队列
BlockingQueue 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(() -> {//生产者
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();
}
}


![在这里插入图片描述](https://img-blog.csdnimg.cn/d5ef88f1abc94993a100a9b0efd40c24.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYnVnIOmDrQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


### 阻塞队列实现


虽然`java`标准库中提供了阻塞队列,但是我们想自己实现一个阻塞队列!


我们就用循环队列实现吧,使用数组!



//循环队列
class MyblockingQueue{
//阻塞队列
private int[] data = new int[100];
//队头
private int start = 0;
//队尾
private int tail = 0;
//元素个数, 用于判断队列满
private int size = 0;
public void put(int x){
//入队操作
if(sizedata.length){
//队列满
return;
}
data[tail] = x;
tail++;//入队
if(tail
data.length){
//判断是否需要循环回
tail=0;
}
size++; //入队成功加1
}
public Integer take(){
//出队并且获取队头元素
if(tailstart){
//队列为空!
return null;
}
int ret = data[start]; //获取队头元素
start++; //出队
if(start
data.length){
//判断是否要循环回来
start = 0;
}
// start = start % data.length;//不建议可读性不搞,效率也低
size–;//元素个数减一
return ret;
}
}


![在这里插入图片描述](https://img-blog.csdnimg.cn/77492dff55f04406a89c9bd3a66b0da2.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYnVnIOmDrQ==,size_20,color_FFFFFF,t_70,g_se,x_16)  
 我们已经创建好了一个循环队列,目前达不到阻塞的效果!  
 而且当多线程并发时有很多线程不安全问题!  
 而我们知道想要阻塞,那不得加锁,不然哪来的阻塞!



//阻塞队列
class MyblockingQueue{
//阻塞队列
private int[] data = new int[100];
//队头
private int start = 0;
//队尾
private int tail = 0;
//元素个数, 用于判断队列满
private int size = 0;
//锁对象
Object locker = new Object();

public void put(int x){
   synchronized (locker){//对该操作加锁
       //入队操作
       if(size==data.length){
           //队列满 阻塞等待!!!直到put操作后notify才会继续执行
           try {
               locker.wait();
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
       data[tail] = x;
       tail++;//入队
       if(tail==data.length){
           //判断是否需要循环回
           tail=0;
       }
       size++; //入队成功加1
       //入队成功后通知take 如果take阻塞
       locker.notify();//这个操作线程阻塞并没有副作用!
   }
}
public Integer take(){
    //出队并且获取队头元素
    synchronized (locker){
        if(size==0){
            //队列为空!阻塞等待 知道队列有元素put就会继续执行该线程
            try {
                locker.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        int ret = data[start]; //获取队头元素
        start++; //出队
        if(start==data.length){
            //判断是否要循环回来
            start = 0;
        }
        // start = start % data.length;//不建议可读性不搞,效率也低
        size--;//元素个数减一
        locker.notify();//通知 put 如果put阻塞!
        return ret;
    }
}

}



//测试代码
public class Test3 {
public static void main(String[] args) {
MyblockingQueue queue = new MyblockingQueue();
Thread customer = new Thread(()->{
int i = 0;
while (true){
System.out.println(“消费了”+queue.take());
}
});

                Thread producer = new Thread(()->{
                    Random random = new Random();
                    while (true){
                        int x = random.nextInt(100);
                        System.out.println("生产了"+x);
                        queue.put(x);
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                });
                customer.start();
                producer.start();
}

}


![在这里插入图片描述](https://img-blog.csdnimg.cn/5a7e4617803848f080b758155b05afe1.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYnVnIOmDrQ==,size_15,color_FFFFFF,t_70,g_se,x_16)  
 可以看到通过`wait`和`notify`的配和,我就实现了阻塞队列!!!


![在这里插入图片描述](https://img-blog.csdnimg.cn/3ac6fad4ea1c4fb9866d49821f1f359c.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYnVnIOmDrQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


## 定时器


*定时器是什么*



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


也就是说定时器有像`join`和`sleep`等待功能,不过他们是基于系统内部的定时器,  
 而我们要学习的是在`java`给我们提供的定时器包装类,用于到了指定时间就执行代码!  
 并且定时器在我们日常开发中十分常用!


`java`给我们提供了专门一个定时器的封装类`Timer`在`java.util`包下!



> 
> `Timer`定时器
> 
> 
> 


`Timer`类下有一个`schedule`方法,用于安排指定的任务和执行时间!  
 也就达到了定时的效果,如果时间到了,就会执行`task`!  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/2235a30273704d3eacd2828fe9929051.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYnVnIOmDrQ==,size_20,color_FFFFFF,t_70,g_se,x_16)


* `schedule` 包含两个参数.
* 第一个参数指定即将要执行的任务代码,
* 第二个参数指定多长时间之后执行 (单位为毫秒).



//实例
import java.util.Timer;
import java.util.TimerTask;
public class Demo1 {
public static void main(String[] args) {
//在java.util.Timer包下
Timer timer = new Timer();
//timer.schedule()方法传入需要执行的任务和定时时间
//Timer内部有专门的线程负责任务的注册,所以不需要start
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(“hello Timer!”);
}
},3000);
//main线程
System.out.println(“hello main!”);
}
}


![在这里插入图片描述](https://img-blog.csdnimg.cn/cd97ce03ea8a47efa60fd5e0e85ee8c4.png)  
 我们可以看到我们只需要创建一个`Timer`对象,然后调用`schedule`返回,传入你要执行的任务,和定时时间便可完成!


### 定时器实现


我们居然知道`java`中定时器的使用,那如何自己实现一个定时器呢!


我们可以通过`Timer`中的源码,然后进行操作!


*`Timer`内部需要什么东西呢!*


我们想想`Timer`的功能!  
 可以定时执行任务!(线程)  
 可以知道任务啥时候执行(时间)  
 可以将多个任务组织起来对比时间执行


* 描述任务  
 也就是`schedule`方法中传入的`TimerTake`  
 创建一个专门表示定时器中的任务



class MyTask{
//任务具体要干啥
private Runnable runnable;
//任务执行时间,时间戳
private long time;
///delay是一个时间间隔
public MyTask(Runnable runnable,long delay){
this.runnable = runnable;
time = System.currentTimeMillis()+delay;
}
public void run(){ //描述任务!
runnable.run();
}
}


* 组织任务  
 组织任务就是将上述的任务组织起来!  
 我们知道我们的任务需要在多线程的环境下执行,所以就需要有线程安全,阻塞功能的数据结构!并且我们的任务到了时间就需要执行,也就是需要时刻对任务排序!  
 所以我们采用`PriorityBlockingQueue`优先级队列!阻塞!  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/0abf85ed3bd548a3b9496545a0218559.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYnVnIOmDrQ==,size_20,color_FFFFFF,t_70,g_se,x_16)  
 但是这里我们使用了优先级队列,我们需要指定比较规则,就是让`MyTask`实现`Comparable`接口,重写 `compareTo`方法,指定升序排序,就是小根堆!  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/f2276a0e31bd4eecacd0395b387231b5.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYnVnIOmDrQ==,size_20,color_FFFFFF,t_70,g_se,x_16)
* 执行时间到了的任务  
 我们可以创建一个线程,执行时间到了的任务!



//执行时间到了的任务!
public MyTimer(){
Thread thread = new Thread(()->{
while (true){
try {
MyTask task = queue.take();//获取到队首任务
//比较时间是否到了
//获取当前时间戳
long curTime = System.currentTimeMillis();
if(curTime<task.getTime()){//当前时间戳和该任务需要执行的时间比较
//还未到达执行时间
queue.put(task); //将任务放回
}else{//时间到了,执行任务
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();//启动线程!
}



//定时器完整代码
import java.util.concurrent.PriorityBlockingQueue;
class MyTask implements Comparable{
//任务具体要干啥
private Runnable runnable;

public long getTime() {
    return time;
}

//任务执行时间,时间戳
private long time;
///delay是一个时间间隔
public MyTask(Runnable runnable,long delay){
        this.runnable = runnable;
        time = System.currentTimeMillis()+delay;
}
public void run(){ //描述任务!
    runnable.run();
}
@Override
public int compareTo(MyTask o) {
    return (int)(this.time - o.time);
}

}
public class MyTimer{
//定时器内部需要存放多个任务
private PriorityBlockingQueue queue = new PriorityBlockingQueue<>();
public void schedule(Runnable runnable,long delay){
MyTask task = new MyTask(runnable,delay);//接收一个任务!
queue.put(task);//将任务组织起来
}
//执行时间到了的任务!
public MyTimer(){
Thread thread = new Thread(()->{
while (true){
try {
MyTask task = queue.take();//获取到队首任务
//比较时间是否到了
//获取当前时间戳
long curTime = System.currentTimeMillis();
if(curTime<task.getTime()){//当前时间戳和该任务需要执行的时间比较
//还未到达执行时间
queue.put(task); //将任务放回
}else{//时间到了,执行任务
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();//启动线程!
}
}



//测试
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println(“hello Timer”);
}
}, 3000);
System.out.println(“hello main”);
}


![在这里插入图片描述](https://img-blog.csdnimg.cn/956212b1e8a7460db91def9ce519e46f.png)  
 我们再来检查一下下面代码存在的问题!



//执行时间到了的任务!
public MyTimer(){
Thread thread = new Thread(()->{
while (true){
try {
MyTask task = queue.take();//获取到队首任务
//比较时间是否到了
//获取当前时间戳
long curTime = System.currentTimeMillis();
if(curTime<task.getTime()){//当前时间戳和该任务需要执行的时间比较
//还未到达执行时间
queue.put(task); //将任务放回
}else{//时间到了,执行任务
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();//启动线程!
}


我们上述代码还存在一定缺陷就是执行线程到了的代码,我们的`while`循环一直在处于忙等状态!  
 就好比生活中:  
 你9点要去做核酸,然后你过一会就看时间,一会就看时间,感觉就有啥大病一样!  
 所以我们可以定一个闹钟,到了时间就去,没到时间可以干其他的事情!  
 此处的线程也是如此!我们这里也可以使用`wait`阻塞! 然后到了时间就唤醒,就解决了忙等问题!  
 我们的`wait`可以传入指定的时间,到了该时间就唤醒!!!


我们再思考另一个问题!


*如果又加入了新的任务呢?*  
 我们此时也需要唤醒一下线程,让线程重新拿到队首元素!



//最终定时器代码!!!
import java.util.concurrent.PriorityBlockingQueue;
class MyTask implements Comparable{
//任务具体要干啥
private Runnable runnable;

public long getTime() {
    return time;
}
//任务执行时间,时间戳
private long time;
///delay是一个时间间隔
public MyTask(Runnable runnable,long delay){
        this.runnable = runnable;
        time = System.currentTimeMillis()+delay;
}
public void run(){ //描述任务!
    runnable.run();
}
@Override
public int compareTo(MyTask o) {
    return (int)(this.time - o.time);
}

}
public class MyTimer{
//定时器内部需要存放多个任务
Object locker = new Object();//锁对象
private PriorityBlockingQueue queue = new PriorityBlockingQueue<>();
public void schedule(Runnable runnable,long delay){
MyTask task = new MyTask(runnable,delay);//接收一个任务!
queue.put(task);//将任务组织起来

    //每次拿到新的任务就需要唤醒线程,重新得到新的队首元素!
    synchronized (locker){
        locker.notify();
    }
}

//执行时间到了的任务!
public MyTimer(){
Thread thread = new Thread(()->{
while (true){
try {
MyTask task = queue.take();//获取到队首任务
//比较时间是否到了
//获取当前时间戳
long curTime = System.currentTimeMillis();
if(curTime<task.getTime()){//当前时间戳和该任务需要执行的时间比较
//还未到达执行时间
queue.put(task); //将任务放回
//阻塞到该时间唤醒!
synchronized (locker){
locker.wait(task.getTime()-curTime);
}
}else{//时间到了,执行任务
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();//启动线程!
}
}


**总结:**


* 描述一个任务 `runnable + time`
* 使用优先级队列组织任务`PriorityBlockingQueue`
* 实现`schedule`方法来注册任务到队列
* 创建扫描线程,获取队首元素,判断是否执行
* 注意这里的忙等问题



//最后梳理一遍
import java.util.concurrent.PriorityBlockingQueue;
/**
* Created with IntelliJ IDEA.
* Description:定时器
* User: hold on
* Date: 2022-04-09
* Time: 16:07
*/
//1.描述任务
class Task implements Comparable{
//任务
private Runnable runnable;

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

});
thread.start();//启动线程!
}
}


**总结:**


* 描述一个任务 `runnable + time`
* 使用优先级队列组织任务`PriorityBlockingQueue`
* 实现`schedule`方法来注册任务到队列
* 创建扫描线程,获取队首元素,判断是否执行
* 注意这里的忙等问题



//最后梳理一遍
import java.util.concurrent.PriorityBlockingQueue;
/**
* Created with IntelliJ IDEA.
* Description:定时器
* User: hold on
* Date: 2022-04-09
* Time: 16:07
*/
//1.描述任务
class Task implements Comparable{
//任务
private Runnable runnable;

[外链图片转存中…(img-qv6P4k1V-1715826402653)]
[外链图片转存中…(img-4nFvVhpQ-1715826402653)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值