精华内容
下载资源
问答
  • Java 线程同步机制

    2019-07-15 09:33:34
    本章介绍了 Java 平台提供的各种线程同步机制。   Java 线程同步机制的幕后助手是内存屏障。不同同步机制的功能强弱不同,相应的开销以及可能导致的问题也不同,如下表所示。因此,我们需要根据实际情况选择一个...

    本文摘抄自《Java 多线程编程实战指南》核心篇 第三章小结

    个人博客:DoubleFJ の Blog

    本章介绍了 Java 平台提供的各种线程同步机制。


      Java 线程同步机制的幕后助手是内存屏障。不同同步机制的功能强弱不同,相应的开销以及可能导致的问题也不同,如下表所示。因此,我们需要根据实际情况选择一个功能适用且开销较小的同步机制。

    Java 线程同步机制的功能与开销/问题

    volatile CAS final static
    原子性保障 具备 具备② 具备 不涉及 不涉及
    可见性保障 具备 具备 不具备 不具备 具备③
    有序性保障 具备 具备 不涉及 具备 具备④
    上下文切换? 可能① 不会 不会 不会 可能⑤
    备注 ①被争用的锁可能导致上下文切换 ②仅能够保障对 volatile 变量读/写操作本身的原子性 ③④仅在一个线程初次读取一个类的静态变量时起作用
    ⑤静态变量所属类的初始化可能导致上下文切换

      锁是 Java 平台中功能最强大的一种线程同步机制,同时其开销也最大,可能导致的问题也最多。被争用的锁会导致上下文切换,锁还可能导致死锁/锁死等线程活性故障。锁适用于存在多个线程对多个共享数据进行更新/check-then-act 操作或者 read-modify-write 操作这样的场景。

      锁的排他性以及 Java 虚拟机在临界区前后插入的内存屏障使得临界区中的操作具有原子性。由此,锁还保障了写线程在临界区中执行操作在读线程看来是有序的,即保障了有序性。Java 虚拟机在 MonitorExit 对应的机器码后插入的内存屏障则保障了可见性。锁能够保障线程安全的前提是访问同一组共享数据的多个线程必须同步在同一个锁之上,否则原子性/可见性和有序性均无法得以保障。在满足貌似串行语义的前提下,临界区内以及临界区外的操作可以在各自范围内重排序。临界区外的操作可能会被 JIT 编译器重排到临界区内,但是临界区内的操作不会被编译器/处理器重排到临界区之外。

      Java 中的所有锁都是可重入锁。内部锁(synchronized)仅支持非公平锁,因此它可能导致饥饿。而显式锁(ReentrantLock)既支持非公平锁又支持公平锁,显式锁可能导致锁泄露。内部锁和显式锁各有所长,各有所短。读写锁(ReadWriteLock)由于其内部实现的复杂性,仅适用于只读操作比更新操作要频繁得多且读线程持有锁的时间比较长的场景。读写锁(ReadWriteLock)中的读锁和写锁是一个锁实例所充当的两个角色,并不是两个独立的锁。

      线程转储中可以包含锁的相关信息——线程在等待哪些锁,这些锁又是被哪些线程持有的。

      volatile 相当于轻量级锁。在线程安全保障方面与锁相同的是,volatile 能够保障可见性/有序性;与锁不同的是 volatile 不具有排他性,也不会导致上下文切换。与锁类似,Java 虚拟机实现 volatile 对有序性和可见性的保障也是借助于内存屏障。从这个角度来看,volatile 变量写操作相当于释放锁,volatile 变量读操作相当于获得锁——Java 虚拟机通过在 volatile 变量写操作之前插入一个释放屏障,在 volatile 读操作之后插入一个获取屏障这种成对的释放屏障和获取屏障的使用实现了 volatile 对有序性的保障。类似地,Java 虚拟机在 volatile 变量写操作之后插入一个存储屏障,在 volatile 变量读操作之前插入一个加载屏障这种成对的存储屏障与加载屏障的使用实现了 volatile 对可见性的保障。

      在原子性方面,volatile 仅能够保障 long/double 型变量写操作的原子性。**如果要保障对 volatile 变量的赋值操作的线程安全,那么赋值操作右边的表达式不能涉及任何共享变量(包括被赋值的变量本身)。**volatile 关键字在可见性/有序性和原子性方面的保障并不会对其修饰的数组元素的读/写起作用。

      volatile 变量写操作的成本介于普通变量的写操作和在临界区内进行的写操作之间。读取一个 volatile 变量总是意味着(通过高速缓存进行的)读内存操作,而不是从寄存器中读取。因此,volatile 变量读操作的成本比读取普通变量要略高一些,但比在临界区中读取变量要低。

      volatile 的典型运用场景包括:一,使用 volatile 变量作为状态标志;二,使用 volatile 保障可见性;三,使用 volatile 变量替代锁;四,使用 volatile 实现简易版读写锁。

      CAS 使得我们可以在不借助锁的情况下保障 read-modify-write 操作/check-then-act 操作的原子性,但是它并不保障可见性。原子变量类相当于基于 CAS 实现的增强型 volatile 变量(保障 volatile 无法保障的那一部分操作的原子性)。常用的原子变量类包括 AtomicInteger/AtomicLong/AtomicBoolean 等。AtomicStampedReference 则可以用于规避 CAS 的 ABA 问题。

      static 关键字能够保证一个线程即使在未使用其他同步机制的情况下也总是可以读取到一个类的静态变量的初始值(而不是默认值)。对于引用型静态变量,static 还确保了该变量引用的对象已经初始化完毕。但是,static 的这种可见性和有序性保障仅在一个线程初次读取静态变量的时候起作用。

      final 关键字在多线程环境下也有其特殊作用:当一个对象被发布到其他线程的时候,该对象的所有 final 字段(实例变量)都是初始化完毕的。而非 final 字段没有这种保障,即这些线程读取该对象的非 final 字段时所读取到的值可能仍然是相应字段的默认值。 对于引用型 final 字段,final 关键字还进一步确保该字段所引用的对象已经初始化完毕。

      实现对象的安全发布,通常可以依照以下顺序选择适用且开销最小的线程同步机制。

    • 使用 static 关键字修饰引用该对象的变量。
    • 使用 final 关键字修饰引用该对象的变量。
    • 使用 volatile 关键字修饰引用该对象的变量。
    • 使用 AtomicReference 来引用该对象。
    • 对访问该对象的代码进行加锁。

      为避免将 this 代表的当前对象逸出到其他线程,我们应该避免在构造器中启动工作者线程。通常我们可以定义一个 init 方法,在该方法中启动工作者线程。在此基础上,定义一个工厂方法来创建(并返回)相应的实例,并在该方法中调用该实例的 init 方法。

    本章知识图谱

    展开全文
  • Java线程同步机制

    2020-05-24 19:49:49
    线程同步机制是一套用于协调线程间的数据访问及活动的机制,该机制用于保障线程安全以及实现这些线程的共同目标。 锁概述 线程安全问题的前提是多个线程并发访问共享变量。针对这个情况,将多个线程对共享变量的并发...

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

    锁概述

    线程安全问题的前提是多个线程并发访问共享变量。针对这个情况,将多个线程对共享变量的并发访问转换为串行访问,既一个共享数据同时只能有一个线程访问,该线程访问结束后其他线程才能访问。这个思想的具体实现就是锁。
    一个线程在访问共享数据时必须申请对应的锁,获得锁以后才能访问共享数据,访问完共享数据后释放锁,其他线程才能继续申请锁。执行线程在获取锁后到释放锁之前的这段时间执行的代码被称为临界区。临界区一次只能被一个线程访问执行。
    锁具有排他性,既一个锁同时刻只能被一个线程持有。这种锁被称为互斥锁或者排他锁。这是最常见的锁。
    按照Java虚拟机对锁的实现方式划分,分为内部锁和显式锁。内部锁是synchronized;显示锁是指java.util.concurrent.locks.Lock接口的实现类(如 java.util.concurrent.locks.ReentrantLock类)。
    在这里插入图片描述

    锁的作用

    锁能够保护共享数据以实现线程安全,作用包括保障原子性、保障可见性和保障有序性。
    锁通过互斥性保障原子性,互斥就是指一个锁一次只能被一个线程持有,也就意味着临界区代码同一时刻只能被持锁线程执行。因此,持锁线程执行临界区期间没有其他线程能够访问相应的共享数据,也就具备了原子性。
    可见性的保障是通过写线程冲刷处理器缓存和读线程刷新处理器缓存这两个动作实现的。Java平台的锁获得后在执行临界区代码前可以将写线程对共享变量所做的更新同步到该线程执行处理器的高速缓存中;锁释放时会执行冲刷处理器缓存,这使得写线程对共享变量所做的更新能够被“推送”到该线程执行处理器的高速缓存中,从而对读线程可同步。

    锁对可见性、原子性和有序性的保证是有条件的,需要满足以下亮点:

    • 这些线程在访问同一组共享变量的时候必须使用同一个锁。
    • 这些线程即便是对共享变量只读时也必须获取锁。

    上边两条有一个不满足都会使原子性、可见性和有序性没有保障。

    锁相关的几个概念

    1. 可重入性

    下图是个伪代码,调用methodA方法时申请到lock锁,然后临界区代码中调用methodB方法,methodB方法也使用lock进行加锁。这时候就有一个问题啦:methodA的执行线程持有lock锁的时候调用methodB,那么methodB执行的时候又去申请锁lock,而lock此时正被当前线程持有(未被释放)。那么,此时methodB究竟能否获得lock呢?可重入性就是讲的这个问题。
    可重入性伪代码

    2. 锁的争用与调度

    Java锁的调度分为公平策略和非公平策略,相应的锁称为公平锁和非公平锁。内部锁是非公平锁,显示锁既支持公平锁也支持非公平锁。

    3. 锁的粒度

    一个锁所保护的共享数据大小称为锁的粒度。
    锁的粒度过大会导致无所谓的等待。
    锁的粒度过小会增加调度的开销。

    锁的开销

    锁的开销主要包括申请锁和释放锁,以及锁竞争引发的上下文切换的开销。这些开销主要是处理器时间。
    锁可能导致上下文切换,多个线程争用排他性资源可能导致上下文切换。因此,锁作为一种排他性资源,一旦争用就可能导致上下文切换。

    上下文切换

    简单解释下上下文切换,上下文切换是指线程在时间片用完或者其自身的原因(比如,他需要稍后在继续运行)暂停其运行时,另一个线程可以被操作系统(线程调度器)选中占用处理器开始或者继续其运行。这种一个线程被暂停,即被剥夺处理器的使用权,另一个线程被选中开始或者继续运行的过程就叫做线程上下文切换。

    这里的上下文是指计算的中间结果以及执行到哪条指令。其实这很类似我们打电话,说到一半来了另外一个重要电话,

    内部锁:synchronized关键字

    Java平台每个类都有个一个内部锁,内部锁是一种排他锁,能够保证原子性、可见性和顺序性。
    内部锁是通过synchronized关键字实现的,synchronized可以给方法和代码块加锁。
    语法如下:
    在这里插入图片描述
    锁句柄是一个对象的引用,可以写this表示当前对象,我们习惯称为锁句柄为锁。
    作为锁句柄的变量一般要求final修饰。因为如果不实用final修饰可能这个变量会被修改,导致多个线程使用的锁句柄不一致,导致这个锁失效。有鉴于此,也会用private修饰。
    一个线程访问synchronized修饰的方法或者代码块时,jvm会代为尝试申请锁,获取锁后执行完临界区代码或者出现异常时jvm也会自动释放锁,避免出现锁泄漏的问题。这也就是被称为内部锁的原因。

    内部锁的调度

    jvm会为每个内部锁分配一个入口集,用于存储等待获取相应内部锁的线程。多个线程竞争一个内部锁的时候,只有一个线程能得到锁,剩下的线程就会存储在入口集中。这个内部锁被释放后,入口集中的任意一个线程会被jvm唤醒,从而得到再次申请锁的机会。由于内部锁仅支持非公平锁,所以如果这时候有个不在入口集的活跃线程也去竞争这个锁,入口集被唤醒的线程与不在入口集的活跃线程会竞争这个内部锁,都有可能拿到这个锁。具体实现是当一个不在入口集的活跃线程想获取锁时,先试图插队,如果占用锁的线程释放了锁,被唤醒的线程还没来得及拿锁,那么不在入口集的活跃线程就可以直接获取锁;如果锁被其他线程占用,那就进入口集,和其他入口集线程等待唤醒再次争夺锁。
    jvm从一个内部锁的入口集中选择一个等待线程,作为下一个可以参与再次申请内部锁的线程与jvm的具体实现有关:这个被选中的线程可能是入口集中等待时间最长的线程,也可能是等待时间最短的线程,或者完全是随机的一个线程。因为不能依赖这个具体的选择算法。

    展开全文
  • java线程同步机制

    2020-09-25 23:46:31
    1、(这部分内容属于了解)关于线程的调度 1.1、常见的线程调度模型有哪些?...1.2、java中提供了哪些方法是和线程调度有关系的呢? 实例方法: void setPriority(int newPriority) 设置线程的优先级 in

    1、(这部分内容属于了解)关于线程的调度

    1.1、常见的线程调度模型有哪些?

    抢占式调度模型:
    那个线程的优先级比较高,抢到的CPU时间片的概率就高一些/多一些。
    java采用的就是抢占式调度模型。

    均分式调度模型:
    平均分配CPU时间片。每个线程占有的CPU时间片时间长度一样。
    平均分配,一切平等。
    有一些编程语言,线程调度模型采用的是这种方式。

    1.2、java中提供了哪些方法是和线程调度有关系的呢?

    实例方法:
    void setPriority(int newPriority) 设置线程的优先级
    int getPriority() 获取线程优先级
    最低优先级1
    默认优先级是5
    最高优先级10
    优先级比较高的获取CPU时间片可能会多一些。(但也不完全是,大概率是多的。)

    静态方法:
    static void yield() 让位方法
    暂停当前正在执行的线程对象,并执行其他线程
    yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。
    yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。
    注意:在回到就绪之后,有可能还会再次抢到。

    实例方法:
    void join()
    合并线程
    class MyThread1 extends Thread {
    public void doSome(){
    MyThread2 t = new MyThread2();
    t.join(); // 当前线程进入阻塞,t线程执行,直到t线程结束。当前线程才可以继续。
    }
    }
    class MyThread2 extends Thread{
    }

    2、关于多线程并发环境下,数据的安全问题。

    2.1、为什么这个是重点?
    以后在开发中,我们的项目都是运行在服务器当中,
    而服务器已经将线程的定义,线程对象的创建,线程
    的启动等,都已经实现完了。这些代码我们都不需要
    编写。

    最重要的是:你要知道,你编写的程序需要放到一个
    多线程的环境下运行,你更需要关注的是这些数据
    在多线程并发的环境下是否是安全的。(重点:*****)

    2.2、什么时候数据在多线程并发的环境下会存在安全问题呢?
    三个条件:
    条件1:多线程并发。
    条件2:有共享数据。
    条件3:共享数据有修改的行为。

    满足以上3个条件之后,就会存在线程安全问题。

    2.3、怎么解决线程安全问题呢?
    当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在
    线程安全问题,怎么解决这个问题?
    线程排队执行。(不能并发)。
    用排队执行解决线程安全问题。
    这种机制被称为:线程同步机制。

    专业术语叫做:线程同步,实际上就是线程不能并发了,线程必须排队执行。

    怎么解决线程安全问题?
    使用“线程同步机制”。

    线程同步就是线程排队了,线程排队了就会牺牲一部分效率,没办法,数据安全
    第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事儿。
    2.4、说到线程同步这块,涉及到这两个专业术语:
    异步编程模型:
    线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,
    谁也不需要等谁,这种编程模型叫做:异步编程模型。
    其实就是:多线程并发(效率较高。)
    异步就是并发。
    同步编程模型:
    线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行
    结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,
    两个线程之间发生了等待关系,这就是同步编程模型。
    效率较低。线程排队执行。
    同步就是排队。
    3、Java中有三大变量?【重要的内容。】
    实例变量:在堆中。
    静态变量:在方法区。
    局部变量:在栈中。
    以上三大变量中:
    局部变量永远都不会存在线程安全问题。
    因为局部变量不共享。(一个线程一个栈。)
    局部变量在栈中。所以局部变量永远都不会共享。
    实例变量在堆中,堆只有1个。
    静态变量在方法区中,方法区只有1个。
    堆和方法区都是多线程共享的,所以可能存在线程安全问题。
    局部变量+常量:不会有线程安全问题。
    成员变量:可能会有线程安全问题。
    4、如果使用局部变量的话:
    建议使用:StringBuilder。
    因为局部变量不存在线程安全问题。选择StringBuilder。
    StringBuffer效率比较低。
    ArrayList是非线程安全的。
    Vector是线程安全的。
    HashMap HashSet是非线程安全的。
    Hashtable是线程安全的。
    5、总结:
    synchronized有三种写法:
    第一种:同步代码块
    灵活
    synchronized(线程共享对象){
    同步代码块;
    }
    第二种:在实例方法上使用synchronized
    表示共享对象一定是this
    并且同步代码块是整个方法体。
    第三种:在静态方法上使用synchronized
    表示找类锁。
    类锁永远只有1把。
    就算创建了100个对象,那类锁也只有一把。
    对象锁:1个对象1把锁,100个对象100把锁。
    类锁:100个对象,也可能只是1把类锁。
    6、开发中应该怎么解决线程安全问题?
    synchronized会让程序的执行效率降低,用户体验不好。
    系统的用户吞吐量降低。用户体验差。在不得已的情况下再选择
    线程同步机制。
    第一种方案:尽量使用局部变量代替“实例变量和静态变量”。
    第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样
    实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,
    对象不共享,就没有数据安全问题了。)
    第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候
    就只能选择synchronized了。线程同步机制。
    7、守护线程
    java语言中线程分为两大类:
    一类是:用户线程
    一类是:守护线程(后台线程)
    其中具有代表性的就是:垃圾回收线程(守护线程)。
    守护线程的特点:
    一般守护线程是一个死循环,所有的用户线程只要结束,
    守护线程自动结束。
    注意:主线程main方法是一个用户线程。
    守护线程用在什么地方呢?
    每天00:00的时候系统数据自动备份。
    这个需要使用到定时器,并且我们可以将定时器设置为守护线程。
    一直在那里看着,没到00:00的时候就备份一次。所有的用户线程
    如果结束了,守护线程自动退出,没有必要进行数据备份了。
    8、定时器
    定时器的作用:
    间隔特定的时间,执行特定的程序。
    每周要进行银行账户的总账操作。
    每天要进行数据的备份操作。
    在实际的开发中,每隔多久执行一段特定的程序,这种需求是很常见的,
    那么在java中其实可以采用多种方式实现:
    可以使用sleep方法,睡眠,设置睡眠时间,没到这个时间点醒来,执行
    任务。这种方式是最原始的定时器。(比较low)
    在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。
    不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持
    定时任务的。
    在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架,
    这个框架只要进行简单的配置,就可以完成定时器的任务。
    9、实现线程的第三种方式:实现Callable接口。(JDK8新特性。)
    这种方式实现的线程可以获取线程的返回值。
    之前讲解的那两种方式是无法获取线程返回值的,因为run方法返回void。
    思考:
    系统委派一个线程去执行一个任务,该线程执行完任务之后,可能
    会有一个执行结果,我们怎么能拿到这个执行结果呢?
    使用第三种方式:实现Callable接口方式。
    9、关于Object类中的wait和notify方法。(生产者和消费者模式!)
    第一:wait和notify方法不是线程对象的方法,是java中任何一个java对象
    都有的方法,因为这两个方式是Object类中自带的。
    wait方法和notify方法不是通过线程对象调用,
    不是这样的:t.wait(),也不是这样的:t.notify()…不对。
    第二:wait()方法作用?
    Object o = new Object();
    o.wait();
    表示:
    让正在o对象上活动的线程进入等待状态,无期限等待,
    直到被唤醒为止。
    o.wait();方法的调用,会让“当前线程(正在o对象上
    活动的线程)”进入等待状态。
    第三:notify()方法作用?
    Object o = new Object();
    o.notify();
    表示:
    唤醒正在o对象上等待的线程。
    还有一个notifyAll()方法:
    这个方法是唤醒o对象上处于等待的所有线程。

    展开全文
  • Java线程同步机制 简介 线程安全问题的产生是由于多线程应用程序缺乏某种东西——线程同步机制。线程同步机制是一套用于协调线程间的数据访问(Data access)及活动(Activity)的机制,该机制用于保障线程安全以及...

    Java线程同步机制

    简介

    线程安全问题的产生是由于多线程应用程序缺乏某种东西——线程同步机制。线程同步机制是一套用于协调线程间的数据访问(Data access)及活动(Activity)的机制,该机制用于保障线程安全以及实现这些线程的共同目标。

    广义上看,Java平台提供的线程同步机制包括锁、volatile关键字、final关键字、static关键字以及一些相关的API,如Object.wait()/Object.notify()等。

    锁是将多个线程对共享数据的访问转换为串行访问的一种同步机制。一个线程在访问共享数据前必须申请相应的锁,称为锁的获得(Acquire),一个线程获得某个锁就称该线程为相应锁的持有线程,一个锁一次只能被一个线程持有。访问结束后必须释放(Release)相应的锁。锁的持有线程在其获得锁之后和释放锁之前这段时间内所执行的代码被称为临界区(Critical Section)。共享数据只允许在临界区内进行访问,临界区一次只能被一个线程执行。

    锁具有排他性(Exclusive),即一个锁一次只能被一个线程持有。因此称为排他锁或者互斥锁(Mutex)。
    按照Java虚拟机对锁的实现方式,Java平台中的锁包括内部锁(Intrinsic Lock)和显式锁(Explicit Lock)。内部锁是通过synchronized关键字实现的;显式锁是通过java.concurrent.locks.Lock接口的实现类(如java.concurrent.locks.ReentrantLock类)实现的。

    锁的作用

    锁能够保护共享数据以实现线程安全,其作用包括保障原子性、保障可见性和保障有序性。尽管锁能够保障有序性,但是并不意味着临界区内的内存操作不能够被重排序。

    注意,锁对可见性、原子性和有序性的保障是有条件的,需要同时保证以下两点:

    • 这些线程在访问同一组共享数据的时候必须使用同一个锁
    • 这些线程中的任意一个线程,即使其仅仅是读取这组共享数据而没有对其进行更新的话,也需要在读取时持有相应的锁。

    与锁相关的几个概念

    1.可重入性
    一个线程在其持有一个锁的时候能否再次或者多次申请该锁,如果能够成功申请,则是可重入的(Reentrant),否则就是非可重入的(Non-reentrant)。

    可重入锁可以被理解为是一个对象,该对象是一个计数器属性。计数器属性的初始值为0,表示相应的锁还没有被任何线程持有。每次线程获得一个可重入锁的时候,该锁的计数器值会被增加1。每次一个线程释放锁的时候,该锁的计数器属性值就会被减1。

    2.锁的争用与调度
    Java平台中锁的调度策略包括公平策略和非公平策略,相应的锁就被称为公平锁和非公平锁。内部锁属于非公平锁,而显式锁则既支持公平锁又支持非公平锁。

    3.锁的粒度
    一个锁实例可以保护一个或者多个共享数据。一个锁实例所保护的共享数据的数量大小称为该锁的粒度(Granularity)。根据数据量的大小分为粒度粗和粒度细,且它们的标准是相对的。

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

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

    此外,锁的不正确使用也会导致如下一些线程活性故障:

    • 锁泄露(Lock Leak)。锁泄露是指一个线程获得某个锁之后,由于程序的错误、缺陷致使该锁一直无法被释放而导致其他线程一直无法获得该锁的现象。
    • 锁的不正确使用还可能导致死锁、锁死等线程活性故障。

    内部锁:synchronized关键字

    Java平台中的任何一个对象都有唯一一个与之关联的锁。这种锁被称为监视器(Monitor)或者内部锁(Intrinsic Lock)。内部锁是一种排他锁,它能够保障原子性、可见性和有序性。

    内部锁是通过synchronized关键字实现的。synchronized关键字可以用来修饰方法以及代码块。synchronized关键字修饰的方法称为同步方法(Synchronized Method)。synchronized修饰的静态方法被称为同步静态方法,synchronized修饰的实例方法被称为同步实例方法。synchronized关键字修饰的代码块被称为同步块(Synchronized Block),即临界区。其语法如下:

    synchronized(锁句柄){
    //在此代码块中访问共享数据
    }

    锁句柄是一个对象的引用(或者能够返回对象的表达式)。锁句柄对应的监视器被称为相应同步块的引导锁。相应的,称相应的同步块为该锁引导的同步块。
    作为锁句柄的变量通常采用final修饰。这是因为锁句柄变量的值一旦改变,会导致执行同一个同步块的多个线程实际上使用不同的锁,从而导致竞态。因此,通常使用private修饰作为锁句柄的变量,即private final修饰。

    同步静态方法相当于以当前类对象为引导锁的同步块。线程在执行临界区代码的时候必须持有该临界区的引导锁。线程对内部锁的申请与释放的动作由Java虚拟机负责代为实施。

    内部锁的使用不会导致锁泄露,这是因为Java编译器javac在将同步块代码编译为字节码的时候,对临界区中可能抛出的而程序代码中又未捕获的异常进行特殊处理,这使得临界区的代码即使抛出异常也不会妨碍内部锁的释放。
    内部锁的调度
    Java虚拟机会为每个内部锁分配一个入口集(Entry Set),用于记录等待获得相应内部锁的线程。入口集中的线程就被称为相应内部锁的等待线程。由于Java虚拟机对内部锁的调度仅支持非公平调度,被唤醒的等待线程占用处理器运行时可能还有其他新的活跃线程与该线程抢占这个被释放锁,因此被唤醒的线程不一定就能够成为该锁的持有线程。

    显式锁:Lock接口

    显式锁(Explicit Lock)是java.util.concurrent.locks.Lock接口的实例。该接口对显式锁进行了抽象。类java.util.concurrent.locks.ReentrantLock是Lock接口的默认实现类。

    一个Lock接口实例就是一个显式锁对象,Lock接口定义的lock方法和unlock方法分别用于申请和释放相应Lock实例表示的锁。显式锁的使用方法如下:

    private final Lock lock=...;//创建一个Lock接口实例
    ......
    lock.lock();//申请锁lock
    try{
    	// 在此对共享数据进行访问
    	......
    }finally{
    	// 总是在finally块中释放锁,以避免锁泄露
    	lock.unlock();//释放锁lock
    }
    

    显式锁的使用包括以下几个方面。

    • 创建Lock接口的实例。
    • 在访问共享数据前申请相应的显式锁。
    • 在临界区中访问共享数据。
    • 共享数据访问结束后释放锁。

    显式锁的调度

    ReentrantLock既支持非公平锁也支持公平锁。ReentrantLock的一个构造器的签名如下:

    ReentrantLock(boolean fair)

    该构造器使得在创建显式锁实例的时候可以指定相应的锁是否是公平锁。公平锁保证锁调度的公平性往往是以增加了线程的暂停和唤醒的可能性,即增加了上下文切换为代价的,因此适合于锁被持有的时间相对长或者线程申请锁的平均间隔时间相对长的情形。使用公平锁的开销比使用非公平锁的开销更大,因此显式锁默认使用的是非公平调度策略。

    显式锁与内部锁的比较

    内部锁是基于代码块的锁;而显式锁是基于对象的锁;内部锁从代码角度看仅仅是一个关键字,无法发挥面向对象编程的灵活性,而显式锁支持在一个方法内申请锁,却在另一个方法里释放锁。

    内部锁基于代码块的这个特征的优势是简单易用,且不会导致锁泄露。使用显式锁必须注意将锁的释放操作放在finally块中。

    如果一个内部锁的持有线程一直不释放这个锁,那么同步在该锁之上的所有线程就会一直被暂停而使其任务无法进展。而显式锁则可以轻松避免这个问题。Lock接口定义了一个tryLock方法。该方法的作用是尝试申请相应Lock实例锁表示的锁。tryLock方法是个多载(Overload)的方法。
    在锁的调度方面,内部锁仅支持非公平锁,而显式锁既支持非公平锁,又支持公平锁。

    另外,在问题定位方面,线程转储(Thread dump)可以报告Java虚拟机中关于线程的详细信息。线程转储中会包含内部锁的相关信息,包括一些线程等待哪些锁以及这些锁的当前持有线程。而JDK1.5以下,线程转储并不包含显式锁的相关信息。不过JDK1.6提供的工具jstack所产生的线程转储中可以包含显式锁的信息。

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    // 演示线程转储显式锁信息的示例程序
    public class ExplicitLockInfo {
        private static final Lock lock = new ReentrantLock();
        private static int sharedData = 0;
    
        public static void main(String[] args) throws Exception {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    lock.lock();
                    try {
                        try {
                            Thread.sleep(2200000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        sharedData = 1;
                    } finally {
                        lock.unlock();
                    }
                }
            });
            t.start();
            Thread.sleep(100);
            lock.lock();
            try {
                System.out.println("sharedData: " + sharedData);
            } finally {
                lock.unlock();
            }
        }
    }
    

    显式锁提供了一些接口(指方法)可以用来对锁的相关信息进行监控,而内部锁不支持这种特性。RentrantLock中定义的方法isLocked()可用于检测相应锁是否被某个线程持有,getQueueLength()方法可用于检查相应锁的等待线程的数量。

    锁的选用:内部锁还是显式锁

    内部锁的优点是简单易用,显式锁的优点是功能强大。
    一般来说,新开发的代码中我们可以选用显式锁;或者是默认情况下选用内部锁,仅在需要显式锁所提供的特性的时候才选用显式锁。

    改进型锁:读写锁

    对于同步在同一锁之上的线程而言,对共享变量仅进行读取而没有进行更新的线程称为只读线程,简称读线程。对共享变量进行更新(包括先读取后更新)的线程就被称为写线程
    读写锁(Read/Write Lock)是一种改进型的排他锁,也被称为共享/排他(Shared/Exclusive)锁。读写锁允许多个线程可以同时读取(只读)共享变量,但是一次只允许一个线程对共享变量进行更新(包括读取后再更新)。任何线程读取共享变量的时候,其他线程无法更新这些变量;一个线程更新共享变量的时候,其他任何线程都无法访问该变量。
    读写锁的功能是通过其扮演的两种角色——读锁(Read Lock)和写锁(Write Lock)实现的。
    读锁是共享的,一个读线程持有一个读锁的时候并不妨碍其他读线程获得该读锁。写线程在访问共享变量的时候必须持有相应读写锁的写锁,即写锁是排他的,一个线程持有写锁的时候其他线程无法获得相应锁的写锁或读锁。因此写锁保障了写线程对共享变量的访问(包括更新)是独占的。读锁实际上只是在读线程之间是共享的。写线程对共享变量的更新对读线程是可见的。

    获得条件 排他性 作用
    读锁 相应的写锁未被任何线程持有 对读线程是共享的,对写线程是排他的 允许多个读线程可以同时读取共享变量,并保障读线程读取共享变量期间没有其他任何线程能够更新这些共享变量
    写锁 该写锁未被其他任何线程持有并且相应的读锁未被其他任何线程持有 对写线程和读线程都是排他的 使得写线程能够以独占的方式访问共享变量

    java.util.concurrent.locks.ReadWriteLock接口是对读写锁的抽象,其默认实现类是java.util.concurrent.locks.ReentrantReadWriteLock。ReadWriteLock接口定义了两个方法:readLock()和writeLock()。
    读写锁的使用方法如下:

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReadWriteLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    public class ReadWriteLockUsage {
        private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
        private final Lock readLock = rwlock.readLock();
        private final Lock writeLock = rwlock.writeLock();
    
        //读线程执行该方法
        public void reader() {
            readLock.lock();//申请读锁
            try {
                //在此区域读取共享变量
            } finally {
                readLock.unlock();
            }
        }
    
        //写线程执行该方法
        public void writer() {
            writeLock.lock();//申请写锁
            try {
                //在此区域访问(读写)共享变量
            } finally {
                writeLock.unlock();
            }
        }
    }
    

    读写锁适合于以下条件同时得到满足的场景中使用:

    • 只读操作比写(更新)操作要频繁得多
    • 读线程持有锁的时间比较长

    ReentrantReadWriteLock所实现的读写锁是个可重入锁。ReentrantReadWriteLock支持锁的降级(Downgrade),即一个线程持有读写锁的写锁的情况下可以继续获得相应的读锁。实例代码如下:

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReadWriteLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    public class ReadWriteLockDowngrade {
        private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
        private final Lock readLock = rwLock.readLock();
        private final Lock writeLock = rwLock.writeLock();
    
        public void operationWithLockDowngrade() {
            boolean readLockAcquired = false;
            writeLock.lock();//申请写锁
            try {
                // 共享数据进行更新
                ///......
                //当前线程在持有写锁的情况下申请读锁readLock
                readLock.lock();
                readLockAcquired = true;
            } finally {
                writeLock.unlock();
            }
            if (readLockAcquired) {
                try {
                    // 读取共享数据并据此执行其他操作
                    // ......
    
                } finally {
                    readLock.unlock();
                }
            } else {
                // ......
            }
        }
    }
    

    锁的降级的反面是锁的升级(Upgrade),即一个线程在持有读写锁的读锁的情况下,申请相应的写锁。读线程如果要转而申请写锁,需要先释放读锁,然后申请相应的写锁。

    锁的适用场景

    多个线程共享同一组数据的时候,如果其中有线程涉及如下操作,可以考虑使用锁:

    • check-then-act操作:一个线程读取共享数据并在此基础上决定其下一个操作时什么
    • read-modify-write操作:一个线程读取共享数据并在此基础上更新该数据。
    • 多个线程对多个共享数据进行更新。如果这些共享数据之间存在关联关系,那么为了保障操作的原子性可以考虑使用锁。

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

    线程获得和释放锁时分别执行的两个动作:刷新处理器缓存和冲刷处理器缓存。对于同一个锁所保护的共享数据而言,前一个动作保证了该锁的当前持有线程能够读取到前一个持有线程对这些数据所做的更新,后一个动作保证了该锁的持有线程对这些数据所做的更新对该锁的后续持有线程可见。

    Java虚拟机底层实际上是借助内存屏障(Memory Barrier)来实现上述两个动作的。内存屏障是对一类仅针对内存读、写操作指令(Instruction)的跨处理器架构的比较底层的抽象。内存屏障是被插入到两个指令之间进行使用的,其作用是禁止编译器、处理器重排序从而保障有序性。具体地,读操作(Load,Read)指将主内存中的数据(通过高速缓存)读取到寄存器中。写操作(Store或者Write)指将数据写到共享内存中。
    按照内存屏障所起的作用可分为以下几种:

    • 按照可见性保障划分,内存屏障分为加载屏障(Load Barrier)和存储屏障(Store Barrier)。加载屏障的作用是刷新处理器缓存,存储屏障的作用冲刷处理器缓存。可见性的保障是通过写线程和读线程成对地使用存储屏障和加载屏障实现的。
    • 按照有序性保障划分,内存屏障分为获取屏障(Acqure Barrier)和释放屏障(Release Barrier)。获取屏障的使用方式是在一个读操作之后插入该内存屏障,其作用是禁止该读操作与其后的任何读写操作之间进行重排序。释放屏障的使用方式是在一个写操作之前插入该内存屏障,其作用是禁止该写操作与其前面的任何读写操作之间进行重排序。

    锁的有序性保障是通过写线程和读线程配对使用释放屏障和加载屏障实现的。

    锁与重排序

    与锁有关的重排序规则可以理解为语句(指令)相对于临界区的“许进不许出”。具体来说,无论是编译器还是处理器,均还需要遵循以下重排序规则:

    1. 临界区内的操作不允许被重排序到临界区之外
    2. 临界区内的操作之间允许被重排序
    3. 临界区外的操作之间可以被重排序
    4. 锁申请(MonitorEnter)与锁释放(MonitorExit)操作不能被重排序
    5. 两个锁申请操作不能被重排序
    6. 两个锁释放操作不能被重排序
    7. 临界区外的操作可以被重排到临界区之内

    volatile关键字:轻量级同步机制

    volatile关键字用于修饰共享可变变量,即没有使用final关键字修饰的实例变量或静态变量,相应的变量就被称为volatile变量。
    volatile变量的不稳定性意味着对这种变量的读和写都必须从高速缓存或者主内存中读取,以读取变量的相对新值。volatile变量不会被编译器分配到寄存器进行存储,对volatile变量的读写操作都是内存访问。

    volatile关键字称为轻量级锁,与锁相同的地方是保证可见性和有序性,不同的是在原子性方面仅能保障写volatile变量操作的原子性,没有锁的排他性,其次,volatile关键字的使用不会引起上下文切换。

    volatile的作用

    volatile关键字的作用包括:保障可见性、保障有序性和保障long/double型变量读写操作的原子性。
    一般而言,对volatile变量的赋值操作,其右边表达式中只要涉及共享变量(包括被赋值的volatile变量本身),那么这个赋值操作就不是原子操作。要保障这样操作的原子性,仍然需要借助锁。

    volatile关键字在原子性仿麦呢仅保障对被修饰的变量的读操作、写操作本身的原子性。如果要保障对volatile变量的赋值操作的原子性,那么这个赋值操作不能涉及任何共享变量(包括被赋值的volatile变量本身)的访问。
    volatile对有序性的保障示例如下:

    import util.stf.*;
    
    @ConcurrencyTest(iterations = 200000)
    public class VolatileOrderingDemo {
        private int dataA = 0;
        private long dataB = 0L;
        private String dataC = null;
        private volatile boolean ready = false;
    
        @Actor
        public void writer() {
            dataA = 1;
            dataB = 10000L;
            dataC = "Content...";
            ready = true;
        }
    
        @Observer({
                @Expect(desc = "Normal", expected = 1),
                @Expect(desc = "Impossible", expected = 2),
                @Expect(desc = "ready not true", expected = 3)
        })
        public int reader() {
            int result = 0;
            boolean allISOK;
            if (ready) {
                allISOK = (1 == dataA) && (10000L == dataB) && "Content...".equals(dataC);
                result = allISOK ? 1 : 2;
            } else {
                result = 3;
            }
            return result;
        }
    
        public static void main(String[] args) throws InstantiationException, IllegalAccessException {
            //调用测试工具运行测试代码
            TestRunner.runTest(VolatileOrderingDemo.class);
        }
    }

    volatile虽然能够保障有序性,但是不具备排他性,所以不能保障其他操作的原子性,而只能保证对被修饰变量的写操作的原子性。volatile在有序性保障方面也可以从禁止重排序的角度理解,volatile禁止了如下重排序:

    • 写volatile变量操作与该操作之前的任何读、写操作不会被重排序
    • 读volatile变量操作与该操作之后的任何读、写操作不会被重排序

    如果被修饰的变量是个数组,那么volatile关键字只能够对数组引用本身的操作起作用,而无法对数组元素的操作起作用。对于引用型volatile变量,volatile关键字只是保障读线程能够读取到一个指向对象的相对新的内存地址(引用),而这个内存地址指向的对象的实例/静态变量值是否是相对新的则没有保障。

    volatile变量的开销

    volatile变量的开销包括读变量和写变量。因为不会导致上下文切换,所以volatile的开销要比锁小。volatile变量写操作的成本介于普通变量的写操作和在临界区内进行的写操作之间。读取volatile变量的成本也比在临界区中读取变量要低,但其成本可能比读取普通变量要高一些。

    volatile的典型应用场景

    1. 使用volatile变量作为状态标志
    2. 使用volatile保障可见性
    3. 使用volatile变量替代锁。多个线程共享一组可变状态变量的时候,通常需要使用锁来保障对这些变量的更新操作的原子性,以避免产生数据不一致问题。利用volatile变量写操作具有的原子性,可以把这一组可变状态变量封装成一个对象,那么对于这些状态变量的更新操作就可以通过创建一个新的对象并将该对象引用赋值给相应的引用型变量来实现。在这个过程中,volatile保障了原子性和可见性,从而避免了锁的使用。
    4. 使用volatile实现简易版读写锁。读写锁是通过混合使用锁和volatile变量来实现的,其中锁用于保障共享变量写操作的原子性,volatile变量用于保障共享变量的可见性。
    public class Counter {
        private volatile long count;
    
        public long value() {
            return count;
        }
    
        public void increment() {
            synchronized (this) {
                count++;
            }
        }
    }

    CAS与原子变量

    CAS(Compare and Swap)是对一种处理器指令的称呼。

    CAS

    事实上,保障像自增这种比较简单的操作的原子性可以选择CAS。CAS能够将read-modify-write和check-and-act之类的操作转换为原子操作。
    示例:使用CAS实现线程安全的计数器

    import util.Debug;
    import util.Tools;
    
    import java.util.HashSet;
    import java.util.Set;
    import java.util.concurrent.atomic.AtomicLongFieldUpdater;
    
    public class CASBasedCounter {
        private volatile long count;
        private final AtomicLongFieldUpdater<CASBasedCounter> fieldUpdater;
    
        public CASBasedCounter() throws SecurityException, NoSuchFieldException {
            fieldUpdater = AtomicLongFieldUpdater.newUpdater(CASBasedCounter.class, "count");
        }
    
        public long value() {
            return count;
        }
    
        public void increment() {
            long oldValue;
            long newValue;
            do {
                oldValue = count; //读取共享变量的当前值
                newValue = oldValue + 1;//计算共享变量的新值
            } while (!compareAndSwap(oldValue, newValue)); ///调用CAS来更新共享变量的值
        }
    
        private boolean compareAndSwap(long oldValue, long newValue) {
            boolean isOK = fieldUpdater.compareAndSet(this, oldValue, newValue);
            return isOK;
        }
    
        public static void main(String[] args) throws Exception {
            final CASBasedCounter counter = new CASBasedCounter();
            Thread t;
            Set<Thread> threads = new HashSet<Thread>();
            for (int i = 0; i < 20; i++) {
                t = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Tools.randomPause(50);
                        counter.increment();
                    }
                });
                threads.add(t);
            }
            for (int i = 0; i < 8; i++) {
                t = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Tools.randomPause(50);
                        Debug.info(String.valueOf(counter.value()));
                    }
                });
                threads.add(t);
            }
            //启动并等待指定的线程结束
            Tools.startAndWaitTerminated(threads);
            Debug.info("final count:" + String.valueOf(counter.value()));
        }
    }

    注意:CAS只是保证了共享变量更新这个操作的原子性,它并不保障可见性。

    原子操作工具:原子变量类

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

    对象的发布与逸出

    对象发布(Publish)是指对象能够被其他作用域之外的线程访问。常见的对象发布形式如下:

    1. 将对象引用存储到public变量在.
    2. 在非private方法中返回一个对象。
    3. 创建内部类,使得当前对象(this)能够被这个内部类使用。
    4. 通过方法调用将对象传递给外部方法。外部方法(Alien Method)指相对于某个类而言其他类的方法或者该类的可覆盖方法。将一个对象传递给外部方法也会被视为对象发布。

    对象的初始化安全:final与static

    Java中类的初始化实际上采用了延迟加载的技术,即一个类被Java虚拟机加载之后,该类的所有静态变量的值都仍然是其默认值(引用型变量的默认值为null,boolean变量的默认值为false),直到有个线程初次访问了该类的任意一个静态变量才使这个类被初始化——类的静态初始化块(“static{}”)被执行,类的所有静态变量被赋予初始值。

    //类的延迟初始化
    public class ClassLazyInitDemo {
        public static void main(String[] args){
            Debug.info(Collaborator.class.hashCode());
            Debug.info(Collaborator.number);
            Debug.info(Collaborator.flag);
        }
        static class Collaborator{
            static int number = 1;
            static boolean flag = true;
            static {
                Debug.info("Collaborator initializing.......");
            }
        }
    }

    static关键字在多线程环境下有其特殊的含义,它能够保证一个线程即使在未使用其他同步机制的情况下也总是可以读取到一个类的静态变量的初始值(而不是默认值),但是这种可见性保障仅限于线程初次读取该变量。

    import util.Debug;
    import util.Tools;
    
    import java.util.HashMap;
    import java.util.Map;
    
    public class StaticVisibilityExample {
        private static Map<String, String> taskConfig;
    
        static {
            Debug.info("The class being initialized...");
            taskConfig = new HashMap<>();
            taskConfig.put("url", "https://www.baidu.com");
            taskConfig.put("timeout", "1000");
        }
    
        public static void changeConfig(String url, int timeout) {
            taskConfig = new HashMap<>();
            taskConfig.put("url", url);
            taskConfig.put("timeout", String.valueOf(timeout));
        }
    
        public static void init() {
            Thread t = new Thread() {
                @Override
                public void run() {
                    String url = taskConfig.get("url");
                    String timeout = taskConfig.get("timeout");
                    doTask(url, Integer.valueOf(timeout));
                }
            };
            t.start();
        }
    
        private static void doTask(String url, int timeout) {
            //省略其他代码
    
            //模拟实际操作的耗时
            Tools.randomPause(500);
        }
    }
    

    注意:对于引用型静态变量,static关键字还能够保障一个线程读取到该变量的初始值时,这个值所指向(引用)的对象已经初始化完毕。
    static关键字仅仅保障读线程能够读取到相应字段的初始值,而不是相对新值。

    在多线程环境下final关键字有其特殊的作用:当一个对象被发布到其他线程的时候,该对象的所有final字段(实例变量)都是初始化完毕的,即其他线程读取这些字段的时候所读取到的值都是相应字段的初始值(而不是默认值)。而非final字段没有这种保障,即这些线程读取该对象的非final字段时所读取到的值可能仍然是相应字段的默认值。对于引用型final字段,final关键字还进一步确保该字段所引用的对象已经初始化完毕,即这些线程读取该字段所引用的对象的各个字段时所读取到的值都是相应字段的初始值。

    注意:final关键字只能保障有序性,并不保障对象引用本身对外的可见性。

    安全发布与逸出

    安全发布指对象以一种线程安全的方式被发布。当一个对象的发布出现不期望的结果或者对象发布本身不是所期望的时候,称该对象逸出(Escape)。
    创建内部类、使得当前对象this能够被这个内部类使用的方式是最容易导致对象逸出的一种发布,它具体包括以下几种形式:

    • 在构造器中将this赋值给一个共享变量
    • 在构造器中将this作为方法参数传递给其他方法
    • 在构造器中启动基于匿名类的线程。

    由于构造器未执行结束意味着相应对象的初始化未完成,因此在构造器中将this关键字代表的当前对象发布到其他线程会导致这些线程看到的可能是一个未初始化完毕的对象,因此可能导致程序运行结果错误。

    一般地,如果一个类需要创建自己的工作者线程,那么可以为该类定义一个init方法,相应的工作者线程可以在该方法或者该类的构造器创建,但是线程的启动则是在init方法中执行的。示例如下:

    import util.Debug;
    import java.util.Map;
    
    public class SafeObjPublishWhenStartingThread {
        private final Map<String, String> objectState;
    
        private SafeObjPublishWhenStartingThread(Map<String, String> objectState) {
            this.objectState = objectState;
            //不在构造器中启动工作者线程,以避免this逸出
        }
    
        private void init() {
            //创建并启动工作者线程
            new Thread() {
                @Override
                public void run() {
                    //访问外层类实例的状态变量
                    String value = objectState.get("someKey");
                    Debug.info(value);
                    //省略其他代码
                }
            }.start();
        }
    
        //工厂方法
        public static SafeObjPublishWhenStartingThread newInstance(Map<String, String> objectState) {
            SafeObjPublishWhenStartingThread instance = new SafeObjPublishWhenStartingThread(objectState);
            instance.init();
            return instance;
        }
    }
    

    一个对象在其初始化过程中没有出现this逸出,就称该对象为正确创建的对象(Properly Constructed Object)。要安全发布一个正确创建的对象,可以根据以下几种方式选择:

    • 使用static关键字修饰引用该对象的变量
    • 使用final关键字修饰引用该对象的变量
    • 使用volatile关键字修饰引用该对象的变量
    • 使用AtomicReference来引用该对象
    • 对访问该对象的代码进行加锁

    参考资料

    《Java多线程编程实战指南》

    展开全文
  • Java线程同步机制的幕后助手是内存屏障。不同同步机制的功能强弱不同,相应的开销以及可能导致的问题也不同。 锁 volatile CAS final static 原子性保障 具备 具备 具备 不涉及 不涉及 可见性保障 具备 ...
  • 一、java线程同步机制以及对象锁1、线程同步众所周知,在多线程编程中,多个 线程同时对同一个资源进行访问,就可能会出现这样的情况-----这几个线程根据时间片机制会争向访问该资源,特别是在访问某一静态变量的...
  • 广义上将Java线程同步机制包括锁(内部锁、显式锁)、volatile关键字、final关键字、static关键字、一些相关的API(如:wait()、notify()等) 2.什么是锁 可以理解为有一种许可证可以保证线程安全,它将多线程对...
  • 在之前,已经学习到了线程的创建和状态控制,但是每个线程之间几乎都没有什么太大的联系。可是有的时候,可能存在多个线程多同一个数据进行操作,这样,可能就会引用各种奇怪的问题。现在就来学习多线程对数据访问的...
  • 要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制 (synchronized)来解决。 1:Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一...
  • Java线程同步机制详解

    2012-11-04 08:46:28
    任何一个有经验的软件开发者都知道,对于多...Java开发中的同步机制也是非常重要的内容,本文将详细阐述Java线程同步机制。  Java同步机制简介  JAVA中synchronized关键字能够作为函数的修饰符,也可作为函数内
  • 文章目录1. 前言2. 锁概述3. 内部锁:synchronized 关键字代码块同步方法同步4....从广义来说,Java平台提供的线程同步机制包括锁、volatile关键字、final关键字、static关键字以及一些相关的API...
  • 【总结】Java线程同步机制深刻阐述 见http://hxraid.iteye.com/blog/667437

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 9,338
精华内容 3,735
关键字:

java线程同步机制

java 订阅