Java多线程之锁的基本使用

文章目录

  • 一、重入锁
    • 1、重入锁简介
    • 2、中断响应
    • 3、限时等待
    • 4、公平锁
    • 5、重入锁实现原理
  • 二、Condition
  • 三、信号量Semaphore
  • 四、读写锁ReadWriteLock
  • 五、CountDownLatch
  • 六、CyclicBarrier

一、重入锁

1、重入锁简介

重入锁是用于线程间协同工作的一种机制,可以完全替代synchronized关键字,在java中为java.util.concurrent.locks包下的ReentrantLock类。之所以叫重入锁,是因为该锁可以反复获取多次,在释放锁的时候也必须释放相同次数。
与synchronized相比,重入锁必须在程序中指出何时加锁,何时释放锁,因此对程序的逻辑控制灵活性要远远好于synchronized。

public class ReentrantLockTest implements Runnable{
  public static ReentrantLock lock = new ReentrantLock();
  public static int i = 0;
  @Override
  public void run(){
    for(int j=0;j<10000000;j++){
      // 获取锁,只有获取到了才会执行后续代码
      lock.lock();
      try{
        i++;
      }finally{
        // 释放锁
        lock.unlock();
      }
    }
  }
  public static void main(String[] args)throws InterruptedException{
    ReentrantLockTest lockTest = new ReentrantLockTest();
    Thread t1 = new Thread(lockTest);
    Thread t2 = new Thread(lockTest);
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(i);
  }
}    
            

2、中断响应

使用synchronized时,一个线程要么获得锁继续执行,要么就会一直等待。而使用重入锁,则可以根据需要取消对锁的请求,也可以有效避免死锁

