【Java--并发】

目录

1、什么是线程

1.1、为什么要有线程

2、线程状态

2.1、可运行程序

2.2、阻塞和等待线程

2.3、终止线程

2.4、等待一个线程-join()

​编辑

​编辑

2.5 获取当前线程引用

3、线程属性

3.1、中断线程

3.2、守护线程

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

4.1、 观察线程不安全

4.2、线程安全的概念

4.3 线程不安全的原因     

4.3.1、可见性

4.3.2、Java 内存模型 (JMM)

 4.3.3、代码顺序性

5、线程安全

5.1、synchronized 的特性

5.1.1、互斥

5.1.2、刷新内存

5.1.3、可重入

5.2 synchronized 使用示例

5.2.1 直接修饰普通方法:

5.2.2 修饰静态方法:

5.2.3 修饰代码块:

6. volatile 关键字

6.1 volatile 能保证内存可见性

6.2 volatile 不保证原子性

7. wait 和 notify

7.1 wait()方法

7.2 notify()方法

7.3 notifyAll()方法

7.4 wait 和 sleep 的对比

9. 多线程案例

9.1 单例模式

9.2 阻塞式队列

9.2.1 阻塞队列是什么

9.2.2 生产者消费者模型

9.2.3 标准库中的阻塞队列

9.3 定时器

9.3.1 定时器是什么

9.3.2 标准库中的定时器

9.3.3 实现定时器

9.4 线程池

9.4.1 线程池是什么

9.4.2 标准库中的线程池

9.4.3 实现线程池

10. 总结-保证线程安全的思路

11. 对比线程和进程

11.1 线程的优点

11.2 进程与线程的区别


1、什么是线程

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

1.1、为什么要有线程

        首先, "并发编程" 成为 "刚需"。单核 CPU 的发展遇到了瓶颈.,要想提高算力, 就需要多核CPU.。而并发编程能更充分利用多核 CPU 资源.,有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程.。

        其次, 虽然多进程也能实现并发编程, 但是线程比进程更轻量. 创建线程比创建进程更快. 销毁线程比销毁进程更快. 调度线程比调度进程更快

        最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 "线程池"(ThreadPool) 和 "协程" 
