关于多线程的经典案例~

static修饰的成员叫“类成员”-》“类属性/类方法”

不加static修饰的成员,叫做“实例成员”-》“实例属性/实例方法”

一个Java程序中,一个类对象只存在一份(JVM保证的),进一步的也就保证了类的static成员也是只有一份

类对象:就是.class文件,被JVM加载到内存后,表现出的模样。类对象里就有.class文件中的一切信息,包括类名是啥,类里有哪些属性,每个属性叫啥名字,每个属性是啥类型,属性是public、private……

类:相当于实例的模板,基于模板可以创建出很多的对象来

对象:就是实例 


单例模式的两种实现:

饿汉模式:比较着急的去进行创建实例

懒汉模式:不太着急的去创建实例,只是在用的时候,才真正创建

关于多线程的案例:

案例一、实现一个线程安全的单例模式要求代码中的某个类,只能有一个实例,不能有多个

  通过Singleton这个类来实现单例模式,保证Singleton这个类只有唯一实例

(1)饿汉模式:

//实现单例模式-饿汉模式
class Singleton{
    //1、使用static创建一个实例,并且立即进行实例化
    //这个instance对应的实例,就是该类的唯一实例
    private static Singleton instance=new Singleton();//立即初始化实例,所以叫饿汉模式
    //被static修饰的类成员只有一份
    //2、为了防止程序员在其他地方不小心new这个Singleton,就可以把构造方法设为private
    private Singleton(){}
    //3、提供一个方法,让外面能够拿到唯一实例
    public static Singleton getInstance(){
        return instance;
    }
}

public class Demo3 {
    public static void main(String[] args) {
        Singleton singleton=Singleton.getInstance();
    }
}

饿汉模式中getInstance仅仅是读取了变量的内容,如果多个线程只是读同一个变量,不修改,此时仍然是线程安全的~

(2)懒汉模式:

//实现单例模式-懒汉模式
class Singleton2{
    //1、就不是立即就初始化实例
    private static Singleton2 instance=null;
    //2、把构造方法设为peivate
    private Singleton2(){}
    //3、提供一个方法来获取到上述单例的实例
    //只有当真正需要用到这个实例的时候,才会真正去创建这个实例-》懒汉模式
    public static Singleton2 getInstance(){
        if(instance==null){
            instance=new Singleton2();
        }
        return instance;
    }
}
public class Demo4 {
    public static void main(String[] args) {
        Singleton2 instance=Singleton2.getInstance();
    }
}

懒汉模式中,既包含了读,又包含了修改,而且这里的读和修改还是分成两个步骤的(不是原子的),存在线程安全问题~

改进方案:进行加锁

关于多线程的经典案例~_第1张图片

但是事实上,线程不安全问题只发生在instance初始化之前,也就是说初始化之后,已经线程安全了,可是按照上述的加锁方式,每次调用getInstance都会进行加锁,会存在已经线程安全了但是还因为加锁存在大量的锁竞争(存在的问题)

改进方案:让getInstance初始化之前进行加锁,初始化之后就不再加锁(在加锁之前加上一层判断条件)

关于多线程的经典案例~_第2张图片

但是还是会有问题,当刚开始就有很多线程都去调用getInstance时,就会造成大量的读instance内存的操作,可能会让编译器把这个读内存操作优化成读寄存器操作,也就是出现内存可见性问题,可能会引起第一个if判定失效,但是对于第二个if判定影响不大(因为synchronized可以保证内存可见性),由于可能会引起第一个if判定失效,会导致不该加锁的给加锁了,但是不会引起第二层if的误判(不至于说创建多个实例)

改进方案:给instance加上volatile

关于多线程的经典案例~_第3张图片

因此要想利用懒汉模式创建一个线程安全的单例模式需要注意:

1、正确的位置加锁:为了保证线程安全

2、双重if判定:第一层if是为了避免不必要的加锁,初始化之后就不用加锁了

3、volatile:为了避免第一层if由于内存可见性问题失效

案例二、阻塞队列

阻塞队列同样也是一个符合先进先出规则的队列,相比于普通对列,阻塞队列又有一些其他方面的功能

阻塞队列的特性:(就是在普通队列中加了这两个特性)

1、线程安全

