通常在使用多线程环境时,使用synchronized
来保证线程安全。
大多数情况下,synchronized
关键字是一个不错的选择,但它有一些缺点导致在Java Concurrency
包中包含Lock API。Java 1.5 Concurrency API提供了带有Lock
接口的java.util.concurrent.locks
包和一些实现类来改进对象锁定机制。
Java Lock API中的一些重要接口和类是:
Lock
:它是Lock API的基本接口。它提供了synchronized
关键字的所有功能,以及为锁定创建不同条件的其他方法,为线程等待锁定提供超时。一些重要的方法是lock()
获取锁,unlock()
释放锁,tryLock()
等待锁定一段时间,newCondition()
创建条件等。Condition
:Condition
对象类似于对象等待通知模型,具有创建不同等待集的附加功能。Condition
对象始终由Lock
对象创建。一些重要的方法是await()
,它类似于wait()
和signal()
,signalAll()
,类似于notify()
和notifyAll()
方法。ReadWriteLock
:它包含一对关联的锁,一个用于只读操作,另一个用于写入。只要没有写入器线程,读锁定可以由多个读取器线程同时保持。写锁是独占的。
ReentrantLock
:这是最广泛使用的Lock
接口实现类。此类以与synchronized
关键字类似的方式实现Lock
接口。除了Lock
接口实现之外,ReentrantLock
还包含一些实用程序方法来获取持有锁的线程,线程等待获取锁等。
synchronized
块本质上是可重入的,即如果一个线程锁定了监视器对象,并且另一个同步块需要锁定在同一个监视器对象上,则线程可以进入该代码块。下面通过一个简单的例子来理解这个特性。
public class Test{
public synchronized foo(){
//do something
bar();
}
public synchronized bar(){
//do some more
}
}
如果一个线程进入foo()
,它就会锁定Test
对象,所以当它尝试执行bar()
方法时,允许该线程执行bar()
方法,因为它已经在Test
对象上持有锁,即如同同步(本)。
Java锁示例 - ReentrantLock
现在让我们看一个简单的例子,使用Java Lock API替换synchronized
关键字。
假设有一个Resource
类,其中包含一些操作,我们希望它是线程安全的,以及一些不需要线程安全的方法。
public class Resource {
public void doSomething(){
//do some operation, DB read, write etc
}
public void doLogging(){
//logging, no need for thread safety
}
}
现在假设有一个实现Runnable
的类,并使用Resource
对象。
public class SynchronizedLockExample implements Runnable{
private Resource resource;
public SynchronizedLockExample(Resource r){
this.resource = r;
}
@Override
public void run() {
synchronized (resource) {
resource.doSomething();
}
resource.doLogging();
}
}
请注意,使用synchronized
块来获取Resource
对象上的锁。我们可以在类中创建一个虚拟对象,并将其用于锁定目的。
现在让我们看看如何使用java Lock API并重写上面的程序而不使用synchronized
关键字。并将在java中使用ReentrantLock
。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConcurrencyLockExample implements Runnable{
private Resource resource;
private Lock lock;
public ConcurrencyLockExample(Resource r){
this.resource = r;
this.lock = new ReentrantLock();
}
@Override
public void run() {
try {
if(lock.tryLock(10, TimeUnit.SECONDS)){
resource.doSomething();
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
//release lock
lock.unlock();
}
resource.doLogging();
}
}
正如所看到的,代码中使用tryLock()
方法来确保线程只等待确定的时间,如果它没有获得对象的锁定,它只是记录和退出。另一个要注意的重点是使用try-finally
块来确保即使doSomething()
方法调用抛出任何异常也会释放锁定。
Java Lock 与 synchronized 比较
基于以上细节和程序,可以很容易地得出Java Lock
和synchronized
之间的以下区别。
- Java Lock API为锁定提供了更多的可见性和选项,不像在线程可能最终无限期地等待锁定的同步,可以使用
tryLock()
来确保线程仅等待特定时间。 synchronized
代码更清晰,更易于维护,而使用Lock
,需要尝试使用try-finally
块来确保即使在lock()
和unlock()
方法调用之间抛出一些异常也会释放Lock
。synchronized
块或方法只能覆盖一种方法,而可以在一种方法中获取锁,并使用Lock API在另一种方法中释放它。synchronized
关键字不提供公平性,而可以在创建ReentrantLock
对象时将公平性设置为true
,以便最长等待的线程首先获得锁定。- 可以为
Lock
创建不同的条件,不同的线程可以针对不同的条件执行await()
。
这就是Java Lock示例,Java中的ReentrantLock
以及使用synchronized
关键字的比较分析。