【Java多线程编程实战】Java线程同步机制

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多线程编程实战指南》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

镰刀韭菜

看在我不断努力的份上,支持我吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值