2、产生阻塞效果

   (1)如果队列为空,尝试出队列,就会出现阻塞,阻塞到队列不为空为止

   (2)如果队列为满,尝试入队列,也会出现阻塞,阻塞到队列不为满为止

基于上述特性,就可以实现“生产者消费者模型”

阻塞队列就可以作为生产者消费者模型中的交易场所

假设:A、B、C三个人一起来擀饺子皮包饺子,A负责擀饺子皮,B、C负责包饺子,此时A就是饺子皮的生产者,要不断生产一些新的饺子皮,B、C就是饺子皮的消费者,要不断的使用饺子皮,对于包饺子来说,用来放饺子皮的“盖帘”就是”交易场所“

假设:有两个服务器A、B,A作为入口服务器直接接受用户的网络请求,B作为应用服务器,来给A提供一些数据

如果不使用生产者消费者模型:

关于多线程的经典案例~_第4张图片

此时A和B的耦合性是比较强的,在开发A代码的时候就得充分了解B提供的一些接口,在开发B的时候也得充分了解到A是怎么调用的,一旦想把B换成C,此时A的代码就需要较大的改动,而且如果B挂了,也可能直接导致A也顺带挂了~

如果使用生产者消费者模型: 关于多线程的经典案例~_第5张图片

对于请求:A是生产者,B是消费者;

对于响应:A是消费者,B是生产者;阻塞队列就是交易场所

此时,A只需要关注如何和队列交互,不需要认识B,B也只需要关注如何和队列交互,也不需要认识A,如果B挂了,对A没有影响,如果把B换成了C,A也完全感知不到,能够做到让多个服务程序之间更充分的解耦合

如果不使用生产者消费者模型且当请求量突然暴涨时:

关于多线程的经典案例~_第6张图片

此时A的请求量暴涨,就会直接导致B暴涨~

A作为入口服务器,计算量很轻,如果请求量暴涨,问题也不是很大,但是B作为应用服务器,计算量可能很大,需要的系统资源也更多,如果请求更多了,需要的资源进一步增加,如果主机的硬件不够,可能程序就挂了

如果使用生产者消费者模型且当请求量突然暴涨时:

关于多线程的经典案例~_第7张图片

此时A的请求量暴涨,就会导致阻塞队列的请求暴涨,而不会直接导致B暴涨(“削峰”)~

由于阻塞队列没啥计算量,只是单纯的存个数据,可以抗住更大的压力

此时B这边仍然按照原来的速度来消费数据(“填谷”-》此时就算请求没没那么多了,B还是会按照原来的速度来从阻塞队列中拿已经存好的数据消费),不会因为A的暴涨而引起暴涨,B会被保护的很好,不会因为这种请求的波动而引起崩溃

生产者消费者模型的优点:

(1)能够让多个服务程序之间更充分的解耦合

(2)能够对于请求进行“削峰填谷”

Java标准库中阻塞队列的用法:

BlockingDeque queue=new LinkedBlockingDeque<>();
queue.put("hello");//往阻塞队列里放元素
String s=queue.take();//从阻塞队列里拿元素

自己实现一个阻塞队列:

class MyBlokingQueue{//利用循环数组来创建阻塞队列
    private int[]data=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){//由于put的每一行都在操作公共变量,所以直接给整个方法加锁
            if(size==data.length){
                locker.wait();//对于put来说,阻塞条件就算队列为满
                //针对哪个对象加锁,就使用哪个对象wait
            }
            data[tail]=value;//在队尾添加元素
            tail++;
            if(tail>=data.length){//当tail到达数组的末尾,此时该将tail循环到数组的0下标位置
                tail=0;
            }
            size++;
            //如果入队列成功,则队列非空,就唤醒take中的阻塞等待
            locker.notify();
        }
    }

    public Integer take() throws InterruptedException {//出队列
        synchronized (locker){//由于take的每一行都在操作公共变量,所以直接给整个方法加锁
            if(size==0){
                locker.wait();//对于take来说,阻塞条件就算队列为空
            }
            int ret=data[head];//从队头出元素
            head++;
            if(head>=data.length){
                head=0;
            }
            size--;
            //take 成功之后,就唤醒put中的等待
            locker.notify();
            return ret;
        }
    }
}

通过阻塞队列实现一个生产者消费者模型:

