并发编程 Java 三把锁(Synchronized、ReentrantLock、ReadWriteLock)

Synchronized

synchronized 的 3 种用法:

指定加锁对象(代码块):对给定对象加锁,进入同步代码前要获得给定对象的锁。

void resource1() {
    synchronized ("resource1") {
      System.out.println("作用在同步块中");
    }
  }

直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。

synchronized void resource3() {
    System.out.println("作用在实例方法上");
  }

直接作用于静态方法:相当于对当前类加锁,进入同步代码块前要获得当前类的锁。

static synchronized void resource2() {
      System.out.println("作用在静态方法上");
  }

synchronized 在发生异常的时候会释放锁,这点需要注意一下。


Lock接口:

/**
 * @since 1.5
 * @author Doug Lea
 */
public interface Lock {

    void lock();

    void lockInterruptibly() throws InterruptedException;

    boolean tryLock();

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

    void unlock();

    Condition newCondition();

}

lock():用来获取锁,若锁已被其他线程获取,则需等待
Lock()方法必须主动去释放锁,并且在发生异常时也不会自动释放锁
使用Lock必须在try…catch…块中进行:

  1. 释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生
  2. 获取锁的操作应该方法try的外面,防止误释放锁。

(如果说在获取锁时发生了异常,那么肯定也会走 finally 代码块,执行lock.unlock();去释放锁,可问题是我还没获取到锁啊!!!)
在 try-finally 外加锁的话,如果因为发生异常导致加锁失败,try-finally 块中的代码不会执行。
相反,如果在 try{ } 代码块中加锁失败,finally 中的代码无论如何都会执行,但是由于当前线程加锁失败并没有持有 lock 对象锁,所以程序会抛出异常。

链接:
https://blog.csdn.net/u013568373/article/details/98480603?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase

unlock():解锁

tryLock():尝试获取锁,若获取不到,立刻返回false;这个方法无论如何都会立即返回,在拿不到锁时不会一直在那等待。

tryLock(long time, TimeUnit unit):在给定的时间里等待锁,超过时间则自动放弃,返回false;在等待期间内拿到了锁,返回true。

lockInterruptibly():
获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的;
而用synchronized修饰的话,synchronized 只有2种情况:1继续执行,2保持等待。

 public void method(Lock lock) throws InterruptedException {
        lock.lockInterruptibly();
        try {
            //执行代码
        } catch (Exception e) {
            // 异常处理
        } finally {
            lock.unlock();
        }
    }

一般将lockTry.lockInterruptibly();写在了try{}catch{}之外,原因同上。


Lock 的标准实现是重入锁 ReentrantLock, 和读写锁 ReadWriteLock。

ReentrantLock 重入锁

可重入性:

重入锁ReentrantLock,是支持重进入的锁,该锁能够支持一个线程对资源的重复加锁。
先来一个简单的例子:

package cn.think.in.java.lock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockText implements Runnable {

  /**
   * Re - entrant - Lock
   * 重入锁,表示在单个线程内,这个锁可以反复进入,也就是说,一个线程可以连续两次获得同一把锁。
   * 如果你不允许重入,将导致死锁。注意,lock 和 unlock 次数一定要相同,如果不同,就会导致死锁和监视器异常。
   *
   * synchronized 只有2种情况:1继续执行,2保持等待。
   */
  static Lock lock = new ReentrantLock();
  static int i;

  public static void main(String[] args) throws InterruptedException {
    LockText lockText = new LockText();
    Thread t1 = new Thread(lockText);
    Thread t2 = new Thread(lockText);
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(i);
  }

  @Override
  public void run() {
    for (int j = 0; j < 1000000; j++) {
      lock.lock();
      try {
        i++;
      } finally {
        // 因为lock 如果发生了异常,是不会释放锁的,所以必须在 finally 块中释放锁
        // synchronized 发生异常会主动释放锁
        lock.unlock();
      }
    }
  }
}

PS:
Synchronized 也支持重进入,但只支持隐式的重进入。

public synchronized void test1() {
	value = value + 1;
	test2();
}
public synchronized void test2() {
	value = value + 1;
}

ReentrantLock与synchronized隐式获取和释放锁相比,它缺少了便捷性,但却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。
Lock的使用:

Lock lock = new ReentrantLock();
lock.lock();
lock.lock();
try {
	// 业务逻辑
} finally {
	lock.unlock();
	lock.unlock();
}