public class IntRunable implements Runnable{
  public static ReentrantLock lock1 = new ReentrantLock();
  public static ReentrantLock lock2 = new ReentrantLock();
  int lock;
  public IntRunable(int lock){
    this.lock = lock;
  }
  @Override
  public void run(){
    try{
      if(lock == 1){
        // 获取锁lock1并设置为可响应中断的
        lock1.lockInterruptibly();
        try{
          Thread.sleep(500);
        } catch(InterruptedException e){
          e.printStackTrace();
        }  
        // 获取锁lock2并设置为可响应中断的    
        lock2.lockInterruptibly();
      } else {
        // 获取锁lock2并设置为可响应中断的 
        lock2.lockInterruptibly();
        try{
          Thread.sleep(500);
        } catch(InterruptedException e){
            e.printStackTrace();
        } 
        // 获取锁lock1并设置为可响应中断的 
        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{
    IntRunable intRunableA = new IntRunable(1);
    IntRunable intRunableB = new IntRunable(2); 
    Thread tA = new Thread(intRunableA);
    Thread tB = new Thread(intRunableB);  
    tA.start();
    tB.start();
    Thread.sleep(1000);
    // 中断tB,打破tA、tB之间的死锁
    tB.interrupt();
  }
}        

线程tA和tB启动后,tA先占用lock1,休眠0.5秒后再去尝试占用lock2,,tB则先抢占了lock2,休眠0.5秒后再去尝试占用lock1,因此两者之间就会形成互相等待的死锁。但由于主线程中对tB线程进行中断,故tB会放弃对lock1的申请,同时释放已获得lock2。tA线程就可以顺利得到lock2而继续执行下去了。

3、限时等待

除了等待外部通知外,避免死锁的另一个方法就是限时等待。也就是说给线程获取锁的时候加个时间限制,当超过这个时间后,线程仍未获取到锁,就放弃。重入锁的tryLock() 方法可进行一次限时的等待。tryLock()方法接收两个参数,一个等待时长,一个时间单位。ReentrantLock.tryLock()方法也可以不带参数直接运行。在这种情况下,当前线程会尝试获得锁,如果锁并未被其他线程占用,则申请锁会成功,并立即返回true。如果锁被其他线程占用,则当前线程不会进行等待,而是立即返回false。

public class LimitTimeRunnable implements Runnable{
  public static ReentrantLock lock = new ReentrantLock();
  @Override
  public void run(){
    try{
      // 尝试获取锁,5秒内没获取到就放弃
      if(lock.tryLock(5, TimeUnit.SECONDS){
        Thread.sleep(6000);
      }else{
        System.out.println(Thread.currentThread().getName() + "获取锁失败");
      }
    }catch(InterruptedException e){
      e.printStackTrace();
    }finally{
      if(lock.isHeldByCurrentThread()){
        lock.unlock();
      }
    }
  }
  public static void main(String[] args){
    LimitTimeRunnable ltRunnable = new LimitTimeRunnable();
    Thread t1 = new Thread(ltRunnable, "t1");
    Thread t2 = new Thread(ltRunnable, "t2");
    t1.start();
    t2.start();
  }
}                  

执行结果:
Java多线程之锁的基本使用_第1张图片
在本例中,两个线程会同时尝试去获取同一把锁,并且获取到后sleep6秒,所以未获取到锁的线程在等待5秒后就会自动放弃,从而获取失败。

4、公平锁

在大多数情况下,锁的获取都是非公平的,也就是说并不是按线程的先来后到给线程分配锁,系统只是会从这个锁的等待队列中随机挑选一个,后申请锁的线程可能会先获取到锁,并且更倾向于将锁分配给已经持有锁的线程。而公平的锁会按照时间的先后顺序,保证先到先得,因此公平锁的一大特点是它不会产生饥饿现象。
synchronized关键字产生的锁就是非公平的,而重入锁允许对其公平性进行设置。ReentrantLock有这样一个构造函数:

public ReentrantLock(boolean fair);

当参数fair为true时,就表示将重入锁设置为公平的。公平锁虽保障了公平,但也由此需要维护一个有序队列,实现成本比较高,性能相对也非常低下。所以默认情况下,锁是非公平的。

5、重入锁实现原理

在重入锁的实现中,主要包含三个要素:

  • 使用CAS(Compare And Swap,比较交换)来存储当前锁的状态,判断锁是否已经被别的线程持有;
  • 所有申请锁的线程在没有获取到锁时,会进入等待队列进行等待,直到有线程释放锁后,系统就从等待队列中选择一个线程;
  • 使用阻塞原语park()和unpark(),用来挂起和恢复线程,没有得到锁的线程将会被挂起。

二、Condition

Condition是与重入锁相关联的,通过Lock接口的newCondition()方法可以生成一个与当前重入锁相关联的COndition实例。Condition对象可以在某一个时刻通过其await()方法使线程等待或通过signal()方法通知其他线程继续执行,与Object.wait()和Object.notify()类似。具体来说:

  • await() 方法会使当前线程等待,同时释放当前锁,直到其他线程中使用了signal()或signalAll()方法时,才可能重新获取锁并继续执行;
  • awaitUninterruptibly() 方法与await()方法基本相同,但是不会在等待过程中响应中断;
  • signal() 方法用于唤醒一个在等待中的线程,signalAll() 方法则用于唤醒所有等待中的线程,不会释放锁,需要手动释放。
public class ConditionRunnable implements Runnable{
  public static ReentrantLock lock = new ReentrantLock();
  public static Condition condition = lock.newCondition();
  @Override
  public void run(){
    try{
      lock.lock();
      // 使当前线程等待
      condition.await();
      System.out.println("线程被唤醒,继续执行");
    }catch(InterruptedException e){
      e.printStackTrace();
    }finally{
      lock.unlock();
    }
  }
  public static void main(String[] args)throws InterruptedException{
    ConditionRunnable r = new ConditionRunnable();
    Thread t1 = new Thread(r);
    t1.start();
    Thread.sleep(2000);
    lock.lock();
    // 唤醒线程
    condition.signal();
    lock.unlock();
  }
}            

与Object.wait()和notify()方法一样,当线程使用Condition.await()和signal()时,线程需持有相关的重入锁。

三、信号量Semaphore

无论是内部锁synchronized还是重入锁ReentrantLock,一次都只允许一个线程访问一个资源,而信号量可以指定多个线程同时访问某一个资源。

public Semaphore(int permits)
public Semaphore(int permits,boolean fair)

其中permits参数用于指定一次有多少个线程可以访问资源,fair参数指定锁是否是公平的。Semaphore的常用方法有:

// 尝试获取一个准入的许可,若无法获取,则一直等待
public void acquire()
// 与acquire()类似,但不响应中断
public void acquireUninterruptibly()
// 尝试获取一个许可,如果成功返回true,失败false,不会进行等待
public boolean tryAcquire()
// 在指定时间内尝试获取一个许可
public boolean tryAcquire(long timout, TimeUnit unit)
// 释放一个许可
public void release()

下面举例演示Semaphore的使用:

public class SemaphoreRunnable implements Runnable{
  // 每次允许5个线程同时访问资源执行
  final Semaphore semp = new Semaphore(5);
  @Override
  public void run(){
    try{
      // 获取执行许可
      semp.acquire();
      // 模拟耗时操作
      Thread.sleep(2000);
      System.out.println(Thread.currentThread().getId() + "线程执行完毕");
      // 释放许可
      semp.release();
    }catch(InterruptedException e){
      e.printStackTrace();
    }
  } 
  public static void main(String[] args){
    // 创建含有20个线程的线程池
    ExecutorService exe = Executors.newFixedThreadPool(20);
    final SemaphoreRunnable r = new SemaphoreRunnable();
    for(int i=0;i<20;i++){
      exe.submit(r);
    }
  }
}       

执行结果:
Java多线程之锁的基本使用_第2张图片
可以看到,每一次输出是5个线程的执行结果。

四、读写锁ReadWriteLock

读写锁可以通过将读操作和写操作分离,减少锁竞争,进而有效的提高系统性能。因为如果使用重入锁或者内部锁,不管是读还是写操作,都需要先等待获取锁,但读操作并不会破坏数据的完整性,这种等待就显得没有意义。
总的来说,使用读写锁可以只对写操作进行加锁,而使读操作真正的并行执行。

public class ReadWriteLockExample{
  // 实例化一个重入锁,与读写锁进行对比
  private static Lock lock = new ReentrantLock();
  // 实例化一个读写锁
  private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  // 获取读锁
  private static Lock readLock = readWriteLock.readLock();
  // 获取写锁
  private static Lock writeLock = readWriteLock.writeLock();
  // 模拟的临界资源
  private int value;

  // 读操作
  public Object handleRead(Lock lock) throws InterruptedException{
    try{
      lock.lock();
      System.out.println("读操作:" + value);
      Thread.sleep(1000);
      return value;
    }finally{
      lock.unlock();
    }    
  }
  // 写操作
  public void handleWrite(Lock lock,int index) throws InterruptedException{
    try{
      lock.lock();
      System.out.println("写操作:" + index);
      Thread.sleep(1000);
      value = index;
    }finally{
      lock.unlock();
    }
  }
  public static void main(String[] args){
    final ReadWriteLockExample example = new ReadWriteLockExample();
    Runnable readRunnable = new Runnable(){
      @Override
      public void run(){
        try{
          // 通过在此处切换具体的锁,可以发现读写锁的执行时间会比重入锁短得多
          example.handleRead(readLock);
          // 或者 example.handleRead(lock);
        }catch(InterruptedException e){
          e.printStackTrace();
        }
      }
    }Runnable writeRunnable = new Runnable(){
      @Override
      public void run(){
        try{
          example.handleWrite(writeLock,new Random().nextInt());
          // 或者 example.handleWrite(lock,new Random().nextInt());
        }catch(InterruptedException e){
          e.printStackTrace();
        }
      }
    }// 模拟18个读线程
    for(int i=0;i<18;i++){
      new Thread(readRunnable).start();
    }
    // 模拟2个写线程
    for(int i=18;i<20;i++){
      new Thread(writeRunnable).start();
    } 
  }
}                           

五、CountDownLatch

Count Down在英文中意为倒计数,Latch为阀门的意思,CountDownLatch可以让一个线程等待,直到给定的倒计数结束,再开始执行。在创建CountDownLatch对象时,必需传递一个整数,这个整数就是计数个数。

public class CountDownLatchRunnable implements Runnable{
  static final CountDownLatch latch = new CountDownLatch(10);
  static final CountDownLatchRunnable r = new CountDownLatchRunnable();
  @Override
  public void run(){
    try{
      Thread.sleep(new Random().nextInt(10)*1000);
      System.out.println(Thread.currentThread().getId() +"号线程任务完成"); 
      // 完成任务,使倒计数减一
      latch.countDown();
    }catch(InterruptedException e){
      e.printStackTrace();
    }
  }
  public static void main(String[] args)throws InterruptedException{
    ExecutorService exec = Executors.newFixedThreadPool(10);
    for(int i=0;i<10;i++){
      exec.submit(r);
    }
    // 使主线程等到倒计数倒计完毕后才能再继续执行
    latch.await();
    System.out.println("所有准备任务已完成,继续运行主线程");
    exec.shutdown();
  }
}               

执行结果:
Java多线程之锁的基本使用_第3张图片

六、CyclicBarrier

CyclicBarrier中文意为循环栅栏,和CountDownLatch类似,也可以实现线程间的计数等待,但它的功能比CountDownLatch更加复杂强大。比如,假设我们将CyclicBarrier计数器设置为10,那么凑齐第一批10个线程后,计数器就会归零,然后接着凑齐下一批10个线程,这就是循环栅栏内在的含义。
CyclicBarrier可以接收一个参数作为barrierAction。所谓barrierAction就是当计数器一次计数完成后,系统会执行的动作。如我们现在对苹果进行装箱操作,每箱苹果6个,等有6个苹果送来后进行打包封装:

public class CyclicBarrierExample{
  public static class Apple implements Runnable{
    private int appleNo;
    private final CyclicBarrier cyclic;
    
    Apple(CyclicBarrier cyclic, int appleNo){
      this.cyclic = cyclic;
      this.appleNo = appleNo;
    }
  
    @Override
    public void run(){
      try{
        // await()方法会使当前线程阻塞,直到cyclic计数器计数完毕
        // 也就是说要求达到指定线程数时
        cyclic.await();
        putApple();
      }catch(InterruptedException  | BrokenBarrierException e){
        e.printStackTrace();
      } 
    }
    void putApple(){
      try{
        // 模拟放入苹果的操作耗时
        Thread.sleep(Math.abs(new Random().nextInt()%10000));
      }catch(InterruptedException e){
        e.printStackTrace();
      }
      System.out.println("放入" + appleNo +"号苹果");
    }
  }
  public static class PackageApple implements Runnable{
    int N;
    public PackageApple(int N){
      this.N = N;
    }
    @Override
    public void run(){
        System.out.println(N + "个苹果已全部送来,开始封装打包"); 
    }
  }
  public static void main(String[] args){
    final int N = 6;
    Thread[] allApple = new Thread[N];
    CyclicBarrier cyclic = new CyclicBarrier(N, new PackageApple(N));
    for(int i=0;i<N;i++){
       allApple[i] = new Thread(new Apple(cyclic,i));
       allApple[i].start();
     }
   }
 }                        

Java多线程之锁的基本使用_第4张图片

你可能感兴趣的:(#,java多线程,java,后端,重入锁,读写锁,多线程)