多线程编程进阶——Java类库中的锁

在Java多线程中,可使用synchronized关键字来实现线程之间同步互斥,在JDK1.5之后,Java类库中新增了Lock接口用来实现相似的锁功能。下面会逐一介绍关于Java类库中所提供的锁功能。编程

锁能够理解为对共享数据进行保护的许可证,对于同一把锁保护的共享数据而言,任何线程对这些共享数据的访问都须要先持有该锁。一把锁只能同时被一个线程持有,当以一个该锁的持有线程对共享数据访问结束以后必须释放该锁,以便让其余线程持有。锁的持有线程在锁的得到和锁的释放之间的这段时间所执行的代码被称为临界区。设计模式

锁可以保护共享数据以实现线程安全,锁的主要做用有保障原子性、保障可见性和保障有序性。因为锁具备互斥性,所以当线程执行临界区中的代码时,其余线程没法作到干扰,临界区中的代码也就具备了不可分割的原子特性。数组

锁具备排他性,即一个锁一次只能被一个线程持有,这种锁又被称之为排他锁或互斥锁。固然,新版本的JDK中为了性能优化还推出了另外一种锁——读写锁,读写锁是做为了排它锁的一种改进而存在的。缓存

按照Java虚拟机对锁的实现方式划分,Java平台中的锁包括内部锁(主要是经过synchronized实现)和显式锁(主要是经过Lock接口及其实现类实现),下文将逐一介绍。安全

公平锁和非公平锁:性能优化

锁Lock分为"公平锁"和"非公平锁",公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。非公平锁就是一种获取锁的抢占机制,是随机得到锁的,和公平锁不同的就是先来的不必定先获得锁,这个方式可能形成某些线程一直拿不到锁,结果也就是不公平的了。数据结构

内部锁属于非公平锁,而显式锁不只支持公平锁并且支持非公平锁。多线程

内部锁——众所周知的synchronized

Java平台中的任何一个对象都有惟一一个与之关联的锁,这种锁被称之为监视器(或者叫内部锁)。内部锁是一种排它锁,它能保证原子性、可见性和有序性。内部锁就由synchronized关键字实现。并发

synchronized能够修饰方法或者代码块。当synchronized修饰方法的时候,该方法内部的代码就属于一个临界区,该方法就属于一个同步方法。此时一个线程对该方法内部的变量的更新就保证了原子性和可见性,从而实现了线程安全。当synchronized修饰代码块的时候,须要一个锁句柄(一个对象的引用或者是一个能够返回对象的表达式),此时synchronized关键字引导的代码块就是临界区;同步块的锁句柄能够写为this关键字,此时表示为当前对象,锁句柄对应的监视器就被称之为相应同步块的引导锁。框架

做为锁句柄的变量一般以private final修饰,防止锁句柄变量的值改变以后,致使执行同一个同步块的多个线程使用不一样的锁,从而避免了竞态。

同步实例方法至关于以"this"为引导锁的同步块;同步静态方法至关于以当前类对象为引导锁的同步块。

线程读内部锁的申请和释放均由Java虚拟机负责代为实施,内部锁的使用不会致使锁泄漏,这是由于Java编译器在将同步块代码编译成字节码的时候,对临界区中可能抛出的而程序代码中又未捕获的异常进行了特殊的处理,这使得临界区的代码即便抛出异常也不会妨碍内部锁的释放。

注意Java虚拟机会为每个内部锁分配一个入口集用于存放等待得到相应内部锁的线程,当内部锁的持有线程释放当前锁的时候,多是入口集中处于BLOCKED状态的线程得到当前锁也多是处于RUNNABLE状态的其余线程。内部锁的竞争是激烈的,也是不公平的,可能等待了长时间的线程没有得到锁,也多是没有通过等待的线程直接就得到了锁。

显式的加锁和解锁——Lock接口

在Java5.0以前,在协调对共享对象的访问时可使用的机制只有synchronized和volatile。在Java 5.0中增长了一种新的机制:Lock接口(以及其实现类如ReentrantLock等),Lock接口中定义了一组抽象的加锁操做。与synchronized不一样的是,synchronized能够方便的隐式的获取锁,而Lock接口则提供了一种显式获取锁的特性。

显式锁是自从JDK1.5以后开始引入的排它锁。显式锁是Lock接口的实例,Lock接口的默认实现类是ReentrantLock。

重入锁——ReentrantLock类

在详细介绍关于ReentrantLock类的详细信息以前,先介绍一下锁的可重入性的概念。

若是一个线程持有一个锁的时候还能继续成功的申请该锁,那么咱们就称该锁是可重入的,不然咱们就称该锁是非可重入的。
复制代码

ReentrantLock是一个可重入锁,ReentrantLock类与synchronized相似,均可以实现线程之间的同步互斥。但ReentrantLock类此外还扩展了更多的功能,如嗅探锁定、多路分支通知等,在使用上也比synrhronized更加的灵活。

上面已经提到ReentrantLock是一个既支持公平支持非公平的显示锁,因此在实例化ReentrantLock类的时候咱们能够明确的看到ReentrantLock的一个构造签名为ReentrantLock(boolean fair),当咱们传入true的时候获得的锁是一个公平锁。公平锁的开销较非公平锁的开销大,所以显式锁默认使用的是非公平的调度策略。因为ReentrantLock能够具备公平性,所以:

默认状况下使用内部锁,而当多数线程持有一个锁的时间相对较长或者线程申请锁的平均时间间隔相对长的状况下咱们能够考虑使用显式锁。

读写锁——(Read/Write Lock)

