并发编程归纳总结
-
AQS:AbstractQueueSynchronized
-
这个可以扩展出许多的并发类
-
举例:FurtureTask,Synchronized也是根据其原理进行实现的
-
源码分析:
- state:同步状态,记录当前线程对锁的持有情况(解决锁的可重入)
-
使用场景:自定义并发同步工具类(搞一个内部类去继承AQS)
-
AQS设计模式:模板方法设计模式
-
模板方法:在抽象类中定义好模板(任务有一定顺序),具体任务类A继承抽象类,执行具体业务逻辑
-
抽象类:
package cn.enjoyedu.concurrent.theory.aqs.templatepattern; /** * 类说明:抽象蛋糕模型 */ public abstract class AbstractCake { protected abstract void shape();//蛋糕造型 protected abstract void apply();//蛋糕涂抹 protected abstract void brake();//烤蛋糕 /*模板方法*/ public final void run(){ this.shape(); this.apply(); this.brake(); } }
-
具体任务类:继承抽象类,重写方法(具体逻辑)
/** * 类说明:芝士蛋糕 */ public class CheeseCake extends AbstractCake { @Override protected void shape() { System.out.println("芝士蛋糕造型"); } @Override protected void apply() { System.out.println("芝士蛋糕涂抹"); } @Override protected void brake() { System.out.println("芝士蛋糕烘焙"); } }
-
使用具体类实例执行任务
/** * 类说明:生产蛋糕 */ public class MakeCake { public static void main(String[] args) { AbstractCake cake = new CheeseCake(); cake.run(); } }
-
模板模式在Android中的体现:View.Draw() 需要去重写onDraw(takeView)与dispatch(ServiceView),实现自己的具体方法,
-
-
-
自定义并发工具类创建:需要继承AQS(已经定义好了模板方法),并且重写里面的方法
-
独占锁:实现AQS 中的独占的方法:tryAcquire
protected boolean tryAcquire(int arg) { //空实现,需要我们实现这个方法 throw new UnsupportedOperationException(); }
-
实现一个共享并发工具类:需要重写tryAcquireShared
protected int tryAcquireShared(int arg) { throw new UnsupportedOperationException(); }
-
-
独占锁:线程拿到同步代码块的锁后,才能执行里面的业务
- 假如说原state是0,拿到锁后变成1,就证明拿到锁了
-
自定义显示独占锁:所有的显示锁均继承Lock接口
-
实现Lock接口(需要重写里面的一些方法:lock,unlock)
-
定义静态(1.工具类,2.方便调用)内部类继承AQS
-
判断锁的占用状态:
/*判断处于占用状态*/ @Override protected boolean isHeldExclusively() { return getState()==1; }
-
获得锁,需要实现一个独占的方法(进行CAS操作(尝试修改state属性),CAS成功登记锁,返回true;如果不成功,那么就返回false(证明其他类拿到这把锁))
/*获得锁,需要实现一个独占的方法*/ @Override protected boolean tryAcquire(int arg) { //里面是有一步CAS操作的,当CAS比较成功(其他的线程拿到了锁,CAS),将这个锁的内部登记一下,只是试了一次 if(compareAndSetState(0,1)){ //这里是不支持锁的可重入的,一个线程只能拿对一把锁拿一次 //下面那个是排他的所有线程,就是说谁拿到这把锁,现在这把锁被我拿到了 setExclusiveOwnerThread(Thread.currentThread()); return true; } //其他的线程拿到了锁,CAS return false; }
-
释放锁(这个时候,同步工具类已经拿到了这个锁):修改state属性
/*释放锁,在同步工具类中拿到(修改了state变量)了这个锁,现在就是对齐释放*/ @Override protected boolean tryRelease(int arg) { //将state从1改成0 if(getState()==0){ throw new IllegalMonitorStateException(); } setExclusiveOwnerThread(null); //将state从1改成0 setState(0); return true; }
-
// 返回一个Condition,每个condition都包含了一个condition队列 Condition newCondition() { return new ConditionObject(); }//此时静态内部类完成
-
实现lock与onLock:调用这个acquire方法
public void lock() { System.out.println(Thread.currentThread().getName()+" ready get lock"); sync.acquire(1); System.out.println(Thread.currentThread().getName()+" already got lock"); }
-
细节:在静态内部类中调用的是tryAcuire(),在外部类中调用的是acquire方法:因为AQS内部的模板方法acquire内调用了tryAcquire,后者就是我们需要去实现的(具体业务逻辑)
对比两个方法
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
-
-
-
-
-
AQS基本思想CLH队列锁
-
概述:
-
任意时刻只能有一个线程拿到锁,没有拿到锁的线程排成一个队列(打包成QNode)
-
CLH队列锁基本思想:
-
QNode:没有拿到锁的线程组成的队列
-
组成部分
- 当前线程
- myPred:指向前驱结点,线程形成链表(单向的链表形成队列)
- locked:当前是否需要锁
-
-
工作表现:当线程A想要获得锁
- 使用CAS将自己所在的QNode挂载CLH队列尾部
- 将所在QNode中的locked变量设置为true;
- 将所在QNode的myPred执行原来队列的尾节点
- 当线程B想要获得锁,还是这种操作
-
线程A怎么去拿到锁:对所在节点内部的myPred自旋(查看线程A前面那个QNode中的locked是否为false--->表示将锁释放掉了)
-
对线程B,就是去对A操作
-
但是不是这样的:
- AQS里面是双向链表
- 自旋超过一定次序,那么就让当前线程阻塞
-
公平锁非公平:
-
概述:
- 公平锁:所有的线程想要拿锁都要先插到CLH队列尾巴上面来
- 非公平锁:链表上面已经有很多在等了,但是来了一个线程,进来不排队,直接就拿锁
-
代码:在显示锁ReentrantLock(显示锁,独占锁要去实现tryAcquire)中实现了公平与非公平
-
示意图:两者都实现了tryAcquire,基本一样
-
区别:有没有去检查等待队列里面
//公平锁,会判断当前队列中有没有元素在等待 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) //非公平锁,直接就兴起CAS操作了 if (compareAndSetState(0, acquires))
-
-
-
锁的可重入
-
锁不可重入:一个线程只能对一把锁拿一次,第二次就拿不到(造成死锁了)
-
就像上面写的那个独占锁一样:第二次拿CAS失效
if(compareAndSetState(0,1)){ //这里是不支持锁的可重入的,一个线程只能拿对一把锁拿一次 //下面那个是排他的所有线程,就是说谁拿到这把锁,现在这把锁被我拿到了 setExclusiveOwnerThread(Thread.currentThread()); return true; }
-
解决锁的不可重入性:增加一个else判断这个线程的名字是不是已经拿到了的
public boolean tryAcquire(int acquires) { if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; }else if(getExclusiveOwnerThread()==Thread.currentThread()){ setState(getState()+1);//因为要释放锁,拿了几次;那么我在释放锁的时候,也要对state减1,直到等于0,就说明已经退出了最外围的同步代码块,那么就可以拿给其他线程用了 return true; } return false; }
-
-
测试方法:
-
递归:一个线程不断获取同一把锁
-
方法调用
sync A(){ B() } sync B(){ }
-
-
-
-
-
-
JMM:java内存模型-
操作与响应时间:IO极大影响了CPU计算
-
为了解决IO的问题,引入了高速缓存
-
概述:在内存与CPU之间引入高速缓存
-
离CPU越近,越贵,内存越小
-
Android虚拟机(这个是基于寄存器的)好像是两级缓存
-
多级缓存之间相互传递数据
-
示意图:
-
-
-
为了充分利用高速缓存,引入了JMM(工作内存与主内存)
-
工作内存与主内存:抽象概念,是许多存储设备的综合
-
工作内存:
- CPU寄存器
- 高速缓存
- 主内存(就是那个内存条)的一部分(百分之一)
-
-
工作表现:
- 当有一个变量count在主内存中,
- 当线程需要操作变量的时候,放到每一个线程的工作内存中有一个count副本,每个线程的工作内存是线程独享的,类似ThreadLocal
- 线程不能直接操作主内存的东西,也不能操纵其他线程工作内存中的东西,只能在自己的工作内存中搞;
-
-
JMM的问题:
-
场景:线程A与线程B,需要执行count = count+1;
-
但是需要编写许多的指令,进行数据的装入等
-
-
volatile详解:
-
概述:
- 在取值,赋值操作中可以保证并发安全(相当于在get/set上加了锁)
- 进行复核操作时,就不能保证了,例如i++
-
指令重排序与流水线:CPU是可以同时执行多条指令的
- CPU是可以同时执行多条指令的(流水线),指令并发会导致程序不一定按照预设(处理那些没有关联的,调整顺序后可以加快,在单线程中不用担心,但是在多线程中就会出问题)去走,这个时候CPU中有个缓冲,来处理指令执行循序不同
- 但是CPU里面还有条件预测,因特尔支持10级流水线(意思就是可以同时执行10条CPU指令),Android使用的ARM架构,一般是三级流水线(CPU执行3条语句)
-
volatile可以保持可见性(ThreadLocalMap)还可以抑制重排序,不准指令重排,但是不能保证原子性(还是要加锁)
-
可见性:JMM
- 强制写线程将变量放到主内存
- 强制读线程,每次都要去主内存中读一个
-
-
volatile应用场景:
-
一个线程写,多个线程读
-
写操作之间没有任何关联:
count = count+1;//不行 //这种就可以,写操作不关联 count = 5; count = 6;
-
在JDK并发工具包中常用volatile+CAS替换锁:在counrrentHashMap1.8中就是这个干的,1.7就不是这样的;无锁化编程(循环CAS操作+volatile)
-
-
volatile实现原理:
-
有volatile修饰的共享变量进行写操作的时候会使用CPU提供的Lock前缀(特殊的CPU指令)
-
线程对变量进行操作,都是在缓存中,这个Lock前缀将当前处理器缓存行的数据写会系统内存(从高速缓存写会主内存)
-
这个写回内存的操作会使其他CPU缓存的这个数据失效
- 因为在其他线程中也是缓存了这个变量的,让其失效后,就会从主内存从新拉一次,实际上每次都要去拿
- 线程是运行在CPU之上的,每个线程在执行任务的时候都会占据一个CPU
-
-