Java高效并发

来源:互联网 发布:比特彗星端口设置 编辑:程序博客网 时间:2024/06/04 17:46

Java高效并发

1.并发和并行的区别

简而言之,并发就是一段时间间隔内发生的多个任务,不一定同时执行,可能是交替执行的。并行是在同一时间点执行的任务。网站最大连接数指的是并发,如1000个socket,但是只有4个CPU,那就交替执行,分时处理。并行指的是同时连接的数目。

并发:让计算机并发的处理更多的任务,更充分利用计算机处理器的效能。
但是现代计算机中,处理器包括:计算能力,读取内存能力,I/O能力等等。而这些能力的速度是不对等的,因此计算机引入了 高速缓存(Cache),将一部分常用数据放在缓存中,从而提高一定的性能。
缓存的存在,就带来了”缓存一致性”问题。
缓存一致性:多处理器中每个处理器都要自己的缓存,又共享主内存,各自计算过程中,可能会导致彼此缓存的数据不一样,那么如何保证一致性缓存呢?

此外,为了使处理器内部的运算单元能充分被利用,处理器还对输入代码进行乱序执行优化,保证执行结果一样,但不保证执行顺序一样。

总而言之,为了提高计算机并发能力, 两个措施,高速缓存和指令重排序。

2.Java内存模型

JMM(Java Memory Model)
简而言之就是:主内存+工作内存+这两种内存间的交互+三大特性
主内存(线程共享)+工作内存(线程私有)
工作内存和主内存交互,Java提供了8大指令
lock, unlock, read, load, use, assign, store, write

  1. lock:作用于主内存,将一个变量变为线程独占
  2. unlock:作用于主内存,将一个变量解锁,其他线程可对该变量进行锁定
  3. read:作用于主内存,将一个变量从主内存传到工作内存中,供load使用
  4. load:作用于工作内存,将read读后的值放在工作内存变量副本中
  5. use:作用于工作内存,将工作内存的值传递给执行引擎
  6. assign:作用于工作内存,将执行引擎的值赋给工作内存的变量
  7. store:作用于工作内存,将工作内存值传递到主内存中去,共write使用
  8. write:作用于主内存,将store得到的变量值放在主内存的变量中去

总结:从主内存到工作内存,顺序执行read, load操作,不一定连续执行,只是顺序执行。同理从工作内存到主内存,顺序执行store, write操作。

这8大指令的操作,必须满足如下8大规则:

  1. (read,load) (store,write)必须成对出现,不允许单独出现
  2. 发生了assign操作,必须要写会主内存(时间待定)
  3. 未发生assign操作,不运行写会主内存
  4. 工作内存不会产生新变量,新变量只能在主内存中产生,use之前load必须执行,store之前,assign必须执行
  5. 同一时刻一个变量只能被一个线程lock一次
  6. 执行lock操作,工作内存值被清空,必须重新初始化该变量(load,assign)可见性保证
  7. unlock必须在lock后执行
  8. unlock一个变量前,必须将工作内存写入到主内存(store, write操作) 可见性的保证

以上是对于普通变量的8大规则,对于volatile变量,还有如下三个规则:
volatile额外三大规则

  1. 一个线程中,load, use必须连续出现,(用use指令,前面必须有load指令;用load指令,下一个必须是use指令)
    即保证在工作内存中,每次使用volatile变量,必须从主内存刷新最新的值,也就保证其他线程对该变量执行的操作是可见的。
  2. 一个线程中,assign, store必须连续出现,(用store指令,前面必须有assign指令;用assign指令,下一个必须是store指令)即保证在工作内存中,每次对volatile变量作出的修改,都能够同步到主内存中去,保证对其他线程的可见性。
  3. 一个线程中,设A是对变量X进行(use/assign),F和A相关(load/store),P和F相关(read/write)的动作,类似B是对变量Y进行(use/assign),M和B相关(load/store),N和B相关(read/write)的动作,如果A优先于B,那么P优先于M。(即保证了volatile变量的指令不会被重排序优化)

JMM三大特性:

  1. 原子性:变量操作原子性指令read, load, assign, use, store, write。为了保证更大范围的原子性,提供了lock/unlock指令,虚拟机未直接提供lock/unlock,但提供了这两个指令的更高层次实现,monitorenter/monitorexit,即对应synchronized关键字
  2. 可见性:当一个线程修改一个变量值,另外的线程能够立刻得知这个修改。如何实现?变量修改后将新值同步到主内存,变量读取前从主内存刷新该变量的值。这条规则对于普通变量和volatile变量都适用,但volatile关键字保证了能够立即同步到主内存,立即从主内存刷新。因此volatile保证了多线程的变量可见性。同样synchronized和final关键字也能够实现可见性。synchronized保证unlock之前,变量同步到主内存,见规则8;final保证一旦变量初始化完成,并且this指针未发生引用逃逸,该变量值对其他线程是可见的。
  3. 有序性:一句话描述:本线程内观察,操作都有序;另外线程观察该线程,所有操作无序。即表现为线程内串行执行,线程外,指令重排序,工作内存和主内存同步延迟。volatile和synchronized关键字保证有序性,volatile自带有序性语义,见volatile规则3,synchronized保证一个时刻只有一个线程进入,因此两个同步块就能够串行执行了。

    到这里可以稍微总结一下:
    final保证可见性,不保证原子性和有序性
    volatile保证可见性和有序性,不保证原子性
    synchronized保证原子性,可见性和有序性

