【JAVA EE初阶】多线程(下)

发布于:2025-08-06 ⋅ 阅读:(16) ⋅ 点赞:(0)

目录

多线程的代码案例

1.单例模式

2.生产者消费者模型

3.线程池

4.定时器


多线程的代码案例

1.单例模式

单例模式(一种设计模式),单个实例(对象),强制要求,某个类在某个程序中,只有唯一一个实例(不允许创建多个实例,不允许new多次)。

JDBC代码的基本流程:

1.创建DataSource => 描述了数据库服务器在哪里(url,user,password)

2.建立连接        dataSource.getConnection();

3.拼装SQL语句        Statement或者PreparedStatement

4.执行SQL        execute方法/executeQuery/executeUpdate

5.遍历结果集合        ResultSet,迭代器遍历

6.关闭资源        1)ResultSet  2)Statement  3)Connection

  • 饿汉方式的单例模式:
package thread;

class Singleton{
    //饿汉方式的单例模式
    private static  Singleton istance=new Singleton();
    //静态成员的初始化,是在类加载的阶段触发的
    //类加载往往就是在程序一启动就触发
    public static   Singleton getInstance(){
        //后续通过此方法获取实例
        return istance;
    }
    private  Singleton(){

    }
}
public class Demo27 {
    public static void main(String[] args) {
        Singleton t1=Singleton.getInstance();
        Singleton t2=Singleton.getInstance();
        System.out.println(t1==t2);//true
    }
}
  • 懒汉方式的单例模式:

懒和饿相对,饿是尽早创建实例,懒是尽晚创建实例(甚至不创建)


//通过懒汉模式构造单例模式
class SingletonLazy{
    private static SingletonLazy instance=null;
    public  static SingletonLazy getInstance(){
        if(instance==null){
            //创建实例时机-第一次使用的时候,而不是程序启动的时候
            instance=new SingletonLazy();
        }
        return instance;
    }
    private SingletonLazy(){

    }
}
public class Demo28 {
    public static void main(String[] args) {
        SingletonLazy s1=SingletonLazy.getInstance();
        SingletonLazy s2=SingletonLazy.getInstance();
        System.out.println(s1==s2);//true
    }
}

对于饿汉方式来说,是线程安全的。return instance是读操作,String不可变对象,天然线程安全。

对于懒汉方式,是线程不安全的。instance=new SingletonLazy();涉及到多线程的修改。随着第二个线程的覆盖操作,第一个线程new出来的对象会被GC释放掉。

为了解决懒汉模式线程不安全,引入了加锁。引入加锁之后,后执行的线程就会在加锁的位置阻塞,阻塞到前一个线程解锁。当后一个线程进入条件的时候,前一个线程已经修改完毕,Instance不再为null,就不会进行后续的new操作。


//通过懒汉模式构造单例模式
class SingletonLazy{
    private static SingletonLazy instance=null;
    private static Object lock=new Object();
    public  static SingletonLazy getInstance(){
        synchronized (lock){
            if(instance==null){
                //创建实例时机-第一次使用的时候,而不是程序启动的时候
                instance=new SingletonLazy();
            }
        }
        return instance;
    }
    private SingletonLazy(){

    }
}
public class Demo28 {
    public static void main(String[] args) {
        SingletonLazy s1=SingletonLazy.getInstance();
        SingletonLazy s2=SingletonLazy.getInstance();
        System.out.println(s1==s2);//true
    }
}

当把实例创建好了之后,后续再调用getInstance,此时都是直接执行return .

如果只是进行if判定+return,就是纯粹的读操作了。读操作不涉及线程安全问题。

但是,每次调用上述方法,都会触发一次加锁操作,多线程情况下,加锁就会互相阻塞,影响程序的执行效率。

解决方法就是按需加锁,真正涉及到线程安全的时候再加锁。如果实例已经创建过了,就不涉及线程安全问题;如果没有创建,就涉及线程安全问题。


//通过懒汉模式构造单例模式
class SingletonLazy{
    private static volatile SingletonLazy instance=null;
    //可能存在“内存可见性”问题,编译器优化比较复杂。
    //为了稳妥起见,给Instance直接加上volatile,从根本上杜绝内存可见性问题
    private static Object lock=new Object();
    public  static SingletonLazy getInstance(){
        if(instance==null){//判定是否加锁
            //在多线程中,两次判定之间,可能存在其他线程把if中的Instance变量修改了
            //导致两次if的结果不同
            synchronized (lock){
                if(instance==null){//判定是否需要new对象
                    //创建实例时机-第一次使用的时候,而不是程序启动的时候
                    instance=new SingletonLazy();
                }
            }
        }
        return instance;
    }
    private SingletonLazy(){

    }
}
public class Demo28 {
    public static void main(String[] args) {
        SingletonLazy s1=SingletonLazy.getInstance();
        SingletonLazy s2=SingletonLazy.getInstance();
        System.out.println(s1==s2);//true
    }
}

