天天看点

【Java并发】锁

简介

        锁是控制多个线程对共享资源访问(读/写,读:读内容;写:修改内容+删除文件)的工具。通常,锁提供了对共享资源的独占访问:对共享资源的任何访问都需要首先获取锁,一次只能有一个线程获取锁。

       锁不仅是关于同步和互斥 的,也是关于内存可见 的。为了保证所有线程都能够看到共享的/可变变量的最新值,读取和写入线程必须使用公共的锁进行同步。

     针对有读/写两种不同访问方式,能不能做的更智能一些,有没有锁机制能允许对共享资源并发“读”访问,利用 ReadWriteLock 的读锁可以!!。

JDK 1.5 之前,协调共享对象 的访问(coordinate access to shared data )能采取的机制是synchronized 和volatile 。在1.5 后,通过显式定义同步锁对象(Lock对象)来实现同步,增加了ReentrantLock 。这样,大致可将锁分为implicit lock(内部锁) 和explicit lock(显示锁)。

一些概念

同步和互斥

当多个线程对共享资源访问的时候,很有可能会带来线程安全问题,可以通过同步监视器(synchronized同步块or同步方法)。

独占锁,互斥锁

死锁

两个线程互相等待对方释放同步监视器时就会发生死锁。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,所有线程处于阻塞状态。

用锁来保护状态

一个错误观念:只有在写入共享变量时才需要同步。

构造线程安全类一种常见的锁规则:

在对象内部封装可变状态,通过对象的内部锁 来同步任何对这些可变状态访问的代码路径,从而保证在并发访问中的安全。

Synchronized

同步方法和同步代码块的粒度:

同步方法可能会带来活跃度与性能的问题,通过缩小synchronized块的范围我们既可以维护线程安全性,还能比较容易地提升并发性。但也不能让synchronized太小:不可以将一个原子操作分解到多个synchronized块中。不过应该尽量从synchronized块中分离耗时的且不影响共享状态的操作(参考并发编程实践p30) 。对于独享地操作本地(基于栈)变量,这些变量不被多线程共享,可以不放在同步块当中。

重进入(Reentrancy)

内部锁是可以重进入的,即线程在试图获取它自己占有的锁时,请求会成功。重进入的实现是通过为每个锁关联一个请求计数和一个占有它的线程。

何时释放同步监视器的锁定?

Lock接口

Lock lock = new ..;
...
lock.lock();
try{
//需要锁保护的更新对象的状态
}finally{
lock.unlock();
}
           

 finally千万不能丢!!

Lock接口实现类有:    ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock。

其中ReentrantLock实现了标准的互斥锁。

ReentrantLock

可重入

线程可以对它已经加锁的ReentrantLock锁再次加锁。所以一段被锁保护的代码可以调用另一个被相同锁保护的代码。

在 synchronized 与 ReentrantLock 间选择

使用 ReentrantLock 时有些要额外注意的地方,比如在 finally 里调用 unlock() 。

ReentrantLock 可伸缩性( scalability )更好。--如何理解??

对于如何选择可以遵循这样的原则:在内部锁不能满足需求时,再考虑使用 ReentrantLock 。 (内部锁能处理哪些问题??)当需要一些高级特性 时, ReentrantLock 才应该被使用:可定时的( timed )、可轮询的( polled )、可中断的( interruptible )锁获取操作,公平队列,非块结构锁( none-block-structured )。

Volatile

读一个 volatile 变量时,总能返回由某一线程写的最新值。 Volatile 的一个典型应用是:检查状态标记(完成、中断、其他状态) 。

访问volatile变量的操作不会加锁,也就不会引起执行线程的阻塞,volatile相对synchronized而言可以说是轻量级的同步机制。

Volatile 只能保证变量的“可见性 ”,却不能保证“原子性”。(相比较:加锁既能保证可见性也能保证原子性)。

示例

// 判断是否睡觉,没有的话继续数数,如果已睡,则停止数数。

volatile boolean asleep;

while(!asleep){

   countNum();

}

在考虑使用 volatile 时,思考下面问题 :

1. 写入变量时是否依赖变量的当前值?或者是否只有一个线程执行写操作?

如果不依赖或者能确定只有一个线程写则可以考虑

【Java并发】锁

2. 变量是否要与其他变量一起参与不变约束?

如果没有则可以考虑。

3. 访问变量时,是否有其他原因需要加锁?

如果没有则可以考虑。

关于CAS和原子变量的信息可参见http://kenwublog.com/the-theory-of-volatile

读-写锁

读写锁能够保证一个资源能同时被多个读者访问,或者只被一个写者访问。当对容器“读”操作较多,而很少写的时候,使用ReadWriteLock比使用ReentrantLock能获得更高的吞吐量。如为LinkedHashMap提供更高的并发访问。

ReadWriteLock lock = new ReentrantReadWriteLock();
Lock w = lock.writeLock();//获得"写锁"实例
Lock r = lock.readLock()//获得“读锁”实例

//对写操作加锁
w.lock();
try{
   //write operation
}finally{
   w.unlock();
}
//对读操作加锁
r.lock();
try{
   //read operation
}finally{
   r.unlock();
}
           

能够用同一个读写锁保护不同的资源么?

阅读材料

http://wenku.baidu.com/view/a0104d39580216fc700afd90.html

http://www.jdon.com/concurrency.html

【公司源码查看】: UserAccountLockService