java开发采坑之路续集
1.在web环境中使用ThreadLocal出现数据错乱
原因:
线程可能重用,导致ThreadLocal中的数据会串
解决办法:
ThreadLocal<String> localName = new ThreadLocal();
try {
localName.set("Java");
// 其它业务逻辑
} finally {
localName.remove();
}
2.使用ConcurrentHashMap出现线程安全问题和性能并未提升
原因:
ConcurrentHashMap只能保证提供的原子性读写操作(例如:putIfAbsent、computelfAbsent、replace、compute)是线程安全的。性能没有提升,可能使用ConcurrentHashMap时,采用加锁方式,推荐使用(getOrDefault、putIfAbsent、computIfAbsent的API进行操作)。1.7版本,它采用ReentrantLock+Segment(分段锁)+HashEntry的方式解决多线程安全问题。1.8版本 Oracle对其进行调整采用synchronized+CAS+HashEntry+红黑树方式进行了处理。 利用红黑树结构优化增强链表遍历的效率。
错误代码:
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String,Integer>();
map.put("key", 1);
ExecutorService executorService = Executors.newFixedThreadPool(100);
for (int i = 0; i < 1000; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
int key = map.get("key") + 1; //step 1
map.put("key", key);//step 2
}
});
}
Thread.sleep(3000); //模拟等待执行结束
System.out.println("------" + map.get("key") + "------");
executorService.shutdown();
}
这段使用ConcurrentHashMap的代码,产生了线程安全的问题。我们来分析一下为什么会发生这种情况。在step1跟step2中,都只是调用ConcurrentHashMap的方法,各自都是原子操作,是线程安全的。但是他们组合在一起的时候就会有问题了,A线程在进入方法后,通过map.get(“key”)拿到key的值,刚把这个值读取出来还没有加1的时候,线程B也进来了,那么这导致线程A和线程B拿到的key是一样的。
解决方法:
1、可以用synchronized可能会导致性能降低。
2、采用原子性AtomicInteger方法
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap<String,AtomicInteger>();
AtomicInteger integer = new AtomicInteger(1);
map.put("key", integer);
ExecutorService executorService = Executors.newFixedThreadPool(100);
for (int i = 0; i < 1000; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
map.get("key").incrementAndGet();
}
});
}
Thread.sleep(3000); //模拟等待执行结束
System.out.println("------" + map.get("key") + "------");
executorService.shutdown();
}
3.加锁需谨慎
锁无效:
1、明确锁和要保护的资源的关系与范围
锁粒度:
1、尽量要降低锁的粒度,仅对必要的代码块或者需要保护的资源进行加锁
场景锁:
1、1.8版本考虑使用StampedLock的乐观锁的特性(写,读,乐观读)
2、对于读写比例差异场景:使用ReentrantReadWriteLock细化区分读写锁
3、在没有明确需求的情况下,不要轻易使用公平锁
4.线程池的优化
声明式线程池导致2种类型的OOM:
1、newFixedThreadPool使用*队列,队列堆积太多数据导致OOM
2、newCachedThreadPool不限制最大线程数,使用没有任何容量的SynchronousQueue队列,容易开启太多线程导致OOM
实现一个更激进的线程池:
1、重写队列的offer方法直接返回false,数据不入队列,并自定义RejectExecutionHandler,触发拒绝策略的时候,把任务加入队列,参考:Tomcat的ThreadPoolExecutor和TaskQueue类
扩展:
1、java8的parallelStream背后是一个公共线程池,别把IO任务使用ParallelStream处理
5.Spring Cloud Feign 和 Ribbon
1、默认情况下Feign的读取超时是1秒、需要根据需要设置长一些
2、如果Fegin的读取超时,就必须同时配置连接超时才生效
3、Fegin读取超时设置 readTimeout和connectTimeout Ribbin使用ReadTimeout 和 ConnectTimeout 注意大小写
4、Ribbin会重试接口,需要修改设置ribbin.MaxAutoRetriesNextServer=0; 并且对与get请求需要保持接口幂等
6.Lombok @EqualsAndHashCode可能的2个坑
1、@EqualsAndHashCode注解实现equals和hashCode时候,默认使用类的所有非static、非transient的字段、使用@EqualsAndHashCode.Exclude排除一些字段
2、@EqualsAndHashCode注解实现equals和hashCode时候,默认不考虑父类,设置callSuper = true
7.Equals与BigDecimal判空
1、如果只比较BigDecimal的value,可以使用compareTo方法
2、把BigDecimal作为Key加入HashSet,使用TreeSet替换HashSet,或者使用stripTraillingZeros方法去掉尾部的零
8.日志一些坑
1、设置太大的queueSize,日志量大的时候导致OOM,可以使用discardingThreshold,可以丢失一些级别的日志,或者使用大数据日志项目,存储日志方便分析与记录
2、异步日志可能存在丢失、可以设置discardingThreshold为0,即使<=INFO的级别日志也不会丢,但最好把queueSize设置大一些
3、异步日志出现阻塞,可以设置neverBlock为true,永不阻塞,但可能会丢失数据。
4、使用{}占位符,只是减少日志参数对象.toString()和字符串拼接的耗时
9.IO与文件一些坑
1、设置缓冲区、建议使用BufferedInputStream和BufferedOutputStream、如果希望更高性能,可以使用FileChannel
public static void main(String[] args) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(10);//100kbytes
FileChannel readChannel = FileChannel.open(new File("D:/in.txt").toPath());
//out.txt必须已经存在, writeChannel必须以WRITE方式打开
FileChannel writeChannel = FileChannel.open(new File("D:/out.txt").toPath(), StandardOpenOption.WRITE);
int read;
while ((read = readChannel.read(byteBuffer)) != -1) {
//buffer从读切换到写
byteBuffer.flip();
writeChannel.write(byteBuffer);
// 写完之后清空缓冲区,否则read=0一直死循环
byteBuffer.clear();
}
writeChannel.close();
readChannel.close();
}
10.OOM一些坑
1、WeakHashMap的key虽然是弱引用,但是其value持有key中对象的强引用会导致Key无法回收,无限向WeakHashMap加入数据同样会OOM、使用spring提供ConcurrentReferenceHashMap或者使用WeakReference来包装value
2、设置不合理server.max-http-header-siez=10M, 每一个请求需要占用20M内存,并发导致OOM
3、@Inherited只能实现类上的注解继承,无法实现方法上注解继承、使用Spring的AnnotatedElementUtils.findMergedAnnotation
11.Spring一些坑
1、单例Bean注入多例的Bean,不仅仅设置Scope,还需要设置proxyMode=ScopedProxyMode.TARGET_CLASS走代理方式
2、Feign AOP切不到、去掉Ribbin模块依赖,让ApacheHttpClient直接成为一个Bean或者把配置参数proxy-target-class的值修改为false,以切换到使用JDK动态代理的方式
3、Springboot2.0因为需要实现Relaxed Binding2.0 通过自定义ConfigurationPropertySourcesPropertySource并且把它作为配置源的第一个,实现了对PropertySourcePropertyresolver中遍历逻辑的‘劫持’
参考:Java 高手笔记本
上一篇: MySQL设置远程访问权限
下一篇: Python环境采坑小记