(Coroutine)

        Thread myThread = new Thread(new Runnable() {
            @Override
            public void run() {

                for (int i = 0; i < 1000000; i++) {
                    System.out.println(i);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
        myThread.start();
//使用Runnable(),低耦合。
        Thread myThread2 = new Thread(()-> {
            for (int i = 0; i < 1000000; i++) {
                System.out.println(i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        myThread2.start();
//lambda表达式书写



2、线程状态

线程状态有以下六种状态:

  • New(新建)安排了工作, 还未开始行动
  • Runnable(可运行)可工作的. 又可以分成正在工作中和即将开始工作
  • Blocked(阻塞)表示排队等着其他事情
  • Waiting(等待)表示排队等着其他事情
  • Timed waiting(计时等待)表示排队等着其他事情
  • Terminated(终止)工作完成了

2.1、可运行程序

        一个线程开始运行后,它不是一直在运行,而是处于可运行状态(可能正在运行或不在运行,取决于系统调度)。

        java中yiled()方法的作用是:让当前处于运行状态的线程退回到可运行状态,让出抢占资源的机会。

        注意:调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间(也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的。

2.2、阻塞和等待线程

        线程等待和线程阻塞的区别 两者都表示线程当前暂停执行的状态,而两者的区别,基本可以理解为:进入 waiting 状态是线程主动的,而进入 blocked 状态是被动的

2.3、终止线程

线程会因为以下两种原因之一而终止:

  • run方法的正常退出,线程自然终止;
  • 因为一个没有捕获的异常而意外终止。
线程的常用方法:
void join();等待指定的线程终止。
void join(long millis);等待指定的线程终止或等待时间经过等待的毫秒数
Thread.State getState();得到该线程的状态。 取值为:New(新建)、Runnable(可运行)、Blocked(阻塞)、Waiting(等待)、Timed waiting(计时等待)、Terminated(终止)
void stop();停止该线程。该方法已经废弃。
void suspend();暂停该线程。该方法已经废弃。
void resume();恢复线程,只能在调用suspend()之后使用。该方法已经废弃。

Thread 的常见构造方法
方法
说明
Thread()
创建线程对象
Thread(Runnable target)
使用 Runnable 对象创建线程对象
Thread(String name)
创建线程对象,并命名
Thread(Runnable target, String name)
使用 Runnable 对象创建线程对象,并命名
【了解】 Thread(ThreadGroup group,
Runnable target)
线程可以被用来分组管理,分好的组即为线程组,这
个目前我们了解即可

2.4、等待一个线程-join()

        有的时候,当前线程需要等待另一个线程完成其任务后在开始本线程任务,我们可以使用jion()方法去等待线程结束;

public class demo1 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0 ; i < 5; i++) {
                    System.out.println("正在跑步");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
        thread1.start();
        thread1.join();
        System.out.println("跑步结束");
    }
}

public class demo1 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0 ; i < 5; i++) {
                    System.out.println("正在跑步");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
        thread1.start();
        thread1.join(2000);
        System.out.println("跑步结束");
    }
}

方法
说明
public void join()
等待线程结束
public void join(long millis)
等待线程结束,最多等 millis 毫秒
public void join(long millis, int nanos)
同理,但可以更高精度

2.5 获取当前线程引用

方法
说明
public static Thread currentThread();
返回当前线程对象的引用

public class Demo2 {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            System.out.println(Thread.currentThread());
        });
        thread.start();
        System.out.println(Thread.currentThread());
    }
}

3、线程属性

3.1、中断线程

        interrupt() 它基于「一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。」思想,是一个比较温柔的做法,它更类似一个标志位。其实作用不是中断线程,而是「通知线程应该中断了」,线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。

public class Demo3 {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            while(!Thread.currentThread().isInterrupted()){
                System.out.println("运行中+");
                System.out.println(Thread.currentThread().isInterrupted());
            }
        });
        thread.start();
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        thread.interrupt();

    }
}

        如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知,清除中断标志 ,当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法。可以选择忽略这个异常, 也可以跳出循环结束线程。

public class Demo3 {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            while(!Thread.currentThread().isInterrupted()){
                System.out.println("运行中+");
                System.out.println(Thread.currentThread().isInterrupted());
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        thread.start();
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        thread.interrupt();

    }
}

public class Demo3 {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            while(!Thread.currentThread().isInterrupted()){
                System.out.println("运行中+");
                System.out.println(Thread.currentThread().isInterrupted());
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().isInterrupted());
                    return;
                }
            }
        });
        thread.start();
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        thread.interrupt();

    }
}

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

3.2、守护线程

        当 JVM 中不存在任何一个正在运行的非守护线程时,则 JVM 进程即会退出。通常来说,守护线程经常被用来执行一些后台任务,但是呢,你又希望在程序退出时,或者说 JVM 退出时,线程能够自动关闭,此时,守护线程是你的首选。

        

