《java多线程编程实战指南》——第三章笔记

JAVA线程同步机制

线程同步机制是一套用于协调线程间的数据访问(Data access)及活动(Activity)的机制,该机制用于保障线程安全以及实现这些线程的共同目标。

1、Java提供的线程同步机制包括锁、volatile关键字、final关键字、static关键字以及一些相关的API。

2、锁

        java平台中的锁包括内部锁(Intrinsic Lock)和显示锁(Explicit Lock)。内部锁通过synchronized关键字实现的;显示锁通过java.concurrent.locks.Lock接口的实现类实现的。

    锁的持有线程在其获得锁之后和释放锁之前这段时间内锁执行的代码被称为临界区

2.1、锁有排他性,一次只能被一个线程持有;

2.2、锁可以保证原子性、可见性、有序性。

    两个条件:

        a、这些线程在访问同一组数据必须使用同一个锁

        b、任意线程读写都需要持有相应的锁

2.3、可重入性ReentrantLock

/**

A调用B,B也用lock,但是A正持有lock,但是可重入性保证B申请lock成功

*/

void methodA() {

  acquireLock(lock); // 申请锁

  methodB();

  releaseLock(lock);// 释放锁

}

void methodB() {

  acquireLock(lock);

  releaseLock(lock);

}

2.4、锁的争用与调度

内部锁属于非公平锁,显示锁既支持公平锁也支持非公平锁。

2.5、锁的粒度(Granularity)

    定义:一个锁实例所保护的共享数据的数量大小。数量大的叫粒度粗,数量小叫粒度细。

2.6、锁开销及其可能导致的问题

2.6.1、锁的开销

包括所的申请和释放所产生的开销,以及所可能导致上下文切换的开销。主要体现在处理器时间。

2.6.2、导致的线程活性故障

a、锁泄露

    指一个线程获得锁之后一直无法释放锁而导致其他线程无法获得该锁。

b、锁的不正确使用可能导致死锁、锁死等活性故障。

3、内部锁:synchronized

3.1、内部锁通过synchronized关键字实现,不会导致锁泄露。

3.2、synchronized关键字修饰的代码块称为同步块。synchronized关键字所引导的代码块就是临界区

    synchronized(锁句柄){

    //在此代码块访问共享数据

    }

3.3、锁句柄变量通常使用private final修饰

因为锁句柄变量的值一旦改变,会导致执行同一个同步块的多个线程实际上使用不同的锁,从而导致竞态。

4、显式锁:Lock接口

4.1、排他锁,作用和内部锁相同

Lock接口定义的方法:void lock():获取锁;void lockInterruptibly():如果当前线程未被中断,则获取锁;Condition newCondition():返回绑定到此Lock实例的新Condition实例;boolean tryLock():仅在调用时锁为空闲状态才获取该锁;boolean tryLock(long time,TimeUnit unit):如果锁在给定的等待时间内空闲,并且当前线程未中断,则获取锁;void unLock():释放锁。

4.2、是java.util.concurrent.locks.Lock接口的实例。ReentrantLock是Lock接口发默认实现类。

4.3、公平锁保障锁调度的公平性往往是以增加上下文切换为代价的,因此显式锁默认非公平调度策略

4.4、读写锁:允许多个线程同时读取(只读)共享变量,但是一次只允许一个线程更新(读取后再更新)

    满足以下两个条件选择读写锁:

        4.4.1、只读操作比写(更新)操作要频繁得多

        4.4.2、读线程持有锁的时间比较长

4.5、ReentrantReadWriteLock可重入读写锁,支持锁的降级,即一个线程持有读写锁的写锁的情况下可以继续获得相应的读锁。

5、显示锁和内部锁应用区别与选用

区别:

5.1、内部锁简单易用,且不会导致锁泄露;显示锁容易被错用而导致锁泄露(缺少释放锁的动作)。

5.2、内部锁是基于代码块的锁,灵活性较差,要么使用,用么不使用;而显示锁是基于对象的锁,灵活性强,比如可以在一个方法内申请锁,在另一个方法释放锁,而内部锁是无法做到的。

5.3、调度方面,内部是只支持非公平调度;显示锁两者都支持。

5.4、如果内部锁的持有线程一直不释放该锁(通常代码错误导致),同步在该锁的所以线程都会被暂停而使任务无法进展。显示锁可以避免此问题,使用显示锁的tryLock() 方法,锁处于空闲状态返回true,否则返回false。

选用:

一般来说,新开发的代码中我们可以选用显示锁。但是选用显示锁注意:显示锁的不正确使用会导致锁泄露问题;线程转储可能无法包含显示锁的相关信息,从而导致问题定位的困难。

另外,保守策略——默认情况选用内部锁,仅在需要显示锁所提供的特性的时候才选用显示锁。

6、读写锁

是一种改进型的排它锁,也被称为共享/排他锁。读写锁允许多个线程同时读取共享变量,当一次只能允许一个线程对共享变量进行更新。

