JAVA高并发学习(四)——显式锁

一、显式锁和内置锁

JAVA中主要有两种锁,显式锁和内置锁。下面就是一点我个人的理解:

内置锁:synchronized就是内置锁,它存在可选择性差、固话的锁。控制起来并不灵活。

显示锁:指的就是可以显式声明的锁。和synchronized这种内置锁不同的是,显式锁可以手动的声明和释放,应用起来会更加灵活。提供了超时、中断等利于开发的功能。

至于这两种锁应当如何选择的问题,首先JAVA对于内置锁的优化一直在进行,在现在jdk8中很多显式锁的代码也都改用了内置锁synchronized。而且synchronized要比显式锁占用的资源要小一点(最起码少一个要声明的对象)。所以当我们对锁的操作逻辑并不复杂,没有用到尝试取锁、中断等机制,尽量使用synchronized。

二、显式锁的常用方法介绍

所有的显式锁,都是实现了Lock接口的对象。

lock() 拿锁

 unlock()释放锁 

 lockInterruptibly()中断机制  

tryLock()尝试去拿锁,超时机制。

PS :unlock()方法一定要包在finally中,业务代码也定要放在try-catch中,用来保证unlock一定会释放。

三、锁的公平和非公平

先申请锁的线程一定先拿到,这个锁是公平锁。如果资源是无序发放的,则这个锁是非公平的。

非公平锁的性能要比公平锁更好:因为唤醒线程,需要上下文切换。  上下文切换需要消耗5000~10000CPU周期。如果纯粹的公平锁,每个线程都需要一个这个消耗。而非公平锁则可以充分的利用上下文切换的时间做点事情。

四、ReentrantLock  重入锁

ReentrantLock是非常典型的显式锁之一,而它的功能基本可以替代synchronized关键字(synchronized也是重入锁)。而所谓的重入锁,就是这个锁对于当前线程是可以反复进入的(比如递归调用。如果是不可重入,则线程会把自己锁死)。

ReentrantLock使用方法也和synchronized很相似,不过就是在需要加锁的地方 lock.lock(),在释放的地方lock.unlock()。

具体的实例我们放到Condition中,一起发一个完整的实例。

五、Condition条件

Condition条件主要用来控制线程等待和释放当前锁,作用与Object.wait()和Object.notify()方法类似。

如果想要获得一个Condition对象,我们可以通过通过Lock的newCondition()方法来获取这个lock对应的一个Condition。(多次调用这个方法,最后返回的是不同的对象,同时等待和释放的效果也是各自独立的)。

public class ReentrantLockDemo {
    private static ReentrantLock lock = new ReentrantLock();
    private static Condition condition1 = lock.newCondition();
    private static Condition condition2 = lock.newCondition();

    public static void main(String[] args) {
        System.out.println(condition1);
        System.out.println(condition2);
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    lock.lock();
                    System.out.println("线程1启动");
                    System.out.println("线程1开始等待");
                    condition1.await();
                    System.out.println("线程1执行完毕");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }
        }).start();
        SleepTools.second(2);
        new Thread(new Runnable() {
            @Override
            public void run() {
               try {
                   lock.lock();
                   System.out.println("线程2启动");
                   condition1.signal();
                   //创建的 condition2并不能唤醒线程1,导致线程1永远等待
//                condition2.signal();
                   System.out.println("signal");
                   SleepTools.second(2);
                   System.out.println("释放锁");
               }finally {
                   lock.unlock();
               }
            }
        }).start();
    }
}

执行结果:

JAVA高并发学习(四)——显式锁_第1张图片

六、读写锁ReadWriteLock

在当前这个互联网时代,我们数据的读取规模远大于写入的规模。而这个时候,而每一次读操作对数据的完整性并没有任何的影响。所以如果使用传统的锁,每一次读都需要等待上一个线程释放锁,那样会大大的降低系统的效率。

而ReadWriteLock读写锁就是在JDK5中提供的读写分离锁,这个锁并不是一味的把线程锁死,而是会根据情况来进行线程的释放。具体的释放规则如下:

1、读操作不阻塞读操作

2、读操作阻塞写操作

3、写操作也阻塞读操作

如果在系统中读操作远大于写操作的话,这个读写锁就能很好的发挥它的作用。

使用读写锁,我们需要先创建一个ReadWriteLock对象(注意:这里的ReadWriteLock只是一个接口,具体的实现类是ReentrantReadWriteLock),然后通过这个对象的getReadLock()和getWriterLock()分别获取读锁和写锁。具体的操作是使用getReadLock()和getWriterLock()获得的对象来进行的。

接下来是一个读写锁的例子,并且和使用普通的重入锁的对比:

public class ReadWriteLockDemo {
    private static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static Lock readLock = readWriteLock.readLock();
    private static Lock writeLock = readWriteLock.writeLock();
    private static Lock lock = new ReentrantLock();
    private static CountDownLatch latch = new CountDownLatch(100);
    static class ReadLockHandle implements Runnable{
        private Lock lock;
        private int id;
        public ReadLockHandle(Lock lock, int id) {
            this.lock = lock;
            this.id = id;
        }
        @Override
        public void run() {
            try {
                lock.lock();
                SleepTools.ms(10);
                System.out.println(id + ": 读操作完成!");
                latch.countDown();
            }finally {
                lock.unlock();
            }
        }
    }
    static class WriteLockHandle implements Runnable{
        private Lock lock;
        private int id;

        public WriteLockHandle(Lock lock, int id) {
            this.lock = lock;
            this.id = id;
        }
        @Override
        public void run() {
            try {
                lock.lock();
                SleepTools.ms(50);
                System.out.println(id+": 写操作完成!");
                latch.countDown();
            } finally {
                lock.unlock();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newFixedThreadPool(10);
        long begin = System.currentTimeMillis();
        for (int i=0 ; i<100; i++){
            if(i % 5 == 0){
                exec.submit(new WriteLockHandle(writeLock,i));
//                exec.submit(new ReadLockHandle(lock,i));
            }else{
                exec.submit(new ReadLockHandle(readLock,i));
//                exec.submit(new WriteLockHandle(lock,i));
            }
        }
        latch.await();
        System.out.println(System.currentTimeMillis() - begin);
        exec.shutdown();
    }
}

(这个代码中提前用到了线程池的概念,我们这里就不详细介绍了,就可以忽视掉,认为就是不停的new出来的线程就可以了。)

在使用读写锁的时候,这段代码运行花费了1172毫秒。

而使用普通的重入锁的时候,使用了4219毫秒。

这两者在这种情况下的效率差距还是非常明显的。

你可能感兴趣的:(JAVA)