注意:
1.在finally块中释放锁,保证在获取到锁之后,最终能够被释放。
2.不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。

中断响应

synchronized修饰的线程在等待锁,那么只有2种情况:1获取到锁继续执行,2保持等待;
lockInterruptibly 方法修饰的线程,可在获取锁的过程种响应线程中断,那么就会抛出异常。

package cn.think.in.java.lock;

import java.util.concurrent.locks.ReentrantLock;

/**
 * ReentrantLock(重入锁)
 *
 * Condition(条件)
 *
 * ReadWriteLock(读写锁)
 */
public class IntLock implements Runnable {

  /**
   * 默认是不公平的锁,设置为 true 为公平锁
   *
   * 公平:在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程;
   * 使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢,常常极其慢)
   * 还要注意的是,未定时的 tryLock 方法并没有使用公平设置
   *
   * 不公平:此锁将无法保证任何特定访问顺序
   *
   * 拾遗:1 该类的序列化与内置锁的行为方式相同:一个反序列化的锁处于解除锁定状态,不管它被序列化时的状态是怎样的。
   *      2.此锁最多支持同一个线程发起的 2147483648 个递归锁。试图超过此限制会导致由锁方法抛出的 Error。
   */
  static ReentrantLock lock1 = new ReentrantLock(true);
  static ReentrantLock lock2 = new ReentrantLock();
  int lock;

  /**
   * 控制加锁顺序,方便制造死锁
   * @param lock
   */
  public IntLock(int lock) {
    this.lock = lock;
  }

