线程基础学习二

一.什么是死锁:

当两个现场都持有对方需要的资源,而双方都处于循环等待的状态;

条件是:

互斥条件 :二者之间是互斥的。当一个线程占用着资源是,另一个线程只能等待;

请求和保持条件: 需要的资源在别的线程手中时,只能等待资源得到释放

不打断:不去强制性打断;

死循环;

避免死锁的方式:

1.加超时释放;

等待一段时间之后,无法获取,进行回退,在释放已获得的锁;

缺点 :需要自己创造一个锁;

2.规定好线程的执行顺序;

缺点:这种方式需要你事先知道所有可能会用到的锁

3.死锁检测机制

当每个线程获得锁的时候,都记录下来(可以通过map之类),当获取其他锁获取不到的时候,可以看下是不是正在被持有,发生了死锁,如果是死锁就全部释放;相当于超时释放,不过释放肯定是因为发生了死锁;

更好的方案: 设置线程优先级,根据优先级先释放几个线程,让其他的线程继续保持;

二 自旋锁 

当前线程尝试获取锁,如果获取不到会一直进行尝试获取,直到获取到为止。

自旋锁与互斥锁相比较: 都是为了保护共享资源,任意时刻,只有一个线程拥有资源;不同的是互斥锁会是的线程进入休眠,而自旋锁会一直长期处于活跃状态;

缺点:

如果某个线程一直长期占有锁,就会造成其他等待锁的线程 持续占有cpu,cpu使用率增大;

不公平锁;

优点:

不会是线程进入阻塞状态,一直处于活跃状态,减少线程间的上下文切换;

同时,可以加个计数器实现可重入锁;

其他变种的自旋锁:

ticketLock :加了一个号码牌,锁会对应一个服务号,每当线程释放锁的时候,服务号+1; 同时,每当线程尝试获取锁的时候,会分配一个递增的id排对号;

缺点:
多个线程在读写当前的锁的服务号时,会造成大量的缓存同步;

CLHLock: 基于链表的,把当前线程放在尾节点(节点中 的变量是 isLocked =true),将节点放入threadLocal中,如果前面还有节点,说明无法获取当前锁;一直轮循,知道释放 ;释放锁: 当前节点 置空,isLocked=false;

MCSLock: 增加了一个next指向;

  • 都是基于链表,不同的是CLHLock是基于隐式链表,没有真正的后续节点属性,MCSLock是显示链表,有一个指向后续节点的属性。
  • 将获取锁的线程状态借助节点(node)保存,每个线程都有一份独立的节点,这样就解决了TicketLock多处理器缓存同步的问题。

 

三 CAS

compare and swape ,cas有三个操作数,v内存地址的值,a预期的旧值,b将要修改的新值,如果v==a,那么进行修改,否则说明与其他线程冲突了,不进行任何操作。

CAS 原理: 不经过jvm,直接通过直接利用java本地方 JNI,实现java的非阻塞方法,直接利用cpu的cas指令,保证操作的原子性; 

使用总线保证原子性;确保对内存的读-改-写操作都是原子操作执行。在多cpu下cpu与内存是通过总线进行交流,锁住这条总线让其他cpu无法访问,但是这样的开销较大,

或者使用缓存锁机制: 锁定缓存行对应的内存区域,使得其他CPU无法访问这篇缓存。

当操作不在缓存中进行,或者涉及到多个缓存行是,使用总线锁;加上lock#指令,在总线上输出此信号,通知其他处理器,,不允许在对该变量进行操作;

CAS缺点:

ABA 问题:原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。

循环时间长开销大: 自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销

只能保证一个共享变量的原子操作。

 四 AQS 

抽象的队列式同步器, 对于当前请求资源如果空闲,则让当前线程设置为有效线程,并且将共享资源设置为锁定状态;如果当前资源被占用着,那么就设置一套线程等待以及分配锁的队列机制,这个队列使用CLH实现。

构成:

