并发核心技术总结(三)

(10)关于锁优化的几点建议

  • 减少锁持有的时间

  • 减小锁粒度

    HashMap是线程不安全的

    HashTable无论是进行读还是写操作都需要获取锁,因此有了ConcurrentHashMap的出现

    如ConcurrentHashMap有16个Segment,也就是有16把锁,这样的话不同的线程获取不同锁锁住某一个Segment,就可以实现高并发的操作,这也是减小锁粒度的一个典型应用

    image

  • 使用读写锁替换独占锁

    ReentrantLock的升级版ReentrantReadWriteLock就可以实现效率的提升。

    ReentrantReadWriteLock有两个锁:一个是与读相关的锁,称为“共享锁”;另一个是与写相关的锁,称为“排它锁”。

  • 锁分离

    只要操作互不影响,锁就可以分离。最典型的一个例子就是LinkedBlockingQueue ,示意图如下:

    image

LinkedBlockingQueue是基于链表的,take和put方法分别是向链表中取数据和写数据,他们的操作一个是从队列的头部一个是队列的尾部,从理论上说他们是不冲突的,也就是说可以锁分离的。

如果使用独占锁的话,则要求两个操作在进行时首先要获取当前队列的锁,那么take和put就不是先真正的并发了,因此,在JDK中正是实现了两种不同的锁,一个是takeLock一个是putLock

  • 锁粗化

首先简单例子

public void syncMethod() {
        synchronized (lock) { //第一次加锁
            method1();
        }
		method3();
        synchronized (lock) { //第二次加锁
            mutextMethod();
        }
		method4();
        synchronized (lock) { //第三次加锁
            method2();
        }
    }

如果加锁和线程上下文切换的时间超过方法的执行时间,倒不如使用下面的方式

public void syncMethod() {
        synchronized (lock) {
	        method1();
	        method3();
            mutextMethod();
            method4();
            method2();
        }
    }
  • 锁消除

锁消除是在编译器级别的事情。

在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。

也许你会觉得奇怪,既然有些对象不可能被多线程访问,那为什么要加锁呢?写代码时直接不加锁不就好了。

但是有时,这些锁并不是程序员所写的,有的是JDK实现中就有锁的,比如Vector和StringBuffer这样的类,它们中的很多方法都是有锁的。当我们在一些不会有线程安全的情况下使用这些类的方法时,达到某些条件时,编译器会将锁消除来提高性能。

  • Java虚拟机对锁的优化

(11)原子操作类

image


(12)读写锁ReentrantReadWriteLock深入分析

image

image

image

image

image

锁降级实例演示

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private Lock readLock = lock.readLock();
    private Lock writeLock = lock.writeLock();

    private boolean update;

    public void processData() {
        readLock.lock(); //读锁获取
        if (!update) {
            readLock.unlock(); //必须先释放读锁
            writeLock.lock(); //锁降级从获取写锁开始
            try {
                if (!update) {
                    //准备数据流程(略)
                    update = true;
                }
                //获取读锁。在写锁持有期间获取读锁
                //此处获取读锁,是为了防止,当释放写锁后,又有一个线程T获取锁,对数据进行改变,
                //而当前线程下面对改变的数据无法感知。
                //如果获取了读锁,则线程T则被阻塞,直到当前线程释放了读锁,那个T线程才有可能获取写锁。
                readLock.lock();
            } finally {
                writeLock.unlock();//释放写锁
            }
            //锁降级完成
        }

        try {
            //使用数据的流程
        } finally {
            readLock.unlock(); //释放读锁
        }
    }

LockSupport工具

不管是ReentrantReadWriteLock还是ReentrantLock,当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类完成相应的工作。LockSupport方法如下

image

image

总结

  • 1、读锁的重入是允许多个申请读操作的线程的,而写锁同时只允许单个线程占有,该线程的写操作可以重入。

  • 2、如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。

  • 3、对于同时占有读锁和写锁的线程,如果完全释放了写锁,那么它就完全转换成了读锁,以后的写操作无法重入,在写锁未完全释放时写操作是可以重入的。

  • 4、公平模式下无论读锁还是写锁的申请都必须按照AQS锁等待队列先进先出的顺序。非公平模式下读操作插队的条件是锁等待队列head节点后的下一个节点是SHARED型节点,写锁则无条件插队。

  • 5、读锁不允许newConditon获取Condition接口,而写锁的newCondition接口实现方法同ReentrantLock。


(13)等待/通知模式接口Condition接口深入分析

Condition接口实现原理

ConditionObject实现了Condition接口,是AQS的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类是一个比较合理的方式。每一个Condition对象都包含一个等待队列,该队列是Condition实现等待通知机制的关键。

和synchronized一样,在调用wait和notify等方法之前都必须要先获取锁,同样使用Condition对象的await和signal方法的时候也是要先获取到锁!

1.等待队列

  • FIFO队列
  • 每个节点包含一个线程的引用
  • Condition拥有首节点和尾节点
  • 节点更新过程没有使用CAS,通过锁来保证线程安全

image

Lock模式下同步队列和等待队列的关系 image

2.等待的实现

image

3.通知的实现

image


(14)多线程异步调用之Future模式

image

image

Future的类图关系 image

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值