[多线程]Java多线程05_Lock类使用

一. Lock介绍

jdk5中新增了Lock对象,可以用来更加精确的控制线程之间的安全以及线程之间的通讯。

二. ReentrantLock类的使用

通过生产者和消费者来示范ReentrantLock类的使用,这个例子中包含了Lock加锁解锁的使用以及线程之间的通讯

通过创建5个生产者和5个消费者,对同一个资源进行操作,一个生产者一次生产5个,一个消费者一次生产一个。

/**
 * 资源类,包含创建资源和消费资源的方法
 * @author liweidan
 * @date 2017.12.19 下午2:05
 * @email toweidan@126.com
 */
public class Library {
    /** 资源锁 */
    ReentrantLock lock = new ReentrantLock();
    /** 生产者通知 */
    Condition producerCond = lock.newCondition();
    /** 消费者通知 */
    Condition consumerCond = lock.newCondition();
    /** 资源 */
    List<String> stringList = new ArrayList<>();
    /**
     * 生产方法
     */
    public void produce() {
        while (true) {
            try {
                /** 开始加锁 */
                lock.lock();
                /** 如果为空才进入生产 */
                if (stringList.isEmpty()) {
                    for (int i = 0; i < 5; i++) {
                        stringList.add("String - " + i);
                    }
                    System.out.println(Thread.currentThread().getName() + ":生产完成");
                }
                /** 唤醒所有的消费者 */
                consumerCond.signal();
                /** 抢到线程的生产者开始睡眠 */
                producerCond.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
    /**
     * 消费方法
     */
    public void consume() {
        while (true) {
            try {
                /** 开始加锁 */
                lock.lock();
                /** 叛空,如果没有资源提醒消费者生产 */
                if (stringList.isEmpty()) {
                    /** 如果已经没有了则唤醒一个生产者 */
                    producerCond.signal();
                    /** 然后该线程进入休眠 */
                    consumerCond.await();
                } else {
                    /** 消费一个 */
                    stringList.remove(stringList.size() - 1);
                    System.out.println(Thread.currentThread().getName() + ":消费完成");
                    if (stringList.isEmpty()) {
                        /** 如果已经没有了则唤醒一个生产者 */
                        producerCond.signal();
                    } else {
                        /** 如果还有资源,唤醒其他消费者 */
                        consumerCond.signal();
                    }
                    /** 该线程进入休眠 */
                    consumerCond.await();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

/**
 * 消费者演示Lock类
 * @author liweidan
 * @date 2017.12.19 下午2:05
 * @email toweidan@126.com
 */
public class Consumer implements Runnable {
    private Library library;
    public Consumer(Library library) {
        this.library = library;
    }
    @Override
    public void run() {
        library.consume();
    }
}

/**
 * 生产者演示Lock类
 * @author liweidan
 * @date 2017.12.19 下午2:04
 * @email toweidan@126.com
 */
public class Producer implements Runnable {
    private Library library;
    public Producer(Library library) {
        this.library = library;
    }
    @Override
    public void run() {
        library.produce();
    }
}

private static void startConsumerAndProducer() {
    Library library = new Library();
    Thread[] consumerThreads = new Thread[5];
    Thread[] producerThreads = new Thread[5];
    for (int i = 0; i < 5; i++) {
        consumerThreads[i] = new Thread(new Consumer(library), "Consumer" + i);
        producerThreads[i] = new Thread(new Producer(library), "Producer" + i);
    }
    for (int i = 0; i < 5; i++) {
        consumerThreads[i].start();
        producerThreads[i].start();
    }
}

// 结果:
Producer4:生产完成
Consumer1:消费完成
Consumer4:消费完成
Consumer0:消费完成
Consumer3:消费完成
Consumer2:消费完成
Producer2:生产完成
Consumer3:消费完成
Consumer2:消费完成
Consumer1:消费完成
Consumer4:消费完成
Consumer0:消费完成
Producer3:生产完成
......

可以看到,一个生产线程完成以后可以准确的有四个消费者消费,而且也不会像notifyAll()方法一样盲目的唤醒所有线程,控制粒度更加精细。

(一)公平锁和非公平锁

锁Lock分为公平锁以及非公平锁,两者的特点很好理解:

  1. 公平锁:公平所表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得
  2. 非公平锁:谁先抢到锁就是谁的。

可以看到上面的例子,消费者基本都有按照一定的顺序来做业务,而非公平锁则只要在创建锁的使用使用ReentrantLock lock = new ReentrantLock(false);的构造方法来构建。

(二)Lock类部分方法的使用

  1. int getHoldCount() 查询当前线程锁定(调用lock()方法)的次数
  2. int getQueueLength() 查询当前正等待获取锁定线程的估计数
  3. int getWaitQueueLength(Condition condition) 获取等待一个条件锁的线程数(多少个线程调用了condition.await()方法)
  4. boolean hasQueueThread(Thread thread) 查询指定的线程是否正在等待获取此锁定
  5. booleanhasQueueThreads() 查询是否有线程正在等待此锁定
  6. boolean hasWaiters(Condition condition) 查询是否有现成正在等待与此锁有关的条件
  7. boolean isFair() 查询该锁是否是公平锁
  8. boolean isHeldByCurrentThread() 查询当前线程是否保持此锁定(是否调用了lock()方法)
  9. boolean isLocked() 查询此锁是否由任意线程锁定着(是否有线程调用了lock()方法)
  10. boolean lockInterruptibly()lock()方法作用一样,只不过是如果当前线程被中断了就会抛出异常
  11. boolean tryLock() 调用的时候如果没有其他线程持有锁则锁定,锁定成功返回true
  12. boolean tryLock(long timeout, TimeUnit unit) 指定在等待时间内尝试获取锁

(三)Condition类部分方法的使用

  1. awaitUninterruptibly() 调用此方法如果线程在wait状态则不被标记interrupted状态
  2. awaitUntil(Date deadLine) 线程在指定时间到达前可以被唤醒。

三. ReentrantReadWriteLock类的使用

这是一个把读写锁分离的锁

使用也很简单:
1. 通过ReentrantReadWriteLock lock = new ReentrantReadWriteLock()
2. 通过lock.readLock().lock()对读取操作进行加锁
3. 通过lock.writeLock().lock()对写入操作进行加锁

特性:
1. 读写、写写互斥
2. 读读不互斥

示例读读不互斥

/**
 * 
 * @author liweidan
 * @date 2017.12.19 下午5:12
 * @email toweidan@126.com
 */
public class Library {
    public List<String> stringList;
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    public void read() {
        /** 锁定读取锁 */
        lock.readLock().lock();
        /** 开始读取数据 */
        Iterator<String> iterator = stringList.iterator();
        while (iterator.hasNext()) {
            String next = iterator.next();
            System.out.println(Thread.currentThread().getName() + ": " + next);
        }
        /** 解锁 */
        lock.readLock().unlock();
    }
}

/**
 * 写进程
 * @author liweidan
 * @date 2017.12.19 下午5:09
 * @email toweidan@126.com
 */
public class ReadThread implements Runnable {
    private Library library;
    public ReadThread(Library library) {
        this.library = library;
    }
    @Override
    public void run() {
        library.read();
    }
}

/**
 * 
 * @author liweidan
 * @date 2017.12.19 下午5:10
 * @email toweidan@126.com
 */
public class ThreadStart {
    public static void main(String[] args) {
        startReadThreads();
    }
    private static void startReadThreads() {
        Library aLibrary = new Library();
        aLibrary.stringList = new ArrayList<>(5);
        for (int i = 0; i < 10; i++) {
            aLibrary.stringList.add("String" + i);
        }
        for (int i = 0; i < 5; i++) {
            new Thread(new ReadThread(aLibrary)).start();
        }
    }
}

// 结果: 
...
Thread-3: String8
Thread-3: String9
Thread-0: String5
Thread-0: String6
Thread-0: String7
Thread-0: String8
Thread-0: String9
Thread-1: String5
Thread-1: String6
Thread-1: String7
Thread-1: String8
Thread-1: String9
...
这是乱序打印的

但是如果加上去写的操作那么就是互斥的了:

/**
 * 
 * @author liweidan
 * @date 2017.12.19 下午5:12
 * @email toweidan@126.com
 */
public class Library {
    public List<String> stringList;
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

     ......

    public void write() {
        /** 锁定读取锁 */
        lock.writeLock().lock();
        /** 开始读取数据 */
        for (int i = 0; i < 10; i++) {
            stringList.add("StringO" + i);
        }
        /** 解锁 */
        lock.writeLock().unlock();
    }
}

/** 启动线程用于写数据以及读数据 */
private static void startReadThreads() {
    Library aLibrary = new Library();
    aLibrary.stringList = new ArrayList<>(5);
    for (int i = 0; i < 5; i++) {
        new Thread(new WriteThread(aLibrary)).start();
        new Thread(new ReadThread(aLibrary)).start();
    }
}

// 结果:
Thread-5: StringO8
Thread-5: StringO9
WriteThread: StringO0
WriteThread: StringO1
WriteThread: StringO2
WriteThread: StringO3
WriteThread: StringO4
WriteThread: StringO5
WriteThread: StringO6
WriteThread: StringO7
WriteThread: StringO8
WriteThread: StringO9
Thread-7: StringO0
Thread-7: StringO1
Thread-7: StringO2
Thread-7: StringO3
Thread-9: StringO0
Thread-9: StringO1

// 可以看到读库是乱序的,但是写库是一定在一起的

四. 总结

这一篇写起来发现特别简单,因为复用了之前的东西,只是用法不同了,所以并没有写多少。

  1. Lock类的使用,其中分为普通的锁以及读写锁
  2. 普通锁的使用以及公平锁和非公平锁的选择
  3. Condition决定唤醒的对象,不需要像以前那样全部唤醒
  4. 读写锁分开读和写两个操作,提高线程的性能。
点赞