首页 > 代码库 > Java显式锁学习总结之一:概论

Java显式锁学习总结之一:概论

  我们都知道在java中,当多个线程需要并发访问共享资源时需要使用同步,我们经常使用的同步方式就是synchronized关键字,事实上,在jdk1.5之前,只有synchronized一种同步方式。而在jdk1.5中提供了一种新的同步方式--显示锁(Lock)。显示锁是随java.util.concurrent包一起发布的,java.util.concurrent包是并发大神Doug Lea写的一个并发工具包,里面除了显示锁,还有许多其他的实用并发工具类。

什么是显示锁

  什么是显示锁?用一段代码来说明:

package com.gome;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockPractice {
    private static int a=0;
    private static Lock lock=new ReentrantLock();
    
    public static void increateBySynchronized(){
        a=0;
        for (int i = 0; i < 1000; i++) {
            Thread t=new Thread(new Runnable() {
                public void run() {
                    for (int j = 0; j < 100; j++) {
                        synchronized (LockPractice.class) {
                            a++;
                        }
                    }
                }
            });
            t.start();
        }
        while (Thread.activeCount()>1) {
            Thread.yield();
        }
        System.out.println(a);
    }
    
    public static void increateByLock(){
        a=0;
        for (int i = 0; i < 1000; i++) {
            Thread t=new Thread(new Runnable() {
                public void run() {
                    for (int j = 0; j < 100; j++) {
                        lock.lock();
                        try {
                            a++;
                        } finally {
                            lock.unlock();
                        }
                    }
                }
            });
            t.start();
        }
        while (Thread.activeCount()>1) {
            Thread.yield();
        }
        System.out.println(a);
    }
    
    public static void main(String[] args) {
        increateBySynchronized();
        increateByLock();
    }
}

执行结果:

技术分享

解释:

类LockPractice 中有两个方法increateBySynchronized()和increateByLock(),这两个方法都成功地用多线程并发将a累加到100000而没有出现竞态条件(race condition)问题。

其中increateBySynchronized()的同步是我们熟悉的synchronized关键字实现的:

synchronized (LockPractice.class) {
  a++;
}

这句代码的含义是:当有一个线程A进入synchronized代码块后,阻塞其他要进入该代码块的线程直到A执行完代码块。synchronized关键字会关联一个锁对象,这里是LockPractice.class。synchronized关键字底层是由jvm来实现的,当一个线程进入synchronized块时,会在关联的锁对象的对象头(MarkWord)中记录下线程信息(可以简单的理解为线程id),这样这个锁对象就被当前线程独占了,其他试图获取这个锁对象的线程将被阻塞。

因此一个线程进入、退出synchronized代码块的本质就是这个线程对锁对象的获取、释放。

 

而increateByLock()的同步代码如下,其中 lock是全局变量 private static Lock lock=new ReentrantLock();

lock.lock();
try {
    a++;
} finally {
    lock.unlock();
}

从代码上可以看出,显示锁Lock的使用和synchronized的本质很像,也是定义了一个锁对象(new ReentrantLock()),然后在进入同步代码前加锁,执行同步代码后释放锁。

但是显示锁的底层却和synchronized完全不同,并没有使用到对象头(MarkWord)这样底层的东西,显示锁只是表现出了和synchronized一样的行为(第一个访问同步代码的线程获得锁,阻塞后来的线程)。

 

显示锁的优点

从上面的描述来看,显示锁实现了和synchronized一样的功能,但是写起来更复杂(需要手动加锁解锁,还需要写finally防止发生异常后锁不能释放),那为什么还要加入显示锁呢?

我们可以从Lock接口提供的方法看出端倪:

方法名称 描述
void lock() 获取锁
void lockInterruptibly() throws InterruptedException 可中断地获取锁,在线程获取锁的过程中可以响应中断
boolean tryLock()   尝试非阻塞获取锁,调用方法后立即返回,成功返回true,失败返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException 在超时时间内获取锁,到达超时时间将返回false,也可以响应中断
void unlock(); 释放锁
Condition newCondition(); 获取等待组件,等待组件实现类似于Object.wait()方法的功能

从Lock提供的接口可以看出来,显示锁至少比synchronized多了以下功能:

  1. 可中断获取锁:使用synchronized关键字获取锁的时候,如果线程没有获取到被阻塞了,那么这个时候该线程是不响应中断(interrupt)的,而使用Lock.lockInterruptibly()获取锁时被中断,线程将抛出中断异常。
  2. 可非阻塞获取锁:使用sync关键字获取锁时,如果没有成功获取,只有被阻塞,而使用Lock.tryLock()获取锁时,如果没有获取成功也不会阻塞而是直接返回false。
  3. 可限定获取锁的超时时间:使用Lock.tryLock(long time, TimeUnit unit)。
  4. 其实显示锁还有其他的优势,比如同一锁对象上可以有多个等待队列(相当于Object.wait()),我们后面会讲。

其实除了更多的功能,显示锁还有一个很大的优势:synchronized的同步是jvm底层实现的,对一般程序员来说程序遇到出乎意料的行为的时候,除了查官方文档几乎没有别的办法;而显示锁除了个别操作用了底层的Unsafe类之外,几乎都是用java语言实现的,我们可以通过学习显示锁的源码,来更加得心应手的使用显示锁。

显示锁的缺点 

当然显示锁也不是完美的,否则java就不会保留着synchronized关键字了,显示锁的缺点主要有两个:

  1. 使用比较复杂,这点之前提到了,需要手动加锁,解锁,而且还必须保证在异常状态下也要能够解锁。而synchronized的使用就简单多了。
  2. 效率较低,synchronized关键字毕竟是jvm底层实现的,因此用了很多优化措施来优化速度(偏向锁、轻量锁等),而显示锁的效率相对低一些。

因此当需要进行同步时,优先考虑使用synchronized关键字,只有synchronized关键字不能满足需求时,才考虑使用显示锁。

总结

这篇文章介绍了显示锁是什么,显示锁的优点与缺点,在什么情况下会用到显示锁。

后文将重点学习显示锁的底层实现:队列同步器(AbstractQueuedSynchronizer)的实现、重入锁(ReentrantLock)的实现、读写锁(ReadWriteLock)的实现、等待/通知(Condition)的实现。

Java显式锁学习总结之一:概论