并发编程

– 阅读《并发编程的艺术》所写

并发的挑战

  • 上下文切换带来的时间消耗

    策略:

    • 使用无锁并发编程。如数据分段,不同线程处理不同分段
    • CAS算法。
    • 避免创建不必要的线程。如降低JBOSS的maxThreads
    • 使用协程。
  • 死锁

    • 避免一个线程获取多个锁
    • 尝试使用定时锁

并发机制的底层实现

volatile

volatile是轻量级的synchronized,用于保证共享变量的可见性(即一个线程修改变量时,另一个能够读取到修改后的值)。

底层原则:

  • 使用Lock前缀指令来引起处理器缓存回写到内存中。
  • 一个处理器的缓存回写到内存中会导致其它处理器的缓存无效。

synchronized

Java中每一个对象都可以作为锁。

  • 对于普通同步方法,锁时当前实例对象。
  • 对于静态同步方法,锁时当前类的Class对象。
  • 对于同步方法块,锁时Synchronized里面配置的对象。

JDK1.6为了减少获取和释放锁的性能消耗,引入了偏向锁轻量级锁

所以一共有4种锁状态,由低到高:

  • 无锁状态
  • 偏向锁状态
    • 偏向锁原理:研究发现大多数情况下只有同一线程多次获取锁的情况,为了降低获取锁的代价引入了偏向锁。其原理是,当一个线程访问同步代码块并获取锁时,会在Java对象头和栈帧的锁记录里面存放当前锁偏向的线程ID,下次再次访问时,不要进行CAS操作,只需要简单测试对象头是否存储了指向该线程的偏向锁即可。如果成功,表示已经获取了锁。如果失败,看偏向锁的标志位是否为1(表示当前是偏向锁),如果没有则用CAS竞争锁,如果设置了则尝试使用CAS将对象头的偏向锁指向当前线程。
    • 偏向锁的撤销:由于偏向锁不适用于多个线程对锁的竞争,故当其他线程尝试竞争偏向锁时,持有偏向锁的线程会释放锁。
    • 关闭偏向锁:JDK1.6以上默认开启偏向锁,如果认为锁通常处于竞争状态可以关闭偏向锁。
    • 个人理解:偏向锁用于只有一个线程访问同步代码块的场景。
  • 轻量级锁状态。
    • 加锁过程:在线程执行同步代码块之前,JVM会先在当前线程的栈帧中创建存储锁记录的空间,然后复制对象头(竞争的锁对象)的Mark Word到其中。然后尝试用CAS将对象头的Mark Word替换为指向锁记录的指针,成功则获取锁,失败则表示其它线程在竞争,尝试自旋获取锁
    • 解锁过程:使用CAS将原理复制的记录替换回Mark Word,如果成功表示没有竞争,失败则表示存在竞争,锁会膨胀为重量级锁。
    • 个人理解:线程在竞争锁时,先copy一份对象头的记录到栈空间作为备份,然后用CAS取修改对象头记录,使其指向自己,之所以该过程为轻量级锁,原因在于参与竞争的线程不会直接阻塞,而是可以使用自旋来尝试获取锁(缺点是会消耗CPU资源)。适用于同步代码块执行速度很快的场景。
  • 重量级锁
    • 适用于同步块执行时间较长的场景。

锁可以升级,不能降级,防止无用的自旋(如线程阻塞)

CAS

比较并交换,CAS操作需要两个值,一个旧值和一个新值,在操作期间先比较旧值有没有发生变化,如果没有发生变化才换成新值。

原子操作

从jdk1.5开始,并发包提供了原子操作类:AtomicBoolean、AtomicInteger、AtomicLong。

CAS实现原子操作面临的三大问题:

  1. ABA问题:A变B再变A,CAS认为A没有发生变化,实际上却变化了。解决办法是在变量更新时添加版本号:如JDK1.5添加的AtomicStampedReference类(reference代表引用)的compareAndSet方法在检查值引用的同时会检查当前标志是否等于预期标志

    1
    2
    3
    4
    5
    6
    public boolean compareAndSet{
    V exceptedReference, //预期引用
    V newReference, //更新后的引用
    int exceptedStamp, //预期标志
    int newStamp //更新后的标准
    }
  2. CAS自旋带来的CPU执行消耗。

  3. 只能保证一个共享变量的原子操作。解决办法是多个变量合并为一个或者放在一起,如JDK1.5提供的AtomicReference类保证引用对象之间的原子性,即把多个变量放在一个对象中进行CAS操作。

Java内存模型