zoukankan      html  css  js  c++  java
  • 多线程编程核心技术(十一)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,这种方法我们一般称为异步方法。

     
  • 相关阅读:
    升级python2.7, 实现python2.7与python3并存

    JDK一键部署, 新添加进度条
    银行分类概述
    个人银行结算账户类别
    银联刷卡POS机冲正
    银行怎样处理坏账和贷款展期
    数据加解密和数据签名验签
    一行三会/首批试点民营银行
    前端base64加密
  • 原文地址:https://www.cnblogs.com/SmartCat994/p/14207127.html
Copyright © 2011-2022 走看看