java-并发编程八股-并发安全篇
java 并发编程八股-并发安全篇
概括,总结,反思,对比
juc 下你常用的包有哪些
线程池
- ThreadPoolExecutor
这个就是常用来创建线程池的默认方法。提功能了很多的参数,具有较高的自定义性,满足各种方法。
并发集合
- ConcurrentHashMap
线程安全的 hash 映射表,在高并发的场景下这种分段锁比 hashtable(synchronized 全锁) 的方式好了很多。
- CopyOnWriteArrayList
线程安全的列表。核心思想写时复制。读就是正常的不加锁。在写入的时候会创建一个新的数组(原始长度+1),来存放新数据原始的数据直接覆盖。
同步工具类
- CountDownLacth
允许一个或者多个线程等待(主线程也会阻塞)其他一组线程完成操作后在继续执行。当然一般都是一个主线程去等待一些附属线程去完成某些操作。然后再根据结果去整合操作。
核心就是通过一个计数器来实现的,每个任务结束了就调用countDown将计数器减一。为零的时候那么等待的线程就能继续执行。
- CyclicBarrier
让一组线程互相等待,知道所有线程都达到某个屏障点后,在一起继续执行。
这个类就更加强调的是多个,一组,线程之间的等待。强调线程之前基本没有附属关系(比如王者进入峡谷,要等待大家都加载完成再进入。)
与CountDownLatch不同的是,这个计数器会重置并复用。达到屏障之后就能继续进入下阶段。
- Semaphore
信号量,用来控制某个资源的访问量。通过投放许可来实现。常用于控制有限资源的访问。
原子类
- Atomic 原子类
包括了 integer,Long,引用,类型的原子操作。自增,自减等操作。底层只要考CAS来实现。
java中常见的锁机制
- 内置锁 synchronized
synchronized是我们比较常见的一种锁机制。他作为 java 的一个关键字,可以应用在方法上,代码块中。之所以叫做内置锁,是因为你看不到它的加锁解锁的方法。你看不到锁,就叫监视器锁
当一个线程进入由 synchronized 标记的方法或者代码块的时候就会去尝试获取锁,获取对象锁(修改对象头的 markword)—(偏向锁)。
当此时有线程争抢锁的时候,就会通过锁升级,由 偏向锁—轻量级锁—重量级锁。来防止并发问题的出现。
为什么不直接就是重量级锁:如果这样的话就会有较为严重的锁性能损耗,在少量线程竞争或者只有单线程在运行的时候就会直接启动系统级别的锁。
性能消耗不是一星半点。所以通过锁升级这种机制。来对锁进行性能优化。
- ReentrantLock
这个锁一直用来和 synchronized 来进行比较。但是这个锁是基于 AQS(同步框架) 来实现的功能,所以功能更加丰富。
synchronized:java 关键字,可以修饰方法或者代码块较为灵活。
ReentrantLock:java 提功的一个类库,要手动的加锁解锁,锁的范围自己选择。同时还提功了公平和非公平锁的实现。
非公平锁的实现就是新的线程通过 CAS 去争抢一个多线程可见的共享变量(volatile)。来实现虽然相比公平锁更加公平~~~。但是会导致线程饥饿的问题
- 读写锁 ReadWriteLock
一个互斥锁,读读共享,读写,写写互斥。一般用在读多写少的场景。
- 乐观锁,悲观锁
常见的乐观锁,一般都不锁定资源,比如 CAS。并不是加锁,而是通过更新的时候比对版本号实现的。
上面那些都是悲观锁。
- 自旋锁
如果没有抢到锁,就会循环一下等等,再去获取。
因为线程,挂起在等待锁,在唤醒,的性能损耗比自旋更大。
synchronized锁静态方法和普通方法有什么区别
首先锁的跨度就不一样,当锁静态方法的时候,锁住的是整个类,当然就包括了所有实例要方法这个静态方法的情况,同时只能有一个实例去访问。
跨实例所拥有的。
锁实例方法这个跨度就比较小,锁住的单单就是一个this实例。只能锁住当前对象。因此,针对同一时刻,不同对象的同一方法可以有很多线程访问。
synchronized和reentrantlock 区别
这两个都是 java 提供的可重入锁。
在用法上,synchronized 能修饰普通方法,静态方法,代码块。而 ReentrantLock 只能用在代码块上。
对于 synchronized 来说,无需关心加锁,解锁的操作,编译的时候会自动帮你加上(感觉像个语法糖)。只有非公平锁。通过 jvm 实现。
而 ReentrantLock 通过 AQS 实现
可重入锁
同一个线程可以多次获取同一个锁,而不会造成死锁。这种机制就是通过计数器来实现的,锁一次就自增异常,最后之后为 0的时候才代表锁可以被完全释放
synchronized锁升级机制
- 无锁
就是没有开启偏向锁的状态,需要在 jvm 启动几秒之后才能开启。(当然可以配置)
- 偏向锁
这个是在偏向锁开启之后的一个状态,在还没有线程去拿锁的时候叫做,匿名偏向锁。当一个线程获取到锁之后,就会在对象头的(mark word)位置埋下标记,当第二次获取锁的时候可以直接获取到锁(不用进行 cas 争抢锁)。相当于锁偏向于这个线程。
- 轻量级锁
在偏向锁的基础上,如果有多个线程进行 CAS 争抢对象头的 markword 标志,就会升级为轻量级锁,锁的争抢主要通过 CAS 进行。
- 重量级锁
当检测到 CAS 竞争的锁太多(两个以上)就会升级为重量级锁,因为 CAS 不成功是会自旋的,多个 CAS 自旋的性能消耗还是很大的。这个时候就会将没有争抢到的线程挂起,等待锁释放(优化性能。)
注意,偏向锁(CAS 自旋也不行)—》 轻量级锁(CAS 自旋还是失败)—》 重量级锁
总结就是:自旋失败就升级,因为这个时候就代表着竞争的线程太多。
jvm对synchronized的优化
锁膨胀,就是锁升级的过程。通过从无锁到有锁就是一个性能损耗逐渐增加的场景。
锁消除:如果一段代码都不被竞争就没必要加锁
锁粗化:把多个锁连起来,变成一个更大的锁。
自适应自旋:在挂起之前,循环一下(挣扎一下)
AQS
AQS 就是 java 提功的一个抽象类,包含了一些基础操作:加锁,解锁,共享变量,队列管理的操作(FIFO 的双向列表)。
CAS和AQS的关系
CAS 为 AQS 提供的原子类的操作。AQS 内部针对共享状态变量的更新是通过 CAS 来实现的。
Threadlocal原理解析
threadLocal 在 java 中是用来解决线程安全的一种机制,允许我们创建独属于某个线程的变量副本,这样每个线程都能通过拿到相同的变量副本,来获取不同的 key。
机制:在 threadLocal 中存储的是一个各自维护的 threadLocalMap 对象,其中存储着一个个的Entry 对象。
其中 Entry 的 key就是 threadLocal 对象(弱引用),value 就是我们存储的值
**弱引用:**这里的 key 设计成弱引用是别有用心的。也是为了防止内存泄露的出现。
所以通过传递相同的 threadLocal 对象就能拿到不同的值。
**优点:**线程隔离,代码耦合度低,有不错的性能优势。
threadLocal 看似很好用,我们可以将前端传递的 token 解析后从 redis 中捞取响应的用户信息,存储到 threadLocal 中,这样本次请求之内,就可以方便的获取用户 id,用户权限。
缺点:
我们都知道,线程内部存储的信息如果不手动控制,那么内容的生命周期就和线程一样。平常的线程使用完就会被直接销毁,那么 threadLocalMap 占用的内存也会被回收。
但是线程池就不一样了,因为线程是复用的,如果不手动操作那么,就会导致内存泄露问题。
弱引用这种机制也是来防止内存泄露出现的,当线程被回收,或者没有强引用指向的时候,弱引用就会被回收。
但是这就有个问题,线程一般不会被销毁,及时弱引用被回收了,Entry 中会出现 key 为 null (弱引用被回收)的 value。
即使调用清除方法,也会(Entry 中会出现 key 为 null (弱引用被回收)的 value。)
这种说法只能代表想增加一层保障。
弱引用感觉也没什么用。只有在(Entry 中会出现 key 为 null (弱引用被回收)的 value。)出现的时候。下次在增加数值,set。会自动检测 key==null。
检测到了,就覆盖存储,或者把 value 也设置为 null。来实现回收。
这次的弱引用是为了下次方便回收
**所以:**要求我们在每次使用完之后就调用 remove 方法删除 Entry。
管他是不是弱引用,你不用了,就都清楚掉。
但是在平常的编码中,你不会主动管理这一方面。我们可以利用轮子(或者自己写一个)来代替进行管理
乐观锁-CAS的原理
乐观锁,见名知意,认为不会发生锁竞争因此只有在最后修改数值的时候才会进行。比较并且交换(原子操作。)这种机制利用了,系统本地的原子命令。实现的原子操作。
**缺定:经典的 ABA 问题,因为 CAS 就是一个简单的先比较,和预期值一样就交换成为新值。**如果数值从期望的 A 变成了 B,又变回 A。CAS 是没有办法感知到的。
为什么这个问题,因为数据是发生了变化的,就算是变回去了,也不行。
如和解决呢? 办法也比较简单,java 就是帮我们在 CAS 中增加了一个版本号。比较的时候也比较一个版本号来确定是否符合预期。
现在看着好像很不错了,乐观锁也没什么缺定。为什么不能所有的锁都用这种呢?
因为 CAS 在比较的时候失败了,会进行自旋操作。(当竞争较大的时候就会有大量的自旋操作发生。)这是很恐怖的性能损耗问题。就是系统在空转。
所以这就是为什么synchronized 要从轻量级锁–》升级为重量级锁 的原因
volatile
这种机制是用来保证变量的可见性,以及禁止指令重排。
实现方式就是利用内存屏障。
写-写屏障保证变量之前的所有写操作都已经完成,不能放到变量之后。
读-写屏障保证变量读之前的普通读方法不会到达变量之后。
写-读屏障
这些机制都是一样的,在 volatile 之前的操作不能放到 volatile 之后,之后的操作不能放到 volatile 之前。
当前缺点就是利用不上 jvm 对于指令重排的优化性。
volatile和synchronized对比
volatile 只能保证一个变量的可见性,无法实现变更的原子性(i++)。也无法实现线程安全的问题。只能实现变更的可见性,因为对于 volatile 的变量每次都会刷新到主内存。每次读取都要到主内存读取最新的值。
非公平锁吞吐量比公平锁大
原因就是公平锁讲究一个前后顺序,刚进来的线程就是无法读取到只能让线程休眠,到他了在唤醒他。这就有线程用用户态到内核态在到用户态的性能损耗。
而非公平锁:锁抢不到就先进入等待队列,下次锁释放之后,大家同台竞争,这样增大了吞吐量。