栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

锁的优化及注意事项

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

锁的优化及注意事项

1. 减少锁持有时间

指使用锁进行并发控制时, 在锁竞争过程中, 单个线程对持锁的时间与系统时间有着直接关系。100个人填写报告, 只有一支笔, 怎么处理? 下面用代码演示:

当只有mutextMethod方法需要加锁时, 不推荐这样写:

public synchronized void syncMethod() {

    test01();
    mutextMethod() ;
    test02() ;    
}

推荐加锁方式:

public void syncMethod2() {

    test01();
    synchronized(this){
        mutextMethod() ;
    }
    test02() ;    
}

注意: 减少锁持有时间有助于降低锁冲突的可能性, 进而提升系统的并发能力。

2. 减少锁粒度

技术典型:ConcurrentHashMap类的实现, 对于HashMap中最重要的两个方法get() 和 put()。

一种最自然的方式就是直接对HashMap加锁, 但是这样做锁粒度很大, 所以我们可以对它内部进一步细分若干个小的HashMap, 称之为段(SEGMENT). 默认情况下是16个段。

当ConcurrentHashMap进行put()操作时, 会根据hashCode得到该表项被放到那个段中, 处于不同的段中, 则线程间可以做到真正的并行。

ConcurrentHashMap存在的问题: 当需要获取全局锁时, 意思是我现在要获取count值时, 这时候我需要获取全局锁, 但是由于我们分成了16段,及我们需要对16个段分别上锁后, 才能进行count计算, 接着释放16个段的锁。但事实上ConcurrentHashMap的size()是首先采取无锁的方式求和, 失败才会尝试这种加锁方式。但是还是会影响性能!

所以: 使用减少粒度的方式,我们争对与类似size()获取全局信息的方法调用并不频繁时,使用才能真正意义上提高系统吞吐量。

注意:所谓减少锁粒度, 就是缩小锁定对象的范围, 从而减少锁冲突的可能性, 进而提示高系统的并发能力。

3.读写分离锁替代独占锁

在读多写小的情况下, 使用读写分离锁来替代独占锁, 从而可以有效的提示系统的并发能力。

4. 锁分离:

就是使用ReentrantLock + Condition实现的, 列如数据共享通道BlokingQueue, 其实BlokingQueue就我而言, 向网络与网络间的传输数据的解耦合, 使用到的数据缓冲区, 应该就是使用了BlokingQueue的思想!(生产者和消费者之间的解耦合)

代码演示:

// take()操作 使用
private final RetrantLock takeLock = new RetrantLock() ;
private final Condition notEmpty = takeLock.newCondition() ;

// put() 操作使用
private final RetrantLock putLock= new RetrantLock() ;
private final Condition notFull= putLock.newCondition() ;

在独占锁下肯定不能提高高并发的性能, 我们需要把take锁和put()进行分离, 他们相互独立后, 不存在了竞争关系, 从而得以消弱锁竞争。

5.  锁粗化:

指的是java虚拟机会对遇到一连串地对同一锁不断进行请求和释放操作时, 便会把所有操作整合在一次请求中, 相当于在我们减少锁持有时间时凡事有个度。

java虚拟机对锁优化所做的努力: 1. 锁偏向:

         指的是, 当线程获得锁后, 那么锁就进入偏向模式, 当这个线程再一次请求时, 无需做任何同步操作。这样就节省了大量锁申请操作!

2. 轻量级锁:

        当多个线程竞争锁资源时, 偏向锁将失败, 从而进入轻量级锁, 它只是简单地将对象头部作为指针,指向持有锁的线程堆栈的内部, 来判断一个线程是否持有对象锁, 成功进入临界区, 不成功, 则会膨胀为重量级锁。

3. 自旋锁:

        锁膨胀后, 虚拟机为了不让程序操作系统层面上挂起, 做了最后的努力, 赌一波, 空循环若干次, 得到锁进入, 反之挂起。

4. 锁清除:

        通过对运行上下文进行扫描, 除去不可能存在的共享资源竞争锁, 通过锁清除, 可以节食毫无意义的请求锁时间。什么锁不存在竞争, 我们还会写上去了:

String[] createStrings() {

    Vector v = new Vector<>() ;
    for() {
        v.add(Integer.toString(i))
    }
    return v.toArray(new String[]{}) ;

}

我们看到v 其实就是一个Vetor局部遍历但存在大量同步操作, 又由于是在线程栈上分配, 属于私有变量, 因此不能被其他线程访问, 所以虚拟机检查会进行锁清除。

锁清除涉及的关键技术就是逃逸分析, 如果return v  那么j将逃逸出当前这个函数, 其他线程就可能访问, 则不能清除。

ThreadLocal的理解:

        当我们希望某个变量, 只对当前线程开放, 比如说我们做分页的时候, 也许是我们做Transaction事务管理时, 保证使用同一个实例对象! 这时候我们可以把实例添加到ThreadLocal中,(如果存在,取出来使用, 不存在再创建) 它相当于是一个容器。 只对当前线程可访问的容器。

