聊聊并发处理和java线程

为什么80%的码农都做不了架构师?>>>   hot3.png

 

并发处理

并发处理的引入为了加速,是为了“压榨”计算机的运算能力,例如CPU运算能力比其他存储、通讯子系统的速度快太多。

Amdahl(阿姆达尔定律):

S=1/(1-a+a/n)

S:固定负载情况下描述并行处理效果的加速比
a:为并行计算部分所占比例;
n:为并行处理结点个数;

  1. 当a=0时(即只有串行,没有并行),最小加速比s=1。
  2. 当1-a=0时,(即没有串行,只有并行)最大加速比s=n,当n→∞时,极限加速比s→ 1/(1-a),这也就是加速比的上限。
  3. 例如,若串行代码占整个代码的25%,则并行处理的总体性能不可能超过4
  4. 如果要拿这个算程序处理用几个线程,还要考虑线程切换本身的消耗。
  5. 曲线参考

加速不仅仅靠并发

加速的最终目标是为了让计算机帮我们处理跟多的“任务”,而每个“任务”,不是仅靠“运算”就能完成,至少要和内存交换数据。

  1. 引入读写速度接近处理器的高速缓冲
  2. 对输入代码进行乱序(out-of-order execution)执行但是保障执行结果不变,充分利用运算单远()

上面两个策略也带来2个问题

  1. 缓存一致性(处理高速缓冲和共享内存的数据一致性)
  2. 指令重排序(执行顺序跟代码顺序不一定一致)

java内存模型

主要目标是定义程序各个变量的访问规则,虚拟机中将变量存储到内存和从内存中取出的底层细节(不包括线程私有的,局部变量、方法参数)。

线程 工作内存 操作 主内存
java线程1<---->工作内存1 (sava/load) 主内存(共享)
java线程2<---->工作内存2 (sava/load) 主内存(共享)

这里讲主内存和工作内存和java堆、栈、方法区不是同一层一次的内存划分。
一定要关联主内存堆(物理:内存),工作内存虚拟机栈中(物理:提前放到寄存器和高速缓存)

内存交互操作

定义了8种操作:

  1. lock:作用于主内存变量,标记被一个线程独占,引发清空工作内存此变量值,用前重新load,assign。
  2. unlock:作用于主内存变量,把锁定的变量释放出来,unlock前必须执行store,write写回主内存。
  3. read:作用于主内存变量,主内存传输到线程工作内存,以便后面load操作。
  4. load:作用于工作内存变量,把read得到的变量值,放入工作内存的变量副本中。
  5. use:作用于工作内存变量,把变量值传给执行引擎。
  6. assign:作用于工作内存变量,从执行引擎收到的值赋值给工作内存变量。
  7. store:作用于工作内存变量,包工作内存的值传到主内存,以便随后的write操作。
  8. write:作用于主内存变量,包store拿到的值放入到主内存变量中。

聊聊并发处理和java线程_第1张图片

java虚拟只保障顺序的执行read和load(store和write),但是不保证连线执行。可能中间执行了其他指令。

但是对上面8种基本操作加了一些规则:

  1. 变量只能在主内存“诞生”
  2. 对变量“lock”,必须先清空此变量的工作内存值,在执行引擎执行的时候,重新load。
  3. 其他的就是,不能无缘无顾中间开始或结束,不多讲。

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操作可见。

  1. program order rule
  2. monitor lock rule
  3. volatile varible rule
  4. thread start rule
  5. thread termination rule
  6. thread interruption rule
  7. finalize rule
  8. transitivity

java线程

  • 内核:操作系统的最基础部分。
  • 内核线程(KLT:kernel-level thread):操作系统内核支持的线程,内核调度scheduler调度线程,将线程任务映射到处理器。
  • 轻量级线程(LWP:light weight process):操作内核线程的高级接口。
  • 用户线程(UT:user thread):线程都在用户态实现,内核不知道用户线程的存在。