private static MyBlokingQueue queue=new MyBlokingQueue();
    public static void main(String[] args) {//实现一个生产者消费者模型
        Thread producer=new Thread(()->{
            int num=0;
            while(true){
                try {
                    System.out.println("生产了:"+num);
                    queue.put(num);
                    num++;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();

        Thread customer=new Thread(()->{
             while(true){
                 try {
                     int num=queue.take();
                     System.out.println("消费了:"+num);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
        });
        customer.start();
    }

  关于多线程的经典案例~_第8张图片

  关于多线程的经典案例~_第9张图片 

当生产比较慢时,因为消费者进程循环很快,一下就把队列清空了,当消费者队列为空,就会阻塞等待,直到生产者生产了新的数据

 关于多线程的经典案例~_第10张图片

 关于多线程的经典案例~_第11张图片

此时生产者生产的快了,瞬间就会生产好1000个元素 (队列长度为1000),之后队列满了,再生产就会阻塞,此时每次消费一个元素才能生产一个元素

案例三:定时器-》用于进行计时,在一定时间之后,被唤醒并执行某个之前设定好的任务

Java标准库中定时器的用法:

java.uiil.Timer

方法:schedule(任务TimerTask是啥,多长时间后执行)

import java.util.Timer;
import java.util.TimerTask;

public class Demo4 {
    public static void main(String[] args) {
        Timer timer=new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello timer");
            }
        },1000);//shedule的参数有两个,一个是任务TimerTask,一个是时间1000
        System.out.println("main ");
    }
}

 关于多线程的经典案例~_第12张图片

自己实现一个定时器:

Timer的主要任务: 

1、描述任务:任务的具体内容、任务的执行时间       

     创建一个专门的类来表示一个定时器中的任务(就像TimerTask)

    关于多线程的经典案例~_第13张图片

2、组织任务(通过数据结构堆PriorityBlockingQueue

     定时器内部要能够存放多个任务 ,虽然安排任务的时候,是无序的,但是执行任务的时候,要按照时间先后来执行,保证每次执行的都是最小的时间间隔后的任务

比如说现在给安排了3个任务:分别是1个小时后去做作业、3个小时后去上课、10分钟之后去休息,此时就先执行休息,再执行做作业,最后执行去上课

3、执行时间到了的任务

     需要先执行时间最靠前的任务,此时就需要一个线程,不停的去检查当前优先队列的队首元素,判断当前最靠前的这个任务是不是时间到了 

自己实现一个定时器的总代码:

//创建一个类,表示一个任务
//1、描述一个任务:runnable+time
class MyTask implements Comparable{
    private Runnable runnable;//任务具体要要干啥
    private long time;//任务具体啥时候干,保存任务要执行的毫秒级时间戳

    //after是一个时间间隔,不是绝对的时间戳的值
    public MyTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }
    public void run(){
        runnable.run();//通过调用run方法来执行任务具体要执行的工作
    }
    public long getTime(){
        return time;
    }

    @Override
    public int compareTo(MyTask o) {
        return (int) (this.time-o.time);//时间小的在前,时间长的在后
    }
}
class MyTimer{//定时器内部要能够存放多个任务

    //2、利用优先级队列来组织若干个任务
    private PriorityBlockingQueue queue=new PriorityBlockingQueue<>();

    //3、实现schedule方法来注册任务到队列中
    public void schedule(Runnable runnable,long delay){
        MyTask task=new MyTask(runnable,delay);
        queue.put(task);//把任务都加到定时器里
        synchronized (locker){
            locker.notify();//每次任务插入成功之后,都唤醒一下扫描线程,让线程重新检查一下队首的任务看是否时间到了要执行
        }
    }

    private Object locker=new Object();
    //4、创建一个扫描线程,这个扫描线程不停的获取到队首元素,并且判定时间是否到达
    public MyTimer(){
        Thread t=new Thread(()->{
            while(true){
                try {
                    MyTask task=queue.take();//先取出队首元素
                    long curTime=System.currentTimeMillis();//获取下当前时间
                    //再比较下看看当前这个任务时间到了没
                    if(curTime

案例四:线程池

在线程池里创建线程比直接从系统中创建线程更高效,因为直接从系统里创建线程需要经过内核态,而直接把线程从线程池里取是用户态操作,而用户态更高效,因为用户态是可控的,而内核态由于不知道内核都需要做哪些操作,因此内核态是不可控的,可能有时高效,有时低效

例子:把一个线程池想象成一个公司,公司里面有很多员工在干活,把员工分成两类,正式员工、临时工 

Java标准库中线程池ThreadPoolExecutor的使用:

java.uiil.concurrent

构造方法:

 corePoolSize:核心线程数         (相当于正式员工的数量)

 maximumPoolSize:最大线程数         (相当于正式员工数+临时工数)

 long keepAliveTime:(相当于允许临时工摸鱼的时间)

 unit:时间的单位

 workQueue:任务队列,线程池会提供一个submit方法,让程序员把任务注册到线程池中,加到这个任务队列中

 threadFactory:线程工厂,线程是怎么创建出来的

 hander:拒绝策略,当任务满了怎么做


问题:有一个程序,这个程序要并发的/多线程的来完成一些任务,如果使用线程池的话,这里的线程数设为多少合适??

答:不同类型的程序,由于单个任务里面CPU上计算的时间和阻塞的时间是分布不相同的 ,要通过性能测试的方式找到合适的值,例如,写一个服务器程序,服务器里通过线程池,多线程的处理用户请求,就可以对这个服务器进行性能测试,比如构造有一些请求发送给服务器,要测试想能,这里的请求就需要构造很多,比如每秒发送500/1000/……根据实际的业务场景,构造一个合适的值,根据这里不同线程池的线程数来观察,程序处理任务的速度,程序持有CPU的占用率。当线程数多了,整体的速度是会变快,但是CPU占用率也会高;当线程数少了,整体的速度是会变慢,但是CPU占用率也会下降 ,需要找到一个让程序速度能接受,并且CPU占用也合理这样的平衡点


Java标准库中简化版本线程池Executors的使用:

(本质上是对ThreadPoolExecutor进行了封装,提供了一些默认参数)

//创建一个固定线程数目的线程池,参数指定了线程个数    
ExecutorService pool= Executors.newFixedThreadPool(10);

//创建一个自动扩容的线程池,会根据任务量来自动进行扩容
ExecutorService pool=Executors.newCachedThreadPool();

//创建一个只有一个线程的线程池
ExecutorService pool=Executors.newSingleThreadExecutor();

//创建一个带有定时器功能的线程池,类似于Timer
ExecutorService pool=Executors.newScheduledThreadPool();

关于多线程的经典案例~_第14张图片

 

自己实现一个线程池:

1、先能够描述任务(直接使用Runnable)

2、需要组织任务(直接使用BlockingQueue)

3、能够描述工作线程

4、还需要组织这些线程

5、需要实现往线程里面添加任务

class MyThreadPool {
        //1、描述一个任务,直接使用Runnable,不需要额外的创建类了
        //2、使用一个数据结构来组织若杠任务
        private BlockingQueue queue = new LinkedBlockingDeque<>();

        //3、描述一个线程,工作线程的功能就是从任务队列中取任务并执行
        static class Worker extends Thread {
            //当前线程池中有若干个Worker线程~这些线程内部都持有了上述的任务队列
            private BlockingQueue queue = null;

            public Worker(BlockingQueue queue) {
                this.queue = queue;
            }

            @Override
            public void run() {
                //就需要拿上面的队列
                while (true) {
                    try {
                        //循环的去获取任务队列中的任务
                        //这里如果队列为空,就直接阻塞,如果队列非空,就获取到里面的内容
                        Runnable runnable = queue.take();
                        //获取到之后,就执行任务
                        runnable.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
       //4、创建一个数据结构来组织若干个线程
    private List workers=new ArrayList<>();

    public MyThreadPool(int n){
        //在构造方法中,创建出若干个线程,放到上述的数组中
        for (int i = 0; i < n; i++) {
            Worker worker=new Worker(queue);
            worker.start();
            workers.add(worker);
        }
    }
    //5、创建一个方法,能够允许程序员来放任务到线程池中
    public void submit(Runnable runnable){
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Demo2 {
    public static void main(String[] args) {
        MyThreadPool pool=new MyThreadPool(10);
        for(int i=0;i<100;i++){
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello threadpool");
                }
            });
        }
    }
}

你可能感兴趣的:(JavaEE,java)