欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JUC学习之Lock同步锁

程序员文章站 2024-01-09 23:52:04
...

一、简介

引出Lock同步锁之前,先了解一下synchronized同步的一些缺陷:

如果一段代码被synchronized锁住,那么当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁。如果某个时刻获得锁的线程发生阻塞现象,那么这把锁会被它一直持有,而其他线程永远无法获取锁,正常来说,不能让其他线程永远在那里等待。

  • 使用Lock锁的话,提供了一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断);
  • 使用Lock锁的话,通过tryLock()方法可以尝试获取锁,这就知道线程有没有成功获取到锁;

基于上面Lock的两大优势,我们今天总结一下Lock同步锁相关的知识。

Lock是一个接口,源码如下:

public interface Lock {
    //获取锁,如果锁已被其他线程获取,则进行等待。
    void lock();
    //当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态
    void lockInterruptibly() throws InterruptedException;
    //尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false
    boolean tryLock();
    //指定时间内尝试获取锁,在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    //释放锁
    void unlock();
    //创建Condition
    Condition newCondition();
}

主要的方法有:lock()获取锁、tryLock()尝试获取锁、unlock()手动释放锁,主要使用Lock一定要手动释放锁,一般放在finally块中保证锁得到释放;

二、常用API使用

(1)、如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

下面是JDK官网推荐的写法:

Lock l = ...;
 l.lock();
 try {
   // access the resource protected by this lock
 } finally {
   l.unlock();
 }

(2)、尝试获取锁tryLock()

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理业务
     }catch(Exception ex){
         
     }finally{
         //手动释放
         lock.unlock();   
     } 
} else {
    //未成功获取锁,则处理其他业务逻辑
}

下面我们介绍一下java.util.concurrent.locks包中常用的类和接口。

三、ReentrantLock可重入锁

ReentrantLock是可重入锁,也是经常使用到的同步锁之一。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。

类图如下:

JUC学习之Lock同步锁

public class ReentrantLock implements Lock, java.io.Serializable {
    //...
}

 下面通过一个简单的示例说明ReentrantLock的使用方法:

我们模拟三个窗口卖30张火车票,如果使用synchronized同步的话,相信小伙伴们都会了,这里就不介绍了,这里使用ReentrantLock可重入锁实现。

/**
 * 模拟三个售票员卖出30张票
 */
public class T01_SaleTicketDemo01 {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        //窗口1
        new Thread(ticket::sale, "A").start();
        //窗口2
        new Thread(ticket::sale, "B").start();
        //窗口3
        new Thread(ticket::sale, "C").start();
    }
}

/**
 * 共享资源类
 */
class Ticket {
    private int number = 10;
    //声明可重入锁
    private Lock lock = new ReentrantLock();

    public void sale() {
        //获取锁
        lock.lock();
        try {
            while (true) {
                if (number > 0) {
                    TimeUnit.MILLISECONDS.sleep(10);
                    System.out.println(Thread.currentThread().getName() + "卖出了:" + (number--) + ",剩下:" + number);
                } else {
                    break;
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //必须手动释放锁,且放在finally块中
            lock.unlock();
        }
    }
}

运行结果:

A卖出了:10,剩下:9
A卖出了:9,剩下:8
B卖出了:8,剩下:7
C卖出了:7,剩下:6
A卖出了:6,剩下:5
A卖出了:5,剩下:4
A卖出了:4,剩下:3
A卖出了:3,剩下:2
A卖出了:2,剩下:1
A卖出了:1,剩下:0

可见,三个线程只要有一个线程获取了锁,其他线程就只能等待去操作共享资源。

四、ReadWriteLock读写锁

ReadWriteLock也是一个接口,源码如下:

public interface ReadWriteLock {
    /**
     * 获取读锁
     */
    Lock readLock();

    /**
     * 获取写锁,属于排他锁
     */
    Lock writeLock();
}

上面两个方法一个用来获取读锁,一个用来获取写锁,将读和写的锁分开,正常来说,读操作应该允许多个线程同时读,当一个线程写的时候,其他线程不能写也不能读。这就提高了读的效率。

常见的实现类就是ReentrantReadWriteLock:

JUC学习之Lock同步锁

可见,ReentrantReadWriteLock里面分了两把锁:readerLock和writerLock。下面通过例子来介绍一下ReentrantReadWriteLock具体用法:

先看下面的代码:

/**
 * ReentrantReadWriteLock读写锁
 * <p>
 * 读操作共享,写操作独占
 * 即读读可共享、写读写写要独占
 */
public class T11_ReentrantReadWriteLock {
    public static void main(String[] args) {
        //操作共享资源
        SharedData sharedData = new SharedData();

        //五个线程写
        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                sharedData.put(String.valueOf(num), String.valueOf(num));
            }, "A" + num).start();
        }

