JavaEE学习——多线程的四个案例

本文介绍了Java中的单例模式(包括饿汉模式和懒汉模式,重点讨论线程安全实现),以及阻塞队列在生产者消费者模型中的应用,涉及线程池的构造参数和线程安全策略。
摘要由CSDN通过智能技术生成

目录

1.设计模式

单例模式

饿汉模式:(迫切)程序启动,类加载之后,立即创建出实例

懒汉模式

那么这两个代码,谁才是线程安全的。

指令重排序

 线程安全的单例模式的三个要点

2.阻塞队列

 基于阻塞队列,实现生产者消费者模型——处理多线程的方式

模型优势:减少任务切换开销,减少锁竞争。

解耦合

消峰填谷

BlockingQueue

 现在要实现一下生产消费模型

 下面初步实现一下阻塞队列

只要使用wait建议搭配while来进行条件判定

 3.定时器

需要使用Timer类来实现

接下来自己尝试实现定时器。

4.线程池

面试题:Java标准库中线程池构造方法的参数和含义

拒绝策略

创建线程池时,线程个数怎么来。


1.设计模式

单例模式

对应场景:有些时候我们希望有的对象,在整个程序中只有一个实例对象

在java中有两种模式可以达到这样的效果

饿汉模式:(迫切)程序启动,类加载之后,立即创建出实例

懒汉模式:(延时)在第一次使用实例的时候再常见,否则就不能创建 

public class Main {
    public static void main(String[] args) {
    Singleton s1=Singleton.getInstance();
    Singleton s2=Singleton.getInstance();
        System.out.println(s1==s2);
    }
}
class  Singleton{
    private  static  Singleton instance = new Singleton();
    public static  Singleton getInstance(){
        return  instance;
    }
    private  Singleton(){};
}


true

带有static 类属性,由于每个类的类对象都是单例的,类对象的属性static也就是单例的

同时为了保险,要防止取new一个新对象,所以将构造方法写成private,这样就无法被new

但外面也不一定够无法调用,反射机制就可以在当前的单例模式中,创建出多个实例

懒汉模式
class  Singleton1{
    private  static  Singleton1 instance = null;
    public static  Singleton1 getInstance(){
        if(instance==null){
            instance=new Singleton1();
        }
        return  instance;
    }
    private  Singleton1(){};
}

这个的特点很容易就可以看出和上一个的区别,懒汉模式开始时并不创建新的对象,只有在用时才创建

那么这两个代码,谁才是线程安全的。

饿汉模式返回的都是同一个变量,没有问题。

懒汉模式的判断和创建并非是一个整体,如果两个线程同时调用一个为空的,就会修改两次,线程不安全。但通过枷锁可以让他线程安全。

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

 因为加锁很耗费时间和时间,按照上面这样处理。并且就算加了锁第二个线程未必能读到第一个线程修改后的值,此时就需要个对象前面加一个volatile。同时还有一个用途,避免指令重排序。

指令重排序

是编译器优化的一种手段,保证原有逻辑不变的前提下,对diamagnetic执行顺序进行调整,使调整之后的执行效率提高,如果是单线程,那么这样没问题,但如果是多线程,那么这样就与可能会出错。

 线程安全的单例模式的三个要点

加锁;

双重if;

volatile;

接下来就算第二个案例

2.阻塞队列

 之前因该听过队列和优先级队列,阻塞队列就是一个具有阻塞功能的队列,

当队列满的时候继续入队列就会出现阻塞,阻塞到其他线程从队列中取走元素位置

当队列为空的时候,继续出队列,也会初见阻塞,阻塞到其他线程往队列中添加元素为止。

 用处非常大

 基于阻塞队列,实现生产者消费者模型——处理多线程的方式

模型优势:减少任务切换开销,减少锁竞争。
解耦合

就是降低模块之间的耦合

消峰填谷

主要起到一个缓冲的作用

Java标准库中提供了现成的阻塞队列

BlockingQueue

public static void main(String[] args) throws InterruptedException {
        BlockingDeque<String> queue= new LinkedBlockingDeque<>();
        queue.put("hello");
        String elem=queue.take();//将里面的元素给取出来
        System.out.println(elem);
        elem=queue.take();
        System.out.println(elem);
    }

 对于这个类来说,offer和poll不具有阻塞功能

put和take带有阻塞功能

 现在要实现一下生产消费模型