还有一个问题,是“指令重排序”的问题。也是编译器优化的一种体现形式,编译会在逻辑不变的前提下,调整代码执行的先后顺序,以达到提升性能的效果。

instance=new SingletonLazy();有三步:

1.申请内存空间

2.在空间上构造对象(初始化)

3.内存空间的首地址,赋值给引用变量

volatile的功能:

  1. 确保每次读取操作都是读内存
  2. 关于该变量的读取和修改操作,不会触发重排序

2.生产者消费者模型

优点:

  1. 解耦合(不一定是两个线程之间,也可以是两个服务器之间)。A和B不再直接交互,而是通过中间的阻塞队列进行交互。降低耦合,是为了让后续修改的时候成本低。
  2. 削峰填谷。队列服务器针对单个请求,做的事情很少(存储,转发),队列服务器可以抗很高的请求量。

缺点:

  1. 引入队列之后,服务器整体的结构会更复杂。此时就需要更多的机器进行部署。生产环境的结构会更复杂,管理起来更麻烦。
  2. 效率(性能)降低

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

Java中Linkedlist这个类进行中间插入的时候,add(插入值的下标),需要从头找到指定下标才能插入。O(N)

把阻塞队列单独包装成服务器程序并且使用单独的机器(集群)来部署,这样的队列称为“消息队列”(MQ)。

阻塞队列:

1.线程安全(天然使用在多线程环境中)

2.阻塞特性(队列满,入队列;队列空,出队列)

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

public class Demo29 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> queue=new LinkedBlockingQueue<>(100);//最大容纳100个元素
        queue.put("a");
        String element=queue.take();
        //put和take带有阻塞功能
        System.out.println(element);
    }
}

生产消费:

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

public class Demo30 {
    public static void main(String[] args) {
        BlockingQueue<Integer> queue=new LinkedBlockingQueue<>(1000);
        Thread producer=new Thread(()->{
            int n=0;
            while(true){
                try{
                    queue.put(n);
                    System.out.println("生产元素"+n);
                    n++;
                    Thread.sleep(1000);
                } catch (InterruptedException e){
                    throw new RuntimeException(e);
                }
            }
        },"producer");
        Thread consumer=new Thread(()->{
            //加入sleep队列会满
            while(true){
                try{
                    System.out.println("消费元素"+queue.take());
                }catch (InterruptedException e){
                    throw new RuntimeException(e);
                }
            }
        },"consumer");
        producer.start();
        consumer.start();
    }
}

单位换算中,Thousand 千=>K;Million 百万=>M;Billion 十亿=>G.

模拟实现简单的阻塞队列,基于阻塞队列实现生产者消费者模型:

优势:1.解耦合(生产者只和队列交互,消费者也只和队列交互,生产者和消费者能够更好的解耦合),修改代码更方便

2.削峰填谷


class MyBlockingQueue{
    private String[] data=null;
    private int head=0;//队头
    private int tail=0;//队尾
    private int size=0;//元素个数
    public MyBlockingQueue(int capacity){
        data=new String[capacity];
    }
    public void put(String element) throws InterruptedException {//入队
        synchronized (this){
            while(size>=data.length){
                this.wait();//队列满,需要阻塞等待
                //此时即使wait被唤醒,还会进行再次判断,再次进行阻塞
            }
            data[tail++]=element;
            if(tail>=data.length){
                tail=0;//循环
            }
            //if判断也可写成tail=(tail+1)%data.length;
            //但是运行效率较低(因为取余操作较加减操作慢)
            //开发效率也低,不便于理解
            size++;
            this.notify();
        }
    }
    public String take() throws InterruptedException {//出队
        synchronized (this){
            if(size==0){
                this.wait();//队列空,需要阻塞
            }
            String ret=data[head];
            head++;
            if(head>=data.length){
                head=0;
            }
            size--;
            this.notify();//唤醒wait
            return ret;
        }
    }
}

public class Demo31 {
    public static void main(String[] args) {
        MyBlockingQueue queue=new MyBlockingQueue(1000);
        Thread producer =new Thread(()->{
            int n=0;
            while(true){
                try{
                    queue.put(n+"");
                    System.out.println("生产元素"+n);
                    n++;
                    Thread.sleep(1000);
                }catch(InterruptedException e){
                    throw new RuntimeException(e);
                }
            }
        });
        Thread consumer =new Thread(()->{
            while(true){
                String n=null;
                try{
                    n=queue.take();
                    System.out.println("消费元素"+n);
                }catch(InterruptedException e){
                    throw new RuntimeException(e);
                }
            }
        });
        producer.start();
        consumer.start();
    }
}

