JavaEE《多线程》

文章目录

  • 一、认识线程
    • [1.1 概念](#1.1 概念)
    • [1.2 多线程程序的编写](#1.2 多线程程序的编写)
    • [1.3 创建线程](#1.3 创建线程)
  • 二、Thread类及常见方法
    • [2.1 Thread 常见的构造方法](#2.1 Thread 常见的构造方法)
    • [2.2 Thread 常见的属性](#2.2 Thread 常见的属性)
    • [2.3 中断一个线程](#2.3 中断一个线程)
    • [2.4 等待一个线程 - join()](#2.4 等待一个线程 - join())
  • 三、线程的状态
  • 四、多线程带来的的风险-线程安全
    • [4.1 观察线程不安全](#4.1 观察线程不安全)
    • [4.2 线程不安全的原因](#4.2 线程不安全的原因)
  • [五、synchronized 关键字](#五、synchronized 关键字)
    • [5.1 synchronized 的特性](#5.1 synchronized 的特性)
    • [5.2 synchronized 使用示例](#5.2 synchronized 使用示例)
      • [5.2.1 修饰代码块: 明确指定锁哪个对象.](#5.2.1 修饰代码块: 明确指定锁哪个对象.)
      • [5.2.2 直接修饰普通方法: 锁的 SynchronizedDemo 对象](#5.2.2 直接修饰普通方法: 锁的 SynchronizedDemo 对象)
      • [5.2.3 修饰静态方法: 锁的 SynchronizedDemo 类的对象](#5.2.3 修饰静态方法: 锁的 SynchronizedDemo 类的对象)
    • [5.3 死锁产生的条件](#5.3 死锁产生的条件)
    • [5.4 Java 标准库中的线程安全类](#5.4 Java 标准库中的线程安全类)
  • [六、volatile 关键字](#六、volatile 关键字)
    • [6.1 volatile 能够保证 "内存可见性"](#6.1 volatile 能够保证 "内存可见性")
    • [6.2 volatile 不保证原子性](#6.2 volatile 不保证原子性)
  • [七、wait 和 notify](#七、wait 和 notify)
    • [7.1 wait()方法](#7.1 wait()方法)
    • [7.2 notify()方法](#7.2 notify()方法)
    • [7.3 notifyAll()方法](#7.3 notifyAll()方法)
  • 八、多线程案例
    • [8.1 单例模式](#8.1 单例模式)
      • [8.1.1 饿汉模式](#8.1.1 饿汉模式)
      • [8.1.2 懒汉模式](#8.1.2 懒汉模式)
    • [8.2 阻塞队列](#8.2 阻塞队列)
      • [8.2.1 生产者消费者模型](#8.2.1 生产者消费者模型)
      • [8.1.2 标准库中的阻塞队列](#8.1.2 标准库中的阻塞队列)
      • [8.1.3 模拟实现一个简单的阻塞队列](#8.1.3 模拟实现一个简单的阻塞队列)
    • [8.3 线程池](#8.3 线程池)
      • [8.3.1 标准库中的线程池](#8.3.1 标准库中的线程池)
      • [8.3.2 模拟实现线程池](#8.3.2 模拟实现线程池)
      • [8.3.3 工厂模式解释](#8.3.3 工厂模式解释)
    • [8.4 定时器](#8.4 定时器)
      • [8.4.1 定时器的使用](#8.4.1 定时器的使用)
      • [8.4.2 模拟实现定时器](#8.4.2 模拟实现定时器)
  • 九、进程与线程的区别
  • 总结

一、认识线程

1.1 概念

进程是什么?

进程是操作系统对⼀个正在运行的程序的⼀种抽象,换言之,可以把进程看做程序的一次运行过程; 同时,在操作系统内部,进程又是操作系统进行资源分配的基本单位

随着互联网的快速发展,服务器对于并发执行的要求越来越高,进程的创建和删除的效率变得低下,由此发展出来了线程

线程是什么?

⼀个线程就是⼀个"执行流".每个线程之间都可以按照顺序执行自己的代码.多个线程之间"同时"执行着多份代码.

线程是操作系统中的概念.操作系统内核实现了线程这样的机制,并且对用户层提供了⼀些API供用户使用

Java 标准库中Thread类可以视为是对操作系统提供的API进行了进⼀步的抽象和封装

1.2 多线程程序的编写

每个线程都是⼀个独立的执行流

多个线程之间是"并发"执行的

java 复制代码
class MyThread extends Thread{
    public void run() {//线程的入口,线程开始后就从这里执行
        while (true){//死循环,保证线程的持续存在,一直运行,便于观察
            System.out.println("我建立的MyThread");
            try {
                Thread.sleep(1000);//线程休息1s后再继续
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

}

public class demo1 {
    public static void main(String[] args) throws InterruptedException {
        MyThread t1 = new MyThread();
        t1.start();//开启线程
        while (true){
            System.out.println("main线程");
            Thread.sleep(1000);
        }
    }
}

同时我们要注意,线程的调度是随机的,和我们观察到的执行次数不相关。

在jconsole中我们可以查看我们的线程运行情况

1.3 创建线程

方法1:继承Thread类

如我们上面例子中所写

  1. 创建一个类继承Thread从而来创建⼀个线程类
java 复制代码
class MyThread extends Thread{
	@Override
    public void run() {
        System.out.println("我建立的MyThread");
    }
}
  1. 实例化这个线程类
java 复制代码
MyThread t1 = new MyThread();
  1. 调用start方法启动线程
java 复制代码
t1.start();

方法二:实现Runnable接口

  1. 创建类实现Runnable接口
java 复制代码
class MyThread implements Runnable{

    @Override
    public void run() {
        System.out.println("这是我通过实现Runnable的线程类");
    }
}
  1. 创建Thread类实例,调用Thread的构造方法时将Runnable对象作为target参数
java 复制代码
Thread t1 = new Thread(new MyThread());
  1. 调用start方法
java 复制代码
t1.start();

方法三:匿名内部类创建Thread子类对象

java 复制代码
Thread t1 = new Thread(){
   public void run(){
       System.out.println("匿名内部类创建线程类");
   }
};
t1.start();

方法四:匿名内部类实现Runnable接口

java 复制代码
Thread t2 = new Thread(new Runnable(){
   	public void run(){
        System.out.println("通过匿名内部类实现Runnable接口");
    }
});
t2.start();

方法五:lambda表达式创建Runnable子类对象

java 复制代码
Thread t3 = new Thread(()->{
    System.out.println("通过lambda表达式实现Runnable接口");
});

注意start和run的区别: 调用start方法,才真的在操作系统的底层创建出⼀个线程,而run只是一个回调函数,在start一个线程后自动执行run方法


二、Thread类及常见方法

2.1 Thread 常见的构造方法

方法 说明
Thread() 创建线程对象
Thread(Runnable target) 使用Runnable对象创建线程对象
Thread(String name) 创建线程对象,并命名
Thread(Runnable target, String name) 使用Runnable对象创建线程对象,并命名

2.2 Thread 常见的属性

属性 获取方法
ID getId()
名称 getName()
状态 getState()
优先级 getPriority()
是否为后台线程 isDaemon()
是否存活 isAlive()
是否被中断 isInterrupted
  1. ID是线程的唯⼀标识,不同线程不会重复
  2. 名称是各种调试工具用到,查看线程名字的
  3. 状态表示线程当前所处的⼀个情况,下面我们会进⼀步说明
  4. 优先级高的线程理论上来说更容易被调度到
  5. 关于后台线程,需要记住⼀点:JVM会在⼀个进程的所有非后台线程结束后,才会结束运行。
  6. 是否存活,即简单的理解为run方法是否运行结束了
  7. 是否被中断,处于运行状态下,是否被强行停止了

2.3 中断一个线程

java 复制代码
public class demo3 {
    private static boolean isFinish = false;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            while(!isFinish){
                System.out.println("hello Thread 1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("Thread 1结束");
        });
        t1.start();

        Thread.sleep(3000);
        isFinish = true;
    }
}


小插曲有关lambda表达式

而在成员变量中定义isFinish后,触发的不是变量捕获,而是内部类引用外部类,内部类本来就可以访问外部类,成员变量生命周期是由GC(垃圾回收)来管理的,不用担心生命周期失效的问题

介绍一个静态方法 Thread.currentThread()

这个方法返回的是调用该方法时正在执行的线程对象不同线程调用该方法返回的是各自线程的引用

java 复制代码
public class demo4 {

    public static void main(String[] args) {
    	//在Thread中调用,返回Thread0
        Thread t1 = new Thread(() -> System.out.println(Thread.currentThread().getName()));

        t1.start();
		//这个是在main方法中调用的,返回就是main
        System.out.println(Thread.currentThread().getName());
    }


}

Thread 内部包含了⼀个 boolean 类型的变量作为线程是否被中断的标记------标志位

java 复制代码
public class demo4 {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("Thread is not interrupted");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t1.start();

        Thread.sleep(1000);
        t1.interrupt();//主动中止t1
    }


}
方法 说明
public void interrupt 中断对象关联的线程,如果线程正在阻塞(如sleep),以异常方式通知
public static boolean interrupted() 判断当前线程的中断标志位是否设置,调用后清除标志位
public boolean isInterrupted 判断对象关联的线程的标志位是否设置,调用后便于清除标志位

2.4 等待一个线程 - join()

当我们需要等待一个线程结束后再执行任务时可以用到join,比如,商铺需要等到工厂生产出东西后才能卖

java 复制代码
public class demo5 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = ()->{
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName()+"正在工作");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println(Thread.currentThread().getName()+"工作完了");
        };

        Thread thread1 = new Thread(runnable,"张三");
        Thread thread2 = new Thread(runnable,"李四");
        System.out.println("张三开始工作");
        thread1.start();
        thread1.join();
        System.out.println("张三工作结束了,李四可以开始工作了");
        thread2.start();
    }
}

三、线程的状态

线程的状态是⼀个枚举类型 Thread.State

java 复制代码
public class demo6 {
    public static void main(String[] args) {
        for (Thread.State state: Thread.State.values()) {
            System.out.println(state);
        }
    }
}
  1. NEW : 当一个 Thread 对象被创建,但还没有调用 start() 方法时,线程处于 NEW 状态,安排了工作, 还未开始行动
  2. RUNNABLE : 当线程调用了 start() 方法后,线程进入 RUNNABLE 状态。处于该状态的线程可能正在 Java 虚拟机中执行,也可能正在等待操作系统的资源,可工作的. 又可以分成正在工作中和即将开始工作.
  3. BLOCKED : 当线程试图获取一个已经被其他线程持有的对象锁时,线程会进入 BLOCKED 状态。线程会一直阻塞,直到获取到该锁。
  4. WAITING : 当线程调用了 Object.wait()、Thread.join() 或 LockSupport.park() 方法后,线程会进入 WAITING 状态。线程会一直等待,直到其他线程调用相应的唤醒方法
  5. TIMED_WAITING : 与 WAITING 状态类似,但是 TIMED_WAITING 状态的线程会在指定的时间后自动唤醒。常见的方法如 Thread.sleep(long millis)、Object.wait(long timeout)、Thread.join(long millis) 等会使线程进入该状态。
  6. TERMINATED : 当线程的 run() 方法执行完毕,或者因为异常而终止时,线程进入 TERMINATED 状态。此时线程已经结束执行。工作完成了。

四、多线程带来的的风险-线程安全

4.1 观察线程不安全

java 复制代码
public class demo7 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();//主线程也在进行中,要让主线程等待其他两个线程执行完
        t2.join();
        System.out.println(count);
    }
}


这里的两个join()方法不涉及先后,如果t1先结束,main就阻塞在t2.join。

4.2 线程不安全的原因

线程是随机调度的,即每次调度时可能是,t1拿到count,此时count=0,但与此同时t2也可以拿到这个count,count=0,它们都对这个值++,并传回去,实际上只加了一次

这也破坏了原子性
原子性 :指的是一个操作或一组操作在执行过程中不会被其他线程干扰,要么全部执行完毕,要么完全不执行,不会出现执行到一半被其他线程打断的情况。原子操作可以避免多线程环境下的数据竞争和不一致问题

一条 java 语句不一定是原子的,也不一定只是一条指令

比如上面代码中的 count++,其实是由三步操作组成的:

  1. 从内存把数据读到 CPU
  2. 进行数据更新
  3. 把数据写回到 CPU
    这里的每一个步骤在不原子的情况下都可能被其他线程截取到,从而对结果造成影响

五、synchronized 关键字

5.1 synchronized 的特性

5.1.1互斥

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同⼀个对象 synchronized 就会阻塞等待.

• 进入 synchronized 修饰的代码块, 相当于加锁

• 退出 synchronized 修饰的代码块, 相当于解锁


5.1.2 可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;

java 复制代码
Thread t1 = new Thread(()-> {
            synchronized (lock) {
                synchronized (lock) {
                    for (int i = 0; i < 50000; i++) {
                        count++;
                    }
                }
            }
        });

按照之前的说法,这里第一个锁上了之后,里面那个锁应该无法获取才对,就是自己将自己锁死了,但 synchronized 不会

在可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息.

  • 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己 那么仍然可以继续获取到锁, 并让计数器自增.
  • 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)

5.2 synchronized 使用示例

synchronized锁的本质是两个线程竞争同一把锁,我们需要做的就是设置同一把锁,让他们形成这样的竞争关系

5.2.1 修饰代码块: 明确指定锁哪个对象.

锁任意对象

java 复制代码
public class SynchronizedDemo {
    private Object locker = new Object();

    public void method() {
        synchronized (locker) {
        
        }
    }
}

解决之前的线程不安全问题

java 复制代码
public class demo7 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Thread t1 = new Thread(()-> {
            synchronized (lock) {
                for (int i = 0; i < 50000; i++) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(()-> {
            synchronized (lock) {
                for (int i = 0; i < 50000; i++) {
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();//主线程也在进行中,要让主线程等待其他两个线程执行完
        t2.join();
        System.out.println(count);
    }
}

锁当前对象

java 复制代码
public class SynchronizedDemo {
    public void method() {
        synchronized (this) {

        }
    }
}

5.2.2 直接修饰普通方法: 锁的 SynchronizedDemo 对象

java 复制代码
```java
public class SynchronizedDemo {
    public synchronized void method() {
    }
}

5.2.3 修饰静态方法: 锁的 SynchronizedDemo 类的对象

java 复制代码
public class SynchronizedDemo {
    public synchronized static void method() {
    }
}

5.3 死锁产生的条件

  1. 锁是互斥的:一个线程拿到锁之后,另一个线程再尝试获取锁,就必须阻塞等待
  2. 锁是不可抢占的:线程之间不能抢夺锁,线程1获得锁后其他线程想获取只能等,不能抢
  3. 请求和保持:一个线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求线程阻塞,但又对自己已获得的其他资源保持不放。
  4. 循环等待:多个线程,多吧锁之间的等待过程构成循环,假设有三个线程 T1、T2 和 T3,以及三个资源 R1、R2 和 R3。T1 持有 R1 并等待 R2,T2 持有 R2 并等待 R3,T3 持有 R3 并等待 R1,形成了一个循环等待的关系。

5.4 Java 标准库中的线程安全类

Vector

HashTable

ConcurrentHashMap

StringBuffer

之前有学到StringBuffer和StringBuilder的时候,说过StringBuffer是线程安全的,这里表示StringBuffer是线程安全类,因为它的方法中涉及修改的都带有synchronized修饰


六、volatile 关键字

6.1 volatile 能够保证 "内存可见性"

问题源于编译器优化,提高效率

volatile 修饰的变量, 能够保证 "内存可见性"

加上 volatile , 强制读写内存. 速度慢了, 但是数据变的更准确了

6.2 volatile 不保证原子性

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性

它不是上锁,只是让变量不会被拉入寄存器中,使得线程不从内存中读取,从而保证内存可见性


七、wait 和 notify

由于线程之间是抢占式执行的(是抢着去上厕所,不代表把门砸了,即不是抢夺锁), 因此线程之间执行的先后顺序难以预知.

但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.

synchronized的对象和wait和notify的对象要是同一个

7.1 wait()方法

wait() | wait(long timeout): 让当前线程进入等待状态

wait 做的事情:

  • 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  • 释放当前的锁(意味着其他线程可以调用这个锁)
  • 满足一定条件时被唤醒, 重新尝试获取这个锁

join是一直等,要么等自己设定的时间结束,要么等整个线程结束,而wait是等notify唤醒,其他线程使用notify后就会唤醒。

7.2 notify()方法

上面的wait例子中,我们的线程会一直等待,知道其他线程用同样的锁并且使用notify()唤醒它

notify 方法是唤醒等待的线程

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")
  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。(当一个线程调用 notify() 方法时,它只是通知了在该对象锁上等待的线程,表明 "有资源可能可用了",但调用 notify() 方法的线程并不会立即释放该对象的锁)
java 复制代码
public class demo9 {
    public static void main(String[] args) {
        Object lock = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("t1等待");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1等待结束");
            }
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("t2拿到锁");
                try {
                    Thread.sleep(1000);
                    lock.notify();
                    System.out.println("t2使用notify唤醒wait");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t2.start();
    }
}

7.3 notifyAll()方法

notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程

java 复制代码
public class demo9 {
    public static void main(String[] args) {
        Object lock = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("t1等待");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1等待结束");
            }
        });
        t1.start();


        Thread t3 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("t3等待");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t3等待结束");
            }
        });
        t3.start();

        Thread t4 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("t4等待");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t4等待结束");
            }
        });
        t4.start();

        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("t2拿到锁");
                try {
                    Thread.sleep(1000);
                    lock.notifyAll();
                    System.out.println("t2使用notify唤醒wait");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t2.start();
    }
}


注意:虽然是同时唤醒 3 个线程, 但是这 3 个线程需要竞争锁. 所以并不是同时执行, 而仍然是有先有后的执行.synchronized代码块执行完释放锁,其他线程再拿


八、多线程案例

8.1 单例模式

单例模式能保证某个类在程序中只存在唯⼀⼀份实例, 而不会创建出多个实例

8.1.1 饿汉模式

类加载的同时, 创建实例

java 复制代码
public class HungryMan {
    //饿汉
    private static HungryMan hm = new HungryMan();
    private HungryMan() {}//外部调用构造函数后,类加载的时候会创建一个实例,后续其他引用再调用也只会返回同一个实例,因为类只加载一次
    public static HungryMan getInstance() {
        return hm;
    }
}

8.1.2 懒汉模式

类加载的时候不创建实例. 第⼀次使用的时候才创建实例.
单线程下

java 复制代码
public class TheLazybones {
    //懒汉
    private static TheLazybones theLazybones = null;
    private TheLazybones() {}
    public  static TheLazybones getInstance() {
        if (theLazybones == null) {
            theLazybones = new TheLazybones();
        }
        return theLazybones;
    }
}

而这种方式是不安全的对于多线程,线程安全问题发生在首次创建实例时. 如果在多个线程中同时调用 getInstance 方法(类似于前面所说的count++,多个线程同时进行count++), 就可能导致创建出多个实例.

一旦实例已经创建好了, 后面再多线程环境调用 getInstance 就不再有线程安全问题了(不再修改 theLazybones 了)

多线程下

java 复制代码
public class TheLazybones {
    //懒汉
    private static TheLazybones theLazybones = null;
    private TheLazybones() {}
    public synchronized static TheLazybones getInstance() {
        if (theLazybones == null) {
            theLazybones = new TheLazybones();
        }
        return theLazybones;
    }
}

懒汉模式-多线程(改进)

java 复制代码
public class TheLazybones {
    //懒汉
    private static volatile TheLazybones theLazybones = null;
    private TheLazybones() {}
    public static TheLazybones getInstance() {
        if (theLazybones == null) {
            synchronized (TheLazybones.class) {
                if (theLazybones == null) {
                    theLazybones = new TheLazybones();
                }
            }
        }
        return theLazybones;
    }
}

volatile预防内存可见性问题,避免 "内存可见性" 导致读取的 instance 出现偏差

将方法中的synchronized去掉,换成两个if加synchronized:

  1. 因为懒汉模式只会在首次创建的时候发生线程安全问题,当创建完实例后就没有这样的问题了,如果后续一直使用synchronized意味着后续多个线程会因为创建实例时产生阻塞,效率下降
  2. 第一个if是判断是不是第一次创建,这个时候多个线程都会到达这里,在这个地方上锁,保证只会有一个实例被创建,后续再进行创建的时候就不会再被阻塞了,提升效率
  3. 第二个if是当有一个线程成功创建完实例后,释放了锁,而之前被卡在锁那的线程就会到达第二个if那里,但因为实例已经创建所以无法进入if,阻止了新实例的创建

8.2 阻塞队列

阻塞队列是什么?

阻塞队列是一种特殊的队列,也遵守 "先进先出" 的原则.

阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:

  • 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
  • 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.

阻塞队列的一个典型应用场景就是 "生产者消费者模型". 这是一种非常典型的开发模型

8.2.1 生产者消费者模型

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.

生产者消费者模型的两个重要优势:

  1. 解耦合(不一定是两个线程之间,也可以是两个服务器之间

  2. 削峰填谷

代价:

  1. 引入队列后,整体的结构更复杂
  2. 效率会有影响

8.1.2 标准库中的阻塞队列

Java中提供的有关阻塞队列的接口----BlockingDeque

java 复制代码
public static void main(String[] args) throws InterruptedException {
        BlockingDeque<String> queue = new LinkedBlockingDeque<String>();//建议在()中加入需求的最大值
        queue.put("AAAA");//入队列
        String elem = queue.take();//出队列
        //用put和take才有阻塞功能
    }
java 复制代码
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class demo10 {
    public static void main(String[] args) {
        BlockingQueue<Integer> bq = new ArrayBlockingQueue<Integer>(10);
        Thread customer = new Thread(()->{
            while(true){
                try {
                    int value = bq.take();
                    System.out.println("消费的元素"+value);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"消费者");
        customer.start();
        Thread producer = new Thread(()->{
            Random random = new Random();
            while(true){
                try {
                    int num = random.nextInt(1000);
                    bq.put(num);
                    System.out.println("生产的元素"+num);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        producer.start();

    }
}

把阻塞队列单独包装成服务器程序并且使用单独的机器或者集群来部署,这样的队列称为------消息队列

8.1.3 模拟实现一个简单的阻塞队列

java 复制代码
public class MyBlockingQueue {
    private String[] data = null;

    private int head = 0;//队首
    private int tail = 0;//队尾
    private int size = 0;//队列元素个数

    public MyBlockingQueue() {
        data = new String[10];
    }
    //入队列
    public synchronized void put(String data) throws InterruptedException {
        while (tail == size) {
            //队列满了
            this.wait();
        }
        this.data[tail] = data;
        tail = (tail + 1)%data.length();
        size++;
        this.notifyAll();//当队为空的时候,其他线程使用了put,从而唤醒出队列操作
    }
    //出队列
    public synchronized String take() throws InterruptedException {
        while (size == 0) {
            this.wait();
        }
        String data = this.data[head];
        head = (head + 1)%data.length();
        size--;
        this.notifyAll();//当队列满的时候,其他线程使用了take,从而唤醒入队列操作
        return data;
    }

}

wait,是配合while使用的,因为我们会出现如多个线程彼此随机唤醒,和interrupt提前唤醒wait的情况,这时候会出现条件不正确但任然可以往下执行的情况,比如 size < 0,任然可以take,使用while可以循环判断条件,预防这些错误的发生。


8.3 线程池

与之前学的常量池相似,String(字符串常量,在Java程序最初构建时,就已经准备好,等程序运行时,常量就加载到内存中了)

最初引入线程的原因:为了高效创建和销毁线程

随着互联网发展,对于性能要求越来越高,线程的创建和销毁的性能也不够了

解决方案:

  1. 线程池
  2. 协程(纤程,轻量级线程)-->Java17引入

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

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

一个操作系统 = 内核 + 配套的应用设备

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

  1. 管理硬件设备
  2. 给软件提供稳定的运行环境

一个操作系统,内核就是一份,一份内核,要给所有的应用程序提供服务

如果有一段代码是应用程序自行完成的---整个执行过程是可控的

如果有一段代码需要进入到内核中,由内核完成一系列操作----这个过程是不可控的

从线程池中取出现成的线程,纯应用程序可以完成【可控】

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

使用线程池省下应用程序切换到内核运行的开销

8.3.1 标准库中的线程池

Java标准库提供了直接使用的线程池----里面包含的线程个数是可以动态调整的,任务多自动扩容更多的线程,任务少销毁额外的线程

​------ThreadPoolExecutor

构造这个类的构造方法比较麻烦(参数较多)

  1. int corePoolSize,核心线程数:至少有几个线程,线程池一创建,这些线程也随之创建,直到整个线程池销毁

  2. int maximumPoolSize,最大线程数:核心线程 + 非核心线程:不繁忙就销毁,繁忙就创建

  3. long keepAliveTime,非核心线程允许最大的空闲时间

  4. TimeUnit unit,标准库内置的枚举类型,包含s,ms等时间单位

  5. BlockingQueue< Runnable > workQueue,工作队列,线程池本质上也是生产者消费者模型,调用submit就是生产任务,线程池里的线程就是在消费任务

  6. ThreadFactory threadFactory ,工厂模式(也是一种设计模式,和单例模式是并列的关系),统一的构造并初始化线程用来弥补构造方法的缺陷:构造方法的名字是固定的,想要提供不同的版本要通过重载,有时候不能构成重载。工厂方法的核心:通过静态方法,把构造对象new的过程各种属性初始化的过程封装起来了提供多组静态方法,实现不同情况的构造。

  7. RejectedExecutionHandler handler ,(整个线程池七个参数中最重要、最复杂的),拒绝策略,submit把任务添加到任务队列中,任务队列是阻塞队列,队列满了再添加,阻塞对于线程池来说,发现入队列操作时,队列满了,不会触发入队列操作,不会真阻塞,而是执行拒绝策略相关代码

因为太麻烦,Java提供了另一组类(基于工厂模式),针对ThreadPoolExecutor进行进一步的封装,简化线程池的使用。

------Executors

java 复制代码
ExecutorService threadPool = Executors.newFixedThreadPool(4);//核心线程数和最大线程数一样

ExecutorService threadPool = Executors.newCachedThreadPool();//最大线程数是一个很大的数字(线程可以无限增加)

使用 Executors.newFixedThreadPool(4) 能创建出固定包含 4 个线程的线程池.

• 返回值类型为 ExecutorService

• 通过 ExecutorService.submit 可以注册⼀个任务到线程池中.

java 复制代码
public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            int id = i;
            service.submit(()->{
                System.out.println("hello"+id+",ThreadName: "+Thread.currentThread().getName());
            });
        }
    	//shutdown 能够把线程池里的线程全部关闭,但不能保证线程池内的任务一定能全部执行完毕
        //         ---适合于不是必须要完成的线程
        //如果需要等待线程池内的任务全部执行完毕,需要调用awaitTermination方法
    	//		   ---适用于必须要等待所有线程完毕再关闭的情景

    }

可以发现id的顺序是乱的,按正常想法它应该是顺序执行才对呀

  1. 线程调度机制
    Java 中的线程调度是由操作系统和 JVM 共同管理的。当你使用 ExecutorService 创建一个固定大小的线程池并提交任务时,线程池会为每个任务分配一个线程来执行。但是,线程的执行顺序并不是按照任务提交的顺序来的。操作系统的线程调度器会根据自身的调度算法(如时间片轮转、优先级调度等)来决定哪个线程先获得 CPU 时间片执行。这就意味着,即使任务是按顺序提交的,线程执行的顺序也可能是随机的,因此输出的顺序也会是不确定的。
  2. 异步执行
    service.submit() 方法是异步执行的,也就是说,主线程在提交任务后不会等待任务执行完成,而是会继续执行后续的代码。线程池中的线程会在合适的时机开始执行提交的任务,不同线程的执行时间和完成时间是不确定的,这也导致了输出顺序的随机性。

8.3.2 模拟实现线程池

java 复制代码
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class MyThreadPool{
    private BlockingQueue<Runnable> queue = null;
    public MyThreadPool(int n){
        //初始化线程池,创建固定个数的线程
        //使用ArrayBlockingQueue作为任务队列,容量为1000
        queue = new ArrayBlockingQueue<>(1000);
        //创建n个线程
        for (int i = 0; i < n; i++){//thread是独立的线程,和for循环的执行无关,thread阻塞不会影响新线程创建
            Thread thread = new Thread(()->{
                try {
                    while(true){
                        Runnable task = queue.take();//向队列中拿任务
                        task.run();
                    }
                }catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            thread.start();
        }
    }
    public void submit(Runnable task) throws InterruptedException {
        queue.put(task);
    }
}

public class demo13 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool pool = new MyThreadPool(10);

        for (int i = 0; i < 10; i++){
            int id = i;
            pool.submit(()->{
                System.out.println(Thread.currentThread().getName()+"id:"+id);
            });
        }
    }
}

8.3.3 工厂模式解释

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

java 复制代码
class Point{
    double[] point = new double[2];//坐标
    //想通过构造方法设置坐标

}

class PointFactory{
    public static Point makePointByXY(double x,double y){
        Point p = new Point();
        p.point[0] = x;
        p.point[1] = y;
        return p;
    }
    
    public static Point makePointByRA(double r,double a){
        Point p = new Point();
        p.point[0] = r;
        p.point[1] = a;
        return p;
    }
}

如果解释的不清楚可以看看ai的代码

java 复制代码
// 抽象产品类
interface Shape {
    void draw();
}

// 具体产品类:圆形
class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("绘制圆形");
    }
}

// 具体产品类:矩形
class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("绘制矩形");
    }
}

// 工厂类
class ShapeFactory {
    public static Shape createShape(String shapeType) {
        if ("circle".equalsIgnoreCase(shapeType)) {
            return new Circle();
        } else if ("rectangle".equalsIgnoreCase(shapeType)) {
            return new Rectangle();
        }
        return null;
    }
}

// 客户端代码
public class SimpleFactoryExample {
    public static void main(String[] args) {
        Shape circle = ShapeFactory.createShape("circle");
        if (circle != null) {
            circle.draw();
        }

        Shape rectangle = ShapeFactory.createShape("rectangle");
        if (rectangle != null) {
            rectangle.draw();
        }
    }
}

8.4 定时器

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

8.4.1 定时器的使用

Java标准库的定时器

​------Timer

和线程池一样,Timer中也包含前台线程,组织进程结束

Timer 类的核心方法为 schedule .

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

java 复制代码
Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            public void run() {
                System.out.println("hello");
            }
        },3000)

之前我们描述任务都是Runnable在定时器这里是把Runnable封装了的TimerTask

java 复制代码
import java.util.Timer;
import java.util.TimerTask;

public class demo14 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello,timertask 3000");
            }
        },3000);//3000ms后执行

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello,timertask 2000");
            }
        },2000);//2000ms后执行

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello,timertask 1000");
            }
        },1000);//1000ms后执行
    }
}

执行这段代码我们会发现,为什么执行完,进程还没有结束?

在 Java 中,当所有的非守护线程(用户线程)都执行完毕后,Java 进程才会结束。守护线程(如垃圾回收线程)会在所有非守护线程结束后自动终止。由于 Timer 启动的线程是非守护线程,即使主线程执行完毕,Timer 线程仍然在等待可能的后续任务,所以 Java 进程不会结束。

将 timer.cancel() 方法放在最后一个任务的 run() 方法中。当最后一个任务执行完毕时,会调用 cancel() 方法终止 Timer 线程,这样 Java 进程就会在所有任务执行完毕后正常结束

8.4.2 模拟实现定时器

java 复制代码
import java.util.PriorityQueue;
import java.util.TimerTask;

class MyTimerTask implements Comparable<MyTimerTask> {
    private Runnable task;//任务

    private long startTime;//用时间戳记录开始时间
    public MyTimerTask(Runnable task, long startTime) {
        this.task = task;
        this.startTime = startTime;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        return (int)(this.startTime - o.startTime);
    }

    public long getStartTime() {
        return this.startTime;
    }
    public void run(){
        task.run();
    }

}

class MyTimer{
    //用优先队列存放
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
    private Object lock = new Object();
    public MyTimer(){

        Thread thread = new Thread(()-> {
            try {
                while(true){
                    synchronized(lock){
                        while(queue.isEmpty()){
                            lock.wait();//当队列为空,使执行线程阻塞
                        }
                        MyTimerTask task = queue.peek();//拿出最近的一个任务即时间最短的并且查看这个任务的时间是否到了
                        if(task.getStartTime() > System.currentTimeMillis()){
                            //当前任务的开始时间大于系统时间,还没有到执行时间
                            lock.wait(task.getStartTime()-System.currentTimeMillis());
                            /*task.run();
                            queue.poll();*/
                            //在等待期间,可能会有新的任务插入到队列头部,且新任务的开始时间比当前任务更早。
                            //修改后的代码不会再次检查队列头部的任务,而是直接执行之前查看的那个任务,这可能导致任务执行顺序混乱。
                        }else {
                            task.run();
                            queue.poll();
                        }
                    }
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        });
        thread.start();
    }
    public void schedule(Runnable task, long startTime){//
        synchronized(lock){
            MyTimerTask myTimerTask = new MyTimerTask(task,System.currentTimeMillis()+startTime);
            //将此时的时间+ 需要等待的时间就是开始时间
            queue.offer(myTimerTask);
            lock.notify();
        }
    }
}


public class demo15 {
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello 3000");
            }
        },3000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello 2000");
            }
        },2000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello 1000");
            }
        },1000);
    }
}

九、进程与线程的区别

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

总结

本篇文章大体介绍了线程方面的内容,包括如何创建线程,线程安全问题,synchronized锁及其使用,volatile关键字,以及多线程中的单例模式,阻塞队列,线程池,和定时器,如果有什么不正确不严谨的地方,还望指出,谢谢大家!

相关推荐
技术小齐5 分钟前
网络运维学习笔记(DeepSeek优化版)008网工初级(HCIA-Datacom与CCNA-EI)STP生成树协议与VRRP虚拟路由冗余协议
运维·网络·学习
郑祎亦13 分钟前
【JAVA面试题】设计模式之原型模式
java·设计模式·原型模式
苹果酱056714 分钟前
利用机器学习进行信用风险评估
java·vue.js·spring boot·mysql·课程设计
ヾChen15 分钟前
数据结构——栈
开发语言·数据结构·物联网·学习
V+zmm1013425 分钟前
美食推荐系统的微信小程序+论文源码调试讲解
java·数据库·微信小程序·小程序·毕业设计
猿毕设29 分钟前
【FL0090】基于SSM和微信小程序的球馆预约系统
java·spring boot·后端·python·微信小程序·小程序
无问81735 分钟前
Javaee:IO和文件操作
java-ee·io·文件操作
小志开发36 分钟前
Java 抽象类:深入解析与实践指南
java·开发语言
A boy CDEF girl39 分钟前
【JavaEE】wait 、notify和单例模式
java·单例模式·java-ee
猿周LV42 分钟前
文件操作 -- IO [Java EE 初阶]
java·java-ee