public class Demo4 {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            while(true){
                System.out.println("当其他线程结束时,我才结束");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        Thread thread1 = new Thread(()->{
            for(int i = 0 ; i < 3; i++) {
                System.out.println("我是非守护线程");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        thread.setDaemon(true);
        thread.start();
        thread1.start();
    }

}

void setDeamon(boolean isDeamon)将线程变为守护线程,必须要在线程启动前调用

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

4.1、 观察线程不安全

        我们使用两个线程对同一个变量继续操作,我们的期望是把count加到10000,但以下线程的执行结果并不会如我所愿。

class Count{
    int count;
    public Count(int count){
        this.count = count;
    }
    void run(){
        count++;
    }

}

public class Demo5 {
    public static void main(String[] args) {
        Count con = new Count(0);
        Thread thread1 = new Thread(()->{
            for(int i = 0 ; i < 5000 ; i++){
                con.run();
            }
        });
        Thread thread2 = new Thread(()->{
            for(int i = 0 ; i < 5000 ; i++){
                con.run();
            }
        });
        thread1.start();
        thread2.start();
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(con.count);
    }
}

4.2、线程安全的概念

        如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

4.3 线程不安全的原因     

线程不安全的主要原因:

  1. 系统的随机调度,抢占式执行
  2. 多线程同时尝试修改同一变量
  3. 修改操作不是原子的
  4. 内存可见性引起的问题
  5. 指令重排序引起的问题
        上面的线程不安全的代码中, 涉及到多个Count.count 变量进行修改。此时这个 Count.count 是一个多个线程都能访问到的 " 共享数据。当两个线程执行        count++        指令时,这个指令会有以下处理:
1. 从内存把数据读到 CPU
2. 进行数据更新
3. 把数据写回到 CPU
        如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。

4.3.1、可见性

        可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到 .

4.3.2、Java 内存模型 (JMM)

Java 虚拟机规范中定义了 Java 内存模型 .。目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java 程序在各种平台下都能达到一致的并发效果。

  • 线程之间的共享变量存在主内存 (Main Memory).
  • 每一个线程都有自己的 "工作内存" (Working Memory) .
  • 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
  • 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.

此时引入了两个问题 :
  1. 为什么要整这么多内存?
  2. 为什么要这么麻烦的拷来拷去?
        实际并没有这么多 " 内存 ". 这只是 Java 规范中的一个术语 , 是属于 " 抽象 " 的叫法 . 所谓的 " 主内存 " 才是真正硬件角度的 " 内存 ". 而所谓的 " 工作内存 ", 则是指 CPU 的寄存器和高速缓存。

 4.3.3、代码顺序性

一段代码是这样的:
  • 1. 去前台取下 U
  • 2. 去教室写 10 分钟作业
  • 3. 去前台取下快递
        如果是在单线程情况下,JVM CPU 指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问 题,可以少跑一次前台。这种叫做指令重排序。 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代 码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价

5、线程安全

5.1、synchronized 的特性

5.1.1、互斥

        synchronized 会起到互斥效果 , 某个线程执行到某个对象的 synchronized 中时 , 其他线程如果也执行到 同一个对象 synchronized 就会 阻塞等待 .
  • 进入 synchronized 修饰的代码块, 相当于 加锁
  • 退出 synchronized 修饰的代码块, 相当于 解锁
理解 "阻塞等待"
        针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁。

        当我们给这个成员方法加上synchronized关键字时,操作系统在执行count++时就会完整地执行所有指令,最后的count结果也会如我们预期一样。

    synchronized void run(){
        count++;
    }

5.1.2、刷新内存

synchronized 的工作过程 :
  1.  获得互斥锁
  2.  从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4.  将更改后的共享变量的值刷新到主内存
  5.  释放互斥锁

所以 synchronized 也能保证内存可见性

5.1.3、可重入

        synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
在下面的代码中
  • increase increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前对象加锁的.
  • 在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释, 相当于连续加两次锁)
        这个代码是完全没问题的. 因为 synchronized 是可重入锁 .
static class Counter {
    public int count = 0;
    synchronized void increase() {
        count++;
   }
    synchronized void increase2() {
        increase();
   }
}
        在可重入锁的内部, 包含了 " 线程持有者 " " 计数器 " 两个信息 .
  • 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
  • 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)

5.2 synchronized 使用示例

        synchronized 本质上要修改指定对象的 " 对象头 ". 从使用角度来看 , synchronized 也势必要搭配一个具体的对象来使用。

5.2.1 直接修饰普通方法:

锁的 SynchronizedDemo 对象
public class SynchronizedDemo {
    public synchronized void methond() {
   }
}

5.2.2 修饰静态方法:

锁的 SynchronizedDemo 类的对象
public class SynchronizedDemo {
    public synchronized static void method() {
   }
}

5.2.3 修饰代码块:

        明确指定锁哪个对象。
锁当前对象
public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            
       }
   }
}
锁类对象
public class SynchronizedDemo {
    public void method() {
        synchronized (SynchronizedDemo.class) {
       }
   }
}

6. volatile 关键字

6.1 volatile 能保证内存可见性

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

代码在写入 volatile 修饰的变量的时候 ,
  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主内存
代码在读取 volatile 修饰的变量的时候 ,
  • 从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本