比如有若干个线程使用这个队列,要么所有的线程阻塞在put,要么所有的线程阻塞在take,不可能一部分线程阻塞在put一部分线程阻塞在take.

3.线程池

常量池

字符串常量,在Java程序最初构建的时候就已经准备好。等程序运行的时候,这样的常量也就加载到内存中了。剩下了构造/销毁的开销。

线程池:把线程提前创建好,放到一个地方(类似数组),需要用的时候去取,用完了还回池子中。

操作系统的用户态和内核态:

操作系统=内核+配套的应用程序

内核包含操作系统的各种核心功能:

1.管理硬件设备

2.给软件提供稳定的运行环境

从线程池取线程,纯应用程序代码就可完成【可控】

从操作系统创建新线程,就需要操作系统内核配合完成【不可控】

使用线程池,就可以省下应用程序切换到内核中运行这样的开销。

ThreadPoolExecutor线程池执行器

核心方法:submit,任务放到线程池中,此时线程池里的线程就会执行这样的任务。

  • int corePoolSize核心线程数(至少有多少个线程,线程池一创建,这些线程也要随之创建,直到整个线程池销毁,这些线程才会销毁)
  • int maximumPoolSize最大线程数(核心线程+非核心线程,非核心线程不繁忙就销毁,繁忙就再创建)

线程也不是越多越好

Java的线程池里面包含几个线程是可以动态调整的。任务多的时候自动扩容成更多的线程;任务少的时候把额外的线程干掉,节约资源。

  • long keepAliveTime非核心线程允许空闲的最大时间
  • TimeUnit unit枚举(TimeUnit.SECONDS)
  • BlockingQueue<Runnable> workQueue工作队列

线程池本质上也是生产者消费者模型,调用submit就是在生产任务,线程池里的线程就是在消费任务。

  • ThreadFactory threadFactory线程工厂

工厂模式(也是一种设计模式,和单例模式并列关系)

用来弥补构造方法的缺陷。构造方法的名字是固定的,要想提供不同的版本就需要通过重载,有时候不一定能构成重载。

工厂方法的核心,通过静态方法,把构造对象new的过程,各种属性初始化的过程封装起来。提供多组静态方法,实现不同情况的构造。


class Point{
    public static Point makePointByXY(double x,double y){
        Point ret=new Point();//通过x和y给p进行属性设置
        return ret;
    }
    public static Point makePointByRA(double r,double a){
        Point ret=new Point();//通过r和a给p进行属性设置
        return ret;
    }
}

public class Demo33 {
    public static void main(String[] args) {
        Point p=Point.makePointByXY(10,20);//工厂设计模式
        
    }
}
  • RejectedExecutionHandler handler拒绝策略

对于线程池来说,发现入队列操作时,队列满了,不会真的触发“入队列操作”,不会真阻塞,而是执行拒绝策略相关的代码。

4种策略:

  1. ThreadPoolExecutor.AbortPolicy线程池直接抛出异常(线程池可能无法继续工作)
  2. ThreadPoolExecutor.CallerRunsPolicy让调用submit的线程自行执行任务
  3. ThreadPoolExecutor.DiscardOldestPolicy丢弃队列中最老的任务
  4. ThreadPoolExecutor.DiscardPolicy丢弃最新的任务,当前submit的这个任务

Java标准库提供了另一种类,针对ThreadPoolExecutor进行进一步封装,简化线程池的使用,也是基于工厂设计模式。

import java.util.concurrent.Executors;

public class Demo34 {
    public static void main(String[] args) {
        Executors.newFixedThreadPool(5);//创建固定数量的线程池(核心线程数和最大线程数一样)
        Executors.newCachedThreadPool();//最大线程数是一个很大的数字(线程可以无限增加)
    }

}

线程池,最核心的就是submit这样的操作,往线程池中添加任务(任务就是runnable).

得有线程来执行队列中的任务。在构造方法中,把线程创建出来。

线程池里,提前准备好10个线程,有100个客户端把请求发过来,把这100个客户端的请求封装成任务(Runnable)添加到线程池里,线程池中由10个线程负责处理这100个任务,这个过程就不涉及线程的创建销毁了。

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

