JAVA并发编程实战-性能和可伸缩性

本文探讨了性能与可伸缩性之间的关系,介绍了Amdahl定律,强调了线程开销如上下文切换、内存同步和阻塞的影响。通过缩小锁的范围、减小锁粒度、分离锁和避免热点域来减少锁的竞争,从而提升并发程序的性能和可伸缩性。
摘要由CSDN通过智能技术生成

思维导图

在这里插入图片描述

1 性能的思考

使用线程最主要原因是增加性能。

改进性能意味着更少的资源做更多的事。

1.1 性能“遭遇”可伸缩性

可伸缩性:当增加计算资源(cpu、内存、贷款等),吞吐量和生产力更高相应的改善。

性能的两个方面——有多快和有多少是完全分离的。为了实现更好的可伸缩性,我们通常停止增加任务的工作量,而是把任务分解到多个子任务。

1.2 对性能的权衡进行评估

避免不成熟的优化。首先使程序正确,然后再加快。

2 Amdahl定律

Amdahl定律:描述在一个系统中,基于可并行化和串行化组件所占的比重,程序通过额外的资源,理论能够加速多少。

S p e e d U p ≤ 1 ( F + ( 1 − F ) N ) SpeedUp \le \frac{1} {(F+\frac{(1-F)}{N})} SpeedUp(F+N(1F))1
F:串行化所占比重,N:处理器个数

以F:0.1,N:100为例,则最多达到9.2%使用率。

下面demo-1是一个串行任务例子:

private final BlockingQueue<Runnable> taskQueue;

    public WorkThread(BlockingQueue<Runnable> queue) {
        taskQueue = queue;
    }

    /**
     * 串行从任务队列拿取任务
     */
    @Override
    public void run() {
        while (true) {
            try {
                Runnable task = taskQueue.take();
                task.run();
            } catch (InterruptedException e) {
                break;
            }
        }
    }

一个线程从队列取任务,其它线程想要取就必须等待——任务处理中串行部分。

所有的并发程序都有一些串行源。

2.1 示例:框架中隐藏的串行化

比如LinkedList加同步锁和ConcurrentLinkedQueue。两者的串行化不同,

LInkedList通过Collections.synchronizedList()对整个更新操作都会进行加锁,从而变为串行化。
在这里插入图片描述

ConcurrentLinkedQueue通过原子化来更新操作。
在这里插入图片描述

3 线程引入的开销

对于性能改进的线程来说,并行带来的性能优势必须超过并发带来的开销。

3.1 切换上下文

切换上下文是要付出代价的:线程的调度需要操纵OS和JVM中的共享数据结构。

比如如果一个线程因为竞争锁陷入阻塞,JVM通常会挂起这个锁,运行它被换出。如果线程频繁发生阻塞,将导致频繁的上下文切换,增加调度的开销。

3.2 内存同步

synchronized和volatile提供的可见性保证要求需要使用一个名为存储关卡的指令,来刷新缓存,使缓存无效。

现在的JVM已经可以通过优化解除不存在竞争的锁,减少同步,如demo-2:

//锁优化
    private int a ;
    public void meaninglessSynchronized() {
        //无意义的同步,jvm会进行优化去掉锁
        synchronized (new Object()) {
            a++;
        }
    }

更加成熟的JVM可以分析逸出,如果本地对象引用没有暴露,是线程本地的,则可以安全访问。如下的demo-3:

/**
     *
     * 没有逸出,不需要进行加锁,会直接优化掉锁
     * @return
     */
    public String escape() {
        List<String> scoope = new Vector<>();
        scoope.add("shanghai");
        scoope.add("beijing");
        scoope.add("xian");
        return scoope.toString();
    }

上述中Vector的加锁操作会被取消,如果没有逸出分析,编译器会进行锁的粗化,会将三次add操作的锁合并。

3.3 阻塞

当竞争锁失败的线程通常采取下面两种方式:

  • 自旋等待:适合短期的等待。
  • 操作系统中挂起:适合长时间等待。

具体取决于上下文切换的开销。

4 减少锁的竞争

减少锁的竞争可以改进性能和可伸缩性。

并发程序中,对可伸缩性的首要威胁就是独占的锁资源。

通常有两个因素影响锁的竞争性:

  • 锁被请求的频率。
  • 锁持有的时间。