前面我们讨论内存可见性时说了 , 直接访问工作内存 ( 实际是 CPU 的寄存器或者 CPU 的缓存 ), 速度非常快, 但是可能出现数据不一致的情况.加上 volatile , 强制读写内存 . 速度是慢了 , 但是数据变的更准确了.
代码示例
在这个代码中
  • 创建两个线程 t1 t2
  • t1 中包含一个循环, 这个循环以 flag == 0 为循环条件.
  • t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.
  • 预期当用户输入非 0 的值的时候, t1 线程结束.
static class Counter {
    public int flag = 0;
}
public static void main(String[] args) {
    Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        while (counter.flag == 0) {
            // do nothing
       }
        System.out.println("循环结束!");
   });
    Thread t2 = new Thread(() -> {
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入一个整数:");
        counter.flag = scanner.nextInt();
   });
    t1.start();
    t2.start();
}
// 执行效果
// 当用户输入非0值时, t1 线程循环不会结束. (这显然是一个 bug)
t1 读的是自己工作内存中的内容 .
t2 flag 变量进行修改 , 此时 t1 感知不到 flag 的变化 .
如果给 flag 加上 volatile
static class Counter {
    public volatile int flag = 0;
}
// 执行效果
// 当用户输入非0值时, t1 线程循环能够立即结束.

6.2 volatile 不保证原子性

        volatile 和 synchronized 有着本质的区别 . synchronized 能够保证原子性 , volatile 保证的是内存可见性.
代码示例
这个是最初的演示线程安全的代码 .
  • increase 方法去掉 synchronized
  • count 加上 volatile 关键字.
static class Counter {
    volatile public int count = 0;
    void increase() {
        count++;
   }
}
public static void main(String[] args) throws InterruptedException {
    final Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
       }
   });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
       }
   });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(counter.count);
}
此时可以看到 , 最终 count 的值仍然无法保证是 100000.
synchronized 也能保证内存可见性
synchronized 既能保证原子性 , 也能保证内存可见性 .
对上面的代码进行调整 :
  • 去掉 flag volatile
  • t1 的循环内部加上 synchronized, 并借助 counter 对象加锁.
static class Counter {
    public int flag = 0;
}
public static void main(String[] args) {
    Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        while (true) {
            synchronized (counter) {
                if (counter.flag != 0) {
                    break;
               }
           }
            // do nothing
       }
        System.out.println("循环结束!");
   });
    Thread t2 = new Thread(() -> {
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入一个整数:");
        counter.flag = scanner.nextInt();
   });
    t1.start();
    t2.start();
}

7. wait notify

        由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知, 但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.
完成这个协调工作 , 主要涉及到三个方法
  • wait() / wait(long timeout): 让当前线程进入等待状态.
  • notify() / notifyAll(): 唤醒在当前对象上等待的线程.
注意 : wait, notify, notifyAll 都是 Object 类的方法 .

7.1 wait()方法

wait 做的事情 :
  • 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  • 释放当前的锁
  • 满足一定条件时被唤醒, 重新尝试获取这个锁.
wait 要搭配 synchronized 来使用 . 脱离 synchronized 使用 wait 会直接抛出异常 .
wait 结束等待的条件 :
  • 其他线程调用该对象的 notify 方法.
  • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.
代码示例 : 观察 wait() 方法使用
public static void main(String[] args) throws InterruptedException {
    Object object = new Object();
    synchronized (object) {
        System.out.println("等待中");
        object.wait();
        System.out.println("等待结束");
   }
}
        这样在执行到object.wait() 之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了另外一个方法唤醒的方法notify().

7.2 notify()方法

notify 方法是唤醒等待的线程 .
  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的
  • 其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")
  • notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
代码示例 : 使用 notify() 方法唤醒线程
  • 创建 WaitTask , 对应一个线程, run 内部循环调用 wait.
  • 创建 NotifyTask , 对应另一个线程, run 内部调用一次 notify
  • 注意, WaitTask NotifyTask 内部持有同一个 Object locker. WaitTask NotifyTask 要想配合 就需要搭配同一个 Object.
