多线程进阶

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的构造方法:

.

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值