int 类型的state变量。     用 volatile修饰,保证可见性,0表示当前没有线程持有锁,持有锁后state+1,在释放的时候=0,才表示完全释放。对state的更新都是通过cas操作保证更新操作的原子性。

双向队列。    除了头结点,每个节点都是由一个线程引用和状态码构成,代表一个线程。

以ReentrantLock 为例, 

请求锁:

当前如果没有线程占用锁,则直接获取。

持有进行可重入检测。

否则加入到队列中去。

把线程封装节点加入到末端,节点node内容是:线程引用+ 状态码 初始值为0;  如果为-1,表示下一个节点的线程挂起状态,如果是1 则是代表着移除队列。

相对列末端添加节点的操作:  cas+死循环。如果多个线程同时调用,只有一个线程可以成功,不成功的重新进行等待下次插入,所以解决了并发问题。

挂起操作:

判断节点的前一个节点,如果是头结点,则进行尝试获取锁;获取成功就移除,获取失败就下一步。

判断前一个节点,如果singal,返回true,将线程挂起。

判断前一个节点,如果是cancle,则将前一个节点移除。

如果是其他值,  将前一个节点标注为single,进入下一次循环.

非公平锁这是,如果是公平锁,先直接判断下等待队列中有没有线程。

 队列的头节点是成功获取锁的节点,当头节点线程释放锁时,会唤醒后面的节点并释放当前头节点的引用。 

对于入队,采用CAS操作,每次比较尾节点是否一致,然后插入到尾节点中。对于出队列,因为每个节点缓存了一个状态位,不满足条件时自旋等待,直到满足条件时将头节点设置为下一个节点。

AQS中还有一个表示状态的字段state,例如ReentrantLocky用它表示线程重入锁的次数,Semaphore用它表示剩余的许可数量,FutureTask用它表示任务的状态。对state变量值的更新都采用CAS操作保证更新操作的原子性

 

五 乐观锁 悲观锁

悲观锁 :就是假设坏的情况,每次线程都会修改数据,因此采取独占的方式,任何时刻 都只能一个线程进行操作;

乐观锁 : 假设最好的情况,就是一般情况不会产生数据冲突,但在每次更新数值之前 会进行检测下。可以使用版本号或者cas算法进行实现。

使用场景 :对于读操作较多的时候,使用乐观锁,对于写操作比较多的时候,数据冲突的概率较大则使用悲观锁。

六 读写锁

读写锁虽然有两个锁,但实际上只有一个等待队列。

  • 获取写锁时,要保证没有任何线程持有锁;
  • 写锁释放后,会唤醒队列第一个线程,可能是读锁和写锁;
  • 获取读锁时,先判断写锁有没有被持有,没有就可以获取成功;
  • 获取读锁成功后,会将队列中等待读锁的线程挨个唤醒,知道遇到等待写锁的线程位置;
  • 释放读锁时,要检查读锁数,如果为0,则唤醒队列中的下一个线程,否则不进行操作。
  • 独占锁和共享锁在实现上的区别
    独占锁的同步状态值为1,即同一时刻只能有一个线程成功获取同步状态
    共享锁的同步状态>1,取值由上层同步组件确定
    独占锁队列中头节点运行完成后释放它的直接后继节点
    共享锁队列中头节点运行完成后释放它后面的所有节点
    共享锁中会出现多个线程(即同步队列中的节点)同时成功获取同步状态的情况
  •  

 

七 阻塞队列

阻塞队列就是在普通队列上在添加两个方法,一个是当队列满时,在继续添加的时候,会发生阻塞;当队列为空时,在继续移除队列发生阻塞;

解决生产者消费者问题:  通知机制:当生产者满时,通知消费者;当消费者满时,通知生产者。

Callable 和 Future

callable 是一个可以返回结果的任务,而future则是返回的对象。

多线程的上下文切换是指,cpu分配给每个线程一定的时间片,当时间片用完之后,cpu记录下当前运行的位置,以便下次运行时继续执行,然后接着切换到下一段线程。

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值