static class WaitTask implements Runnable {
    private Object locker;
    public WaitTask(Object locker) {
        this.locker = locker;
     }
    @Override
    public void run() {
        synchronized (locker) {
            while (true) {
                try {
                    System.out.println("wait 开始");
                    locker.wait();
                    System.out.println("wait 结束");
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
           }
       }
   }
}
static class NotifyTask implements Runnable {
    private Object locker;
    public NotifyTask(Object locker) {
        this.locker = locker;
   }
    @Override
    public void run() {
        synchronized (locker) {
            System.out.println("notify 开始");
            locker.notify();
            System.out.println("notify 结束");
       }
   }
}
public static void main(String[] args) throws InterruptedException {
    Object locker = new Object();
    Thread t1 = new Thread(new WaitTask(locker));
    Thread t2 = new Thread(new NotifyTask(locker));
    t1.start();
    Thread.sleep(1000);
    t2.start();
}

7.3 notifyAll()方法

        notify方法只是唤醒某一个等待线程. 使用 notifyAll 方法可以一次唤醒所有的等待线程 .
范例:使用 notifyAll() 方法唤醒所有等待线程 , 在上面的代码基础上做出修改 .
创建 3 WaitTask 实例 . 1 NotifyTask 实例 .
static class WaitTask implements Runnable {
 // 代码不变
}
static class NotifyTask implements Runnable {
 // 代码不变
}
public static void main(String[] args) throws InterruptedException {
    Object locker = new Object();
    Thread t1 = new Thread(new WaitTask(locker));
    Thread t3 = new Thread(new WaitTask(locker));
    Thread t4 = new Thread(new WaitTask(locker));
    Thread t2 = new Thread(new NotifyTask(locker));
    t1.start();
    t3.start();
    t4.start();
    Thread.sleep(1000);
    t2.start();
}
此时可以看到 , 调用 notify 只能唤醒一个线程 .
        修改 NotifyTask 中的 run 方法 , notify 替换成 notifyAll
public void run() {
    synchronized (locker) {
        System.out.println("notify 开始");
        locker.notifyAll();
        System.out.println("notify 结束");
   }
}
此时可以看到 , 调用 notifyAll 能同时唤醒 3 wait 中的线程
        注意: 虽然是同时唤醒 3 个线程 , 但是这 3 个线程需要竞争锁 . 所以并不是同时执行 , 而仍然是有先有后的执行.

7.4 wait sleep 的对比

        其实理论上 wait sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻 塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间.
当然为了面试的目的,我们还是总结下:
1. wait 需要搭配 synchronized 使用 . sleep 不需要 .
2. wait Object 的方法 sleep Thread 的静态方法 .

9. 多线程案例

9.1 单例模式

        单例模式是校招中最常考的设计模式 之一 .
啥是设计模式 ?
        设计模式好比象棋中的 " 棋谱 ". 红方当头炮 , 黑方马来跳 . 针对红方的一些走法 , 黑方应招的时候有 一些固定的套路. 按照套路来走局势就不会吃亏 .
        软件开发中也有很多常见的 " 问题场景 ". 针对这些问题场景 , 大佬们总结出了一些固定的套路 . 按照 这个套路来实现代码, 也不会吃亏 .
        单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例 .
这一点在很多场景上都需要 . 比如 JDBC 中的 DataSource 实例就只需要一个 .
        单例模式具体的实现方式, 分成 " 饿汉 " " 懒汉 " 两种 .
饿汉模式
类加载的同时 , 创建实例 .
class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
   }
}
懒汉模式 - 单线程版
类加载的时候不创建实例 . 第一次使用的时候才创建实例 .
class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
       }
        return instance;
   }
}
懒汉模式 - 多线程版
        上面的懒汉模式的实现是线程不安全的.
        线程安全问题发生在首次创建实例时. 如果在多个线程中同时调用 getInstance 方法 , 就可能导致 创建出多个实例.
        一旦实例已经创建好了, 后面再多线程环境调用 getInstance 就不再有线程安全问题了 ( 不再修改 instance 了 )
        加上 synchronized 可以改善这里的线程安全问题 .
