多线程&并发
大约 8 分钟
多线程&并发
线程和进程
- 进程:系统运行的一个程序,程序执行的过程;启动Main函数即是 启动了一个JVM 进程
- 线程:
- main函数的执行就是一个线程,成为主线程、
- 进程不同的是同类的多个线程共享进程的堆和方法区资源
- Java 线程的本质其实就是操作系统的线程
- 是内核线程,可以利用多核
程序计数器
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
虚拟机栈和本地方法栈
- 保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
- 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
多线程
- 多核 CPU 意味着多个线程可以同时运行
- 多线程并发编程正是开发高并发系统的基础
- 可能问题
- 线程不安全
- 同一份数据的正确性和一致性
- 不安全导致:数据混乱、错误或丢失等
- 死锁等
- 线程不安全
生命周期和状态:
- NEW: 初始状态
- RUNNABLE: 运行状态
- BLOCKED:阻塞状态
- WAITING:等待状态
- TIME_WAITING:超时等待状态
- TERMINATED:终止状态
- 线程上下文切换:线程在执行过程中会有自己的运行条件和状态(也称上下文)
- 上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。
- 主动让出 CPU,比如调用了 sleep(), wait() 等。
- 时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。
- 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
线程死锁
- 多个线程同时被阻塞
- 由于线程被无限期地阻塞,因此程序不可能正常终止。
- 符合产生死锁的四个必要条件,破坏!!!
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 破坏:一次性申请所有的资源
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 破坏:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
- 破坏:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
- 避免死锁
- 靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
线程方法
- sleep() 方法和 wait() 方法 : 两者都可以暂停线程的执行。
- sleep() 方法没有释放锁,而 wait() 方法释放了锁 。
- wait() 通常被用于线程间交互/通信,sleep()通常被用于暂停执行。
- wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。
- 每个对象(Object)都拥有对象锁,
- wait() 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁
- 既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object)而非当前的线程(Thread)。
- sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。
- run 方法
- 调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。
- start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。
- 直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行
JMM(Java 内存模型)
- 主要定义了对于一个共享变量,当另一个线程对这个共享变量执行写操作后,这个线程对这个共享变量的可见性。
volatile
- volatile 关键字可以保证变量的可见性
- 共享且不稳定的,每次使用它都到主存中进行读取。
- 还有一个重要的作用就是防止 JVM 的指令重排序
- 单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!
volatile 关键字修饰也是很有必要
new Singleton()
1、为 uniqueInstance 分配内存空间
2、初始化 uniqueInstance
3、将 uniqueInstance 指向分配的内存地址
由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2
多线程环境下会导致一个线程获得还没有初始化的实例
例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
乐观锁和悲观锁
悲观锁通常多用于写比较多的情况
乐观锁通常多用于写比较少的情况
- 避免频繁加锁影响性能
悲观锁
- 总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改)
- 所以每次在获取资源操作的时候都会上锁
- Java 中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现
- 乐观锁
- 总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了
- 具体方法可以使用版本号机制或 CAS 算法
- AtomicInteger:乐观锁的一种实现方式 CAS
- CAS: Compare And Swap(比较与交换)
- 就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
- Java 语言并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的(JNI 调用)。
- 问题:
- ABA 问题
- 循环时间长开销大
- 只能保证一个共享变量的原子操作
synchronized
- 属于 重量级锁,效率低下。
synchronized vs volatile
- volatile 关键字是线程同步的轻量级实现,所以 volatile性能肯定比synchronized关键字要好 。
- 但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。
- volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
- volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。
ThreadLocal
- ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
- 如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本
线程池
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
- ThreadPoolExecutor
- 设定线程池的大小:N CPU核心数
- CPU 密集型任务(N+1)
- 利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。
- I/O 密集型任务(2N)
- 涉及到网络读取,文件读取这类都是 IO 密集型
- 计算耗费时间相比于等待 IO 操作完成的时间来说很少
- CPU 密集型任务(N+1)