线程安全与锁

线程安全:

当多个线程访问某个类时,不管运行环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能正确表现出正确的行为,你们就称这个类是线程安全的

一、原子性与可见性

1、可见性

可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。

例子就不举了,比较好理解直接写出结论

1.1、主内存与工作内存

根据Java Language Specification中的说明, jvm系统中存在一个主内存(Main Memory或Java Heap Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。

每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。

1.2、volatile关键字

  • 能够保证volatile变量的可见性
  • 不能保证volatile变量复合操作的原子性

通俗地讲:volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样任何时刻,不同的线程总能看到该变量的最新值。

volatile关键字应该属于一种最轻量级的同步机制。

1.3、volatile、long与原子性

关于volatile与原子性的问题,有一点绕。有一道面试就是volatile能不能把一个非原子操作变成原子操作?答案是有的。

要了解这个问题,首先知道一下它能把哪些非原子操作变成原子操作。

在JVM中,int等不大于32位的基本类型的操作都是原子操作,但是某些jvm对long和double类型的操作并不是原子操作,这样就会造成错误数据的出现。 因为这两种数据类型是64位的,在某些32位的虚拟机中,对一个变量的读写就可能分两步完成,这样可能会造成多线程的问题,也就说即使是对一个变量的读或者写操作,也不是原子操作。

而volatile把一个非原子操作变成原子操作的范围,也仅限于以上的这种情况。

而且,volatile保证的,也只是对于变量读或者写操作的原子性保证,对于变量的自加操作,还是无法保证的。

2、原子性

虽然volatile关键字可以保证变量的可见性,但是并不能保证原子性,所以想要保证原子性,就必须采取其他措施:

  • 使用java.util.concurrent.atomic 包中的原子类
  • 加锁
volatile 加锁
可见性 可以 可以
原子性 不可以 可以

二、锁

2.1、内置锁-Synchronized

Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)

当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码

  1. 当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
  2. 然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
  3. 尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
  4. 第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
  5. 以上规则对其它对象锁同样适用.

2.2、锁的等级

从等级上,锁可以分为方法锁,对象锁,类锁。

这些概念很绕,花了很长时间才搞明白,首先,我觉得他们三个在概念上并不是同级的。

首先看一下synchronized的用法:synchronized修饰方法和synchronized修饰代码块。

而方法分为实例方法和静态方法,正是这两种方法的区别,才有了对象锁和类锁

2.2.1、对象锁的synchronized修饰方法和代码块:

1
2
3
4
5
6
7
8
9
10
11
public class TestSynchronized {    
public void test1(){
synchronized(this){
///
}
}

public synchronized void test2(){
///
}
}

第一个方法时用了同步代码块的方式进行同步,传入的对象实例是this,表明是当前对象,当然,如果需要同步其他对象实例,也不可传入其他对象的实例;

第二个方法是修饰方法的方式进行同步。在修饰方法的时候默认是当前对象作为锁的对象.

所以两个同步代码所需要获得的对象锁都是同一个对象锁

2.2.2、类锁的synchronized修饰(静态)方法和代码块:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestSynchronized   
{
public void test1(){
synchronized(TestSynchronized.class){
///
}
}

public static synchronized void test2(){
///
}

}

第一个方法时用了同步代码块的方式进行同步,传入的对象实例是TestSynchronized.class,表明是当前的类

在修饰类时候默认是当前类的Class对象作为锁的对象.

所以这两个方法获得的也是同一个锁。

2.2.3、synchronized同时修饰静态和非静态方法

1
2
3
4
5
6
7
8
9
public class TestSynchronized{    
public synchronized void test1(){
///
}

public static synchronized void test2(){
///
}
}

上面代码synchronized同时修饰静态方法和实例方法,但是运行结果是交替进行的,这证明了类锁和对象锁是两个不一样的锁,控制着不同的区域,它们是互不干扰的。同样,线程获得对象锁的同时,也可以获得该类锁,即同时获得两个锁,这是允许的。

2.2.4、总结

通过以上几个例子可以看出,方法锁可以使用实例对象或者类对象,他们的关系如下:

修饰方法 修饰代码块
对象锁 synchronized void test1(){} synchronized(this){}
类锁 static synchronized void test2(){} synchronized(TestSynchronized.class){}

有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的

2.3、显式锁-ReetrantLock

在Java5.0之前,协调对共享对象的访问时可使用的机制只有synchronized和volatile。Java5.0新增了一种新的机制:ReentrantLock。

在java.util.concurrent.locks包中定义了一个Lock接口

1
2
3
4
5
6
7
8
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}

与内置锁不同的是,Lock:

  • 可轮询的
  • 定时的
  • 可中断的

锁获取操作,所有加锁和解锁的方法都是显示的。

2.3.1、ReetrantLock

也叫重入锁,是一种递归无阻塞的同步机制(利用CAS机制)。ReentrantLock类实现了Lock接口

1
2
3
4
5
6
7
8
Lock lock = new ReentrantLock();  
lock.lock();
try {
// update object state
}
finally {
lock.unlock();
}

Lock和 synchronized 有一点明显的区别 —— Lock 必须在 finally 块中释放

2.3.2、轮询锁

在内置锁中,死锁是致命的——唯一的恢复方法是重新启动程序,唯一的预防方法是在构建程序时不要出错。

而可轮询的与可定时的锁提供了另一种选择:避免死锁的发生。

