Java多线程技术之二(线程同步)

一、线程同步的概念

在多线程环境下,一些敏感数据不允许被多个线程同时访问,为保证数据的完整性,需要一种技术来保证敏感数据在任何时刻,最多有一个线程访问,这种技术叫线程同步。

二、竞态条件与临界区

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。在任意时刻只允许一个线程对临界区进行访问,如果有多个线程试图访问临界区,那么在有一个线程进入后,其他试图访问临界区的线程将被挂起,并一直等到进入临界区的线程离开,临界区被释放后,其他线程才可以抢占。

三、操作系统内核中的同步

操作系统内核提供给外部调用的过程或者函数称为原语(Primitive),原语在执行过程中不会被中断,所有上层的同步机制最终都要靠这些原语来保证。操作系统提供了wait原语和signal原语,wait原语阻塞调用线程,signal原语唤醒被阻塞的线程。

1、信号量(Semaphore)

信号量是操作系统内核维护的一个数据结构,用来记录有多少个线程正在使用它,它包括一个计数器和一个等待线程队列,使用时将计数器初始化为最大允许线程并发访问数量。

操作系统内核针对信号量提供两个原语,P操作和V操作。

  • P操作对应着wait,P操作时,如果信号量的计数大于0,则进程无须等待,它将信号量的计数减1。如果信号量的计数为0,则将线程加入信号量的等待队列,信号量的计数不做变更。

  • V操作对应着signal,如果V操作时,有线程等待在这个信号量上,那么操作系统会从信号量的等待队列中选择一个线程将其唤醒,信号量的计数不做变更。如果V操作时没有线程等待在信号量上,那么它将信号量的计数加1。

2、互斥量(Mutex)

互斥量是信号量的一种特例,它的计数器初始化为1,也被称为二元信号量,用于实现互斥访问机制。它使临界区同一时间只允许一个线程进入,而信号量是允许多个线程同时进入临界区的。

四、监控器(Monitor)

在操作系统内核之外,管程是更高层次的同步原语,管程是在编程语言中实现的,它内部封装了内核同步原语,它向外部提供一个简洁易用的接口,管程体现了面向对象思想。管程实现了互斥访问机制,同一个时刻,只有一个线程能进入管程中定义的临界区,无法进入管程临界区的线程将被阻塞,并且在必要的时候会被唤醒。

Java语言实现了管程,monitor就是管程的一种实现,monitor实现了MESA管程模型并作了简化。每个Java对象都关联了一个monitor,当这个对象的临界区被线程执行到时,执行的线程必须先获取该对象的monitor的许可才能进入临界区,没有获取到monitor许可的线程将会被阻塞在临界区的入口处。

五、线程同步与synchronized

为方便进行线程同步,Java在语法上提供了synchronized关键字,用于声明临界区。每个临界区都对应一个Java对象,每个Java对象都关联了一个monitor,monitor会保护临界区。线程在进入临界区前,需要获得monitor的许可,否则就处于阻塞状态。

重入性是指线程获得monitor的许可后,访问嵌套的临界区不需要再次获取monitor的许可,synchronized 具有重入性。

声明临界区示例
public class CriticalDemo {

    private Object lock = new Object();

    public void fun1() {
        // 声明一段代码块为临界区,关联lock对象的monitor
        synchronized (lock) {
            // 同步代码块
        }

        // 声明一段代码块为临界区,关联this对象的monitor
        synchronized (this) {
            // 同步代码块
        }

        // 声明一段代码块为临界区,关联CriticalDemo.class对象的monitor
        synchronized (CriticalDemo.class) {
            // 同步代码块
        }
    }

    // 声明实例方法块为临界区,关联this对象的monitor
    public synchronized void fun3() {
        // 同步代码块
    }

    // 声明静态方法块为临界区,关联CriticalDemo.class的monitor
    public static synchronized void fun4() {
        // 同步代码块
    }

}

monitor就是所谓的“互斥锁”,使用synchronized实现的同步机制叫做互斥锁机制。synchronized是一个高开销的操作,需要调用操作系统内核相关接口,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。通常没有必要同步整个方法,一般同步关键代码即可。

Java虚拟机对synchronized进行了许多的优化,有适应自旋、锁消除、锁粗化等,引入了偏向锁和轻量级锁,效率有了本质上的提高。

