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 高手笔记本

猜你喜欢

转载自blog.csdn.net/qq_32447301/article/details/108505080