减少锁的竞争:

  • 减少持锁时间。
  • 减少请求锁频率。
  • 使用协调机制取代独占锁。

4.1 缩小锁的范围

如下demo-4:

private final Map<String, String> attributesMap = new HashMap<>();

    /**
     * 持有锁超过必要的时间,其实只有get操作需要加锁,其余不需要进行同步
     * @param name
     * @param regexp
     * @return
     */
    public synchronized boolean userLocationMather(String name, String regexp) {
        String key = "user." + name + ".location";
        String value = attributesMap.get(key);
        if (value == null) {
            return false;
        }
        return Pattern.matches(regexp, value);
    }

demo-4锁是加在方法上,锁持有时间超过了必要的时间,因为方法中许多操作不需要加锁。

改进如demo-5:

 /**
     * 进行优化,减少持有锁的时间
     * @param name
     * @param regexp
     * @return
     */
    public boolean betterUserLocationMather(String name, String regexp) {
        String key = "user." + name + ".location";
        String value;
        synchronized (this) {
            value = attributesMap.get(key);
        }
        if (value == null) {
            return false;
        } else {
            return Pattern.matches(regexp, value);
        }
    }

demo-5也就是缩小加锁范围。

4.2 减小锁的粒度

减小锁的粒度可以通过分拆锁或者分离锁实现。

如下demo-6:

/**
 * 应当拆分锁
 */
public class ServerStatus {
    private final Set<String> users = new HashSet<>();
    private final Set<String> queries = new HashSet<>();

    public synchronized void addUser(String user) {
        users.add(user);
    }
    public synchronized void removeUser(String user) {
        users.remove(user);
    }

    public synchronized void addQuery(String query) {
        queries.add(query);
    }

    public synchronized void removeQuery(String query) {
        queries.remove(query);
    }
}

demo-6中users和queries关联不大,不需要使用同一个锁进行操作,完全可以分离。

改进为demo-7:

/**
 * 通过锁拆分重构ServerStatus
 */
public class BetterServerStatus {
    private final Set<String> users = new HashSet<>();
    private final Set<String> queries = new HashSet<>();

    public void addUser(String user) {
        synchronized (users) {
            users.add(user);
        }
    }

    public void removeUser(String user) {
        synchronized (users) {
            users.remove(user);
        }
    }

    public void addQuery(String query) {
        synchronized (queries) {
            queries.add(query);
        }
    }

    public void removeQuery(String query) {
        synchronized (queries) {
            queries.remove(query);
        }
    }

}

通过users和queries两个锁改善了竞争情况

4.3 分离锁

分拆锁有时候可以扩展为加锁块的集合,这就是分离锁。比如ConcurrentHashMap就是使用了16个锁的Array。

如下demo-8是对基于hash的map的演示:

/**
 * 基于哈希map使用分离锁
 */
public class StripedMap {
    private static final int N_LOCKS = 16;
    private final Object[] locks;
    private final Node[] map ;

    public StripedMap(int size) {
        map = new Node[size];
        locks = new Object[N_LOCKS];
        for (int i = 0; i < N_LOCKS; i++) {
                locks[i] = new Object();
        }
    }

    public int hash(Object key) {
        return Math.abs(key.hashCode() % map.length);
    }

    public Object get(Object key) {
        int hash = hash(key);
        synchronized (locks[hash % N_LOCKS]) {
            for (Node node = map[hash]; node != null; node = node.next) {
                if (node.key.equals(key)) {
                    return node.value;
                }
            }
        }
        return null;
    }

    public void clear() {
        for (int i = 0; i < map.length; i++) {
            synchronized (locks[i%N_LOCKS]) {
                map[i] = null;
            }
        }
    }
    
    private static class Node {
        public Node next;
        public Object key;
        public Object value;
    }
}

通过多个分离锁分别处理map不同部分。

负面:对容器加锁,进行独占的访问更加困难和昂贵。

4.4 避免热点域

比如容器中的计数器size,每个更改操作都要访问size的话,将导致可伸缩性问题。这个情况下,size被称为热点域。

总结

伸缩性通常可以通过下面方式进行提升:减少获取锁的时间、减小锁的粒度、减少锁占有时间或者使用非独占非阻塞锁代替独占锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LamaxiyaFc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值