        //五个线程读
        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                sharedData.get(String.valueOf(num));
            }, "A" + num).start();
        }
    }
}

class SharedData {
    private volatile Map<String, Object> map = new HashMap<>();

    /**
     * 模拟写操作
     */
    public void put(String key, Object value) {
        System.out.println(Thread.currentThread().getName() + "写入开始....");
        try {
            TimeUnit.MILLISECONDS.sleep(400);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "写入完成....value = " + String.valueOf(value));
    }

    /**
     * 模拟读操作
     */
    public Object get(String key) {
        System.out.println(Thread.currentThread().getName() + "读取开始....");
        try {
            TimeUnit.MILLISECONDS.sleep(400);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Object result = map.get(key);
        System.out.println(Thread.currentThread().getName() + "读取完成....result = " + result);
        return result;
    }
}

运行结果:

A1写入开始....
A2写入开始....
A3写入开始....
A4写入开始....
A5写入开始....
A1读取开始....
A2读取开始....
A3读取开始....
A4读取开始....
A5读取开始....
A1读取完成....result = null
A2读取完成....result = null
A3读取完成....result = null
A4读取完成....result = null
A3写入完成....value = 3
A2写入完成....value = 2
A1写入完成....value = 1
A5读取完成....result = 5
A5写入完成....value = 5
A4写入完成....value = 4

从上面的结果可以看出,比如A1开始写入数据,到A1写入成功期间,居然那么多线程对共享资源进行了操作, 这明显有问题。正确结果应该是A1开始写入数据紧接着就是A1写入成功...依次类推。下面我们使用ReentrantReadWriteLock改造一下:

/**
 * ReentrantReadWriteLock读写锁
 * <p>
 * 读操作共享,写操作独占
 * 即读读可共享、写读写写要独占
 */
public class T11_ReentrantReadWriteLock {
    public static void main(String[] args) {
        //操作共享资源
        SharedData sharedData = new SharedData();

        //五个线程写
        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                sharedData.put(String.valueOf(num), String.valueOf(num));
            }, "A" + num).start();
        }

        //五个线程读
        for (int i = 1; i <= 5; i++) {
            final int num = i;
            new Thread(() -> {
                sharedData.get(String.valueOf(num));
            }, "A" + num).start();
        }
    }
}

class SharedData {
    private volatile Map<String, Object> map = new HashMap<>();
    /**
     * 引入读写锁
     */
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    /**
     * 模拟写操作
     */
    public void put(String key, Object value) {
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "写入开始....");
            try {
                TimeUnit.MILLISECONDS.sleep(400);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "写入完成....value = " + value);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    /**
     * 模拟读操作
     */
    public Object get(String key) {
        readWriteLock.readLock().lock();
        Object result = null;
        try {
            System.out.println(Thread.currentThread().getName() + "读取开始....");
            try {
                TimeUnit.MILLISECONDS.sleep(400);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            result = map.get(key);
            System.out.println(Thread.currentThread().getName() + "读取完成....result = " + result);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
        return result;
    }
}

运行结果:

A1写入开始....
A1写入完成....value = 1
A2写入开始....
A2写入完成....value = 2
A3写入开始....
A3写入完成....value = 3
A4写入开始....
A4写入完成....value = 4
A5写入开始....
A5写入完成....value = 5
A1读取开始....
A2读取开始....
A3读取开始....
A4读取开始....
A5读取开始....
A1读取完成....result = 1
A3读取完成....result = 3
A2读取完成....result = 2
A5读取完成....result = 5
A4读取完成....result = 4

由此可知,多个线程只能有一个线程进行写操作,其他线程不能加塞,而读操作则允许多个线程同时读,这样就大大提升了读操作的效率。不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

五、总结

这里我们总结一下Lock和synchronized的区别:

  • Lock是一个接口,而synchronized是Java中的关键字;
  • Lock在代码执行时出现异常时不会自动释放锁,必须手动释放,而synchronized抛出异常时会自动释放锁;
  • Lock如果没有及时释放锁,很可能产生死锁现象,而synchronized由于会自动释放锁,不会导致死锁现象发生;
  • Lock可以让等待锁的线程响应中断,而synchronized不能响应中断,只能一直等待;
  • Lock通过tryLock尝试获取锁可知是否成功获取锁,而synchronized则不行;
  • 如果对共享资源竞争不激烈情况下,两者的性能其实差不多,但是如果竞争激烈,此时Lock的性能要远远优于synchronized方式;