import  java.util.*;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
//阻塞队列
public class Main {
    public static void main(String[] args) throws  InterruptedException{
        BlockingDeque<Integer> queue =new LinkedBlockingDeque<>();
        //生产
        Thread t1=new Thread(()->{
            int count=1;
            while(true){
                try{
                    queue.put(count);

                    System.out.println("生产元素:"+count);
                    count++;

                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        });
        //消费
        Thread t2=new Thread(()->{
            while(true){
                try{
                    Integer n=queue.take();
                    System.out.println("消费元素:"+n);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        t2.start();
    }
}


生产元素:1
消费元素:1
生产元素:2
消费元素:2
生产元素:3
消费元素:3
生产元素:4
消费元素:4
 下面初步实现一下阻塞队列
class MyBlockQueue{
    private  String[] items = new String[1000];
    private  int head=0;
    //有效范围【head,tail); 当head和tail相等重合时,相当于空队列。
    private  int tail=0;
    private  int size=0;
    public void put(String elem){
        if(size>= items.length){
            return;
        }
        items[tail]=elem;
        tail++;
        if(tail>=items.length)tail=0;
        size++;
    }
    public String take(){
        if(size==0)return null;
        String elem=items[head];
        head++;
        if(head>=items.length) {
            head = 0;
        }
        size--;
        return elem;
    }
}
public class Main {
    public static void main(String[] args) throws  InterruptedException{
            MyBlockQueue queue = new MyBlockQueue();
            queue.put("aaa");
            queue.put("bbb");
            queue.put("ccc");
            String elem=queue.take();
        System.out.println(elem);
    }
}

但这个队列线程不安全,如果有多个线程同时size++--就会出现问题

所以,先给put和take加锁,能够保证线程安全

class MyBlockQueue{
    private  String[] items = new String[1000];
    volatile private  int head=0;
    //有效范围【head,tail); 当head和tail相等重合时,相当于空队列。
    volatile private  int tail=0;
    volatile private  int size=0;
    public void put(String elem) throws  InterruptedException{
        synchronized (this) {
            while(size >= items.length) {
                    this.wait();
            }
            items[tail] = elem;
            tail++;
            if (tail >= items.length) tail = 0;
            size++;
            this.notify();
        }
    }
    public synchronized String take() throws  InterruptedException{
        while(size==0) {
                this.wait();
        }
        String elem=items[head];
        head++;
        if(head>=items.length) {
            head = 0;
        }
        size--;
        this.notify();
        return elem;
    }
}

加上wait,notify,synchronized,volatile。保证线程安全和阻塞的效果。

只要使用wait建议搭配while来进行条件判定

 3.定时器

这个也是日常开发中常见的组件,类似于闹钟

需要使用Timer类来实现
import  java.util.*;
public class Main {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {//类似Runnable表示任务,它实现了RUnnable接口
            @Override
            public void run() {
                System.out.println("hello");
            }
        },3000);//安排
        System.out.println("程序开始运行");
        System.out.println("world");
    }
}

这个的使用还是比较简单的。

接下来自己尝试实现定时器。
import java.util.PriorityQueue;
import  java.util.*;
//实现定时器
class MyTimerTask{
    private  long time;
    //具体的任务是什么
    private  Runnable runnable;
    public MyTimerTask(Runnable runnable,long delay){
        //delay是一个相对的时间差,构造time时要根据当前的系统时间和delay进行构造。
        time = System.currentTimeMillis()+delay;
        this.runnable  = runnable;
    }

    public long getTime() {
        return time;
    }
    public Runnable getRunnable() {
        return runnable;
    }
}
//定时器类的本体
class  MyTimer{
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    // 定时器的核心方法
    public void schedule(Runnable runnable ,long delay){
        MyTimerTask task = new MyTimerTask(runnable,delay);
        queue.offer(task);
    }
    //还需要构造一个扫描线程,一方面取负责监控队首元素是否到点了,以及调用
    public  MyTimer (){
        Thread t = new Thread(()->{
           while(true){
               try{
                   if(queue.isEmpty())continue;
                   MyTimerTask task=queue.peek();
                   long curtime =System.currentTimeMillis();
                   if(curtime>task.getTime()){
                       queue.poll();
                       task.getRunnable().run();
                   }else {
                       //让当前的扫描线程休眠一下
                       Thread.sleep(task.getTime()-curtime);
                   }
               }catch(InterruptedException e){
                   e.printStackTrace();
               }
           }
        });
        t.start();
    }
}

4.线程池

如果我们需要频繁的创建销毁线程,此时创建销毁线程的成本就不能忽视了,因此就可以使用线程池

前提是创建好一波线程,后续需要使用线程,就直接从池子里哪一个即可

当不再使用时,就放回到池子中

如果是从系统这里创建线程,就需要调用系统api,进一步的由操作系统内核完成线程的创建过程。——内核给所有的进程提供服务,非常繁忙不可控

如果是从线程池这里获取线程,上述的内核中进行的操作,都是提前做好了,现在的取线程的过程,纯粹的用户代码完成——可控

使用代码

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(4);
        //线程池对象和后面的创建线程的过程。最后面是工厂方法
        //Executors 称为工厂类一般创建对象,都是通过new。但方法名字由缺陷,构造方法的名字固定就是类名。只能使用方法重载的方式来实现。
        //里面的线程数量是固定的
        //Executors.newCachedThreadPool();//创建一个动态调整线程数目的线程池。
        //Executors.newSingleThreadExecutor();// 创建单个线程
        //Executors.newScheduledThreadPool(100);
        //类似于定时器的效果,添加一些任务,任务都在后续的某个时刻再执行,被执行的时候不是只有一个扫描线程来执行任务,可能是由多个线程共同执行所有的任务。
        for(int i=0;i<10;i++)
        service.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        });
    }
}