读写锁是一种改进型的排它锁。读写锁容许多个线程能够同时读取(只读)共享变量。读写锁是分为读锁和写锁两种角色的,读线程在访问共享变量的时候必须持有相应读写锁的读锁,并且读锁是共享的、多个线程能够共同持有的;写锁是排他的,以一个线程在持有写锁的时候,其余线程没法得到相应锁的写锁或读锁。总之,读写锁经过读写锁的分离从而提升了并发性。

ReadWriteLock接口是对读写锁的抽象,其默认的实现类是ReentrantReadWriteLock。ReadWriteLock定义了两个方法readLock()和writeLock(),分别用于返回相应读写锁实例的读锁和写锁。这两个方法的返回值类型都是Lock。

读写锁主要用于读线程持有锁的时间比较长的情景下。

锁的替代

多个线程共享同一个非线程安全对象时,咱们每每采用锁来保证线程安全性。可是,锁也有其弊端,好比锁的开销和在使用锁的时候容易发生死锁等。因此在Java中也提供了一些对于某些状况下替代锁的同步机制解决方案,如volatile关键字、final关键字、static关键字、原子变量以及各类并发容器和框架,这些大多数内容我将之后介绍;此外咱们还能够采用必定的多线程设计模式来完成多线程的同步。

首先介绍在并发程序设计中,咱们使用和共享对象能够采用的一些策略。上面所提到的Java内置的一些工具类和关键字以及咱们所采用的设计模式大多都基于这些策略的思想。

  1. 采用线程特有对象: 各个不一样的线程建立各自的实例,一个实例只能被一个线程访问的对象就被称之为线程的特有对象。采用线程特有对象,保障了对非线程安全对象的访问的线程安全。
  2. 只读共享:在没有额外同步的状况下,共享的只读对象能够有能够由多个线程并发访问,可是任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
  3. 线程安全共享:线程安全的对象在其内部实现同步,多个线程能够经过对象的公有接口来进行访问而不须要进一步的同步。
  4. 保护对象:被保护的对象只能经过持有特定的锁来访问。保护对象包括封装在其余线程安全对象中的对象,以及已发布的而且由某个特定锁保护的对象。

这里我首先介绍其中的volatile关键字、ThreadLocal两者在锁的某些功能上的替代做用。

volatile关键字

经过volatile关键字的使用,咱们能够保证共享变量的可见性和有序性。不一样于常见的锁,在原子性方面,volatile仅能保障写volatile变量操做的原子性,没有锁的排他性;此外,volatile关键字的使用不会引发上下文的切换,所以volatile常被称为轻量级锁。

多线程编程基础一文中,我已经初步介绍了Java的内存模型。volatile最主要的就是实现了共享变量的内存可见性,其实现的原理是:volatile变量的值每次都会从高速缓存或者主内存中读取,对于volatile变量,每个线程再也不会有一个副本变量,全部线程对volatile变量的操做都是对同一个变量的操做。

volatile变量的开销包括读变量和写变量两个方面。volatile变量的读、写操做都不会致使上下文的切换,所以volatile的开销比锁小。可是volatile变量的值不会暂存在寄存器中,所以读取volatile变量的成本要比读取普通变量的成本更高。

ThreadLocal

ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程能够根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。

ThreadLocal采用的是上述策略中的第一种设计思想——采用线程的特有对象.采用线程的特有对象,咱们能够保障每个线程都具备各自的实例,同一个对象不会被多个线程共享,ThreadLocal是维护线程封闭性的一种更加规范的方法,这个类能使线程中的某个值与保存值的对象关联起来,从而保证了线程特有对象的固有线程安全性。

ThreadLocal类至关于线程访问其线程特有对象的代理,即各个线程经过这个对象能够建立并访问各自的线程特有对象,泛型T指定了相应线程持有对象的类型。一个线程可使用不一样的ThreadLocal实例来建立并访问其不一样的线程持有对象。多个线程使用同一个ThreadLocal实例所访问到的对象时类型T的不一样实例。代理的关系图以下:

Aaron Swartz

ThreadLocal提供了get和set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,所以get老是能返回由当前执行线程在调用set时设置的最新值。其主要使用的方法以下:

public T get(): 获取与当前线程中ThreadLocal实例关联的线程特有对象。
	public void set(T value):从新关联当前线程中ThreadLocal实例所对应的线程特有对象。
	protected T initValue():若是没有调用set(),在初始化threadlocal对象的时候,该方法的返回值就是当前线程中与ThreadLocal实例关联的线程特有对象。
	public void remove():删除当前线程中ThreadLocal和线程特有对象的关系。
复制代码

那么ThreadLocal底层是如何实现Thread持有本身的线程特有对象的?查看set()方法的源代码:

Aaron Swartz

Aaron Swartz
能够看到,当咱们调用threadlocal的set方法来保存当前线程的特有对象时,threadlocal会取出当前线程关联的threadlocalmap对象,而后调用ThreadLocalMap对象的set方法来进行当前给定值的保存。

Aaron Swartz

每个Thread都会维护一个ThreadLocalMap对象,ThreadLocalMap是一个相似Map的数据结构,可是它没有实现任何Map的相关接口。ThreadLocalMap是一个Entry数组,每个Entry对象都是一个"key-value"结构,并且Entry对象的key永远都是ThreadLocal对象。当咱们调用ThreadLocal的set方法时,实际上就是以当前ThreadLocal对象自己做为key,放入到了ThreadLocalMap中。

可能发生内存泄漏:

经过查看Entry结构可知,Entry属于WeakReference类型,所以Entry不会阻止被引用的ThreadLocal实例被垃圾回收。当一个ThreadLocal实例没有对其可达的强引用时,这个实例就能够被垃圾回收,即其所在的Entry的key会被置为null,可是若是建立ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,从而发生内存泄露。

解决内存泄漏的最有效方法就是,在使用完ThreadLocal以后,要注意调用threadlocal的remove()方法释放内存。

相关文章
相关标签/搜索