  /**
   * lockInterruptibly 方法: 获得锁,但优先响应中断
   * tryLock 尝试获得锁,不等待
   * tryLock(long time , TimeUnit unit) 尝试获得锁,等待给定的时间
   */
  @Override
  public void run() {
    try {
      if (lock == 1) {
        // 如果当前线程未被中断,则获取锁。
        lock1.lockInterruptibly();// 即在等待锁的过程中,可以响应中断。
        try {
          Thread.sleep(500);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        // 试图获取 lock 2 的锁
        lock2.lockInterruptibly();
      } else {

        lock2.lockInterruptibly();
        try {
          Thread.sleep(500);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        // 该线程在企图获取 lock1 的时候,会死锁,但被调用了 thread.interrupt 方法,导致中断。中断会放弃锁。
        lock1.lockInterruptibly();
      }

    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      if (lock1.isHeldByCurrentThread()) {
        lock1.unlock();
      }

      // 查询当前线程是否保持此锁。
      if (lock2.isHeldByCurrentThread()) {
        lock2.unlock();
      }

      System.out.println(Thread.currentThread().getId() + ": 线程退出");
    }
  }


  public static void main(String[] args) throws InterruptedException {

    /**
     * 这部分代码主要是针对 lockInterruptibly 方法,该方法在线程发生死锁的时候可以中断线程。让线程放弃锁。
     * 而 synchronized 是没有这个功能的, 他要么获得锁继续执行,要么继续等待锁。
     */

    IntLock r1 = new IntLock(1);
    IntLock r2 = new IntLock(2);
    Thread t1 = new Thread(r1);
    Thread t2 = new Thread(r2);
    t1.start();
    t2.start();
    Thread.sleep(1000);
    // 中断其中一个线程(只有线程在等待锁的过程中才有效)
    // 如果线程已经拿到了锁,中断是不起任何作用的。
    // 注意:这点 synchronized 是不能实现此功能的,synchronized 在等待过程中无法中断
    t2.interrupt();
    // t2 线程中断,抛出异常,并放开锁。没有完成任务
    // t1 顺利完成任务。
  }
}

锁申请

trylock(),tryLock(long time, TimeUnit unit)在获取锁如果尝试失败或者超时,线程就放弃获取=锁,这点synchronized 是不支持的,这样可以有效避免死锁。那么,如何使用呢?

package cn.think.in.java.lock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class TimeLock implements Runnable {

  static ReentrantLock lock = new ReentrantLock(false);

  @Override
  public void run() {
    try {
      // 最多等待5秒,超过5秒返回false,若获得锁,则返回true
      if (lock.tryLock(5, TimeUnit.SECONDS)) {
        // 锁住 6 秒,让下一个线程无法获取锁
        System.out.println("锁住 6 秒,让下一个线程无法获取锁");
        Thread.sleep(6000);
      } else {
        System.out.println("get lock failed");
      }
    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      if (lock.isHeldByCurrentThread()) {
        lock.unlock();
      }
    }
  }

  public static void main(String[] args) {
    TimeLock tl = new TimeLock();
    Thread t1 = new Thread(tl);
    Thread t2 = new Thread(tl);

    t1.start();
    t2.start();


  }
}

公平锁和非公平锁

公平锁:不会产生饥饿现象。线程按照等待顺序得到资源
非公平锁:系统在选择锁的时候都是随机的,不会按照某种顺序,比如时间顺序。
synchronized 得到的锁是非公平锁,而ReentrantLock可以自由选择使用公平锁或者非公平锁,同时非公平锁 比 公平锁 更有效率,一般选择非公平锁。

 package cn.think.in.java.lock;

import java.util.concurrent.locks.ReentrantLock;

public class FairLock implements Runnable {

  // 公平锁和非公平锁的结果完全不同
  /*
  * 10 获得锁
    10 获得锁
    10 获得锁
    10 获得锁
    10 获得锁
    10 获得锁
    10 获得锁
    10 获得锁
    10 获得锁
    10 获得锁
    9 获得锁
    9 获得锁
    9 获得锁
    9 获得锁
    9 获得锁
    9 获得锁
    9 获得锁
    9 获得锁
    9 获得锁
    9 获得锁
    ======================下面是公平锁,上面是非公平锁
    10 获得锁
    9 获得锁
    10 获得锁
    9 获得锁
    10 获得锁
    9 获得锁
    10 获得锁
    9 获得锁
    10 获得锁
    9 获得锁
    10 获得锁
    9 获得锁
    10 获得锁
    9 获得锁
    10 获得锁
    9 获得锁
    10 获得锁
    9 获得锁
    10 获得锁
    9 获得锁
    10 获得
  *
  * */
  static ReentrantLock unFairLock = new ReentrantLock(false);
  static ReentrantLock fairLock = new ReentrantLock(true);

  @Override
  public void run() {
    while (true) {
      try {
        fairLock.lock();
        System.out.println(Thread.currentThread().getId() + " 获得锁");
      } finally {
        fairLock.unlock();
      }
    }
  }

  /**
   * 默认是不公平的锁,设置为 true 为公平锁
   *
   * 公平:在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程;
   * 使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢,常常极其慢)
   * 还要注意的是,未定时的 tryLock 方法并没有使用公平设置
   *
   * 不公平:此锁将无法保证任何特定访问顺序,但是效率很高
   *
   */
  public static void main(String[] args) {
    FairLock fairLock = new FairLock();
    Thread t1 = new Thread(fairLock, "cxs - t1");
    Thread t2 = new Thread(fairLock, "cxs - t2");
    t1.start();
    t2.start();
  }
}

可以看到,公平锁的打印顺序是完全交替运行,而不公平锁的顺序完全是随机的。

重入锁相比 synchronized 有哪些优势:

  1. 可以在线程等待锁的时候中断线程,synchronized 是做不到的。
  2. 可以尝试获取锁,如果获取不到就放弃,或者设置一定的时间,这也是 synchroized 做不到的。
  3. 可以设置公平锁,synchronized 默认是非公平锁,无法实现公平锁。

重入锁的好搭档-----Condition

synchronized 通过 Object 类的 wait 方法和 notify 方法实现线程之间的通信;
重入锁 ReentrantLock 通过 Condition 接口中的await()、signal()方法进行通信。

public interface Condition {

    void await() throws InterruptedException;

    boolean await(long time, TimeUnit unit) throws InterruptedException;

    long awaitNanos(long nanosTimeout) throws InterruptedException;

    boolean await(long time, TimeUnit unit) throws InterruptedException;

    void awaitUninterruptibly();

    boolean awaitUntil(Date deadline) throws InterruptedException;

    void signal();

    void signalAll();
}

Condition 为不和 Object 类的冲突,使用 await 方法 对应 wait 方法,signal 方法对应 notify 方法。signalAll 方法对应 notifyAll 方法;
awaitUninterruptibly 方法,该方法不会响应线程的中断,但 Object 的 wait 方法是会响应的。 awaitUntil 方法是等待到一个给定的绝对时间,除非调用了 signal 或者中断了。

package cn.think.in.java.lock.condition;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 重入锁的好搭档
 *
 * await 使当前线程等待,同时释放当前锁,当其他线程中使用 signal 或者 signalAll 方法时,线程会重新获得锁并继续执行。
 *       或者当线程被中断时,也能跳出等待,这和 Object.wait 方法很相似。
 * awaitUninterruptibly() 方法与 await 方法基本相同,但是它并不会在等待过程中响应中断。
 * singal() 该方法用于唤醒一个在等待中的线程,相对的 singalAll 方法会唤醒所有在等待的线程,这和 Object.notify 方法很类似。
 */
public class ConditionTest implements Runnable {

  static Lock lock = new ReentrantLock();

  static Condition condition = lock.newCondition();


  @Override
  public void run() {
    try {
      lock.lock();
      // 该线程会释放 lock 的锁,也就是说,一个线程想调用 condition 的方法,必须先获取 lock 的锁。
      // 否则就会像 object 的 wait 方法一样,监视器异常
      condition.await();
      System.out.println("Thread is going on");

    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      lock.unlock();
    }
  }

  public static void main(String[] args) throws InterruptedException {
    ConditionTest t = new ConditionTest();
    Thread t1 = new Thread(t);
    t1.start();
    Thread.sleep(1000);
    // 通知 t1 继续执行
    // main 线程必须获取 lock 的锁,才能调用 condition 的方法。否则就是监视器异常,这点和 object 的 wait 方法是一样的。
    lock.lock(); // IllegalMonitorStateException
    // 从 condition 的等待队列中,唤醒一个线程。
    condition.signal();
    lock.unlock();
  }
}

ReadWriteLock:

线程不安全的原因来自于多线程对数据的修改,如果你不修改数据,根本不需要锁。我们完全可以将读写分离,提高性能,在读的时候不使用锁,在写的时候才加入锁。这就是 ReadWriteLock 的设计原理。

public interface ReadWriteLock {
   
    Lock readLock();

    Lock writeLock();
}

读写锁的使用:

package cn.think.in.java.lock;

import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockDemo {

  static Lock lock = new ReentrantLock();
  static ReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

  static Lock readLock = reentrantReadWriteLock.readLock();
  static Lock writeLock = reentrantReadWriteLock.writeLock();

  int value;

  public Object handleRead(Lock lock) throws InterruptedException {
    try {
      lock.lock();
      // 模拟读操作,读操作的耗时越多,读写锁的优势就越明显
      Thread.sleep(1000);
      return value;
    } finally {
      lock.unlock();
    }
  }

  public void handleWrite(Lock lock, int index) throws InterruptedException {
    try {
      lock.lock();
      Thread.sleep(1000); // 模拟写操作
      value = index;

    } finally {
      lock.unlock();
    }
  }

  public static void main(String[] args) {
    final ReadWriteLockDemo demo = new ReadWriteLockDemo();
    Runnable readRunnable = new Runnable() {
      @Override
      public void run() {
        try {
          demo.handleRead(readLock);
//          demo.handleRead(lock);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    };

    Runnable writeRunnable = new Runnable() {
      @Override
      public void run() {
        try {
          demo.handleWrite(writeLock, new Random().nextInt());
//          demo.handleWrite(lock, new Random().nextInt());
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    };

    /**
     * 使用读写锁,这段程序只需要2秒左右
     * 使用普通的锁,这段程序需要20秒左右。
     */

    for (int i = 0; i < 18; i++) {
      new Thread(readRunnable).start();
    }

    for (int i = 18; i < 20; i++) {
      new Thread(writeRunnable).start();
    }


  }

}

两个循环:一个循环开启18个线程去读数据,一个循环开启两个线程去写。如果使用普通的重入锁,将耗时20秒,因为普通的重入锁在读的时候依然是串行的。而如果使用读写锁,只需要2秒,也就是写的时候是串行的。读的时候是并行的,极大的提高了性能。
注意:只要涉及到写都是串行的。比如读写操作,写写操作,都是串行的,只有读读操作是并行的。

ReadWriteLock遵守以下三条基本原则:

  1. 允许多个线程同时读共享变量;
  2. 只允许一个线程写共享变量;
  3. 如果一个写线程正在执行写操作,此时禁止读线程读共享变量。

ReentrantReadWriteLock是ReadWriteLock的一个实现类:

  1. 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。

  2. 重进入:读锁和写锁都支持线程重进入。

  3. 锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值