多线程编程核心技术(十一)Lock

两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。这两大问题,管程都是能够解决的。Java SDK 并发包通过 LockCondition 两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题。

Java 语言本身提供的 synchronized 也是管程的一种实现,既然 Java 从语言层面已经实现了管程了,那为什么还要在 SDK 里提供另外一种实现呢?

在 Java 的 1.5 版本中,synchronized 性能不如 SDK 里面的 Lock,但 1.6 版本之后,synchronized 做了很多优化,将性能追了上来,所以 1.6 之后的版本又有人推荐使用 synchronized 了。

问题就是synchronized没有办法解决“破坏不可抢占条件方案”。 原因是synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。synchronized的加锁释放操作由JVM为我们进行。

如果我们重新设计一把互斥锁去解决这个问题,应该具有以下的能力

1.能够响应interrupt:如果一个线程进入了死锁的阶段是需要进行中断进行锁的释放的。

2.支持超时。如果线程一段时间无法获得到锁,返回一个错误,也释放曾持有的锁,这样也能破坏不可抢占条件

3.非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。

在Lock接口中就实现了这些

    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();

Java Sdk里面的锁Lock保证可见性:利用了volatile相关的Happens-Before规则。获取锁的时候,会读写 state 的值;解锁的时候,也会读写 state 的值(简化后的代码如下面所示)。

class SampleLock {
  volatile int state;
  // 加锁
  lock() {
    // 省略代码无数
    state = 1;
  }
  // 解锁
  unlock() {
    // 省略代码无数
    state = 0;
  }
}

也就是说,在执行 value+=1 之前,程序先读写了一次 volatile 变量 state,在执行 value+=1 之后,又读写了一次 volatile 变量 state。根据相关的 Happens-Before 规则:

顺序性规则:对于线程 T1,value+=1 发生在 释放锁的操作 unlock()  之前;

volatile 变量规则:由于 state = 1 会先读取 state,所以线程 T1 的 unlock() 操作 发生在 线程 T2 的 lock() 操作 之前;

传递性规则:线程 T1 的 value+=1 发生在 线程 T2 的 lock() 操作 之前。

所以说,后续线程 T2 能够看到 value 的正确结果。


什么是可重入锁

同一个线程可以重复获取同一把锁,并且是安全的。

class X {
  private final Lock rtl =
  new ReentrantLock();
  int value;
  public int get() {
    // 获取锁
    rtl.lock();         ②
    try {
      return value;
    } finally {
      // 保证锁能释放
      rtl.unlock();
    }
  }
  public void addOne() {
    // 获取锁
    rtl.lock();  
    try {
      value = 1 + get(); ①
    } finally {
      // 保证锁能释放
      rtl.unlock();
    }
  }
}

例如下面代码中,当线程 T1 执行到 ① 处时,已经获取到了锁 rtl ,当在 ① 处调用 get() 方法时,会在 ② 再次对锁 rtl 执行加锁操作。此时,如果锁 rtl 是可重入的,那么线程 T1 可以再次加锁成功;如果锁 rtl 是不可重入的,那么线程 T1 此时会被阻塞。

 

推荐的三个用锁的最佳实践,它们分别是:

  • 永远只在更新对象的成员变量时加锁
  • 永远只在访问可变的成员变量时加锁
  • 永远不在调用其他对象的方法时加锁

 

活锁问题

class Account {
  private int balance;
  private final Lock lock
          = new ReentrantLock();
  // 转账
  void transfer(Account tar, int amt){
    while (true) {
      if(this.lock.tryLock()) {
        try {
          if (tar.lock.tryLock()) {
            try {
              this.balance -= amt;
              tar.balance += amt;
            } finally {
              tar.lock.unlock();
            }
          }//if
        } finally {
          this.lock.unlock();
        }
      }//if
    }//while
  }//transfer
}

1.线程A给B转账,B给A转账,两个都有自己的锁,没有对象的锁

2.一段时间后释放锁,重复1步骤


Lock接口实现了互斥,Condition 实现了管程模型里面的条件变量,用来实现了同步。

synchronized和Lock的核心的区别 一共四个
1 lock支持中断响应
2 lock支持超时返回
3 lock支持非阻塞获取锁
4 lock配合Condition可以支持多个管程中的条件变量
在很多并发场景下,支持多个条件变量能够让我们的并发程序可读性更好,实现起来也更容易。例如,实现一个阻塞队列,就需要两个条件变量。如下代码
public class BlockedQueue<T>{
  final Lock lock =
    new ReentrantLock();
  // 条件变量:队列不满  
  final Condition notFull =
    lock.newCondition();
  // 条件变量:队列不空  
  final Condition notEmpty =
    lock.newCondition();

  // 入队
  void enq(T x) {
    lock.lock();
    try {
      while (队列已满){
        // 等待队列不满
        notFull.await();
      }  
      // 省略入队操作...
      //入队后,通知可出队
      notEmpty.signal();
    }finally {
      lock.unlock();
    }
  }
  // 出队
  void deq(){
    lock.lock();
    try {
      while (队列已空){
        // 等待队列不空
        notEmpty.await();
      }  
      // 省略出队操作...
      //出队后,通知可入队
      notFull.signal();
    }finally {
      lock.unlock();
    }  
  }
}

线程之间的通信不管是syn还是lock都是对对象进行的

线程等待和通知需要调用 await()、signal()、signalAll(),它们的语义和 wait()、notify()、notifyAll() 是相同的。但是不一样的是,Lock&Condition 实现的管程里只能使用前面的 await()、signal()、signalAll(),而后面的 wait()、notify()、notifyAll() 只有在 synchronized 实现的管程里才能使用。

public class demo13  {
    private final Object lock = new Object();

    public void add() throws InterruptedException {
        synchronized (lock){
            System.out.println(Thread.currentThread().getName()+"  is in");
            System.out.println(Thread.currentThread().getName()+"  is wait");
            lock.wait(2000);
            lock.notifyAll();
            System.out.println(Thread.currentThread().getName()+"  is notify");
        }
    }


    public static void main(String[] args) throws InterruptedException {
        demo13 demo13 = new demo13();

        for (int i = 0; i < 20; i++) {
            Thread thread = new Thread(() -> {
                try {
                    demo13.add();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });

            thread.start();
            thread.join();
        }
    }
}

Java 程序支持异步的两种实现方式: 1、调用方创建一个子线程,在子线程中执行方法调用,这种调用我们称为异步调用。 2、方法实现的时候,创建一个新的线程执行主要逻辑,主线程直接return,这种方法我们一般称为异步方法。

 
posted @ 2020-12-29 16:57  smartcat994  阅读(111)  评论(0编辑  收藏  举报