Java虚拟机在运行时可以将锁从偏向锁升级到轻量级锁,再升级到重量级锁,这种升级过程叫做锁膨胀。

理解monitor的实现机制需要操作系统原理里面进程和线程章节的知识储备,monitor的内部实现机制是面试装逼的高频知识点,各种锁的特性和原理请参考其他资料。

六、线程间共享数据与volatile

在并发编程中,线程之间有些数据需要共享,线程之间交换信息有两种方式,共享内存和消息传递,Java采用了共享内存的方式。

Java的内存模型

堆内存中存放的是类属性和数组元素,堆内存在线程之间是共享的。栈内存中存放的是方法的形参、局部变量和异常处理器参数,栈内存是线程私有的。线程间交换信息需要堆内存作为桥梁。详细可参阅Java内存模型

指令重排序

为了提高程序执行性能,编译器和CPU会对指令做重排序优化,导致指令在实际运行时可能并不是严格按照代码语句顺序执行的。重排序在单线程环境下不会有什么问题,而在多线程环境下,逻辑的正确性依赖于内存访问顺序。

内存可见性

在不使用同步的情况下,Java方法执行前,会将处在堆内存中的共享变量复制一份副本到自己的栈内存中,后期对共享变量的操作都是针对栈内存中的副本进行的,方法退出时再将副本回刷到堆内存中。这导致在多线程情况下,在同一个时间点,各个线程所看到的共享变量的值可能是不一致的。内存可见性要求线程对共享变量值的修改能够及时地被其它线程看到。

内存屏障

Java编译器通过插入一系列指令来保证共享变量的内存可见性。

volatile的作用

使用volatile修饰共享变量后,编译器和虚拟机不会对共享变量进行指令重排序优化,同时还会为共享变量开启内存屏障,保证线程之间可以实时感知volatile共享变量的值变化,这样就能满足一些多线程情况下对变量可见性有要求而对顺序没有要求的需求。

在访问volatile变量时不会执行加锁操作,也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。volatile不是互斥同步,它不能保证共享变量状态的原子性,在代码中过度依赖于volatile变量来控制同步状态,往往比使用同步机制更加不安全。

使用volatile必须符合下列条件:
  1. 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
  2. 该变量没有包含在具有其他变量的不变式中。

总之,只有在状态真正独立于程序内其他内容时才能使用volatile。

正确使用 Volatile 变量

七、线程间隔离数据与ThreadLocal

线程隔离:线程在并发执行时不会影响到其他线程,线程之间不争抢资源,独立执行无需协作。

ThreadLocal提供了线程内存储变量的能力,使用ThreadLocal管理变量时,每一个使用变量的线程都获得该变量的一个副本,副本之间相互独立,每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。ThreadLocal适用于变量在线程间隔离而在方法间共享的场景,可以理解为变量在线程间“同名不同值”。

ThreadLocal的实现原理
public class Thread implements Runnable {
    ......
    // 每个线程都有一个ThreadLocalMap
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ......
}

// ThreadLocal负责访问和维护线程的ThreadLocalMap
public class ThreadLocal<T> {

    public void set(T value) {
        ThreadLocalMap map = Thread.currentThread().threadLocals;
        map.set(this, value);
    }

    public T get() {
        ThreadLocalMap map = Thread.currentThread().threadLocals;
        return map.getEntry(this).value;
    }

    public void remove() {
        ThreadLocalMap map = Thread.currentThread().threadLocals;
        map.remove(this);
    }
}
ThreadLocal 的应用示例
public class Task implements Runnable {

    // 创建线程局部变量threadName
    private ThreadLocal threadName = new ThreadLocal<String>();

    public void run() {        
        // 写入变量
        threadName.set(Thread.currentThread().getName());

        // 读取变量
        String nameValue = threadName.get();
        System.out.println(Thread.currentThread().getName() + "==" + nameValue+ "?");

        // 使用完记得remove,避免内存泄漏
        threadName.remove();
    }
}

public static void main(String[] args) {
    for(int i = 0; i < 20; i++) {
        new Thread(new Task()).start();
    }
}
支持继承的线程局部变量

InheritableThreadLocal类是ThreadLocal的子类,它允许一个线程创建的所有子线程访问其父线程写入的变量。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。