Java线程并发之锁

 既然java内置了synchronized,为什么还要出现lock呢?
 由于synchronized的并发是阻塞的。当一个线程获得了锁,并执行其代码时,其它线程便只能等待锁的释放。
 在这里要释放锁有如下情况:

  • 正常情况下,当代码执行完毕会释放锁。
  • 当线程执行发生异常,JVM会让线程自动释放锁。

 那如果线程内部由于什么原因(IO操作,sleep())阻塞了,又没有抛出异常,那其它钱程只能干等着。
 那我们就要想办法可以中断线程的等待,这就出现了lock。
lock可以知道是否获取到了锁,而synchronized不能。
也就是说lock比synchronized有更多的功能。
 注意一下下面两点:

  • synchronized是java语言的关键字,是java的内置特性,而lock只是一个类。
  • synchronzied是系统释放锁,而不需要手动地去释放锁。但是lock需要手动的调用unlock来释放锁,否则会出现死锁。

Lock

查看源码Lock是一个接口,它有如下方法:

public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throwsInterruptedException;   
void unlock();
Condition newCondition();
}

从方法名字可以看出,lock(),tryLock(),tryLock(long time,TimeUnit unit),lockInterruptibly()是用来获取锁的。

lock()

 此方法就是用来获取一个锁的,如果锁已被别的线程获得,那就进行等待。
 前面讲到必须使用lock必须手动释放锁,并且在线程发生异常时,系统也不会自动释放的,所以我们就应该把任务代码放在try{}catch(){}中执行最后 在finally{}代码里面释放:

lock.lock();
try{
}catch(){
}finally{
lock.unlock()
}

tryLock()&tryLock(long time,TimeUnit unit)

 这个方法是有返回值的,它用来尝试获取锁,如果获取成功则返回true,反之则返回false,这个方法会立即返回,并不会等待。
tryLock(long time,TimeUnit unit)和tryLock类似,只是前者如果一开始没有获取到锁会等待一段时间。如果在时间范围内拿到了锁就会返回true。
 使用格式:

if(lock.tryLock()) {
try{   
//处理任务
}catch(Exception ex){
}finally{
lock.unlock();   //释放锁  
}
}else {
//如果不能获取锁,则直接做其他事情
}

lockInterruptibly()

 这个方法比较特殊。当通过这个方法获取锁时,如果此钱程正在等待获取锁,则这个钱程可以响应等待中断,即中断钱程等待状态。举个粟子,当A,B线程都试图使用lockInterruptibly()获取锁时,如果A获得了锁,B线程正在等待获取锁,则可以调用threadB.interrupt()能够中断线程B的等待。
因为该方法抛出了异常。所以应该放在try代码块或者在方法申明时抛出异常。
 一般使用格式:

try{
lock.lockInterruptibly();
}catch(InterruptedException  e){
}finally{
lock.unlock();
}

 注意的是,如果线程已经获得了锁是不会中断的。并且正在执行的线程是不会被中断的,只有在阻塞中的线程才会被中断。
而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

newCondition()

 此方法返回一个Condition 对象。以ReentrantLock(稍后介绍)为例。一个ReentrantLock对象可以有多个Condition对象。Condition是为解决Object.wait/notify/notifyAll难以使用的问题。下面就简单说一下Condition。
 ynchronized锁配合的线程等待(Object.wait)与线程通知(Object.notify),那么对于JDK1.5 的 java.util.concurrent.locks.ReentrantLock 锁,JDK也为我们提供了与此功能相应的java.util.concurrent.locks.Condition。
 在Condition中,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll(),传统线程的通信方式,Condition都可以实现,这里注意,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。接口方法名称的改变是为了区分Object中的方法,因为Condition也是Object的子类。
 在调用await()方法前线程必须获得重入锁,调用await()方法后线程会释放当前占用的锁。同理在调用signal()方法时当前线程也必须获得相应重入锁,调用signal()方法后系统会从condition.await()等待队列中唤醒一个线程。当线程被唤醒后,它就会尝试重新获得与之绑定的重入锁,一旦获取成功将继续执行。所以调用signal()方法后一定要释放当前占用的锁,这样被唤醒的线程才能有获得锁的机会,才能继续执行。
Condition接口中的方法:

void await() throws InterruptedException;  
void awaitUninterruptibly();  
long awaitNanos(long nanosTimeout) throws InterruptedException;  
boolean await(long time, TimeUnit unit) throws InterruptedException;  
boolean awaitUntil(Date deadline) throws InterruptedException;  
void signal();  
void signalAll();  

 Condition是与Lock绑定的,所以也有Lock的公平性:如果是公平锁,线程为按照FIFO的顺序从Condition.await中释放,如果是非公平锁,那么后续的锁竞争就不保证FIFO顺序了。
 一个使用Condition实现生产者消费者的例子:

final Lock lock = new ReentrantLock();//锁对象  
   final Condition notFull  = lock.newCondition();//写线程条件   
   final Condition notEmpty = lock.newCondition();//读线程条件   
  
   final Object[] items = new Object[100];//缓存队列  
   int putptr/*写索引*/, takeptr/*读索引*/, count/*队列中存在的数据个数*/;  
  
   public void put(Object x) throws InterruptedException {  
     lock.lock();  
     try {  
       while (count == items.length)//如果队列满了   
         notFull.await();//阻塞写线程  
       items[putptr] = x;//赋值   
       if (++putptr == items.length) putptr = 0;//如果写索引写到队列的最后一个位置了,那么置为0  
       ++count;//个数++  
       notEmpty.signal();//唤醒读线程  
     } finally {  
       lock.unlock();  
     }  
   }  
  
   public Object take() throws InterruptedException {  
     lock.lock();  
     try {  
       while (count == 0)//如果队列为空  
         notEmpty.await();//阻塞读线程  
       Object x = items[takeptr];//取值   
       if (++takeptr == items.length) takeptr = 0;//如果读索引读到队列的最后一个位置了,那么置为0  
       --count;//个数--  
       notFull.signal();//唤醒写线程  
       return x;  
     } finally {  
       lock.unlock();  
     }  
   }   

