指使用锁进行并发控制时, 在锁竞争过程中, 单个线程对持锁的时间与系统时间有着直接关系。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. 如果线程发现等待队列时完成节点, 则会帮助这个节点完成任务。