如果你不能获得所有需要的锁,那么使用可轮询的获取方式使你能够重新拿到控制权,它会释放你已经获得的这些锁,然后再重新尝试。可轮询的锁获取模式,由tryLock()方法实现。此方法仅在调用时锁为空闲状态才获取该锁。如果锁可用,则获取锁,并立即返回值true。如果锁不可用,则此方法将立即返回值false。

1
2
3
4
5
6
7
8
9
10
11
12
13
if(l1.tryLock()){
try{
if(l2.tryLock()){
try{
///
}finally{
l2.unlock();
}
}
}finally{
l1.unlock();
}
}

2.3.3、定时锁

定时锁,当在带有时间限制的操作中调用了一个阻塞方法时,它能根据剩余时间来提供一个限时,如果操作不能在指定时间内给出结果,你们就会使程序提前结束。

可定时的锁获取模式,由tryLock(long, TimeUnit)方法实现。

1
2
3
4
5
6
7
8
if(!l.tryLock(time, unit)){
return false;
}
try{
///
}finally{
l.unlock();
}

2.3.4、可中断的锁

可中断的锁获取操作允许在可取消的活动中使用。lockInterruptibly()方法能够使你获得锁的时候响应中断。

1
2
3
4
5
6
7
8
9
10
11
12
public boolean fun1(String s) throws InterruptedException{
l.lockInterruptibly();
try{
return fun2(s);
}finally{
l.unlock();
}
}

private boolean fun2(String s) throws InterruptedException{
///
}

可定时的tryLock()方法同样能响应中断,因此当需要实现一个定时的和可中断的锁获取操作时,可以使用tryLock()方法

2.3.5、ReentrantLock与synchronized比较

ReentrantLock与synchronized比较优势:

  • ReentrantLock在加锁和内存上提供的语义与内置锁相同
  • ReentrantLock提供一些额外的功能:可定时的、可轮询的与可中断的锁获取操作,公平队列以及非块结构的锁
  • 性能上;ReentrantLock优于synchronized。Java5.0中远远胜出,6.0中略有胜出,因为6.0对synchronized进行了优化

但是synchronized也有一些自己的优势:

  • 使用更简洁紧凑,很多现有程序中都使用了内置锁。
  • 不用显示的释放锁,没有安全隐患

因此:

ReentrantLock可以作为是更高级的工具,只有synchronized无法满足的时候才使用它,否则还是优先使用synchronized。

2.4、读写锁-ReadWriteLock

Java中锁从加锁模式上可以分为独占锁共享锁

独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock就是以独占方式实现的互斥锁。

共享锁则允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock。

很显然,独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。 java的并发包中提供了ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,或者被一个 写操作访问,但两者不能同时进行。

三、其他

1、信号量

Java中实现计数信号量的类为java.util.concurrent.Semaphore,是在1.5中引入的。

  • Semaphore中管理着一组许可,许可的初始数量可以通过构造方法来指定。
  • 在执行操作时需要先获得许可(acquire),并在使用完后释放许可(release)。
  • 如果当前没有许可,那么acquire将阻塞下到有许可可用,或者直到被中断,或者操作超时。
  • 当初始值为1时,该信号量就可以实现互斥锁的功能。

Semaphore可以用于实现资源池,例如数据库的连接池。可以构建一个固定长度的资源池,当池为空时,请求资源就会失败,但Semaphore可以实现阻塞而非失败,并且当资源非空时解除阻塞。

可以使用Semaphore将任何容器变成有界的阻塞容器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class BoundedList<T> {  

private final List<T> list;
private final Semaphore semaphore;

public BoundedList(int bound) {
list = Collections.synchronizedList(new LinkedList<T>());
semaphore = new Semaphore(bound);
}

public boolean add(T obj) throws InterruptedException {
semaphore.acquire();
boolean addedFlag = false;
try {
addedFlag = list.add(obj);
}
finally {
if (!addedFlag) {
semaphore.release();
}
}
return addedFlag;
}

public boolean remove(Object obj) {
boolean removedFlag = list.remove(obj);
if (removedFlag) {
semaphore.release();
}
return removedFlag;
}

}

2、Java中的各种锁

到目前为止,我接触到的关于所的概念有:方法锁、对象锁、类锁、独占锁、共享锁、内置锁、显示锁、互斥锁、排它锁、读写锁、自旋锁(微笑脸)。

虽然看上去很多,但是真正理解了其实也不难,因为它们是从不同角度来描述锁的

2.1、方法锁、对象锁与类锁

这三个在介绍synchronized的时候已经说过了

2.2、独占锁与共享锁

这两个是从不同线程之间持有锁的方式上区分的,只是宏观上的概念,而不是具体的某种锁

  • 独占锁模式下每次只能有一个线程持有锁,比如synchronized、ReentrantLock肯定都属于独占锁
  • 共享锁模式下可以有多个线程持有,比如读锁就可以多个线程同时读。

2.3、内置锁与显示锁

这两个应该从Java语言发展的角度来看,最开始的时候Java中只有一种加锁机制就是synchronized,后来才有了locks包中的Java对象形式的锁

  • 内置锁:指的就是synchronized,因为它是Java中最开始内置的
  • 显示锁:指后来加入的locks包中的锁,因为使用的时候必须要显示的获取锁和释放锁,ReentrantLock和ReadWriteLock都是显示锁

2.4、互斥锁与自旋锁

互斥锁和自旋锁应该都属于独占锁,只是他们的实现方式不同

他们的区别在于自旋锁不会引起线程的休眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名

四、参考地址

http://zhh9106.iteye.com/blog/2151791