任何线程读取共享变量的时候其他线程无法对该共享变量进行更新,一个线程更新共享变量的时候其他线程都无法访问该变量。

读写锁是java.util.concurrent.locks.ReadWriteLock 接口的实例,默认实现类是java.util.concurrent.locks.ReentrantReadWriteLock。

ReadWriteLock接口定义了两个方法:readLock() 和 writeLock(),两个方法返回值都是lock类型

读写锁的两种角色

读锁:读线程访问共享变量时必须持有相应的读锁。且读锁可以被多个线程持有。

写锁:写锁是排他锁,一个线程持有写锁,其他成线程无法获得相应的写锁或读锁。

多个读线程提高了并发性,而写锁保障了写线程能够独占的方式安全的更新共享变量。

读写锁的使用

publicclassReadWriteLockUsage{

privatefinalReadWriteLock rwLock =newReentrantReadWriteLock();

privatefinalLock readLock = rwLock.readLock();

privatefinalLock writeLock = rwLock.writeLock();

//读线程执行

publicvoidreader(){

readLock.lock();//申请读锁

try{

//读取共享变量

}finally{

//释放锁避免泄露

            readLock.unlock();

        }

    }

//写线程执行

publicvoidwriter(){

writeLock.lock();//申请写锁

try{

//访问共享变量

}finally{

//释放锁避免泄露

            writeLock.unlock();

        }

    }

}

读写锁应用场景

读操作比写操作频繁得多;读线程持有锁的时间比较长

ReetrantReadWriteLock 说实现的读写锁是可重入锁;且支持锁的降级,即一个线程持有写锁的情况下可以获得相应的写锁。

锁的适用场景

check-then-act操作:一个线程读取共享数据,并在此基础上决定下一步操作是什么。

read-modify-write操作:一个线程读取共享数据并在此基础上更新该数据。

多个线程对多个共享数据更新:如果这些共享数据之间存在关联关系,那么为力保证操作的原子性,可以考虑使用锁。

7、线程同步机制的底层助手:内存屏障

7.1、Java虚拟机底层借助内存屏障Memory Barrier实现两个动作:刷新处理器缓存和冲刷处理器缓存,保证可见性。

按照可见性保障划分,内存屏障分为:加载屏障(Load Barrier)和存储屏障(Store Barrier)。加载屏障刷新处理器缓存,存储屏障冲刷处理器缓存。

按照有序性保障划分,内存屏障分为:获取屏障(Acquire Barrier)和释放屏障(Release Barrier)。

锁与重排序

7.2、临界区外(临界区前、后)的操作可以被重排到临界区内

在编译(JIT动态编译)的时候,编译器可能将语句移到临界区内,然后在临界区开始前和结束后相应地插入获取屏障和释放屏障;而处理器不会再将这些被重排的语句重排到临界区外

8、轻量级同步:volatile

1、volatile的作用:保障可见性、有序性和保障long/double型变量读写操作的原子性

2、volatile仅保障对被修饰的变量的读写操作的原子性,如果要保障对volatile变量的赋值操作原子性,那这个赋值操作不能涉及任何共享变量(包括被赋值的volatile变量本身)的访问

3、如果修饰的是数组变量,volatile只能对数组引用本身的操作(读取数组引用和更新数组引用)起作用,无法对数组元素的操作(读取、更新数组元素)起作用

4、四个应用场景:

使用volatile变量作为状态标志

使用volatile保障可见性

使用volatile变量替代锁

实现简易版读写锁

5、volatile变量开销:不会导致上下文切换,开销介于普通变量和锁之间。

9、CAS

1、Compare and Swap:一种处理器指令,能将read-modify-write和check-and-act之类操作转换为原子操作

2、CAS仅保障共享变量更新操作的原子性,不保障可见性,不会导致上下午切换,ABA问题及规避

3、原子变量类

    是基于CAS实现的能够保障对共享变量进行read-modify-write更新操作的原子性和可见性的一组工具类。

    常见的原子变量类:

        基础数据型:AtomicInteger、AtomicLong、AtomicBoolean

        数组型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

        字段更新器:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

         引用型:AtomicReference、AtomicStampedReference、AtomicMarkableReference

10、static关键字

保障线程初次读取的静态变量时的可见性

保障静态变量被发布前是初始化完毕的

11、final关键字

保障被修饰的变量(及其引用的对象)在其被发布前是初始化完毕的

不能保证包含final字段的对象本身的可见性

不会导致上下文切换

10、对象发布

1、多个线程共享变量的途径

2、对象发布:使对象能够被其作用域之外的线程访问

3、发布的形式:

    a、将对象引用存储到public变量中;

    b、在非private方法中返回一个对象;

    c、创建内部类,使得当前对象(this)能够被这个内部类使用;

            c1、避免在构造器中奖this赋值给一个共享变量

            c2、避免在构造器中奖this作为方法参数传递给其他方法

            c3、避免在构造器中启动基于匿名类的线程

    d、通过方法调用将对象传递给外部方法。

你可能感兴趣的:(《java多线程编程实战指南》——第三章笔记)