class Singleton {
   private static Singleton instance = null;
   private Singleton() {}
   public static Singleton getInstance() {
       if (instance == null) {
           instance = new Singleton();
      }
       return instance;
  }
}
懒汉模式 - 多线程版 ( 改进 )
以下代码在加锁的基础上 , 做出了进一步改动 :
  • 使用双重 if 判定, 降低锁竞争的频率.
  • instance 加上了 volatile.
class Singleton {
   private static Singleton instance = null;
   private Singleton() {}
   public synchronized static Singleton getInstance() {
       if (instance == null) {
           instance = new Singleton();
      }
       return instance;
  }
}
理解双重 if 判定 / volatile:
        加锁 / 解锁是一件开销比较高的事情 . 而懒汉模式的线程不安全只是发生在首次创建实例的时候 . 因此后续使用的时候, 不必再进行加锁了 .
        外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了 .
同时为了避免 " 内存可见性 " 导致读取的 instance 出现偏差 , 于是补充上 volatile .
当多线程首次调用 getInstance, 大家可能都发现 instance null, 于是又继续往下执行来竞争锁 ,
        其中竞争成功的线程, 再完成创建实例的操作 .
        当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了 . 也就不会继续创建其他实例 .
1) 有三个线程 , 开始执行 getInstance , 通过外层的 if (instance == null) 知道了实例还没
有创建的消息 . 于是开始竞争同一把锁。
2) 其中线程 1 率先获取到锁 , 此时线程 1 通过里层的 if (instance == null) 进一步确认实例是

否已经创建. 如果没创建, 就把这个实例创建出来。

3) 当线程 1 释放锁之后 , 线程 2 和 线程 3 也拿到锁 , 也通过里层的 if (instance == null)
确认实例是否已经创建 , 发现实例已经创建出来了 , 就不再创建了。
4) 后续的线程 , 不必加锁 , 直接就通过外层 if (instance == null) 就知道实例已经创建了 ,
而不再尝试获取锁了 . 降低了开销。

9.2 阻塞式队列

9.2.1 阻塞队列是什么

        阻塞队列是一种特殊的队列. 也遵守 " 先进先出 " 的原则 .
        阻塞队列能是一种线程安全的数据结构, 并且具有以下特性 :
  • 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
  • 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.
        阻塞队列的一个典型应用场景就是 " 生产者消费者模型 ". 这是一种非常典型的开发模型 .

9.2.2 生产者消费者模型

        生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
        生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.
1) 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力 .
        比如在 " 秒杀 " 场景下 , 服务器同一时刻可能会收到大量的支付请求 . 如果直接处理这些支付请求 , 服务器可能扛不住( 每个支付请求的处理都需要比较复杂的流程 ).
        这个时候就可以把这些请求都放到一个阻塞队列中, 然后再由消费者线程慢慢的来处理每个支付请求 . 这样做可以有效进行 " 削峰 ", 防止服务器被突然到来的一波请求直接冲垮 .
2) 阻塞队列也能使生产者和消费者之间 解耦 .
        比如过年一家人一起包饺子. 一般都是有明确分工 , 比如一个人负责擀饺子皮 , 其他人负责包 . 擀饺子皮的人就是 " 生产者 ", 包饺子的人就是 " 消费者 ".
        擀饺子皮的人不关心包饺子的人是谁( 能包就行 , 无论是手工包 , 借助工具 , 还是机器包 ), 包饺子的人也不关心擀饺子皮的人是谁( 有饺子皮就行 , 无论是用擀面杖擀的 , 还是拿罐头瓶擀 , 还是直接从超市买的).

9.2.3 标准库中的阻塞队列

        在 Java 标准库中内置了阻塞队列 . 如果我们需要在一些程序中使用阻塞队列 , 直接使用标准库中的即可 .
  • BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
  • put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
  • BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// 入队列
queue.put("abc");
// 出队列. 如果没有 put 直接 take, 就会阻塞. 
String elem = queue.take();
生产者消费者模型
public static void main(String[] args) throws InterruptedException {
    BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<Integer>();
    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();
}
阻塞队列实现
  • 通过 "循环队列" 的方式来实现.
  • 使用 synchronized 进行加锁控制.
  • put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一定队列就不满了, 因为同时可能是唤醒了多个线程).
  • take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)
