多线程优化(性能调优)
目录
性能相关基础:
上下文切换:
2.多线程锁优化
2.1 案例
2.2 优化方案1--使用原子操作类AtomicXXX
2.2 LongAdder对象
3.多线程之并发容器优化
4.多线程之线程池优化
1.多线程基础
性能相关基础:
上下文切换:
无论是单核cpu还是多核cpu,都会有cpu时间片(分配给线程的运行时间),现在有两种情况:
- 线程1 运行完了
- 线程1 阻塞,挂起
当上述两种情况发生,就会发生上下文切换。上文:保存进度。下文:加载进度
上文:线程1 进度(执行进度信息,如执行完,执行了60%以后阻塞挂起)。
下文:线程2 进度(下一个线程执行信息,可能之前执行过,然后被唤醒)。
主频为1GHz的cpu执行一条cpu指令,耗时是1ns。一次上下文切换大约耗时8微秒=8000纳秒。
2.多线程锁优化
2.1 案例
先看这样一段代码:
public class LockTest {
long count = 0;
public void access(){
count++;
}
public static void main(String[] args) throws InterruptedException {
LockTest lockTest = new LockTest();
for (int i=0;i<10;i++){
new Thread(()->{
for (int j=0;j<1000;j++){
lockTest.access();
}
}).start();
}
Thread.sleep(5000);
System.out.println(lockTest.count);
}
}
该代码设置了10个线程,每个线程内对count执行1000次加一操作。由于线程安全问题,代码中打印的count的值一定是小于等于10000的。所以要对其进行优化。
2.2 优化方案1--使用原子操作类AtomicXXX
优化代码如下所示:
public class LockTest {
// long count = 0;
AtomicLong count = new AtomicLong();
public void access(){
count.incrementAndGet();
}
public static void main(String[] args) throws InterruptedException {
LockTest lockTest = new LockTest();
for (int i=0;i<10;i++){
new Thread(()->{
for (int j=0;j<1000;j++){
lockTest.access();
}
}).start();
}
Thread.sleep(5000);
System.out.println(lockTest.count);
}
}
AtomicLong类中的部分代码如下所示。
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicLong.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile long value;
public final long incrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}
这个value也就是上述代码中的count的值,在静态代码块中根据value获取count的内存偏移地址,然后将AtomicLong对象和value属性偏移地址传入unsafe.getAndAddLong()方法中,该方法代码如下所示:
public final long getAndAddLong(Object var1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
return var6;
}
根据传入的AtomicLong对象和value偏移地址获取到value的值,赋值给var6,然后进行cas操作,具体百度cas原理。这里不再详细赘述。
基于cas实现的原子操作类,优势在哪?对比锁sync Lock。
没有上下文切换。假设一行java代码,cpu循环一次需要10ns,10个线程,按照平均法则分配cpu资源,10ns*10=100ns,
加锁10个线程,需要9次上下文切换,9*8微秒=72微秒=72000ns
总结:cas原子操作类 lock锁 sync锁
2.2 LongAdder对象
atomic还可以进一步优化么?如果是jdk8,推荐使用LongAdder对象,比AtomicLong性能更好(减少乐观锁的重试次数)
代码实现:
public class LockTest {
// long count = 0;
// AtomicLong count = new AtomicLong(); //原子操作类
LongAdder count = new LongAdder();
public void access(){
count.add(1);
}
public static void main(String[] args) throws InterruptedException {
LockTest lockTest = new LockTest();
for (int i=0;i<10;i++){
new Thread(()->{
for (int j=0;j<1000;j++){
lockTest.access();
}
}).start();
}
Thread.sleep(5000);
System.out.println(lockTest.count);
}
}
atomicXXX和LongAdder的区别:分裂value,类似于分布式cas,分裂共享资源,最后累加。减少了乐观锁的自旋次数。
性能对比:
public class LongAdderTest {
public static void main(String[] args) {
testAtomicLongVSLongAdder(10,10000);
testAtomicLongVSLongAdder(20,200000);
testAtomicLongVSLongAdder(30,200000);
}
//多线程并发模拟及耗时统计
private static void testAtomicLongVSLongAdder(final int threadcount, final int time) {
try {
long start = System.currentTimeMillis();
testAtomicLong(threadcount,time);
long end = System.currentTimeMillis()-start;
System.out.println("Atomic-time:"+end);
long start1 = System.currentTimeMillis();
testLongAdder(threadcount,time);
long end1 = System.currentTimeMillis()-start1;
System.out.println("LongAdder-time:"+end1);
}catch (InterruptedException e){
e.printStackTrace();
}
}
private static void testLongAdder(final int threadcount,final int time) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(threadcount);
LongAdder longAdder = new LongAdder();
for (int i=0;i<threadcount;i++){
new Thread(new Runnable() {
@Override
public void run() {
for (int j=0;j<time;j++){
longAdder.add(1);
}
countDownLatch.countDown();
}
},"mythread"+i).start();
}
countDownLatch.await();
}
private static void testAtomicLong(final int threadcount,final int time) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(threadcount);
AtomicLong atomicLong = new AtomicLong();
for (int i=0;i<threadcount;i++){
new Thread(new Runnable() {
@Override
public void run() {
for (int j=0;j<time;j++){
atomicLong.incrementAndGet();
}
countDownLatch.countDown();
}
},"mythread"+i).start();
}
countDownLatch.await();
}
}
运行结果:
10个线程数量,10000次数Atomic-time:4
10个线程数量,10000次数LongAdder-time:5
20个线程数量,200000次数Atomic-time:67
20个线程数量,200000次数LongAdder-time:28
30个线程数量,200000次数Atomic-time:76
30个线程数量,200000次数LongAdder-time:5
当线程数较少时(10个线程),LongAdder的分裂操作和最后加和操作耗费的时间相对于cas操作的时间来说就比较明显。优化效果不明显,因为LongAdder的分裂操作和加操作也会耗费一定的时间。
当线程数较多时,优化效果就很明显。
atomicXXX:强一致性
LongAdder:弱一致性
1、强一致性:在任何时刻所有的用户或者进程查询到的都是最近一次成功更新的数据。强一致性是程度最高一致性要求,也是最难实现的。关系型数据库更新操作就是这个案例。简而言之:就是操作完以后立刻拿到数据进行一致性核实。大厂高并发很少有强一致性!!!!!atomic在执行完cas操作以后就会立刻获取数据核实,因此是强一致性。
2、最终一致性:和强一致性相对,在某一时刻用户或者进程查询到的数据可能都不同,但是最终成功更新的数据都会被所有用户或者进程查询到。当前主流的nosql数据库都是采用这种一致性策略。LongAdder在执行完cas操作以后还要对各个结果进行求和以后才会核验一致性,因此是弱一致性。
锁优化过程:
3.多线程之并发容器优化
HashMap--->ConcurrentHashMap
ArrayList--->CopyOnWriteArrayList:适合于多读多写的场景。
实现原理:写时复制,当写入数据的时候,CopyOnWriteArrayList会先进行扩容操作,将老的数据复制到新的数组中。如果此刻有个读的线程来操作资源,当写线程还没完成时,则数组指针还是指向未扩容的老数组,故读的数据还是老数据。若此时写线程已经完成工作,则数组指针就会指向新的数组,此时读到的数据是新的数据。
4.多线程之线程池优化
这些指标都存在与两个map中:
//线程池整个运行状态,存放多个runnableNameMap,key--task,value--runnableNameMap
public Map<String,Map> transactionMap = new ConcurrentHashMap<>();
//存储单个任务状态
public Map<String,String> runnableNameMap= new ConcurrentHashMap<>();
CSDN-Ada助手: 推荐 网络 技能树:https://edu.csdn.net/skill/network?utm_source=AI_act_network
普通网友: 优质好文,博主的文章细节很到位,兼顾实用性和可操作性,感谢博主的分享,文章思路清晰【我也写了一些相关领域的文章,希望能够得到博主的指导,共同进步!】
SeaDhdhdhdhdh: 对的,一般没有在生产环境或者大流量的情况下使用这种方式,但是设置索引操作一般为一次操作,不直接写在代码里面跟随流量去执行,所以就在这记录了这两种方式。
madmacbeth: 但是alter table方式,会导致锁表,如果直接在运行中的生产环境中,不建议用这种方式
CSDN-Ada助手: 推荐 Go 技能树:https://edu.csdn.net/skill/go?utm_source=AI_act_go