并发编程(四)显示锁

1.显示锁

Java程序可以依靠synchronized关键字隐式的获取锁实现锁功能,但是它将锁的获取和释放固话了,也就是先获取再释放。

(synchronized是语言的特性(内置锁),Lock是一个类 使用的时候需要对其实例化 和方法调用,内存,CPU消耗较大。且JDK中对synchonized的优化已经做的很好,一般情况下没有特殊需求 尽量使用synchonized)

显示锁特性:

1)尝试非阻塞的获取锁:当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取,则成功获取并持有锁;

2)能被中断的获取锁:与synchronized不同,获取到锁的线程能够响应中断,当获取到的锁的线程被中断时,中断异常将会被抛出,同时锁会被释放。

3)超时获取锁:在指定的截止时间之前获取锁,如果截止时间到了仍然无法获取到锁,则返回

使用范式:

lock.lock();
try {
num--;
} finally {
lock.unlock();
}

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

常用API

public class UseLock {

    private Lock lock = new ReentrantLock();
    private int num = 100000;

    private int getNum() {
        return num;
    }

    public void add() {
        lock.lock();
        try {
            num++;
        } finally {
            lock.unlock();
        }
    }

    public void remove() {
        lock.lock();
        try {
            num--;
        } finally {
            lock.unlock();
        }
    }


    private static class LockThread extends Thread {

        private UseLock useLock;

        public LockThread(UseLock useLock, String name) {
            super(name);
            this.useLock = useLock;
        }

        @Override
        public void run() {
            for (int i = 0; i < 100000; i++) {
                useLock.add();
            }
            System.out.println(Thread.currentThread().getName() + " the num is:" + useLock.getNum());
        }
    }
    public static void main(String[] args) {
        UseLock useLock = new UseLock();
        new LockThread(useLock, "userLockThread").start();
        for (int i = 0; i < 100000; i++) {
            useLock.remove();
        }
        SleepTools.second(5);
        System.out.println(Thread.currentThread().getName() + " the num is:" + useLock.getNum());
    }
}

 

ReentrantLock

锁的可重入:

简单地讲就是:“同一个线程对于已经获得到的锁,可以多次继续申请到该锁的使用权”。而synchronized关键字隐式的支持重进入,比如一个synchronized修饰的递归方法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获得该锁。ReentrantLock在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。

当方法递归时,线程必然需要再次去获取锁,所以jdk再带的所有锁都必然是可重入的。

公平锁和非公平锁:

如果在时间上,先对锁进行获取的请求一定先被满足,那么这个锁时公平的,反之,是不公平的。(synchronized非公平)

公平的获取锁,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。 ReentrantLock提供了一个构造函数,能够控制锁是否是公平的。事实上,公平的锁机制往往没有非公平的效率高。

在激烈竞争的情况下,非公平锁的性能高于公平锁的性能的一个原因是:在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。假设线程A持有一个锁,并且线程B请求这个锁。由于这个锁已被线程A持有,因此B将被挂起。当A释放锁时,B将被唤醒,因此会再次尝试获取锁。与此同时,如果C也请求这个锁,那么C很可能会在B被完全唤醒之前获得、使用以及释放这个锁。这样的情况是一种“双赢”的局面:B获得锁的时刻并没有推迟,C更早地获得了锁,并且吞吐量也获得了提高。 (线程唤醒 上下文切换 每次约消耗5000-10000 cpu时间周期)

排它锁(独占锁)与读写锁(ReentrantReadWriteLock)

前面说到的锁基本都是排它锁,这些锁在同一时刻只允许一个线程进行访问。

读写锁允许同一时刻有读线程访问,但是在写线程访问时,所有读线程和其他写线程均被阻塞。读写锁维护了一堆锁,一个读锁一个写锁,通过分离读锁和写锁,使得并发性比一般的排它锁效率提升很多。(适用于常见的读多写少的场景)

除了保证写操作对读操作的可见性以及并发性的提升之外,读写锁能够简化读写交互场景的编程方式。假设在程序中定义一个共享的用作缓存数据结构,它大部分时间提供读服务(例如查询和搜索),而写操作占有的时间很少,但是写操作完成之后的更新需要对后续的读服务可见。

在没有读写锁支持的(Java 5之前)时候,如果需要完成上述工作就要使用Java的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行通知之后,所有等待的读操作才能继续执行(写操作之间依靠synchronized关键进行同步),这样做的目的是使读操作能读取到正确的数据,不会出现脏读。改用读写锁实现上述功能,只需要在读操作时获取读锁,写操作时获取写锁即可。当写锁被获取到时,后续(非当前写操作线程)的读写操作都会被阻塞,写锁释放之后,所有操作继续执行,编程方式相对于使用等待通知机制的实现方式而言,变得简单明了。

一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量

ReentrantReadWriteLock 实现的是ReadWriteLock接口

* @see ReentrantReadWriteLock
 * @see Lock
 * @see ReentrantLock
 *
 * @since 1.5
 * @author Doug Lea
 */
public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

写独占,读共享,读写互斥!

Condition接口

用于显示锁的等待通知实现;

任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式。

Condition newCondition();

 

 使用范式

 Lock lock = new ReentrantLock();
 Condition kmCon = lock.newCondition();

public void waitKm() {
        lock.lock();
        try {
            while (this.km < 100) {
                try {
                    kmCon.await();
                    //对应的业务处理
                    System.out.println(Thread.currentThread().getName() + "Check Km changed and be notified");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } finally {
            lock.unlock();
        }
    }


 public void changKm() {
        lock.lock();
        try {
            this.km = 123;
            //通知其他在锁上等待的线程
            kmCon.signal();
//            kmCon.signalAll();
        } finally {
            lock.unlock();
        }
    }

 

 LockSupport:

LockSupport定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也成为构建同步组件的基础工具。

LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark(Thread thread)方法来唤醒一个被阻塞的线程。LockSupport增加了park(Object blocker)、parkNanos(Object blocker,long nanos)和parkUntil(Object blocker,long deadline)3个方法,用于实现阻塞当前线程的功能,其中参数blocker是用来标识当前线程在等待的对象(以下称为阻塞对象),该对象主要用于问题排查和系统监控。

 

参考:http://enjoy.ke.qq.com


 

posted @ 2019-04-26 11:45  苍舒  阅读(194)  评论(0编辑  收藏  举报