先行并发原则(happens-before)
如果java代码所有的有序性,都靠volatile和synchronized完成,那么就太繁琐了。幸亏Java提供了先行并发原则,它是判断数据是否存在竞争,线程是否安全的主要依据。依靠这个原则,我们可以通过几个规则来一揽子解决并发环境下两个操作是否可能存在冲突的所有问题。
Java天然的8大先行发生关系,即无需任何同步就实现了。

  1. 程序次序规则:同一线程内,书写在前面的操作先行于书写在后面的操作,注意是控制流顺序(考虑分支,循环)
  2. 管程锁定原则:unlock在后面一个同一个锁的lock之前发生,时间顺序
  3. volatile变量规则:对一个volatile的写操作先行发生于后面对该变量的读操作
  4. 线程启动规则:线程的start优先于该线程所有其他操作
  5. 线程终止规则:线程的所有操作优先于对此线程的终止检测
  6. 线程终结规则:线程的interrupt方法先行于被中断线程的代码检测到中断事件的发生
  7. 对象终结规则:对象初始化先行于finalize方法
  8. 传递性:A先行于B,B先行于C,那么A先行于C

一个操作“时间上先发生”不代表这个操作“先行发生”。同理“先行发生”无法推导出“时间上先发生”
例子:线程A比B先启动,但B可能先行发生相应操作。

3.Java与线程

线程实现,三种方式

  1. 内核级线程,一一对应
  2. 用户级线程,一对多
  3. 用户级线程加轻量级线程混合实现,多对多

Java如何实现的呢?根据操作系统决定,操作系统支持什么样的线程模型,java虚拟机就支持什么样线程模型。
线程调度
协同式线程调度,抢占式线程调度
java采用的是抢占式线程调度,每个线程都有相应的优先级。
但优先级不太靠谱:不同平台支持的优先级数目不同,java提供了10个,而windows只支持7个,这样一些java不同的优先级会在windows中变得相同。此外,windows中存在优先级推进器,即使该线程优先级不高,但是它表现的非常非常的努力,可能就会跨越优先级去为它分配时间。
线程状态
java定义了5种线程状态

  1. 新建(New):创建后尚未启动
  2. 运行(Runable):包括running和ready两个状态
  3. 等待(Waiting):线程等待一段时间或者唤醒动作的发生。
    无限期等待:不会被系统分配CPU时间,需要被其他线程显著唤醒,wait(),join(),LockSupport.park()
    限期等待:不会被系统分配CPU时间,无需等待其他线程唤醒,隔一段时间会被系统自动唤醒。 sleep(), wait(time), join(time), LockSupport.parkNanos(), LockSupport.parkUntil()
  4. 阻塞(Blocked),阻塞状态在等待一个排他锁
  5. 结束(Terminated):线程被终止

4.线程安全

线程安全定义
一般而言:一个对象可以安全地被多个线程同时使用,那它就是线程安全的。
严谨定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象是线程安全的。
即线程安全的代码具备一个特性:代码本身封装了所有必要的正确性保障手段(如互斥同步),调用者无需关心多线程问题,更无须自己采取任何措施来保证多线程的正确调用。
弱点定义:将调用这个对象行为限定为单次调用这个对象行为,就可以认为是线程安全的了。
java中的线程安全
java中的线程安全按照安全程度由强到弱排序,可分为5类:
不可变,绝对线程安全,相对线程安全,线程兼容和线程对立

  1. 不可变:java中不可变对象一定是线程安全的,无论是对象方法的实现者还是调用者,都不需要考虑线程安全问题。final关键字修饰的对象,只要该对象被正确的构造处理,未发生this引用逃逸,那么其外部的可见性状态永远地不会改变。例子:String属性(private final char value[]) Integer (private final int value)
  2. 绝对线程安全:Java API标注自己是线程安全的类,大多不是绝对的线程安全。例如Vector,尽管它的add, get, size方法都是synchronized修饰的,确实保证了安全。 但是不意味着调用它的时候永远不需要保证同步手段。
  3. 相对线程安全:这就是我们通常意义上所讲的线程安全,保证这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施。但对于一些特定顺序的连续调用,就可能在调用端使用额外的同步手段保证调用的正确性。如Vector, HashTable, Collections中的synchronizedCollection方法
  4. 线程兼容:对象本身不安全,但是可以通过调用端正确地使用同步手段保证对象在并发环境中可以安全地使用,我们一般说一个类不是线性安全的就是指的这种情况,如ArrayList和HashMap
  5. 线程对立:无论调用端是否采取了同步措施,都无法在多线程环境中使用的代码。Java既然天生支持多线程,那么这种代码是有害的,尽量避免。如线程的suspend和resume方法,一个尝试中断线程,一个尝试恢复线程,如果恢复的线程被中断了,那么肯定产生死锁了。目前JDK已经废除了这两种方法。还有System.setIn(), System.setOut()和System.runFinalizersOnExit()等。

线程安全实现方法
1.互斥同步
2.非阻塞同步
3.无同步方案

优化锁
1.自旋锁与自适应自旋
2.锁消除
3.锁粗化
4.轻量级锁
5.偏向锁

参见《深入理解Java虚拟机》

0 0
原创粉丝点击