Java EE初阶 --多线程2
一. 常见的锁策略
1.悲观锁和乐观锁
悲观锁:总是假设是最坏的情况,认为锁竞争十分激烈,每次访问共享数据的时候,都认为别人回家修改它,所以每次访问共享数据的时候,都会先获取锁,其他线程想获取就得阻塞等待,进入到等待队列,工作流程为先加锁再操作
乐观锁:总是假设是最好的情况,认为锁竞争不激烈,访问共享数据时认为别人不会修改数据,所以不对数据进行加锁,工作流程为先操作再进行检查冲突
2.重量级锁和轻量级锁
悲观锁和乐观锁是加锁时遇到的场景,而重量级锁和轻量级锁是解决这些场景的解决方案,重量级锁是应对悲观锁的场景,轻量级锁是应对乐观锁的场景。
3.挂起等待锁和自旋锁
自旋锁:是轻量级锁的典型实现,应用程序级别的,当加锁式发现竞争,不会进入到阻塞状态,而是进入到忙等(不断的申请锁,直到拿到锁为止,此过程中要不断的消耗CPU,但在乐观锁的场景下,可以短时间内拿到锁)的状态。
挂起等待锁:是重量级锁的典型实现,操作内核级别的,加锁时遇到竞争时,会进入到阻塞状态,进入到等待队列,不会持续消耗CPU,此时可以拿CPU做其他事,后续要操作内核进行唤醒了
4.普通互斥锁和读写锁
互斥锁:如Java中的 synchronized
读写锁:把读和写操作区分对待,Java标准库提供了ReentrantReadWriteLock类,实现了读写锁,其类下有ReentrantReadWriteLock.ReadLock类表示读锁和ReentrantReadWriteLock.WriteLock类表示写锁,并分别提供了lock和unlock方法进行加锁和解锁。其中读和读操作不会互斥,多个线程可以同时读,而读和写操作、写和写操作会互斥
5.可重入锁和不可重入锁(讲过)
6.公平锁和非公平锁
公平锁:遵从“先来后到”,多个线程申请一个刚释放的锁时,谁先申请谁就先拿到锁
非公平锁:不遵从“先来后到”,谁都有机会拿到锁
二.synchronized原理
1.synchronized遵从那些锁策略
synchronized始终是悲观锁,具有自适应性(JVM内部会统计锁竞争程度,根据锁竞争程度会从偏向锁→轻量级锁→重量级锁逐步升级)、可从入锁、非公平锁、普通互斥锁。
2.加锁过程
JVM将synchronized关键字分为无锁、偏向锁、轻量级锁、重量级锁。会根据情况,进行依次升级
加锁过程解析:在未刚进入synchronized时为无锁状态,一旦进入到synchronized,不会真正加锁,而是对锁进行标记,此时由无锁转化成偏向锁,该操作比加锁轻量很多,如果其他线程没有来竞争该锁,最终该线程执行代码解锁过程只是把这个标记清除,如果其他线程来竞争该锁,该线程会在锁被其他线程拿到之前加上锁,此时就是真正加锁了,从偏向锁转化为轻量级锁,如果JVM发现该锁竞争十分激烈,轻量级锁就会转化为重量级锁
三.锁消除
是编译器优化的体现,编译器会自行判断某代码块是否需要加锁,如果不需要,即使用户自己加上synchronized,但在编译后生成的机器码消除。
四.锁粗化
锁粒度指的是同步块保护的共享资源范围大小。
锁粗化就是针对反复对细粒度的代码块加锁时,就有可能优化成一次加锁操作和解锁操作,此时锁的粒度就会变粗,这就是锁粗化。
五.CAS
1.什么是CAS
CAS: 全称为Compare And Swap,意思为比较和交换。CAS本质上是CPU指令,操作系统对该指令进行了封装,并提供了api给C++来使用,而JVM又是基于C++实现的,所以JVM可以通过C++调用CAS操作了,单都是由JVM和标准库封装好了。
2.CAS伪代码
注意:该代码块不是原子的,而真实的CAS操作是原子操作

3.CAS的使用
3.1 实现了原子类
Java标准库提供了java.util.concurrent.atomic包,里面对各种类型(如int、long等类型)进行了封装,都是基于CAS操作实现的,如之前的count++涉及到线程安全问题,得进行加锁操作,而该包下的类基于CAS实现count++来确保线程安全问题,就不需要进行加锁操作,提高了性能。
包下的类和使用:

3.2 实现自旋锁

4.CAS的ABA问题
4.1 什么是CAS的ABA问题
假设某一次CAS操作中value为A,此时和oldValue,如果判断相等就认为value没有被修改,但有可能该value被修改过(有其他线程从A改为B,再由B改为A).这就是ABA问题。
4.2 CAS的ABA问题引起的BUG
如出栈操作,刚开始栈类有节点A和节点B,进行线程一进行读取栈顶对old进行赋值,打算进行CAS(head,old,expect)操作就挂起等待,此时线程2进行读取进行CAS操作判断相等后进行pop和push一个新的节点A^,但A^的内存地址分配时刚好分配到和A一样的地址,push完之后线程1CAS操作判断相等,CAS操作成功,弹出A^,但用户并不知道,认为该弹出的是A.这是异常情况,正常情况为不相等,CAS操作失败,重新循环读取head到old重新CAS操作,相等后再进行pop真正弹出新的节点.
4.3解决方案
给要修改的值,引⼊版本号.在CAS⽐较数据当前值和旧值的同时,也要⽐较版本号是否符合预期.
• CAS操作在读取旧值的同时,也要读取版本号.
• 真正修改的时候,
◦ 如果当前版本号和读到的版本号相同,则修改数据,并把版本号+1.
◦ 如果当前版本号⾼于读到的版本号.就操作失败(认为数据已经被修改过了).
六.Callable接口
该接口也可以定义一个任务,线程执行任务时都是调用无返回值的run,但对run方法进行了封装,调用时封装后的可以返回值,方便程序员可以借助多线程的方式计算结果
举例:

总结创建线程的写法:
1.继承Thread类(定义单独的类/匿名内部类)
2.实现Runnable接口(定义单独的类/匿名内部类/lambda)
3.实现Callable
4.线程池
七.ReentrantLock类
作用:可以重入互斥锁,和synchronized关键字类似
1.ReentrantLock类下的方法:
lock()方法:获取锁,如果获取不到锁就会死等
trylock(时间)方法:获取锁,如果获取不到锁,等到超时间结束就放弃获取锁
unlock()方法:解锁
2.ReentrantLock类和synchronized的区别
1.synchronized是一个关键字,是基于JVM内部实现的(大概率基于C++实现的),ReentrantLock是Java标准库的一个类,是基于JVM外部实现的(Java代码实现的)
2.synchronized关键字在申请锁失败情况下会死等,而ReentrantLock提供了一个超时间的加锁方法,可以不死等。
3.synchronized一直都是非公平锁,ReentrantLock类也默认为非公平锁,但ReentrantLock提供了修改为公平锁的构造方法,只要创建实例时传入true就为公平锁。
4.synchronized关键字会自动释放锁,而ReentrantLock类必须通过类下的unlock方法进行解锁,一般容易遗漏,得搭配finally来使用
5.synchronized关键字唤醒锁是通过wait-notify来进行唤醒随机的一个线程,而RenntrantLock类搭配Condition类使用,可以精准唤醒某个线程
八.信号量Semaphore
信号量,也可以表示”可用资源的数量“,本质上是一个计数器。
作用:可以协调多个线程/进程之间的资源分配
使用:

利用可用资源被用完再申请资源可以设置可用资源为1,达到锁的效果:

九.CountDownLatch类
在多线程的使用中经常将一个大任务分成一个个小任务多线程执行,但何时任务全部完成呢?通过CountDownLatch类可以知道。
使用:

十.在多线程中使用线程不安全的类的解决办法
1.在多线程中使用ArrayList
1)自己使用同步机制(使用synchronized关键字或ReentrantLock类)
2)Collections.synchronizedList(new ArrayLsit)返回的List中的方法都是带有synchronized修饰的
3)使用CopyOnWriteArrayList类,该类不涉及加锁,但每次修改数据时都会复制存储数据的容器,在新的容器上增删改查,在复制过程中读取数据就会从旧的数据读取,复制成功之后把新容器的引用赋值给旧容器的引用(此操作为原子的),使引用指向新容器。
2.在多线程中使用哈希表
在多线程中使用HashMap是线程不安全的,但是使用Hashtable是线程安全的,但Hashtable给所有public修饰的方法中加以synchronized关键字修饰,以达到线程安全,效率比较低,于是对Hashtable进行了优化,提供了ConcurrentHashMap类。

