@MRsunhuimin
2021-10-24T14:16:41.000000Z
字数 4713
阅读 170
总结
「进程是系统资源分配和调度的基本单位」,它能并发执行较高系统资源的利用率.
「线程」是「比进程更小」的能独立运行的基本单位,创建、销毁、切换成本要小于进程,可以减少程序并发执行时的时间和空间开销,使得操作系统具有更好的并发性。
「1.保证内存可见性」
可见性是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。
当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。当一个线程向被volatile关键字修饰的变量「写入数据」的时候,虚拟机会「强制它被值刷新到主内存中」。当一个线程「读取」被volatile关键字修饰的值的时候,虚拟机会「强制要求它从主内存中读取」。
「2.禁止指令重排序」
cpu 是和缓存做交互的,但是由于 cpu 运行效率太高,所以会不等待当前命令返回结果从而继续执行下一个命令,就会有乱序执行的情况发生
指令重排序是编译器和处理器为了高效对程序进行优化的手段,cpu 是与内存交互的,而 cpu 的效率想比内存高很多,所以 cpu 会在不影响最终结果的情况下,不等待返回结果直接进行后续的指令操作,而 volatile 就是给相应代码加了「内存屏障」,在屏障内的代码禁止指令重排序。
JMM 就是 「Java内存模型」(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以java内存模型(JMM)「屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果」。
Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。「线程不能直接读写主内存中的变量」。
每个线程的工作内存都是独立的,「线程操作数据只能在工作内存中进行,然后刷回到主存」。这是 Java 内存模型定义的线程基本工作方式。
1.synchronized关键字
可以用于代码块,方法(静态方法,同步锁是当前字节码对象;实例方法,同步锁是实例对象)
2.lock锁机制
Lock lock = new ReentrantLock();
lock. lock();
try {
System. out. println("获得锁");
} catch (Exception e) {
} finally {
System. out. println("释放锁");
lock. unlock();
}
在 Java1.6 之前的版本中,synchronized 属于重量级锁,效率低下,「锁是」 cpu 一个「总量级的资源」,每次获取锁都要和 cpu 申请,非常消耗性能。
在 「jdk1.6 之后」 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了,Jdk1.6 之后,为了减少获得锁和释放锁所带来的性能消耗,引入了偏向锁和轻量级锁,「增加了锁升级的过程」,由无锁->偏向锁->自旋锁->重量级锁
增加锁升级的过程主要是「减少用户态到核心态的切换,提高锁的效率,从 jvm 层面优化锁」
Mark Word的状态变化
25bit 1bit 2bit
锁状态 4bit
23bit 2bit 是否是偏向锁 锁标志位
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向互斥量(重量级锁)的指针 10
GC标记 空 11
偏向锁 线程ID EPoch 对象分代年龄 1 01
无锁 对象HashCode 对象分代年龄 0 01
cas 叫做 CompareAndSwap,「比较并交换」,很多地方使用到了它,比如锁升级中自旋锁就有用到,主要是「通过处理器的指令来保证操作的原子性」,它主要包含三个变量:
「1.变量内存地址」
「2.旧的预期值 A」
「3.准备设置的新值 B」
当一个线程需要修改一个共享变量的值,完成这个操作需要先取出共享变量的值,赋给 A,基于 A 进行计算,得到新值 B,在用预期原值 A 和内存中的共享变量值进行比较,「如果相同就认为其他线程没有进行修改」,而将新值写入内存
「CAS的缺点」
「CPU开销比较大」:在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,又因为自旋的时候会一直占用CPU,如果CAS一直更新不成功就会一直占用,造成CPU的浪费。
「ABA 问题」:比如线程 A 去修改 1 这个值,修改成功了,但是中间 线程 B 也修改了这个值,但是修改后的结果还是 1,所以不影响 A 的操作,这就会有问题。可以用「版本号」来解决这个问题。
「只能保证一个共享变量的原子性」
ReentrantLock 意为「可重入锁」,说起 ReentrantLock 就不得不说 AQS ,因为其底层就是「使用 AQS 去实现」的。
ReentrantLock有两种模式,一种是公平锁,一种是非公平锁。
公平模式下等待线程入队列后会严格按照队列顺序去执行
非公平模式下等待线程入队列后有可能会出现插队情
「公平锁」
第一步:「获取状态的 state 的值」
如果 state=0 即代表锁没有被其它线程占用,执行第二步。
如果 state!=0 则代表锁正在被其它线程占用,执行第三步。
第二步:「判断队列中是否有线程在排队等待」
如果不存在则直接将锁的所有者设置成当前线程,且更新状态 state 。
如果存在就入队。
第三步:「判断锁的所有者是不是当前线程」
如果是则更新状态 state 的值。
如果是则更新状态 state 的值。
「非公平锁」
获取状态的 state 的值
如果 state=0 即代表锁没有被其它线程占用,则设置当前锁的持有者为当前线程,该操作用 CAS 完成。
如果不为0或者设置失败,代表锁被占用进行下一步。
此时「获取 state 的值」
如果是,则给state+1,获取锁
如果不是,则进入队列等待
如果是0,代表刚好线程释放了锁,此时将锁的持有者设为自己
如果不是0,则查看线程持有者是不是自己
「1.corePoolSize」:「核心线程数」,线程池中始终存活的线程数。
「2.maximumPoolSize」: 「最大线程数」,线程池中允许的最大线程数。
「3.keepAliveTime」: 「存活时间」,线程没有任务执行时最多保持多久时间会终止。
「4.unit」: 「单位」,参数keepAliveTime的时间单位,7种可选。
「5.workQueue」: 一个「阻塞队列」,用来存储等待执行的任务,均为线程安全,7种可选。
「6.threadFactory」: 「线程工厂」,主要用来创建线程,默认正常优先级、非守护线程。
「7.handler」:「拒绝策略」,拒绝处理任务时的策略,4种可选,默认为AbortPolicy。
1.开始
2.提交任务
3.线程池是否还在进行 是4 否 任务拒绝 end
4.线程数小于核心数 是 添加工作线程并执行end 否5
5.阻塞队列已满 是6 否 添加任务到阻塞队列等待工作获取进行end
6.线程数小于最大线程数 是 添加工作线程并执行end 否 任务拒绝 end
判断线程池中的线程数「是否大于设置的核心线程数」
如果「没有满」,则「放入队列」,等待线程空闲时执行任务
如果队列已经「满了」,则判断「是否达到了线程池设置的最大线程数」
如果「没有达到」,就「创建新线程」来执行任务
如果已经「达到了」最大线程数,则「执行指定的拒绝策略」
如果「小于」,就「创建」一个核心线程来执行任务
如果「大于」,就会「判断缓冲队列是否满了」
「AbortPolicy」:直接丢弃任务,抛出异常,这是默认策略
「CallerRunsPolicy」:只用调用者所在的线程来处理任务
「DiscardOldestPolicy」:丢弃等待队列中最旧的任务,并执行当前任务
「DiscardPolicy」:直接丢弃任务,也不抛出异常
「强引用 StrongReference」
Object obj = new Object();
//只要obj还指向Object对象,Object对象就不会被回收
垃圾回收器不会回收被引用的对象,哪怕内存不足时,JVM 也会直接抛出 OutOfMemoryError,除非赋值为 null。
「软引用 SoftReference」
软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
「弱引用 WeakReference」
弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。
「虚引用 PhantomReference」
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用,NIO 的堆外内存就是靠其管理。
ThreadLocal其实就是「线程本地变量」,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离。
ThreadLocal 有一个「静态内部类 ThreadLocalMap」,ThreadLocalMap 又包含了一个 Entry 数组,「Entry 本身是一个弱引用」,他的 key 是指向 ThreadLocal 的弱引用,「弱引用的目的是为了防止内存泄露」,如果是强引用那么除非线程结束,否则无法终止,可能会有内存泄漏的风险。
但是这样还是会存在内存泄露的问题,假如 key 和 ThreadLocal 对象被回收之后,entry 中就存在 key 为 null ,但是 value 有值的 entry 对象,但是永远没办法被访问到,同样除非线程结束运行。「解决方法就是调用 remove 方法删除 entry 对象」。
「1.对象头」: 对象头又分为 「MarkWord」 和 「Class Pointer」 两部分。
「MarkWord」:包含一系列的标记位,比如轻量级锁的标记位,偏向锁标记位,gc记录信息等等。
「ClassPointer」:用来指向对象对应的 Class 对象(其对应的元数据对象)的内存地址。在 32 位系统占 4 字节,在 64 位系统中占 8 字节
「2.Length」:只在数组对象中存在,用来记录数组的长度,占用 4 字节
「3.Instance data」: 对象实际数据,对象实际数据包括了对象的所有成员变量,其大小由各个成员变量的大小决定。(这里不包括静态成员变量,因为其是在方法区维护的)
「4.Padding」:Java 对象占用空间是 8 字节对齐的,即所有 Java 对象占用 bytes 数必须是 8 的倍数,是因为当我们从磁盘中取一个数据时,不会说我想取一个字节就是一个字节,都是按照一块儿一块儿来取的,这一块大小是 8 个字节,所以为了完整,padding 的作用就是补充字节,「保证对象是 8 字节的整数倍」。