栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

【Java后台开发规范】---线程与并发

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【Java后台开发规范】---线程与并发

文章目录
  • 前言
    • 其他类型的规范
    • 线程池
    • 线程安全
      • 时间
      • 数值
      • 集合
        • Map
        • List
        • Set
    • 线程并发
      • 分段锁
        • 随机数
        • ConcurrentHashMap
      • 读写锁
        • ReentrantReadWriteLock
      • 写时复制
        • CopyOnWriteArrayList
      • 伪共享
    • ThreadLoad

前言

做Java开发的,大多数可能都有看过阿里的Java后台开发手册,里面有关于Java后台开发规范的一些内容,基本覆盖了一些通用、普适的规范,但大多数都讲的比较简洁,本文主要会用更多的案例来对一些规范进行解释,以及结合自己的经验做补充!

其他类型的规范

【Java后台开发规范】— 不简单的命名
【Java后台开发规范】—日志的输出
【Java后台开发规范】— 长函数、长参数

线程池

池化思想、复用思想这是提升性能的一种有效手段,常见的有线程池、连接池、对象池,但是他们都存在一个相同的问题就是池子的容量,在JDK1.5时提供的几种线程池,默认情况下都没有控制,newFixedThreadPool、newSingleThreadExecutor属于几乎无限大队列数,newCachedThreadPool、newScheduledThreadPool属于几乎无限大线程数,所以一般我们要根据实际情况自己构建合理的线程池,另外了为了方便排查问题,要给线程池起一个有意义的名称。

线程安全

提起线程就一定绕不开线程安全的问题,现在几乎都在使用Spring框架,我们知道Spring Bean对象默认都是单例的,是线程不安全的,但是由于三层架构的方式,一般我们注入的Spring Bean实例,像Contorller、Service、Dao这些都是无状态的,自然也就不存在线程安全的问题,所以搞清楚是否线程安全的关键,不仅仅是判断资源是否共享,还要看看共享的资源是否是有状态的。

JDK在此方面也提供了很多线程安全的类,我们应该清楚它们的使用场景。

时间

使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat。

数值

JDK1.5提供了很多Atomic开头的类,这些类大多数都是通过cas的方式实现原子操作。

集合

关于集合的线程安全类有很多,主要差别在于性能和使用场景上。

Map
// 这两种都是通过使用synchronized关键字来实现,效率都不高
Collections.synchronizedMap(new HashMap<>());
Map m = new Hashtable();
// ConcurrentHashMap一开始采用分段锁实现,之后在JDK对synchronized优化后,又改成synchronized+分段锁实现
Map map = new ConcurrentHashMap();
// 不可变的方式,禁止写的操作,也算是从根源上杜绝线程不安全的可能。。。如果创建后需要禁止写入,则可以使用这种方式
Collections.unmodifiableMap(new HashMap<>());
// 有序的、线程安全的Map
new ConcurrentSkipListMap<>();
List

前两种方式与map一样都是通过synchronized关键字来实现

这里要特别主要CopyOnWriteArrayList的使用场景,对于读多写少的场景CopyOnWriteArrayList效率非常高,但如果是读少写多的情况下,CopyOnWriteArrayList的效率则十分低下,还不如直接使用Collections.synchronizedList(new ArrayList<>())

Collections.synchronizedList(new ArrayList<>());
List list = new Vector();
List safeList = new CopyOnWriteArrayList();
Collections.unmodifiableList(new ArrayList<>());
Set
// 两种方式几乎没有什么区别newKeySet时JDK1.8时提供的,newSetFromMap是JDK1.6时提供的,本质都是通过ConcurrentHashMap实现的
ConcurrentHashMap.newKeySet();
Collections.newSetFromMap(new ConcurrentHashMap<>());
// 写时复制的set,同样需要注意使用场景,只有读多写少的情况才适用
new CopyOnWriteArraySet();
//不可变的
Collections.unmodifiableSet(new HashSet<>());
//有序的set集合,综合了读写性能,如果读写都差不多的情况下,可以使用它
new ConcurrentSkipListSet();
线程并发

除了线程安全之外,就要考虑并发方面的问题了,并发有可能造成一段代码的处理能力急剧下滑,如何利用多线程的并行处理能力解决并发效率问题,也是非常关键的地方。

常见的解决方式包括:无锁化(cas)、分段锁(每个线程分别锁一小段,减少冲突)、读写锁、写时复制、伪共享等。

其实现在的JDK已经对synchronized进行了大量的优化,效率也并没有想象的那么差了,并且synchronized在保证线程安全方面足够的简单,在涉及到资金相关操作时,更加稳妥,不容易出错。

