线程锁和条件对象
在大多数多线程应用中, 都是两个及以上线程需要共享对同一数据的存取, 所以有可能出现两个线程同时访问同一个资源的情况, 这种情况叫做: 竞争条件.
在 Java 中为了解决并发的数据访问问题, 一般使用锁这个概念来解决.
有几种机制防止代码收到并发访问的干扰:
1.synchronized 关键字 (自动创建一个锁及相关的条件)
2.ReentrantLock 类 + Java.util.concurrent 包中的 lock 接口 (在 Java5.0 的时候引入)
ReentrantLock 的使用
- public void Method() {
- boolean flag = false;// 标识条件
- ReentrantLock locker = new ReentrantLock();
- locker.lock();// 开启线程锁
- try {
- //do some work...
- } catch (Exception ex) {
- } finally {
- locker.unlock();// 解锁线程
- }
- }
- locker.lock();
确保只有一个线程进入临界区, 一旦一个线程进入之后, 会获得锁对象, 其他线程无法通过 lock 语句. 当其他线程调用 lock 时, 它们会被阻塞, 知道第一个线程释放锁对象.
locker.unlock();
解锁操作, 一定要放到 finally 里, 因为如果 try 语句里出了问题, 锁必须被释放, 否则其他线程将永远被阻塞
因为系统会随机为线程分配资源, 所以在线程获得锁对象之后, 可能被系统剥夺运行权, 这时候其他线程来访问, 但是发现有锁, 进不去, 只能等拿到锁对象的线程把里面的代码执行完毕后, 释放锁, 第二个线程才能运行.
假设说做一个银行转账的功能, 线程锁操作应该定义在银行类的转账方法里, 因为这样每个银行对象都有一个锁对象, 两个线程访问一个银行对象的时候, 那么锁以串行方式提供服务. 但是, 如果每个线程访问不同的银行对象, 每个线程都会得到不同的锁对象, 彼此之间不会冲突, 所以就不会造成不必要的线程阻塞.
锁是可重入的, 线程可以重复获得已经持有的锁, 锁通过一个持有数量计数来跟踪对 lock 方法的嵌套使用.
假设说, 一个线程获得锁之后, 要执行 A 方法, 但是 A 方法里面又调用了 B 方法, 这时候这个线程获得了两个锁对象, 当线程执行 B 方法的时候, 也会被锁死, 防止其他线程乱入, 当 B 方法执行完毕后, 锁对象变成了一个, 当 A 方法也执行完毕的时候, 锁对象变成了 0 个, 线程释放锁.
synchronized 关键字
前面我们讲了 ReentrantLock 锁对象的使用, 但是在系统里面我们不一定要使用 ReentrantLock 锁, Java 中还提供了一个内部的隐式锁, 关键字是 synchronized.
举个例子:
- public synchronized void Method() {
- //do some work...
- }
只需要在返回值前面加上 synchronized 锁, 就会实现上面 ReentrantLock 锁同样的效果.
Conditional 条件对象
通常, 线程拿到锁对象之后, 却发现需要满足某一条件才能继续向下执行.
拿银行程序来举例子, 我们需要转账方账户有足够的资金才能转出到目标账户, 这时候需要用到 ReentrantLock 对象, 因为如果我们已经完成转账方账户有足够的资金的判断之后, 线程被其他线程中断, 等其他线程执行完之后, 转账方的钱又没有了足够的资金, 这时候因为系统已经完成了判断, 所以会继续向下执行, 然后银行系统就会出现问题.
举例:
- public void Transfer(int from, int to, double amount) {
- if (Accounts[from]> amount)// 系统在结束判断之后被剥夺运行权, 然后账户通过网银转出所有钱, 银行凉凉
- DoTransfer(from, to, amount);
- }
这时候我们就需要使用 ReentrantLock 对象了, 我们修改一下代码:
- public void Transfer(int from, int to, double amount) {
- ReentrantLock locker = new ReentrantLock();
- locker.lock();
- try {
- while (Accounts[from] < amount) {
- // 等待有足够的钱
- }
- DoTransfer(from, to, amount);
- } catch (Exception ex) {
- ex.printStackTrace();
- } finally {
- locker.unlock();
- }
- }
但是这样又有了问题, 当前线程获取了锁对象之后, 开始执行代码, 发现钱不够, 进入等待状态, 然后其他线程又因为锁的原因无法给该账户转账, 就会一直进入等待状态.
这个问题如何解决呢?
条件对象登场!
- public void Transfer(int from, int to, double amount) {
- ReentrantLock locker = new ReentrantLock();
- Condition sufficientFunds = locker.newCondition();// 条件对象,
- lock.lock();
- try {
- while (Accounts[from] < amount) {
- sufficientFunds.await();
- // 等待有足够的钱
- }
- DoTransfer(from, to, amount);
- sufficientFunds.signalAll();
- } catch (Exception ex) {
- ex.printStackTrace();
- } finally {
- locker.unlock();
- }
- }
条件对象的关键字是: Condition, 一个锁对象可以有一个或多个相关的条件对象. 可以通过锁对象. newCondition 方法获得一个条件对象.
一般关于条件对象的命名需要能够反映它表达的条件的名字, 所以在这里我们叫他 sufficientFund, 表示余额充足的意思.
在进入锁之前, 我们创建一个条件, 然后如果金额不足, 在这里调用条件对象的 await 方法, 通知系统当前线程进入挂起状态, 让其他线程执行. 这样你这次调用会被锁定, 然后系统可以再次调用该方法给其他账户转账, 当每一次转账完成后, 执行转账操作的线程在底部调用 signalAll 通知所有线程可以继续运行了, 因为我们有可能是转足够的钱给当前账户, 这时候有可能该线程会继续执行 (不一定是你, 是通知所有线程, 如果通知的线程还是不符合条件, 会继续调用 await 方法, 并完成转账操作, 然后通知其他挂起的线程.
你说为啥不直接通知当前线程? 不行, 可以调用 signal 方法只通知一个线程, 但是如果这个线程操作的账户还是没钱 (不是转账给这个账户的情况), 那这个线程又进入等待了, 这时候已经没有线程能通知其他线程了, 程序死锁, 所以还是用 signal 比较保险.
以上是使用 ReentrantLock+Condition 对象, 那你说我要是使用 synchronized 隐式锁怎么办?
也可以, 而且不需要
- public void Transfer(int from, int to, double amount) {
- while (Accounts[from] < amount) {
- wait();// 这个 wait 方法是定义在 Object 类里面的, 可以直接用, 和条件对象的 await 一样, 挂起线程
- // 等待有足够的钱
- }
- DoTransfer(from, to, amount);
- notifyAll();// 通知其他挂起的线程
- }
Object 类里面定义了 wait,notifyAll,notify 方法, 对应 await,signalAll 和 signal 方法, 用来操作隐式锁, synchronized 只能有一个条件, 而 ReentrantLock 显式声明的锁可以用绑定多个 Condition 条件.
同步块
除了我们上面讲的两种获取线程锁的方式, 还有另外一种机制获得锁, 这种方式比较特殊, 叫做同步块:
- Object locker = new Object();
- synchronized (locker) {
- //do some work
- }
- // 也可以直接锁当前类的对象
- sychronized(this){
- //do some work
- }
以上代码会获得 Object 类型 locker 对象的锁, 这种锁是一个特殊的锁, 在上面的代码中, 创建这个 Object 类对象只是单纯用来使用其持有的锁.
这种机制叫做同步块, 应用场景也很广: 有的时候, 我们并不是整个一个方法都需要同步, 只是方法里的部分代码块需要同步, 这种情况下, 我们如果将这个方法声明为 synchronized, 尤其是方法很大的时候, 会造成很大的资源浪费. 所以在这种情况下我们可以使用 synchronized 关键字来声明同步块:
- public void Method() {
- //do some work without synchronized
- synchronized (this) {
- //do some synchronized operation
- }
- }
监视器的概念
锁和条件是同步中一个很重要的工具, 但是它们并不是面向对象的. 多年来, Java 的研究人员努力寻找一种方法, 可以在不需要考虑如何加锁的情况下, 就能保证多线程的安全性. 最成功的的一个解决方案叫做 monitor 监视器, 这个对象内置于每一个 Object 变量中, 相当于一个许可证. 拿到许可证就可以进行操作, 没有拿到则需要阻塞等待.
监视器具有以下特性:
1. 监视器是只包含私有域的类
2. 每个监视器对象都有一个相关的锁
3. 使用监视器对象的锁对所有的方法进行加锁 (举个例子: 如果调用 obj.Method 方法, obj 对象的锁会在方法调用的时候自动获得, 当方法结束或返回之后会自动释放该锁. 因为所有的域都是私有的, 这样可以确保一个线程在操作类对象的时候, 没有其他线程可以访问里面的域)
4. 该锁对象可以有任意多个相关条件
你也可以自己创建一个监视器类, 只要符合以上的要求即可.
其实我们使用的 synchronized 关键字就是使用了 monitor 来实现加锁解锁, 所以又被称为内部锁. 因为 Object 类实现了监视器, 所以对象又被内置于任何一个对象之中. 这就是我们为什么可以使用 synchronized(locker) 的方式锁定一个代码块了, 其实只是用到了 locker 对象中内置的 monitor 而已. 每一个对象的 monitor 类又是唯一的, 所以就是唯一的许可证, 拿到许可证的线程才可以执行, 执行完后释放对象的 monitor 才可以被其他线程获取.
举个例子:
- synchronized (this) {
- //do some synchronized operation
- }
它在字节码文件中会被编译为:
- monitorenter;//get monitor,enter the synchronized block
- //do some synchronized operation
- monitorexit;//leavel the synchronized block,release the monitor
来源: https://www.cnblogs.com/Fill/p/9379776.html