public class BlockingQueue {
    private int[] items = new int[1000];
    private volatile int size = 0;
    private int head = 0;
    private int tail = 0;
    public void put(int value) throws InterruptedException {
        synchronized (this) {
            // 此处最好使用 while.
            // 否则 notifyAll 的时候, 该线程从 wait 中被唤醒,
            // 但是紧接着并未抢占到锁. 当锁被抢占的时候, 可能又已经队列满了
            // 就只能继续等待
            while (size == items.length) {
                wait();
           }
            items[tail] = value;
            tail = (tail + 1) % items.length;
            size++;
            notifyAll();
       }
   }
    public int take() throws InterruptedException {
        int ret = 0;
        synchronized (this) {
            while (size == 0) {
                wait();
           }
            ret = items[head];
            head = (head + 1) % items.length;
            size--;
            notifyAll();
       }
        return ret;
   }
    public synchronized int size() {
        return size;
   }
    // 测试代码
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue blockingQueue = new BlockingQueue();
        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 {
                    blockingQueue.put(random.nextInt(10000));
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
           }
       }, "生产者");
        producer.start();
        customer.join();
        producer.join();
   }
}

9.3 定时器

9.3.1 定时器是什么

        定时器也是软件开发中的一个重要组件. 类似于一个 " 闹钟 ". 达到一个设定的时间之后 , 就执行某个指定好的代码.
        定时器是一种实际开发中非常常用的组件.
        比如网络通信中, 如果对方 500ms 内没有返回数据 , 则断开连接尝试重连 .
        比如一个 Map, 希望里面的某个 key 3s 之后过期 ( 自动删除 ).
        类似于这样的场景就需要用到定时器.

9.3.2 标准库中的定时器

  • 标准库中提供了一个 Timer . Timer 类的核心方法为 schedule .
  • schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).
Timer timer = new Timer();
timer.schedule(new TimerTask() {
    @Override
    public void run() {
        System.out.println("hello");
   }
}, 3000);

9.3.3 实现定时器

定时器的构成 :
一个带优先级的阻塞队列
为啥要带优先级呢 ?
        因为阻塞队列中的任务都有各自的执行时刻 (delay). 最先执行的任务一定是 delay 最小的 . 使用带优先级的队列就可以高效的把这个 delay 最小的任务找出来 .
  • 队列中的每个元素是一个 Task 对象.
  • Task 中带有一个时间属性, 队首元素就是即将
  • 同时有一个 worker 线程一直扫描队首元素, 看队首元素是否需要执行
/**
* 定时器的构成:
* 一个带优先级的阻塞队列
* 队列中的每个元素是一个 Task 对象.
* Task 中带有一个时间属性, 队首元素就是即将
* 同时有一个 worker 线程一直扫描队首元素, 看队首元素是否需要执行
*/
public class Timer {
    static class Task implements Comparable<Task> {
        private Runnable command;
        private long time;
        public Task(Runnable command, long time) {
            this.command = command;
            // time 中存的是绝对时间, 超过这个时间的任务就应该被执行
            this.time = System.currentTimeMillis() + time;
       }
        public void run() {
            command.run();
       }
         @Override
        public int compareTo(Task o) {
            // 谁的时间小谁排前面
            return (int)(time - o.time);
       }
   }
    // 核心结构
    private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue();
    // 存在的意义是避免 worker 线程出现忙等的情况
    private Object mailBox = new Object();
    class Worker extends Thread{
        @Override
        public void run() {
            while (true) {
                try {
                    Task task = queue.take();
                    long curTime = System.currentTimeMillis();
                    if (task.time > curTime) {
                        // 时间还没到, 就把任务再塞回去
                        queue.put(task);
                        synchronized (mailBox) {
                            // 指定等待时间 wait
                            mailBox.wait(task.time - curTime);
                       }
                   } else {
                        // 时间到了, 可以执行任务
                        task.run();
                   }
               } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
               }
           }
       }
   }
    public Timer() {
        // 启动 worker 线程
        Worker worker = new Worker();
        worker.start();
   }
    // schedule 原意为 "安排"
    public void schedule(Runnable command, long after) {
        Task task = new Task(command, after);
        queue.offer(task);
        synchronized (mailBox) {
            mailBox.notify();
       }
   }
    public static void main(String[] args) {
        Timer timer = new Timer();
        Runnable command = new Runnable() {
            @Override
            public void run() {
                System.out.println("我来了");
                timer.schedule(this, 3000);
           }
       };
        timer.schedule(command, 3000);
   }
}

