为什么80%的码农都做不了架构师?>>>
并发处理
并发处理的引入为了加速,是为了“压榨”计算机的运算能力,例如CPU运算能力比其他存储、通讯子系统的速度快太多。
Amdahl(阿姆达尔定律):
S=1/(1-a+a/n)
S:固定负载情况下描述并行处理效果的加速比
a:为并行计算部分所占比例;
n:为并行处理结点个数;
- 当a=0时(即只有串行,没有并行),最小加速比s=1。
- 当1-a=0时,(即没有串行,只有并行)最大加速比s=n,当n→∞时,极限加速比s→ 1/(1-a),这也就是加速比的上限。
- 例如,若串行代码占整个代码的25%,则并行处理的总体性能不可能超过4
- 如果要拿这个算程序处理用几个线程,还要考虑线程切换本身的消耗。
- 曲线参考
加速不仅仅靠并发
加速的最终目标是为了让计算机帮我们处理跟多的“任务”,而每个“任务”,不是仅靠“运算”就能完成,至少要和内存交换数据。
- 引入读写速度接近处理器的高速缓冲
- 对输入代码进行乱序(out-of-order execution)执行但是保障执行结果不变,充分利用运算单远()
上面两个策略也带来2个问题
- 缓存一致性(处理高速缓冲和共享内存的数据一致性)
- 指令重排序(执行顺序跟代码顺序不一定一致)
java内存模型
主要目标是定义程序各个变量的访问规则,虚拟机中将变量存储到内存和从内存中取出的底层细节(不包括线程私有的,局部变量、方法参数)。
线程 工作内存 | 操作 | 主内存 |
---|---|---|
java线程1<---->工作内存1 | (sava/load) | 主内存(共享) |
java线程2<---->工作内存2 | (sava/load) | 主内存(共享) |
这里讲主内存和工作内存和java堆、栈、方法区不是同一层一次的内存划分。
一定要关联主内存堆(物理:内存),工作内存虚拟机栈中(物理:提前放到寄存器和高速缓存)
内存交互操作
定义了8种操作:
- lock:作用于主内存变量,标记被一个线程独占,引发清空工作内存此变量值,用前重新load,assign。
- unlock:作用于主内存变量,把锁定的变量释放出来,unlock前必须执行store,write写回主内存。
- read:作用于主内存变量,主内存传输到线程工作内存,以便后面load操作。
- load:作用于工作内存变量,把read得到的变量值,放入工作内存的变量副本中。
- use:作用于工作内存变量,把变量值传给执行引擎。
- assign:作用于工作内存变量,从执行引擎收到的值赋值给工作内存变量。
- store:作用于工作内存变量,包工作内存的值传到主内存,以便随后的write操作。
- write:作用于主内存变量,包store拿到的值放入到主内存变量中。
java虚拟只保障顺序的执行read和load(store和write),但是不保证连线执行。可能中间执行了其他指令。
但是对上面8种基本操作加了一些规则:
- 变量只能在主内存“诞生”
- 对变量“lock”,必须先清空此变量的工作内存值,在执行引擎执行的时候,重新load。
- 其他的就是,不能无缘无顾中间开始或结束,不多讲。
volatile变量访问规则
“可见性”:一个线程改变volatiel变量值,另外一个线程使用时可以立即可见,由于每次使用前先刷新,所以对执行引擎来说看起来是一致的。但是运算不一定是原子性的,所以运算在并发下不一定安全(真实是存在不一致的)。
“禁止指令重排序”:添加内存屏障,内存屏障之后对指令不能放到内存屏障之前执行。
volatile bool initailized=false;
// initialize xxxx
initailized=true;//会添加内存屏障,保证判断变量时之前(initialize)代码全部执行完。
if(initailized){
//do something
}
volatile使用场景:
- 运算结果不依赖变量当前值(如:i++),或者保证只有一个线程改变变量值。
- 变量不需要与其他的状态变量共同参与不变约束。
long 和dubbo变量
“long/dubbo非原则性协定”:内存模型要求“lock...write"这8个操作原子性,但是对于没有被volatile修饰的64位的数据类型划分成2次操作,不保证原子性。(一般商用虚拟机还是保证原子性的)
先行发生原则(happens-before)
两项操作直接的偏序关系,A操作先于B操作,就代表A操作产生的影响,B操作可见。
- program order rule
- monitor lock rule
- volatile varible rule
- thread start rule
- thread termination rule
- thread interruption rule
- finalize rule
- transitivity
java线程
- 内核:操作系统的最基础部分。
- 内核线程(KLT:kernel-level thread):操作系统内核支持的线程,内核调度scheduler调度线程,将线程任务映射到处理器。
- 轻量级线程(LWP:light weight process):操作内核线程的高级接口。
- 用户线程(UT:user thread):线程都在用户态实现,内核不知道用户线程的存在。
实现方式:
- 使用内核线程实现:使用了内核线程优势,但是切换成本高。
- 使用用户线程实现:没有内核线程支持,受限制
- 使用混合实现:UT---LWP,多对多
java线程调度
cooperative(协作,线程自己控制时间) 和preemptive(抢占,系统分配执行时间) Thread-scheduling
线程状态
- new
- runnable
- waiting
- timed waiting
- blocked
- terminated
线程安全
一个对象被多线程同时使用,依然可以得到正确的结果,那么这个对象是安全的。
- 不可变:final、不可变类型:String、枚举类型、java.lang.Number的子类
- 绝对线程安全与相对线程安全:如:vector:ArrayIdnexOutOfBoundsException
- 线程兼容:可以正确的使用,打到线程安全
- 线程对立:怎么用都是有风险的,比如thread.suspend和resume
线程安全实现方法
相关概念梳理:
- Blocking Synchronize:基于线程阻塞和唤醒实现。问题就是线程切换的成本问题。
- Non-Blocking Synchronize:乐观并发策略,先进行操作,再根据检查是否有其他线程竞争共享数据,如没有成功,有失败。
互斥同步
保证同一个时间,只有一个线程访问,互斥是方法,同步是目的,同步实现上一些特性如下:
- 可重入:同步块,同一线程可一重复进入,防止自己锁自己的死锁。
- 等待可中断:正在等待锁的线程可以选择放弃等待。
乐观并发策略
依赖硬件指令的原子性实现,比如常见指令(熟悉redis的会觉得眼熟):
- Test-and-Set
- Fecth-And-Increment
- Swap
- Compare-and-swap
- Load-Linked/Store-Conditional
当然也可以自己实现,比如循环重试,成功为止。
其他实现
- 避开共享数据和公用资源,自然线程安全。
- 线程本地存储,避开公共资源争用。
锁优化
JDK1.5到1.6一个重要优化是锁优化,synchronize和ReenTrantLock的性能差距就没有那么大了,主要看实现是的锁的监视对象力度粗细。
- 自旋锁:想让线程“等待”时,不放弃CPU处理,以自己“忙循环”,等下看下锁是不是释放了,对锁释放很快的场景,可以避开线程切换开销,循环策略优化引入自适应自旋锁,尝试一定次数后放弃,进入线程等待。
- 锁消除:代码上要求同步,但是不可能存在共享数据竞争,可以在即时编译是消除锁。比如局部变量,stringBuffer.append().
public synchronized StringBuffer append(StringBuffer sb) {
toStringCache = null;
super.append(sb);
return this;
}
- 锁粗化:连续对同一个对象加锁解锁(比如循环体内),可以把锁放到外面,通过方法内联分析(inlining)。
public int arrayCount() {
int result = 0;
for (int i = 0; i < intArr.length; i++) {
result += getEntry(i);
}
return result; }
public synchronized int getEntry(int entry){
return intArr[entry];
}
优化后:
public int arrayCount() { int result = 0;
synchronized {
for (int i = 0; i < intArr.length; i++) {
result += intArr[entry];
}
}
return result;
}
- 轻量级锁
“轻量”相对于操作系统互斥量来实现的锁而言,通过对象头部分存储锁标志(Mark Word),标识对象是不是被锁定,相当于在使用重量级锁之前,先对轻量级锁标志进行CAS操作解决一部分同步问题。
轻量级锁能提高性能的依据是经验告诉我们“绝大部分的锁,在整个生命周期內都不存在竞争”。
- 偏向锁 “偏向”锁偏向于第一个获得它的线程,基于轻量级锁实现,如果竞争对象标水,线程获得过锁,后面该线程在进入就同步块,CAS操作也省了。当另外一个线程来竞争时,偏向结束。
最后补一个概念
公平锁:是指竞争锁时锁分配的公平性,谁先来申请谁优先给谁,这种算“公平”,synchronized非公平,ReentrantLock默认也非公平。