使用时使用submit()来对线程安排任务。

面试题:Java标准库中线程池构造方法的参数和含义

int corePoolSize 核心线程数:ThreadPoolExecutor里面的线程个数,并非时固定不变的,会根据当前任务的情况动态发生变化,但就算没有任务,也至少得有这些线程。

int maximumPoolSize 最大线程数:无论有多少任务,线程数也不能比这个多

long keepAliveTime,TimeUnit unit:前者是允许摸鱼的最大时间,后面是时间单位。 

Blocking Queue<Runnnable> workQueue:线程池内部有许多任务,可以使用阻塞队列来进行管理

ThreadFactory threadFactory:工厂模式。

RejectedExecutionHandler handler:线程池考察的重点,拒绝方式拒绝策略。线程池有一个阻塞对列,当阻塞队列满了之后,该如何应对

拒绝策略

ThreadPoolExecutor.AborPolicy :终止,直接抛出异常,线程池开摆了 

ThreadPoolExcutor.CallerRunsPolicy:谁是添加这个新的任务的线程,谁就去执行这个任务

ThreadPoolExcutor.Discard0ldestPolicy:丢弃最早的任务,执行新的任务

ThreadPoolExcutor.DiscardPolicy:丢弃新的任务

上面谈到的线程池 Executors是封装过的

ThreadPoolExecutor是原生的。

那个都可以使用,具体选择看需求

自己实现一个线程池

import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

//线程池
public class Main {
    public static void main(String[] args) throws  InterruptedException{
        MyThreadPool myThreadPool = new MyThreadPool(4);
        for(int i=0;i<1000;i++){
            myThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"hello");
                }
            });
        }
    }
}
class  MyThreadPool{
    private BlockingDeque<Runnable> queue = new LinkedBlockingDeque<Runnable>();
    public void submit(Runnable runnable) throws InterruptedException{
        queue.put(runnable);

    }
    //表示线程池中有n个线程
    public MyThreadPool(int n) throws  InterruptedException{
        for(int i=0;i<n;i++){
            Thread t = new Thread(()->{
                while(true){
                    try{
                        Runnable runnable = queue.take();
                        runnable.run();
                    }catch(InterruptedException e){
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
    }
}

创建线程池时,线程个数怎么来。

不同的项目中,cpu要做的工作是不同的

有的线程工作时,是cpu密集型,线程的工作全是运算,大部分工作都要在cpu上完成,所以cpu需要给他安排核心去工作才可以。所以安排N个线程即可,N为cpu的逻辑核心数。

有的线程工作时,是io密集型,读写文件,等待用户输入,网络通信。基本安排2*N个即可,因为大部分都在等待,不消耗cpu

但实际情况没有这么极端,两个都含有。这里最好的做法是,通过性能测试,选出最优的线程数目。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值