多线程 (上) - 学习笔记1:https://developer.aliyun.com/article/1518426
线程安全
什么是线程安全?
如果 多线程和单线程环境下 运行的结果相同, 那么我们就说它是线程安全的 .
线程不安全的原因
根本原因: 线程之间抢占式执行,随机调度
- 修改共享数据
- 原子性 (同步互斥)
- 可见性
- 代码顺序性
什么是原子性?
执行的最小单元
什么是可见性
一个线程对共享变量值的修改, 能够及时的被其他线程看到
主内存就说硬盘角度的 “内存”, 工作内容可以认为是 cache / 寄存器
因为 CPU 对 cache / 寄存器的访问速度要比内存 快 3-4 个数量级. 而且有些操作需要连续访问 N 次某个变量, 读一次放回去一次速度很慢, 因此我们可以第一次读的时候给放到 寄存器 里, 后续的访问都只访问寄存器, 效率就会大大提升
Java 内存模型 (JMM) : Java 虚拟机规范中定义了 Java 内存模型
目的是屏蔽掉各种硬件和 OS 的内存访问差异, 以实现让 Java 程序在各种平台下都能达到一致的并发效果
代码顺序性
编译器会自动对单线程下的代码进行代码重排序, 遵循的前提是 “保持逻辑不发生变化”, 但是在多线程环境下该前提很难遵守
synchronized 关键字
特性 :
- 互斥
- 刷新内存 (即保证内存可见性)
- 可重入
互斥
synchronized 底层是用 OS 的 mutex lock 实现的
互斥的含义是 每个被 synchronized 维护的临界资源, 不会被多个线程同时执行到 .
某个线程执行
- 进入 synchronized 修饰的代码块, 相当于加锁
- 退出 synchronized 修饰的代码块, 相当于解锁
阻塞等待
针对每一把锁, OS 内部都会维护一个等待队列, 当这把锁被某个线程占有的时候, 其他线程再来竞争这把锁, 就上不了锁, 会在队列里等待, 直到之前的线程解锁, 再由 OS 随机唤醒一个 等待队列里的线程来使用这把锁 (没有什么先来后到,一切随机顺序, 先来的也可能得等很长时间 [你喜欢一个妹子, 追了很久,但不是说, 你先喜欢的, 就是你先谈, 人家就是先喜欢上了别人, 就是一眼万年的和别人在一起了, 你也没辙~~]).
翻译翻译, 什么叫做 TM 的可重入?
可重入 和 不可重入
一个线程中, 对一个对象上了两次锁, 并且中间没有释放锁过程
lock(); //第一次 lock(); //第二次
如果是不可重入锁, 由于第一次加锁, 并没有解锁, 所以第二次加锁会失败, 即该线程会在阻塞队列等待, 但是因为第一次锁的解锁过程一定在这个线程后面的某个地方, 就会产生死锁 (卡死在等待队列, 出不来了 [我卡我自己])
可重入锁呐, 就是会自带一个标识类的对象, 第二次加锁之前会判断该线程是不是之前上锁的线程, 如果是, 那你就进去吧 (eg : 你爸回家了, 如果你要进去, 你爸会给你开门, 如果是不认识的人要进去, 你爸就不会开门)
volatile 关键字
特性: 保证修饰变量的内存可见性
代码在写入 volatile 修饰的变量的时候
- 改变线程工作内存的值
- 刷新主内存的值
代码在读取 volatile 修饰的变量的时候
- 先读一下主内存的值, 更新工作内存
- 再从工作内存读取值使用
synchronized 和 volatile 有本质区别
synchronied 保证的是原子性, 衍生出内存可见性这个性质
volatile 保证的是内存可见性, 只是用的时候, 不会读取错误
wait 和 notify
由于线程之间是抢占式执行的, 因此线程之间的先后顺序难以预知
但是我们有方法可以协调多个线程之间的执行先后顺序
- wait() / wait(long timeout) : 让当前线程进入等待状态
- notify() / notifyAll() : 唤醒在当前对象上等待的线程
notify() : 随机唤醒一个在当前对象上等待的线程
notifyAll() : 唤醒在当前对象上等待的所有线程
attention : wait() / notify() / notifyAll() 均为 Object 类的方法
wait()
wait 做的事
- 把当前线程放到等待队列中去
- 释放当前锁
- 满足一定条件被唤醒, 重新尝试获取这个锁
wait 要搭配 synchronized 来使用, 脱离 synchronized 使用 wait 会直接抛出异常 .
wait 结束条件
- 其他线程调用该对象的 notify 方法将其唤醒
- wait 等待时间超时
- 其他线程调用该等待线程的 interrupted 方法, 导致其 wait 抛出 InterruptedException 异常
notify() 方法
随机唤醒一个, 指定对象的等待队列中的线程
attention : 在 notify() 方法后, 当前线程不会马上释放该对象锁, 要等到执行 notify() 方法的线程将程序执行完, 也就是退出同步代码块之后, 才会释放对象锁 (即确保有线程被唤醒之后, 才会释放原本的锁)
notifyAll() 方法
有个注意点, 虽然 notifyAll() 是唤醒当前对象等待队列中的所有线程, 但是这些线程还是需要竞争锁, 所有虽然全部唤醒, 但是并不是同时执行, 仍然是一个一个的执行 .
wait & notify 示例代码
public class test5 { static class waitTask implements Runnable{ private Object locker; public waitTask(Object locker) { this.locker = locker; } @Override public void run() { 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(); } }
wait & slepp
相同点 : 都可以让线程放弃执行一段时间
不同点 :
- wait 用于线程之间的通信, sleep 用于让线程阻塞
- wait 需要搭配 synchronized 使用, sleep 不需要
- wait 是 Object 类的方法, sleep 是 Thread 的静态方法
多线程相关的几个设计模式
单例模式
单例模式就是全局范围内, 该对象只有一个实例
饿汉版本的单例模式 (声明的同时就创建)
class Singleton{ private static Singleton instance = new Singleton(); private Singleton() {} private 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; } }
懒汉模式的多线程版本
class Singleton{ private static Singleton instance = null; private Singleton(){} public synchronized static Singleton getInstance() { if(instance == null) { instance = new Singleton(); } return instance; } }
对于上述版本, 你会发现每次使用的时候都会被加锁, 花销会很大, 因此对此进行改进
class Singleton{ private volatile static Singleton instance = null; // volatile 保证内存可见性 private Singleton(){} public static Singleton getInstance() { if(instance == null) { // 加锁/解锁开销比较高, 这里判断只对首次创建实例的时候进行加锁. synchronized (Singleton.class) { if(instance == null) { // 首次创建完实例后, 仍有很多线程排队在等待队列, 用这个判断让其他等待队列中的线程结束 instance = new Singleton(); } } } return instance; } }
阻塞队列
阻塞队列是什么?
特殊的一种队列, redis 中的 blpop, brpop 也使用了阻塞思想.
既然是队列, 就遵循先进先出思想
阻塞队列是一种线程安全的数据结构,具有特性如下 :
- 当队列满, 继续入队列就会阻塞, 直到队列中有空余位置
- 当队列空, 继续出队列就会阻塞, 直到队列中有元素
典型应用场景 : 生产者消费者模型
定时器
达到某个时间, 就执行某块代码
标准库中的定时器
Timer 类, 核心方法为 schedule .
schedule 包含两个参数, 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间后执行 .
Timer timer= new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("hello lty!"); } }, 3000);
创建线程池的几种方式 (Executors 本质上是对 ThreadPoolExecutor 类的封装)
- newFixedThreadPool: 创建固定线程数的线程池
- newCachedThreadPool: 创建线程数目动态增长的线程池.
- newSingleThreadExecutor: 创建只包含单个线程的线程池.
- newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.