9.4 线程池

9.4.1 线程池是什么

虽然创建线程 / 销毁线程 的开销
想象这么一个场景:
        在学校附近新开了一家快递店,老板很精明,想到一个与众不同的办法来经营。店里没有雇人,而是每次有业务来了,就现场找一名同学过来把快递送了,然后解雇同学。这个类比我们平时来一个任务,起一个线程进行处理的模式。
        很快老板发现问题来了,每次招聘 + 解雇同学的成本还是非常高的。老板还是很善于变通的,知道了为什么大家都要雇人了,所以指定了一个指标,公司业务人员会扩张到 3 个人,但还是随着 业务逐步雇人。于是再有业务来了,老板就看,如果现在公司还没 3 个人,就雇一个人去送快递,否则只是把业务放到一个本本上,等着 3 个快递人员空闲的时候去处理。这个就是我们要带 出的线程池的模式。
        线程池最大的好处就是减少每次启动、销毁线程的损耗.

9.4.2 标准库中的线程池

  • 使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
  • 返回值类型为 ExecutorService
  • 通过 ExecutorService.submit 可以注册一个任务到线程池中.
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
    @Override
    public void run() {
        System.out.println("hello");
   }
});
Executors 创建线程池的几种方式
  • newFixedThreadPool: 创建固定线程数的线程池
  • newCachedThreadPool: 创建线程数目动态增长的线程池.
  • newSingleThreadExecutor: 创建只包含单个线程的线程池.
  • newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.

        Executors 本质上是 ThreadPoolExecutor 类的封装.

        ThreadPoolExecutor 提供了更多的可选参数 , 可以进一步细化线程池行为的设定 . ( 后面再介绍 )

9.4.3 实现线程池

  • 核心操作为 submit, 将任务加入线程池中
  • 使用 Worker 类描述一个工作线程. 使用 Runnable 描述一个任务.
  • 使用一个 BlockingQueue 组织所有的任务
  • 每个 worker 线程要做的事情: 不停的从 BlockingQueue 中取任务并执行.
  • 指定一下线程池中的最大线程数 maxWorkerCount; 当当前线程数超过这个最大值时, 就不再新增线程了.
class Worker extends Thread {
    private LinkedBlockingQueue<Runnable> queue = null;
    public Worker(LinkedBlockingQueue<Runnable> queue) {
        super("worker");
        this.queue = queue;
   }
    @Override
    public void run() {
        // try 必须放在 while 外头, 或者 while 里头应该影响不大
        try {
            while (!Thread.interrupted()) {
                Runnable runnable = queue.take();
                runnable.run();
           }
       } catch (InterruptedException e) {
       }
   }
}
public class MyThreadPool {
    private int maxWorkerCount = 10;
    private LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue();
    public void submit(Runnable command) {
        if (workerList.size() < maxWorkerCount) {
            // 当前 worker 数不足, 就继续创建 worker
            Worker worker = new Worker(queue);
            worker.start();
       }
        // 将任务添加到任务队列中
        queue.put(command);
   }
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool myThreadPool = new MyThreadPool();
        myThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("吃饭");
           }
       });
        Thread.sleep(1000);
   }
}

10. 总结-保证线程安全的思路

1. 使用没有共享资源的模型
2. 适用共享资源只读,不写的模型
  • 不需要写共享资源的模型
  • 使用不可变对象
3. 直面线程安全(重点)
  • 保证原子性
  • 保证顺序性
  • 保证可见性

11. 对比线程和进程

11.1 线程的优点

  1. 创建一个新线程的代价要比创建一个新进程小得多
  2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  3.  线程占用的资源要比进程少很多
  4.  能充分利用多处理器的可并行数量
  5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  6.  计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  7.  I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

11.2 进程与线程的区别

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值