1.常见锁策略
1.1乐观锁VS悲观锁
1.1.1乐观锁:
我们认为一般情况下不会发生并发冲突,在对数据进行提交或者修改的时候,才会正式的进行并发冲突检测,如果发生了并发冲突则会返回用户的错误信息,让用户决定如何修改。
1.1.2悲观锁:
通常考虑最坏情况,每次取数据的时候都会认为别人会修改,所有每次取数据的时候都会上锁,当有其他线程想修改的话,就会阻塞,然后直到它拿到锁。
注意:synchronized初始使用的是乐观锁策略,当冲突变得很频繁的时候就会自动切换成悲观锁
1.2重量级锁VS轻量级锁
锁的核心特性是原子性,追根朔源是由于cpu提供的原子操作指令
cpu提供原子操作指令
操作系统根具原子指令,实现了mutex互斥锁
jvm基于操作系统实现的mutex互斥锁,实现了synchronized和reentrantLock关键锁和类

注意:synchronized不仅是对操作系统提供的mutex互斥锁进行的封装,它自己本身还实现了许多方法
1.2.1重量级锁
依赖os(操作系统)提供的mutex互斥锁
1.2.2轻量级锁
尽量不依赖os提供的互斥锁,而是尽量在用户态代码完成,实现完成不了才依赖os提供mutex互斥锁。
注意:synchronized初始使用的时候是轻量级锁,当冲突过于平方的时候会变成重量级锁。
1.3自旋锁VS挂起等待锁
1.3.1自旋锁(Spin Lock)
概念:当其他线程加锁后,我们当前线程不会放弃cpu,会一直进行请求,当其他线程释放锁后,会第一时间获取到这个锁。
synchronized轻量级锁策略大概就是通过自旋方式实现的。
自旋锁的优点:
不会放弃cpu,不会设计线程阻塞和调度,一旦释放锁,就会立即获得锁
自旋锁的缺点:
如果另一个线程占用锁时间太长,由于自旋锁不会放弃cpu,一直请求,就会消耗大量cpu资源
1.3.2挂起等待锁
概念:当其他线程占用锁的时候,我们当前线程就会放弃cpu,进行阻塞等待,过一段时间再进行请求。
1.4公平锁VS非公平锁
1.4.1公平锁
概念:遵守"先来后到".B⽐C先来的.当A释放锁的之后,B就能先于C获取到锁.
1.4.2非公平锁
概念:不遵守"先来后到".B和C都有可能获取到锁.(不看谁等的时间长)
注意:操作系统的线程调度是随机的,它的基于cpu的原子指令实现的mutex互斥锁,就是非公平锁,想要让其公平就得加额外的数据结构去记录占用锁的时间,从而排一下先后顺序。
公平锁和非公平锁没有好坏之分,关键看使用场景
synchronized就是非公平锁
1.5可重入锁VS不可重入锁
1.5.1可重入锁
概念:同一个线程可以多次获取同一把锁
1.5.2不可重入锁
概念:同一个线程不能多次获取同一把锁
注意:java提供的以reentrant开头的锁,和JDK提供的lock实现类,和synchronized关键字锁都是可重入锁
Linux系统提供的mutex互斥锁是不可重入锁
1.6读写锁
概念:读写锁(readers-writerlock),看英⽂可以顾名思义,在执⾏加锁操作时需要额外表明读写意图,复数读者之间并不互斥,⽽写者则要求与任何⼈互斥。
读写锁就是把读操作和写操作区分,java提供的ReentrantReadWriteLock类就实现了读写锁
ReentRantRead.ReadLock类表示一个读锁,对象提供了lock和unlock方法加锁解锁操作
ReentRantWrite.WriteLock类表示一个写锁,对象提供了lock和unlock方法加锁解锁操作

