文章目录
前言
做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也是一种常用的保证线程安全、并能够保证并发量的方式,只不过在使用时需要注意内存泄漏的风险,只要了解内部的引用关系,自然就能理解。