写在前面
之前花了好几天看完了 JUC 部分的第3章笔记。还是挺多的。
接下来看一下 第4章 关于锁优化部分的内容
第四章 锁的优化及其注意事项
第四章知识框架锁性能的几点建议
- 减小锁持有时间
- 系统有锁时间越长,锁竞争越激烈,我们只对需要同步的方法加锁,来减小锁持有的时间从而提高锁性能
- 减小锁持有时间可以降低锁发生冲突的可能,进而提高锁的并发能力。
- 减小锁粒度
- 减小锁粒度就是 缩小锁定对象的范围,从而减小锁冲突的可能,提高并发能力
- 锁分离(读写分离锁代替独占锁)
- 使用读写锁可以减少操作之间互相等待,提高性能。ConcurrentLinkedQueue (上一章最后提到的并发性能最好的 队列)中的 take 和 put 方法分别使用了两个锁避免了锁竞争,提高了性能。
- 锁粗化
- 一个锁被频繁的请求、释放,也会耗费大量资源
- JVM 在遇到一连串对同一个锁的请求、释放等操作时,将这些操作整合成为一次,这样就是锁粗化。
Java 虚拟机对锁优化做的努力
- 锁偏向
- 一个线程获得锁之后,这个锁就进入了偏向模式,当这个线程再次请求锁时,无需同步操作
- JVM参数 -XX:UseBiasedLocking 开启偏向锁
- 轻量级锁
- 偏向锁失败,JVM 不会立即挂起线程,而是使用轻量级锁进行操作
- 轻量级锁只是 将对象头部作为指针,指向持有锁的线程堆栈内部,来判断一个线程是否持有对象锁
- 线程获得轻量级锁成功,则可以成功进入临界区。轻量级锁加锁失败,则其他线程抢到了锁,当前线程的轻量级锁变成重量级锁
- 自旋锁
- 无法获得锁的时候,会让线程做几个空循环
- 长时间还无法获得锁进入临界区,则会真正挂起线程
- 锁消除
- JVM会去除不可能存在共享资源竞争的锁。
ThreadLocal:人手一支笔
书上给了这么一个例子:100个人都要填写个人信息表,如果只有一个笔,那么我们就通过加锁的方式,让大家合理利用这个笔,从而不抢夺。
但是,从另外一个角度来说,我们可以给他们100个笔,也就是说,人手一支笔。
Sync 和 ThreadLocal 对比:
二者都用于解决多线程并发访问的问题。但是 sync 是通过加锁的方式,使得某个时间段只有一个线程能够访问资源;而 ThreadLocal 是为每个线程都提供了对象的副本,每个线程在同一个时间访问的并不是同一个对象。总结,Sync 用于线程间的数据共享,ThreadLocal 用于线程间的数据隔离。
- ThreadLocal 的使用:
- ThreadLocal 是线程的局部变量,只有当前线程可以访问,因此是线程安全的。
- ThreadLocal 只是起到容器的作用,为每一个线程分配不同对象需要应用层面保证。
public class ThreadLocalExample {
public static class MyRunnable implements Runnable {
private ThreadLocal threadLocal = new ThreadLocal();
@Override
public void run() {
//一旦创建了一个ThreadLocal变量,你可以通过如下代码设置某个需要保存的值
threadLocal.set((int) (Math.random() * 100D));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
//可以通过下面方法读取保存在ThreadLocal变量中的值
System.out.println("-------threadLocal value-------" + threadLocal.get());
}
}
public static void main(String[] args) {
MyRunnable sharedRunnableInstance = new MyRunnable();
Thread thread1 = new Thread(sharedRunnableInstance);
Thread thread2 = new Thread(sharedRunnableInstance);
thread1.start();
thread2.start();
}
}
- ThreadLocal 的原理:
原理就牵扯到 set() 和 get() 方法。简单理解为每一个线程都对应一个 map(其实是 ThreadLocalMap),这个map就存的是 <当前线程,value>。我们 set 和 get 时候都是在取这个 value,所以才会做到每一个线程对应一个 value,做到为每一个线程分配一个对象副本。
-
在 set 时,首先通过 getMap() 获得当前线程的 ThreadLocalMap,写入 key=currnetThread,value = set()的value
-
在 get 时,首先当前线程的 ThreadLocalMap,然后将自己(currentThread)作为key,获得 value
-
线程不退出的情况下,其内部维护的ThreadLocalMap 就不会清除。佐伊在线程池这种场景下,需要使用 ThreadLocal 的 remove 方法来清楚数据。
无锁
之前说过,并发策略都是悲观的策略;与之相对的无锁,就是一种乐观的策略。
无锁的好处:
- 没死锁问题
- 没锁调度消耗
- 没锁竞争消耗
CAS算法
无锁使用的一种技术叫做 比较交换(Compare and Swap),用来鉴别冲突,一旦检测到并发冲突,就重试到当前操作没有冲突为止。
- 包含3个参数(V,E,N),V表示要被更新的变量,E表示期望值,N表示要设置的值
- 仅当 V = E,即被更新的变量的值等于期望值,才会将 V 设置为 N。如果不相等表示V的值已经被其他线程修改了,当前线程不做任何操作
- 当多个线程访问的时候,只有一个线程成功。其他的线程会被告知失败,其他线程可以选择再次执行或者是放弃
无锁线程安全的整数
- AtomicInteger:是最常用的类,它是可变并且线程安全的。
看一个使用的例子
public class AtomicIntegerTest {
static AtomicInteger i = new AtomicInteger();
public static class AtomicIntegerThread implements Runnable {
@Override
public void run() {
for (int k = 0; k < 10000; k++) {
i.incrementAndGet();
}
}
}
public static void main(String[] args) throws InterruptedException {
AtomicIntegerThread atomicIntegerThread = new AtomicIntegerThread();
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int k = 0; k < 10; k++) {
executorService.submit(atomicIntegerThread);
}
Thread.sleep(500);
System.out.println(i);
}
}
程序执行结果会输出 10000。说明其运行正确了。
看到最重要的一行就是 AtomicInteger.incrementAndGet(),这个方法会使用 CAS 操作将自己加1,同时返回当前值。
下面看一下这个方法的的具体实现
public final int increamentAndGet(){
for(;;){
int current = get();
int next = current +1;
if (compareAndSet(current, next))
return next;
}
}
public finanl int get(){
return value;
}
第3行的 get 取得当前值,加1后得到 next。这里, 就得到了 CAS 的两个参数:期望值E 和 新值V。使用 compareAndSet 方法将新值 next 写入,成功的条件时在写入的时刻,当前的值要等于刚刚取得的 current(说明没有被别的线程修改)
总的来说,就是在一个无限循环内,不断重复将比自己大1的新值赋给自己。如果失败,就说明在 “获取-设置” 时候被其他线程修改了,然后做下一次尝试。
Java中的指针 Unsafe 类
- 指针是不安全的,因此 Java 中去除了指针,但是通过 Unsafe 类封装了一些不安全的操作。我们是无法使用这个类的。
无锁对象引用 AtomicReference
- AtomicReference 的作用是对 “对象” 进行原子操作,但是会丢失状态信息
- 如果需要保存状态信息则可以使用 AtomicStampedReference,其维护了一个时间戳
- AtomicIntegArray 是线程安全的数组
原子操作工具类AtomicIntegerFieldUpdater
- 让普通变量,在不改变代码的基础上,也可以享受CAS操作带来的原子性线程安全,就是 原子操作工具类要做的事情
package com.ecut.atomic;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
public class AtomicIntegerFieldUpdaterTest {
public static AtomicInteger allScore = new AtomicInteger();
public final static AtomicIntegerFieldUpdater<Candidate> socoreUpdater =
AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");
public static class Candidate {
int id;
volatile int score;
}
public static class Vote implements Runnable {
Candidate candidate;
public Vote(Candidate candidate) {
this.candidate = candidate;
}
@Override
public void run() {
if (Math.random() > 0.4) {
socoreUpdater.incrementAndGet(candidate);
allScore.incrementAndGet();
}
}
}
public static void main(String[] args) throws InterruptedException {
Candidate candidate = new Candidate();
Vote vote = new Vote(candidate);
ExecutorService executorService = Executors.newFixedThreadPool(10);
for(int i = 0 ; i < 10000 ; i++){
executorService.execute(vote);
}
Thread.sleep(5000);
System.out.println(allScore+":"+candidate.score);
}
}
运行结果中输出的两个数字是相等的。说明通过 原子操作工具类 维护了线程安全 。
使用时要注意:
- Updater只修改范围可见的变量,因为是通过反射得到的
- 这个变量必须是 volatile 的,因为要有可见性
- 不支持 static 字段的变量
网友评论