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

前言

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

其他类型的规范

【Java后台开发规范】— 不简单的命名
【Java后台开发规范】— 日志的输出
【Java后台开发规范】— 长函数、长参数
【Java后台开发规范】— 圈复杂度
【Java后台开发规范】— 设计原则
【Java后台开发规范】— Null值处理
【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也是一种常用的保证线程安全、并能够保证并发量的方式,只不过在使用时需要注意内存泄漏的风险,只要了解内部的引用关系,自然就能理解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

码拉松

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

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

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

打赏作者

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

抵扣说明:

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

余额充值