ReentrantLock

 ReentrantLock 类实现了 Lock,它拥有与 synchronized相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。这意味着当许多线程都在争用同一个锁时,使用 ReentrantLock 的总体开支通常要比 synchronized少得多。
 ReentrantLock构造器的一个参数是 boolean 值,它允许您选择想要一个 公平(fair)锁,还是一个 不公平(unfair)锁。公平锁使线程按照请求锁的顺序依次获得锁;而不公平锁则允许讨价还价,在这种情况下,线程有时可以比先请求锁的其他线程先得到锁。当然,公平总是好的,但是你需要保证它公平那你就需要牺牲性能。花掉性能成本来保证它是公平锁。作为默认设置,除非公平对您的算法至关重要,需要严格按照线程排队的顺序对其进行服务。否则我们都应该把公平设置为 false,默认的无参构造函数就是非公平锁。
 传统的synchronized是不公平的,而且永远都不公平。但是没有人抱怨过线程饥渴,因为 JVM 保证了所有线程最终都会得到它们所等候的锁。确保统计上的公平性,对多数情况来说,这就已经足够了,而这花费的成本则要比绝对的公平保证的低得多。
 从ReentrantLock的名字知道,它是一个可重入锁。也就是说支持同一个线程对同一资源的重复加锁。另外synchronized关键字隐式的支持重进入。
一个小demo:

public class ReentrantLockTest { 
   private Lock lock = new ReentrantLock();  
  private int count;  
  public static void main(String[] args) { 
   ReentrantLockTest test = new ReentrantLockTest();   
     for (int i = 0; i < 3; i++) {   
      new Thread() {            
    @Override       
   public void run() {  
         test.test(this);   
 }  
}.start();  
      } 
  }    
protected void test(Thread thread) {   
     lock.lock();    
    try {           
 for (int i = 0; i < 5; i++) {     
           count++;       
         System.out.println("当前线程" + thread.getName() + "之后" + count);    
        }      
  } finally {  
          lock.unlock();       
 }  
  }}

 还有为保证读写效率提高的读写锁:ReentrantReadWriteLock ,它与ReentrantLock是相互独立的实现。没有继承和实现关系。这里就不多说,可以看看其它资料。

ReentrantLock与synchronized的区别

 算是总结吧。

  • 与synchronized相比,ReentrantLock提供了更多,更加全面的功能,具备更强的扩展性。例如:时间锁等候,可中断锁等候,锁投票。
  • ReentrantLock还提供了条件Condition,对线程的等待、唤醒操作更加详细和灵活,所以在多个条件变量和高度竞争锁的地方,ReentrantLock更加适合(以后会阐述Condition)。
  • ReentrantLock提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而synchronized则一旦进入锁请求要么成功要么阻塞,所以相比synchronized而言,ReentrantLock会不容易产生死锁些。
  • ReentrantLock支持更加灵活的同步代码块,但是使用synchronized时,只能在同一个synchronized块结构中获取和释放。注:ReentrantLock的锁释放一定要在finally中处理,否则可能会产生严重的后果。
  • ReentrantLock支持中断处理,且性能较synchronized会好些。

 看起来ReentrantLock功能比synchronized强大得多,那是不是我们以后在做线程同步的时候都应该用ReentrantLock而抛弃synchronized呢?当然不是,前面 我们说过使用Lock的时候,我们必须在finally块中手动调用lock.unlock()。如果我们忘记了,那么会为程序埋下很大的隐患。还有一个原因就是当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程转储时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源。 Lock类只是普通的类,JVM 不知道具体哪个线程拥有 Lock对象。而且,几乎每个开发人员都熟悉 synchronized,它可以在 JVM 的所有版本中工作。在 JDK 5.0 成为标准(从现在开始可能需要两年)之前,使用 Lock类将意味着要利用的特性不是每个 JVM 都有的,而且不是每个开发人员都熟悉的。
 那我们什么时候应该使用Lock而不是synchronized呢?既然如此,我们什么时候才应该使用 ReentrantLock 呢?答案非常简单 —— 在确实需要一些 synchronized 所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票。 ReentrantLock还具有可伸缩性的好处,应当在高度争用的情况下使用它,但是请记住,大多数 synchronized 块几乎从来没有出现过争用,所以可以把高度争用放在一边。我建议用 synchronized 开发,直到确实证明 synchronized 不合适,而不要仅仅是假设如果使用 ReentrantLock “性能会更好”。请记住,这些是供高级用户使用的高级工具。(而且,真正的高级用户喜欢选择能够找到的最简单工具,直到他们认为简单的工具不适用为止。)。一如既往,首先要把事情做好,然后再考虑是不是有必要做得更快。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,681评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,710评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,623评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,202评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,232评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,368评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,795评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,461评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,647评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,476评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,525评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,226评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,785评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,857评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,090评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,647评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,215评论 2 341

推荐阅读更多精彩内容