注意:

        1.  为每一个线程分配不同的对象, 需要再应用层面保证, ThreadLocal只起到简单的容器作用。如果不能保障每条线程分配不同的对象, 那么ThreadLocal也不能保障线程安全。

        2. 我们的ThreadLocal随着线程的释放而释放, 但是如果自己不采取什么行动, 就如线程池,它的线程是可复用的并没有消亡, 这时候将一些大大小小的对象加入ThreadLocal中, 将导致内存泄露的可能, 这时当我们确定不用它时, 需要我们手动remove()清除, 但是我们也可以采用threadLocal = null ,应为ThreadLocal是弱引用, 虚拟机在发现弱引用会直接回收!

        3. 对性能有何帮助, 当我们为每个线程分配一个独立的对象时, 就是共同访问同一个实例, 也许我们会得到性能优化, 但是如果这个实例对象容易造成引起性能损失, 比如Random, 那么我们可以考虑使用ThreadLocal, 我们可以测试在多线程情况下, a). 访问同一个Random实例对象 b). 访问ThreadLocal为我们包装的实例对象。 效率使用很大的差异的!

无锁:(CAS)

我们可以把锁分为乐观锁和悲观锁, 对于并发控制而言, 锁是一种悲观的策略, 它总是假设每一次临界区会才是冲突, 因此, 宁愿牺牲性能让线程等待, 所有会阻塞线程执行。 而无锁就是乐观策略, 总是假设对资源访问没有冲突,即不会阻塞当前线程,  要是遇见冲突怎么办, 无锁的策略是使用CAS交换技术来鉴别线程冲突, 一旦检查发生冲突, 则重试当前操作直到没有冲突!

1. 不阻塞, 对死锁天生免疫,使用无锁, 完全没有锁竞争带来的开销, 也没有频繁线程调度带来的开销, 因此比基于锁的方式拥有更优越的性能。

2. CAS算法过程, 包含三个参数(V, E, N)V指更新的当前实际变量值, E指预期值, N表示新值, 再每次交换过程中, 只有V 和 E 相等时, 才会进行更新新值

3. 多个线程使用CAS时, 操作一个变量时, 只会有一个线程会胜出, 其余的告知失败, 不会阻塞, 允许再次尝试。

基于无锁的实现的原子操作类 AtomicInteger:
public final int incrementAndGet() {
    
    for(;;) {
        int current = get();  // a1
        int next = current + 1 ;
        if (compareAndSet(current, next)) // a2
            return next ; 
    }
    
}

这里: 就是一个简单的CAS实现如果在进行CAS操作时, 写入的当前值, 和之前得到的current不一样, 说明在a1到a2之间数据被干扰了, 则会修改不成功。

Java中的指针: Unsafe类

接着我们来看一下, compareAndSet的的操作源码:

public final boolean compareAndSet(int expect, int update) {
        
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update) ;

}

unsafe.compareAndSwapInt(Object 0, long offset, int expect, int x) 

我们来解析一下, 刚才说了, 我们需要获取在进行操作时, 当前对象的值与刚才获取出来的current进行比较, 那么current有了, 当前值怎么去了, 就是类似与c的指针, offset是对象内的偏移量, 我们可以根据这个偏移量, 快速定位字段, 从而得到值!

当然我们自己的程序是无法直接使用Unsafe的, 在类加载器中屏蔽了Unsafe

AtimicReference: 让普通对象也具有原子性

首先说一个原子性不足的地方: 谈一谈ABA问题:

线程一的到了一个原值, 之后线程二修改了值, 但在线程一再次获取前, 线程二又修改回来了, 这就造成了, 没有被改动的错觉。在这种情况下使用AtimicReference就无能为力了。

典型的案列: 一家蛋糕店,决定给每个贵宾卡余额小于20元的客户一次性赠送20元, 刺激消费者充值和消费。 但条件是, 每位客户只能被赠送一次。

首先基于AtimicReference实现功能

AtomicStampedReference 带时间戳的对象引用

使用该对象, 即可解决上述问题, 我们让对象有检测数据状态的功能, 就是利用时间戳, 返回读写, 只要时间戳没有发生变化, 就能防止不恰当写入。

数据交换通道: SynchronousQueue

它将put() 和 take() 两个功能截然不同的操作抽象为一个互通的方法Transferer.transfer() 数据传递的意思。任何一个对SynchronousQueue的写都需到等待一个读, 反之亦然。 所有被称为数据交换。

1. 如果队列为空, 或者队列中节点的类型和本次操作一致, 那么将当前操作压入队列等待, 比如俩个操作进来都是读操作, 则会等待 “匹配” 操作

2. 当操作互补时, 则插入一个 “完成”的状态, 比如写操作和读操作互补, 并且让俩个线程继续执行

3. 如果线程发现等待队列时完成节点, 则会帮助这个节点完成任务。

 

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/781673.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号