0%

16.5.5 同步锁Lock

16.5.5 同步锁Lock

Java 5开始,Java提供了一种功能更强大的线程同步机制——通过显式定义同步锁对象来实现同步,在这种机制下,同步锁由Lock对象充当

Lock的优点

Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock允许实现更灵活的结构,可以具有差别很大的属性,并且支持多个相关的Condition对象。
Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

锁接口和实现类

某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁)。
LockReadWriteLockJava 5提供的两个根接口,并为Lock提供了ReentrantLock(可重入锁)实现类,为ReadWriteLock提供了ReentrantReadWriteLock实现类。ReentrantReadWriteLock为读写操作提供了三种锁模式: WritingReadingOptimisticReading

StampedLock类

Java 8新增了新型的StampedLock类,在大多数场景中它可以替代传统的ReentrantReadWriteLock,

比较常用的锁

在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁)。使用该Lock对象可以显式地加锁、释放锁,通常使用ReentrantLock的代码格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class X
{
//定义锁对象
private final ReentrantLock lock new ReentrantLock();
...
//定义需要保证线程安全的方法
public void m()
{
//加锁
lock.lock();
try
{
//需要保证线程安全的代码
}
//使用 finally块来保证释放锁
finally
{
lock.unlock();
}
}
}

使用ReentrantLock对象来进行同步,加锁和释放锁出现在不同的作用范围内时,通常建议使用finally块来确保在必要时释放锁。

程序示例

通过使用ReentrantLock对象,可以把Account类改为如下形式,它依然是线程安全的:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import java.util.concurrent.locks.*;

public class Account
{
// 1.定义锁对象
private final ReentrantLock lock = new ReentrantLock();
// 封装账户编号、账户余额的两个成员变量
private String accountNo;
private double balance;
public Account(){}
// 构造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}

// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
public double getBalance()
{
return this.balance;
}

// 提供一个线程安全draw()方法来完成取钱操作
public void draw(double drawAmount)
{
// 2.加锁
lock.lock();
try
{
// 账户余额大于取钱数目
if (balance >= drawAmount)
{
// 吐出钞票
System.out.println(Thread.currentThread().getName()
+ "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
balance -= drawAmount;
System.out.println("\t余额为: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName()
+ "取钱失败!余额不足!");
}
}
finally
{
// 3.修改完成,释放锁
lock.unlock();
}
}

// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}

上面程序中的代码1定义了一个ReentrantLock对象,程序中实现draw()方法时,进入该方法后立即请求对ReentrantLock对象进行加锁,当执行完draw()方法的取钱逻辑之后,程序使用finally块来确保释放锁。

Lock和同步方法的区别

使用Lock与使用同步方法有点相似,
只是使用Lock时显式使用Lock对象作为同步锁,
而使用同步方法时系统隐式使用当前对象作为同步监视器,
同样都符合”加锁→修改→释放锁”的操作模式,而且使用Lock对象时每个Lock对象对应一个Account对象,一样可以保证对于同一个Account对象,同一时刻只能有一个线程能进入临界区

同步代码块,同步方法 锁三者的区别

同步方法或同步代码块使用与竞争资源相关的、隐式的同步监视器,并且强制要求加锁和释放锁要出现在一个块结构中,而且当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有锁。

Lock提供了同步方法和同步代码块没有的功能

虽然同步方法和同步代码块的范围机制使得多线程安全编程非常方便,而且还可以避免很多涉及锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。Lock提供了同步方法和同步代码块所没有的其他功能,包括用于非块结构的tryLock()方法,以及试图获取可中断锁的lockInterruptibly()方法,还有获取超时失效锁的tryLock(long, TimeUnit)方法。

重入性

ReentrantLock锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪locko方法的嵌套调用,线程在每次调用lockO加锁后,必须显式调用unlock来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。

原文链接: 16.5.5 同步锁Lock