124751_fEpN_811247.png

实现方式:

  1. 使用内核线程实现:使用了内核线程优势,但是切换成本高。
  2. 使用用户线程实现:没有内核线程支持,受限制
  3. 使用混合实现:UT---LWP,多对多

java线程调度

cooperative(协作,线程自己控制时间) 和preemptive(抢占,系统分配执行时间) Thread-scheduling

线程状态

  1. new
  2. runnable
  3. waiting
  4. timed waiting
  5. blocked
  6. terminated

线程安全

一个对象被多线程同时使用,依然可以得到正确的结果,那么这个对象是安全的。

  • 不可变:final、不可变类型:String、枚举类型、java.lang.Number的子类
  • 绝对线程安全与相对线程安全:如:vector:ArrayIdnexOutOfBoundsException
  • 线程兼容:可以正确的使用,打到线程安全
  • 线程对立:怎么用都是有风险的,比如thread.suspend和resume

线程安全实现方法

相关概念梳理:

  1. Blocking Synchronize:基于线程阻塞和唤醒实现。问题就是线程切换的成本问题。
  2. Non-Blocking Synchronize:乐观并发策略,先进行操作,再根据检查是否有其他线程竞争共享数据,如没有成功,有失败。

互斥同步

保证同一个时间,只有一个线程访问,互斥是方法,同步是目的,同步实现上一些特性如下:

  1. 可重入:同步块,同一线程可一重复进入,防止自己锁自己的死锁。
  2. 等待可中断:正在等待锁的线程可以选择放弃等待。

乐观并发策略

依赖硬件指令的原子性实现,比如常见指令(熟悉redis的会觉得眼熟):

  1. Test-and-Set
  2. Fecth-And-Increment
  3. Swap
  4. Compare-and-swap
  5. Load-Linked/Store-Conditional

当然也可以自己实现,比如循环重试,成功为止。

其他实现

  1. 避开共享数据和公用资源,自然线程安全。
  2. 线程本地存储,避开公共资源争用。

锁优化

JDK1.5到1.6一个重要优化是锁优化,synchronize和ReenTrantLock的性能差距就没有那么大了,主要看实现是的锁的监视对象力度粗细。

  1. 自旋锁:想让线程“等待”时,不放弃CPU处理,以自己“忙循环”,等下看下锁是不是释放了,对锁释放很快的场景,可以避开线程切换开销,循环策略优化引入自适应自旋锁,尝试一定次数后放弃,进入线程等待。
  2. 锁消除:代码上要求同步,但是不可能存在共享数据竞争,可以在即时编译是消除锁。比如局部变量,stringBuffer.append().
public synchronized StringBuffer append(StringBuffer sb) {
        toStringCache = null;
        super.append(sb);
        return this;
    }
  1. 锁粗化:连续对同一个对象加锁解锁(比如循环体内),可以把锁放到外面,通过方法内联分析(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;
}

  1. 轻量级锁
    “轻量”相对于操作系统互斥量来实现的锁而言,通过对象头部分存储锁标志(Mark Word),标识对象是不是被锁定,相当于在使用重量级锁之前,先对轻量级锁标志进行CAS操作解决一部分同步问题。
    轻量级锁能提高性能的依据是经验告诉我们“绝大部分的锁,在整个生命周期內都不存在竞争”。
  1. 偏向锁 “偏向”锁偏向于第一个获得它的线程,基于轻量级锁实现,如果竞争对象标水,线程获得过锁,后面该线程在进入就同步块,CAS操作也省了。当另外一个线程来竞争时,偏向结束。

最后补一个概念

公平锁:是指竞争锁时锁分配的公平性,谁先来申请谁优先给谁,这种算“公平”,synchronized非公平,ReentrantLock默认也非公平。

转载于:https://my.oschina.net/greki/blog/1592558

你可能感兴趣的:(聊聊并发处理和java线程)