1.读加锁和读加锁之间,不互斥..
2.写加锁和写加锁之间,互斥
3.读加锁和写加锁之间,互斥.
注意:只要互斥了就会挂起等待,然后不知道等多长时间才能被唤醒,因此减少互斥才是提高效率的重要途经
读写锁特别适合于"频繁读,不频繁写"的场景中.(这样的场景其实也是⾮常⼴泛存在的)
注意:synchronized不是读写锁
1.7CAS
概念:英文名称为(Compare And Swap),比较和交换,顾名思义就是先比较再交换。
规则:
我们假设原来数据为value(内存值)
旧的期望数据为oldValue(寄存器值)
新的期望数据为 values
比较value值与oldvalue值,如果value==oldvalue,那么就 value = values,不相等返回false。
1.71 CAS的伪代码
boolean CAS(address, expectValue, swapValue) {
if (&address == expectedValue) { &address = swapValue; return true; }
return false; }
这就是 CAS的伪代码,它满足我们上面说的规则,它不是一个原子性代码(虽然它是先if再赋值)。实际CAS是由硬件的一条原子指令完成的。
典型不是原子性代码的是:
Check And Set 的这样的操作就不是。
Read And Update这样的不是。
注意:当多个线程针对同一个资源进行操作的时候,只有一个线程最后会成功,其它线程并不会被阻塞,但会返回失败。
1.7.2 CAS的应用
1.实现原子类
标准库里提供了一个包名字叫 java.util.concurent.atomic 包,里面的所有类(原子类),都是通过CAS这种方式实现的,典型的AtomicInterger就是这个类。


伪代码的实现:
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value; }
return oldValue;
}
}
说明:AtomicInterger如何实现的i++这个操作的,我们可以按照这个伪代码的形式来理解。
value 我们认为是内存上的值
oldvalue 认为是寄存器内存上的值。
当面俩个线程同时调用上述伪代码伪代码的执行效果如下:(这里我们假设有俩个线程 1 2,同时他们的 oldvalue==0 内存value = 0)

1.当线程1去调用这个代码的时候,因为value == oldvalue == 0,所以 oldValue+1,值赋值给 value=1.
注意:CAS是直接对内存进行操作的,而不是寄存器。
CAS的读取和写入是一条原子指令,是原子性操作。

2.这里线程2开始进行操作,value=1 不等于oldvalue=0,所以CAS回返回false,然后进入循环然后oldvalue = value =1。

3.当线程2.value == oldvalue==1时候, value == oldvalue+1=2

4.各个线程返回自己的oldvalue即可
通过上述CAS就可以实现一个原子类,不需要使用重量级锁就可以实现各个线程多线程自增操作。
(这里也符合我们上面说的多线程调用CAS只用有一个线程成功同时不会阻塞其他线程)
2.现实自旋锁
1.8CAS的ABA问题
什么是ABA问题:
假设存在两个线程t1和t2.有⼀个共享变量num,初始值为A.
接下来,线程t1想使⽤CAS把num值改成Z,那么就需要
•先读取num的值,记录到oldNum变量中.
•使⽤CAS判定当前num的值是否为A,如果为A,就修改成Z.
但是,在t1执⾏这两个操作之间,t2线程可能把num的值从A改成了B,⼜从B改成了A
解决方法:
引入版本号,不仅要比较当前值和老的值,也要比较版本号。
当当前版本号和老版本号相等的时候,就可以修改
当当前版本号高于老版本号,就修改失败(认为数据已经被修改)
注意:java标志库里提供了AtomicStampedReference<>就实现了对某个类的封装,内部提供了,这个版本号。(这里不说怎么用了可以自己进行查阅)
2.Synchronized锁的原理
2.1Synchronized锁的特点
基于上述锁策略我们总结以下:
1.synchronized前期是乐观锁,后期冲突加剧的时候是悲观锁
2.前期是轻量级锁,后期是重量级锁
3.它的轻量级锁大概率是由自旋锁实现的
4.是非公平锁
5.是可重入锁
6.不是读写锁
2.2synchronized加锁工作过程
JVM将Synchronized锁分为,无锁,偏向锁,轻量级锁(由自旋锁实现),重量级锁。根据情况依次进行升级。

1.偏向锁:
第一次尝试加锁的线程,没有其它线程去竞争,就会有这么个偏向锁的状态
2.轻量级锁:
当有其它线程来竞争的时候就会就变成轻量级锁(自适应自旋锁)
此处的轻量级锁就是通过CAS实现的(CAS的应用2):
.通过CAS来检查更新一块内存
.如果更新成功就加锁成功
.如果更新失败就加锁失败,该2锁被占用,继续自旋等待(并不放弃CPU)。
3.重量级锁:
如果竞争进⼀步激烈,自旋不能快速获取到锁状态,就会膨胀为重量级锁(依赖OS提供的mutex互斥锁)
2.3一些其他的优化操作
2.3.1锁消除:
编译器+JVM判断锁是否可以消除,如果可以直接消除
概念:有些代码,虽然用到了synchronized但它是在单线程的情况下用的,此时它所谓的加锁和解锁就显得很没用,此时可以消除。(例如StringBuffer)
2.3.2锁粗化:
概念:对某些代码来说,每次对某个对象加锁,可以对整体进行加锁。
代码:

3.JUC(java.util.Concurrent的常见类)
3.1Callable接口
Callable是一个接口,它相当于把线程封装成了一个‘返回值’,方便程序员利用多线程来计算返回结果。
下面我来一些代码进行演示:
这个是不利用Callable接口去计算
1.我们在Common这个类里面定义一个静态变量sum=0;同时定一个一个锁对象()不同线程锁对象是同一个才会发生锁竞争。
2.我们在主线程里面当sum==0的时候,我们让其进行等待就不往下执行打印操作了
3.另外创建一个线程是sum不断加加,最后赋值,然后去唤醒主线程,最后就打印。

下面这个是利用Callable接口来计算返回值

说明:我们明显可以看到我们Callable接口,把计算这个线程封装成一个返回值,大大减少代码,看起来不那么繁琐.
理解Callable接口:
Callable接口和Runnable接口,都是对任务的描述,但Callable是对有返回值的进行描述,Runnable是对没有返回值的描述。
Callable通常需要搭配FutureTask取使用,我们Callable计算得到的值,需要传给在等待它的FutureTask,然后利用get()方法就能取到了。
3.2ReentrantLock(Java标准库实现的一个类)
注意:synchronized是jvm基于C++实现的
是可重入互斥锁,与synchronized类似,都是用来实现互斥效果实现线程安全的
//reentant中文意思就算可重入
ReentrantLock的用法:
lock();加锁,要是没获取到锁就死等
trylock(超时时间);加锁,要是没获取到就等待一段时间然后放弃
unlock(); 解锁
下面是一些代码:

ReentrantLock和synchronized锁的区别:
1.ReentrantLock是基于Java实现的一个标准类,而synchronized是基于jvm利用C++实现的。
2.synchronized不需要手动释放锁,而ReentrantLock需要手动释放锁,但代码多了容易注意不到写。
3.synchronized是非公平锁,ReentrantLock默认是非公平锁,但在实例的时候可以传入true参数,取改成公平锁。
4.synchronized在申请锁失败后会死等(轻量级锁由自旋锁实现),而ReentrantLock可以用trylock()方式让其放弃
5.synchronized的在可以通过Object的wait()和notify()来实现线程的等待唤醒,是随机唤醒等待的线程,ReenrantLock搭配Condition实现等待唤醒,可以精确的唤醒某个线程。
判断synchronized锁和ReentrantLock锁的使用场景?
1.当冲突不频繁的时候我们用synchronized的,自动释放,效率高
2.当冲突加剧的时候,我们选择使用ReentrantLock,搭配trylock()更灵活的控制锁,不至于死等
3.如果要使用公平锁就传个true让其变成公平锁
4.原子类
1.AtomicBoolean
2.AtomicInterger
3.AtomicIntergerArray
4.AtomicLong
5.AtomicReference
6.AtomicStampedReference
以AtomicInterger为例常见的方法有:

5.线程池
因为我们线程频繁的创建和销毁,还是很繁琐,为了解决这种情况我们引入了线程池。
当我们执行完某个线程并不会把这个线程进行一个销毁,而是放到线程池里面,当下次用的时候取就行,就不需要再取创建线程了。
代码实例:(上面是一个利用Runnable实现的线程,下面是一个用Callable接口实现的线程这个俩个线程的区别看上面就行)

Executors创建线程的几种方式:
newFixedThreadPool:创建固定线程数的线程池
• newCachedThreadPool:创建线程数⽬动态增⻓的线程池.
• newSingleThreadExecutor:创建只包含单个线程的线程池.
• newScheduledThreadPool:设定延迟时间后执⾏命令,或者定期执⾏命令.是进阶版的Timer.
Executors本质上是ThreadPoolExecutor类的封装.
5.1ThreadPoolExecutor
提供了许多类型的参数,可进一步细化线程池行为的设定
ThreadPoolExecutor的构造方法:

.

被折叠的 条评论
为什么被折叠?



