多线程引用:需要维护并行数据结构间的一致性状态,需要为线程的切换和调度花费时间。
参考:
4.1 合理的锁性能
4.1.1 减少锁持有时间
原有的程序:对整个方法做同步,导致等待线程大量增加;
因为一个线程,在进入该方法时获得内部锁,只有所有任务都执行完后,才会释放锁;
public synchronized void syncMethod(){
othercode1();
mutexMethod();
othercode2();
}
优化的方案:只在必要时进行同步,能减少线程持有锁的时间
public void syncMethod2(){
othercode1();
synchronized(this){
mutexMethod();
}
othercode2();
}
只针对mutexMethod()方法进行了同步;
4.1.2 减小锁粒度
减小锁粒度:缩小锁定对象的范围,从而降低锁冲突的可能性,进而提高系统的并发能力;
4.1.3 用读写分离锁来替换独占锁
ReadWriteLock读写锁:
减小锁粒度:通过分割数据结构来实现的
读写分离锁:对系统功能点的分割
读操作本身不会影响数据的完整性和一致性;在读多写少的场合使用读写锁可有效提升系统的并发能力;
4.1.4 锁分离
读写锁:根据读写操作功能上的不同,进行了有效的锁分离。
依据程序的功能特点,使用类似的分离思想可对独占锁进行分离:LinkedBlockingQueue
LinkedBlockingQueue:
take()方法:使用takeLock,只在get操作中使用;
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
put()方法:使用putLock,只在put操作中使用;
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
take()方法和put()方法相互独立,之间不存在锁竞争;只在take()和take()方法之间,put()和put()方法之间存在竞争。从而削弱了锁竞争的可能性;
通过takeLock和putLock两把锁,LinkedBlockingQueue实现了取数据和读数据的分离;
4.1.5 锁粗化
锁粗化:虚拟机在遇到一连串连续地对同一个锁不断进行请求和释放操作时,便会把所有的锁整合成对锁的一次请求,从而减少对锁的请求同步的次数;
锁粗化之前:
public void demoMethod(){
synchronized(lock){
// do sth.
}
// 做其他不需要的同步的工作,但很快能执行完毕
synchronized(lock){
// do sth.
}
}
锁粗化之后:
public void demoMethod(){
synchronized(lock){
// do sth.
//做其他不需要的同步的工作,但很快能执行完毕
}
}
锁粗化 的思想和 减少锁持有时间 是相反的;
4.2 JVM对锁优化的支持
JDK内部的锁优化策略
4.2.1 锁偏向
4.3 ThreadLocal
ThreadLocal 是一个线程的局部变量,只有当前线程可以访问;自然是线程安全的
1、线程不安全的例子
/**
* SimpleDateFormat不是线程安全的,在线程池中共享这个对象会导致错误。
*
*/
public class ThreadLocaLDemo {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static class ParseDate implements Runnable{
int i = 0;
public ParseDate(int i){this.i=i;}
@Override
public void run() {
try {
Date t = sdf.parse("2020-07-23 22:46:"+i%60);
System.out.println(i+":"+t);
}catch (ParseException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
es.execute(new ParseDate(i));
}
}
}
2、使用ThreadLocal未每一个线程创造一个SimpleDaetformat对象实例
/**
* SimpleDateFormat不是线程安全的,在线程池中共享这个对象会导致错误。
* ThreadLocal只起到了简单的容器作用;
* 为每一个线程分配一个对象的工作不是由ThreadLocal来完成的,而是需要在应用层面来保证的。
* 如果在应用上为每一个线程分配了相同的对象实例,那么ThreadLocal也不能保证线程安全。
*
*/
public class ThreadLocaLDemo2 {
static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>();
public static class ParseDate implements Runnable{
int i = 0;
public ParseDate(int i) {this.i=i;}
@Override
public void run() {
try {
// 如果当前线程不持有SimpleDateformat对象实例,
// 就新建一个并把它设置到当前线程中
if (tl.get() == null) {
tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));;
}
// 若已经持有则直接使用
Date t =tl.get().parse("2020-07-23 22:46:"+i%60);
System.out.println(i+":"+t);
}catch (ParseException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
es.execute(new ParseDate(i));
}
}
}
4.3.2 ThreadLocal的实现原理
java 引用类型:
ThreadLocal类的 ThreadLocalMap类
4.3.3 测试使用ThreadLocal时的性能
package HIighParallel.chp4.p3;
import java.util.Random;
import java.util.concurrent.*;
public class RandMultiThread {
// 定义了每个线程要产生随机数数量
public static final int GEN_COUNT = 10000000;
// 定义了参与工作的线程数量
public static final int THREAD_COUNT = 4;
// 定义了线程池
static ExecutorService exe = Executors.newFixedThreadPool(THREAD_COUNT);
// 定义:被多线程共享的Random实例,用于产生随机数
public static Random rnd = new Random(123);
// 定义了由ThreadLocal封装的Random
public static ThreadLocal<Random> tRnd = new ThreadLocal<Random>(){
@Override
protected Random initialValue() {
return new Random(123);
}
};
// 定义工作线程的内部逻辑,有两种模式
// 第一种模式:多线程共享一个Random(mode=0)
// 第二种模式:多个线程各分配一个Random(mode=1)
public static class RndTask implements Callable<Long>{
private int mode = 0;
public RndTask(int mode){
this.mode = mode;
}
public Random getRandom(){
if (mode == 0){
return rnd;
}else if (mode == 1){
return tRnd.get();
}else {
return null;
}
}
// 定义了线程的工作内容;每个线程都会产生若干个随机数,完成工作后记录所消耗的时间
@Override
public Long call() throws Exception {
long b = System.currentTimeMillis();
for (long i = 0; i < GEN_COUNT; i++) {
getRandom().nextInt();
}
long e = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + "Spend " + (e-b) + " ms");
return e - b;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
Future<Long>[] futs = new Future[THREAD_COUNT];
// 第一种工作模式:多线程共享一个Random(mode=0)
for (int i = 0; i < THREAD_COUNT; i++) {
futs[i] = exe.submit(new RndTask(0));
}
long totaltime = 0;
for (int i = 0; i < THREAD_COUNT; i++) {
totaltime += futs[i].get();
}
System.out.println("多线程访问同一个Random实例:" + totaltime + "ms");
// 第二种工作模式:多个线程各分配一个Random(mode=1)
// ThreadLocal的情况
for (int i = 0; i < THREAD_COUNT; i++) {
futs[i] = exe.submit(new RndTask(1));
}
totaltime = 0;
for (int i = 0; i < THREAD_COUNT; i++) {
totaltime += futs[i].get();
}
System.out.println("使用ThreadLocal包装Random实例:" + totaltime + "ms");
exe.shutdown();
}
}
4.4 无锁
加锁:悲观的策略;
假设每一次的临界区操作会产生冲突,若遇到多个线程同时访问临界区则让其他线程等待;
无所:乐观的策略;
假设对资源的访问没有冲突,使用CAS技术鉴别线程冲突;
4.4.1 CAS 比较并交换
算法的过程:CAS(V, E, N)
V:表示要更新的变量
E:表示变量的期望值
N:表示新值
1)当V值 等于 E值 时,才会将V值修改为N值
当V值不等于E值时,不会进行V值的修改;说明已经有其他线程修改了V值,当前线程什么都不做
2)CAS返回当前V的真实值
3)CAS是一种乐观的策略
4)当多个线程使用CAS操作一个变量时,只有一个能成功修改,其余会失败;失败的线程不会被挂起,仅被告知失败,并且允许再次尝试,也允许失败的线程放弃操作;
CAS的问题:
1、ABA问题:
CAS需要在操作值的时候检查值有没有发生变化,若没有发生变化则更新;
若值从A,变成了B,又变成A,那么CAS进行检查的时候就会认为它的值没有变化,但实际上变化了
解决思路:使用版本号(JUC包中的AtomicStampedReference类)
2、循环时间长开销大
如果CAS不成功,则会原地自旋,会有开销存在;
3、只能保证一个共享变量的原子操作
CAS无法用于对多个共享变量的操作;
4.4.2 AtomicInteger 无锁的线程安全整数
AtomicInteger类
// 无锁的情况下,使用volatile关键字保证线程间的数据的可见性;这样在获取变量时才能直接读取
private volatile int value;
incrementAndGet()方法
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
unsafe.getAndAddInt()方法
/**
var1:待更新的成员变量的对象
var2:成员变量的偏移
var4:要增加多少
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
// do while 自旋操作
do {
// 获取AtomicInteger对象var1,在内存中偏移量未var2处的值;
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
unsafe.getIntVolatile() 方法
// getIntVolatile方法获取:对象中offset偏移地址对应的整型field的值,支持volatile load语义
public native int getIntVolatile(Object var1, long var2);
unsafe.compareAndSwapInt()方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
/**
compareAndSwapInt():是一个本地方法
逻辑类似于:
if( this == expect){
this = update
return true;
}else{
return false;
}
*/
4.4.4 无锁的对象引用:AtomicReference
ABA问题
AtomicInteger:对整数的封装;
AtomicReference:对普通的对象的引用;
package HIighParallel.chp4.p4;
import java.util.concurrent.atomic.AtomicReference;
public class AtomicReferenceDemo {
static AtomicReference<Integer> money = new AtomicReference<Integer>();
public static void main(String[] args) {
money.set(19);;
// 模拟多个线程同时更新后台数据库,未用户充值
for (int i = 0; i < 3; i++) {
new Thread(){
@Override
public void run() {
while (true){
while (true){
Integer m = money.get();
// 判断账户余额并给予赠送金额,如果已被处理则当前线程就会失败
if (m<20){
if(money.compareAndSet(m, m+20)){
System.out.println("余额小于20元,充值成功,余额:"+money.get()+"元");
break;
}else {
break;
}
}
}
}
}
}.start();
}
// 用户消费线程,模拟消费行为
new Thread(){
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
while (true){
Integer m = money.get();
// 赠与金额到账的同时,客户进行了一次消费,总金额小于20,正好累计消费了20元。
// 使得消费、赠与后的金额等于消费前、赠与前的金额,那么就会后台的赠与进程就会误认为这个账户还没有赠与,存在多次赠与的可能;
if (m>10){
System.out.println("大于10元");
if (money.compareAndSet(m, m-18)){
System.out.println("成功消费10元,余额:"+money.get());
break;
}
}else {
System.out.println("没有足够的金额");
break;
}
}
}try {
Thread.sleep(100);
}catch (InterruptedException e){
}
}
}.start();
}
}
4.4.5 带有时间戳的对象引用:AtomicStampedReference
AtomicReference:进行CAS时仅比较对象的值;
AtomicStampedReference: 带有时间戳的对象引用,进行CAS时不仅比较对象的值,还比较时间戳;
package HIighParallel.chp4.p4;
import java.util.concurrent.atomic.AtomicStampedReference;
public class AtomicStampedReferenceDemo {
static AtomicStampedReference<Integer> money = new AtomicStampedReference<Integer>(19, 0);
public static void main(String[] args) {
// 模拟多个线程更新后台数据库,为用户充值
for (int i = 0; i < 3; i++) {
/**
* 获得账户的时间戳;
* 若赠与成功,则修改时间戳,使得系统不可能发生二次赠与;
*/
final int timestamp = money.getStamp();
new Thread(){
@Override
public void run() {
while (true){
while (true){
Integer m = money.getReference();
if (m<20){
// 赠与成功就将时间戳加1;
if (money.compareAndSet(m, m+20, timestamp, timestamp+1)){
System.out.println("余额小于20元, 充值成功, 余额:"+money.getReference()+"元");
break;
}
}else {
break;
}
}
}
}
}.start();
}
// 用户消费进程,模拟消费行为
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
while (true){
int timestamp = money.getStamp();
Integer m = money.getReference();
if (m>10){
System.out.println("大于10元");
if (money.compareAndSet(m, m-10, timestamp, timestamp+1)){
System.out.println("成功消费10元,余额: "+money.getReference());
break;
}
}else {
System.out.println("没有足够的金额");
break;
}
}
try {
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}.start();
}
}
4.4.6 数组无锁:AtomicIntegerArray
JUC可用的原子数组有:AtomicIntegerArray、AtomiclongArray、AtomicReferenceArray
分别表示整数数组、long型数组、普通的对象数组;
AtomicIntegerArray:
本质是对int[ ] 类型的封装;使用Unsafe类通过CAS的方式控制int[ ]在多线程下的安全性;
package HIighParallel.chp4.p4;
import java.util.concurrent.atomic.AtomicIntegerArray;
public class AtomicIntegerArrayDemo {
// 声明了一个包含10个元素的原子整数数组
static AtomicIntegerArray arr = new AtomicIntegerArray(10);
// 该线程类型功能:对数组内10个元素进行累加操作
public static class AddThread implements Runnable{
@Override
public void run() {
for (int k = 0; k < 10000; k++) {
arr.getAndIncrement(k%arr.length());
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] ts = new Thread[10];
// 开启十个累计线程
for (int k = 0; k < 10; k++) {
ts[k] = new Thread(new AddThread());
}
for (int k = 0; k < 10 ; k++) { ts[k].start(); }
for (int k = 0; k < 10; k++) { ts[k].join(); }
System.out.println(arr);
}
}
4.4.7 原子普通变量:AtomicIntegerFieldUpdater
软件设计原则之一:
- 开闭原则:系统对功能的增加应该是开放的,而对修改是保守的;
AtomicIntegerFieldUpdater:
让普通变量也能有CAS操作,从而带来线程安全性;
Updater有三种:
AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
package HIighParallel.chp4.p3;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
public class AtomicIntegerFieldUpdaterDemo {
public static class Candidate{
int id;
volatile int score;
}
public final static AtomicIntegerFieldUpdater<Candidate> scoreUpdater =
AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");
// 检查Updater是否正确工作
public static AtomicInteger allScore = new AtomicInteger(10);
public static void main(String[] args) throws InterruptedException {
final Candidate stu = new Candidate();
Thread[] t = new Thread[10000];
for (int i = 0; i < 10000; i++) {
t[i] = new Thread(){
@Override
public void run() {
if (Math.random()>0.4) {
scoreUpdater.incrementAndGet(stu);
allScore.incrementAndGet();
}
}
};
t[i].start();
}
for (int i = 0; i < 10000; i++) {
t[i].join();
}
System.out.println("score="+stu.score);
System.out.println("allScore="+allScore);
}
}
AtomicIntegerFieldUpdater:
- Updater只能修改它可见范围内的变量,因为Updater使用反射得到这个变量;
- 变量必须是volatile类型。保证可见性
- CAS操作通过对象实例在内存中的偏移量进行复制,因此它不支持static字段;