悲观锁遵循一锁、二判、三更新、四释放的原则。

分段锁

并发造成性能下滑的主要原因就是共享资源的竞争,那么分段锁就是为了减少共享资源的竞争,把一份大的共享资源分成若干份,然后让每个线程各自持有一份,这样自然就减少了冲突。

随机数

Random在多线程并发下效率会比较低,建议使用ThreadLocalRandom

Random通过cas的方式保证了线程安全,但在高并发下很有可能会失败,造成频繁的重试

protected int next(int bits) {
    long oldseed, nextseed;
    AtomicLong seed = this.seed;
    do {
        oldseed = seed.get();
        nextseed = (oldseed * multiplier + addend) & mask;
    } while (!seed.compareAndSet(oldseed, nextseed));
    return (int)(nextseed >>> (48 - bits));
}

所以就有了ThreadLocalRandom,它的优化方式主要就是分段,通过让每个线程拥有独立的存储空间,这样即保证了线程安全,同时效率也不会太差。

public static ThreadLocalRandom current() {
    if (U.getInt(Thread.currentThread(), PROBE) == 0)
        localInit();
    return instance;
}
static final void localInit() {
    int p = probeGenerator.addAndGet(PROBE_INCREMENT);
    int probe = (p == 0) ? 1 : p; // skip 0
    long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
    Thread t = Thread.currentThread();
    U.putLong(t, SEED, seed);
    U.putInt(t, PROBE, probe);
}
public int nextInt() {
    return mix32(nextSeed());
}
final long nextSeed() {
    Thread t; long r; // read and update per-thread seed
    U.putLong(t = Thread.currentThread(), SEED,
              r = U.getLong(t, SEED) + GAMMA);
    return r;
}
ConcurrentHashMap

ConcurrentHashMap也是采用分段锁的思想,只不过不是简单的让每个线程独立拥有一份完整的Map,而是按照map中的table capacity(默认16)来决定,也就是说每个线程会锁1/16的数据段,这样一来并发就差不多提升了16倍。

读写锁

读写锁主要根据大多数业务场景都是读多写少的情况,在读数据时,无论多少线程同时访问都不会有安全问题,所以在读数据的时候可以不加锁,不过一旦有写请求时就需要加锁了

读 读 不冲突

读 写 冲突

写 写 冲突

ReentrantReadWriteLock

一把读锁、一把写锁

写时复制

写时复制最大的优势在于,在写数据的过程时,不影响读,可以理解为读的是数据的副本,而只有当数据真正写完后才会替换副本,当副本特别大、写数据过程比较漫长时,写时复制就特别有用了。

CopyonWriteArrayList
public E get(int index) {
    return elementAt(getArray(), index);
}
final Object[] getArray() {
    return array;
}
public boolean add(E e) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        es = Arrays.copyOf(es, len + 1);
        es[len] = e;
        setArray(es);
        return true;
    }
}
final void setArray(Object[] a) {
    array = a;
}

写时复制有两个缺点,可以看到在add方法时使用了synchronized,也就是说当存在大量的写入操作时,效率实际上是非常低的,另一个问题就是需要copy一份一模一样的数据,可能会造成内存的异常波动。

伪共享

当多线程访问的数据位于同一个缓存行时,就会互相影响彼此的效率。

假设A线程操作数据C,B线程操作数据D,C、D数据位于同一缓存行,那么当A线程修改了C数据时,由于缓存一致性协议的规定,就会造成缓存行失效,那么当B线程读取D数据时,就必须重新加载缓存,尽管B线程之前并没有对D进行过任何操作,同理B线程的操作同样会影响着A线程。

知道了原因之后我们就可以进行优化

public class CacheLinePadding {
    private static class Padding {
        //打开这个注释再执行,效率会提升
        //public volatile long p1, p2, p3, p4, p5, p6, p7;
    }

    // @Contended JDK8提供了这个注解,等同于使用Padding类的效果
    private static class T extends Padding {
        //x变量8个字节,加上Padding中的变量,刚好64个字节,独占一个缓存行。
        public volatile long x = 0L;
    }

    public static T[] arr = new T[2];

    static {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(() -> {
            for (long i = 0; i < 100000000; i++) {
                arr[0].x = i;
            }
        });

        Thread t2 = new Thread(() -> {
            for (long i = 0; i < 100000000; i++) {
                arr[1].x = i;
            }
        });

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start) / 100000);
    }
}
ThreadLoad

ThreadLocal也是一种常用的保证线程安全、并能够保证并发量的方式,只不过在使用时需要注意内存泄漏的风险,只要了解内部的引用关系,自然就能理解。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/591000.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号