class MyThreadPool{
    private BlockingQueue<Runnable> queue=null;
    public MyThreadPool(int n){
        //初始化线程池,创建固定个数的线程
        //这里使用ArrayBlockingQueue作为任务队列,容量为1000
        queue=new ArrayBlockingQueue<Runnable>(1000);
        //创建N个线程
        for(int i=0;i<n;i++){
            Thread t=new Thread(()->{
                try{
                    while(true){
                        Runnable task=queue.take();
                        task.run();
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            //t.setDaemon(true);设为后台线程
            t.start();
        }
    }
    public void submit(Runnable task) throws InterruptedException{
        queue.put(task);
    }
}



public class Demo35 {
    public static void main(String[] args) throws InterruptedException {
//        MyThreadPool pool=new MyThreadPool(10);
        ExecutorService pool= Executors.newCachedThreadPool();
        //向线程池提交任务
        for(int i=0;i<100;i++){
            int id=i;
            pool.submit(()->{
                System.out.println(Thread.currentThread().getName()+"id="+id);
            });
            //线程池里的线程还在take阻塞(等待)
            //线程池中的线程是前台线程,阻止进程结束
            pool.shutdown();
            //shutdown能够把线程池里的线程全部关闭,但不能保证线程池里的任务一定能全部执行完毕
            //如果要等待线程池内的任务全部执行完毕,需要调用awaitTermination方法
        }
    }
}

4.定时器

Java标准库中的Timer

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

public class Demo37 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            //核心就是重写run方法
            public void run() {
                System.out.println("hello 1000");
            }
        },1000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello 2000");
            }
        },2000);//2000ms之后执行
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello 3000");
            }
        },3000);
        //和线程池一样,Timer中也包含前台线程,阻止进程结束
        System.out.println("hello main");
    }
}

匿名内部类的写法:

  1. 创建子类(父类TimerTask),匿名
  2. 重写run
  3. new了子类的实例

模拟实现定时器:

  1. 创建一个类表示一个任务
  2. 定时器中,能够管理多个任务的。必须使用一些集合类把这多个任务管理起来
  3. 实现schedule方法,把任务添加到队列中即可
  4. 创建一个线程负责执行队列中的任务
//这样的写法基于抽象类的方式定义MyTimerTask
//abstract class MyTimerTask implements Runnable{
//    @Override
//    public abstract void run();
//}

import java.util.PriorityQueue;
import java.util.TimerTask;
import java.util.concurrent.Executors;

//另一种方法
class MyTimerTask implements Comparable<MyTimerTask>{
    private long time;
    private Runnable task;//持有成员
    public MyTimerTask(Runnable task,long time){
        this.task=task;
        this.time=time;
    }
    @Override
    public int compareTo(MyTimerTask o){
        return (int)(this.time-o.time);
    }
    public long getTime() {
        return time;
    }
    public void setTime(long time) {
        this.time = time;
    }
    public void run(){
        task.run();
    }

}

class MyTimer{
    private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>();
    Object lock=new Object();
    public void schedule(Runnable task,long delay){
        synchronized (lock){
            MyTimerTask timerTask=new MyTimerTask(task,System.currentTimeMillis()+delay);
            queue.add(timerTask);
            lock.notify();
        }
    }
    public MyTimer(){
        //创建一个线程负责执行队列中的任务
        Thread t=new Thread(()->{
            try{
                while(true){
                    synchronized (lock){
                        //取出队首元素
                        while(queue.isEmpty()){
                            lock.wait();
                        }
                        MyTimerTask task=queue.peek();
                        if(System.currentTimeMillis()<task.getTime()){
                            //当前任务时间比系统时间大,说明任务执行时机未到
                            lock.wait(task.getTime()-System.currentTimeMillis());
                        }else{
                            task.run();
                            queue.poll();
                        }
                    }
                }
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        });
        t.start();
    }
}

public class Demo38 {
    public static void main(String[] args) {
        MyTimer timer = new MyTimer();
        timer.schedule(new TimerTask() {
            @Override
            //核心就是重写run方法
            public void run() {
                System.out.println("hello 1000");
            }
        },1000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello 2000");
            }
        },2000);//2000ms之后执行
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello 3000");
            }
        },3000);
        Executors.newScheduledThreadPool(4);
        //创建了一个带有线程池的定时器
        
    }
}

定时器除了基于堆(优先级队列)方式实现之外,还可以基于“时间轮”:

类似一个循环队列(数组),每个元素是一个“时间单位”,每个元素又是一个链表,每到一个时间单位,光标指向下一个元素,同时把这个元素上对应链表中的任务都执行一遍。优势是性能更高。劣势是时间精度不如优先级队列(优先级队列,堆的调整 logN,适合精度高的情况)。更适合任务多的情况。


网站公告

今日签到

点亮在社区的每一天
去签到