从问题出发看JAVA编程规范

本文会摘选几个阿里的JAVA编程规范,从问题出发看为什么要这么做,少踩一些坑

【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。 说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用RPC方法。

【强制】finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。 说明:如果 JDK7 及以上,可以使用 try-with-resources 方式。

问题:

测试环境服务不可用,返回超过Druid最大并发连接数错误

问题排查:
  • 数据库连接池配置最近没有改动
  • 没有发现数据库死锁现象
  • 排查日志,发现业务日志报mq连接错误
  • 登录mq服务器,查看mq日志,发现有堆溢出现象,mq问题暂且不表
  • 根本原因在于数据库操作过程中加入了远端的mq操作,而mq由于堆溢出导致连接异常,但在异常中没有及时释放数据库连接资源,导致数据库连接一直得不到释放,最终把数据库连接池的资源耗尽
结论:
  • 不要在数据库操作(和锁操作是类似的,都是对有限竞争资源的占用)中加入耗时的rpc调用或者无关耗时计算
  • finally块必须对资源对象、流对象进行关闭,资源对象包括锁、各种连接,如数据库连接、redis连接等,流对象包括文件、socket等各种输入输出流

【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。

List<String> list = new ArrayList<>();
list.add("2");
list.add("1");
list.add("1"); // 第1次测试:带着此行数据运行;2:把此行删掉再测试
for (String item : list) {
    if ("1".equals(item)) {
        list.remove(item);
    }
}
for (int index=0; index<list.size(); index++) {
    String item = list.get(index);
    if ("1".equals(item)) {
        list.remove(item);
    }
}
System.out.println(list);
复制代码
问题:
  • 不详细举例了,这是不少程序员(包括有一些经验的程序员)都会跳的一个坑
  • 通过for-each循环或者index正向遍历都会有跳过遍历元素的问题,因为在remove之后,底层数组会重新复制(arraycopy),删掉元素的空缺被后续元素递补,结果递补元素下一次遍历时被略过
  • for-each循环还会有ConcurrentModificationException的问题,因为for-each循环底层走的是next()操作,会检查遍历过程中modCount是否发生变化,而remove操作已经修改了modCount
结论:
  • 集合类遍历全部强制采用Iterator方式
  • 如果使用index方式遍历,采用倒序

【参考】ThreadLocal 无法解决共享对象的更新问题,ThreadLocal 对象建议使用 static修饰。这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量 ,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量

  • 在此想说的不是上面编程规范的内容,而是线程池环境下使用ThreadLocal时要注意线程会被重用,而此时Thread内部ThreadLocal还是之前存储的内容,要注意ThreadLocal使用的get/set顺序,先set后get,或者请求完成后及时remove

【强制】关于 hashCode 和 equals 的处理,遵循如下规则:
1) 只要重写 equals,就必须重写 hashCode 2) 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须重写这两个方法
3) 如果自定义对象作为 Map 的键,那么必须重写 hashCode 和 equals

问题:
  • map和set(底层由map实现)添加和删除元素时,会先通过hashcode比较,如果hashcode相同再比较equals(hashcode相同的元素由链表或红黑树等方式连接),从而获取、更新或者删除相关元素
  • 自定义对象默认继承了Object类的hashcode和equals方法,比较的是是否同一个对象,不重写就失去了作为map和set key的意义
结论:
  • 养成习惯,重写equals,就重写hashcode
  • 自定义对象作为key时,重写 hashCode 和 equals,特别是放到static类型全局变量map和set中的对象,防止static对象持续增长,又因为是GC Roots无法回收,造成内存泄漏

【强制】所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较

问题
  • 对于 Integer var = ? 在-128 至 127 范围内的赋值,Integer 对象是在 IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,对于Byte,Short,Long,Character等类型对象同样有这个问题
结论
  • 基本数据类型之外的对象(含包装类对象),除了明确就是比较是否同一个对象,全部用equals比较是否相等

【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。

public class TimerTaskThread extends Thread {
    public TimerTaskThread() {
        super.setName("TimerTaskThread");
    }
}
复制代码
问题:
  • 一个jstorm技术栈工程启动后空转时,线程多达200多个
  • 通过visualvm分析线程信息,发现大部分线程都是用的线程池默认线程创建工厂创建的线程,从名字看根本不知道做什么的,给快速定位带来了很大麻烦
结论:
  • 通过线程池线程创建工厂和自定义创建线程时使用具有业务含义的命名区分线程

【强制】获取单例对象需要保证线程安全,其中的方法也要保证线程安全
【推荐】在并发场景下,通过双重检查锁(double-checked locking)实现延迟初始化的优化问题隐患(可参考 The "Double-Checked Locking is Broken" Declaration),推荐解决方案中较为简单一种(适用于 JDK5 及以上版本),将目标属性声明为 volatile 型

  • 单例是个老生常谈的问题了,懒汉式这种延迟加载的会存在线程安全问题,如果不关心是否延迟加载,可以通过饿汉式或者枚举方式获取单例
  • 建议通过静态内部类或者双重检查锁的方式获取单例,双重检查锁要注意锁对象和将目标属性修饰为volatile类型,volatile可以防止指令重排序,避免持有锁线程在单例对象未初始化完成时就将引用暴露给其他线程,关于volatile的原理可参考你真的懂volatile吗
  • 静态内部类通过static类只在加载时初始化一次的特性实现单例,如
public class Singleton {
    private Singleton() {
    }

    private static class SingletonHolder {
        private final static Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}
复制代码

【参考】volatile解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。如果是count++操作,使用如下类实现:AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是 JDK8,推 荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)

  • volatile可以解决修饰变量(基本数据类型或者引用性变量,不保证引用型变量指向对象的可见性)的内存可见性,可以保证double和long型变量的get/set操作的原子性,而++操作本身不是原子性的,在多线程操作下可能发生混乱,如果要保证多线程操作的正确性,需要使用原子类(Atomic*)或者同步锁

【推荐】表达异常的分支时,少用 if-else 方式,这种方式可以改写成:

if (condition) {
    ...
    return obj;
}
复制代码

//接着写 else 的业务逻辑代码;
说明:如果非得使用 if()...else if()...else...方式表达逻辑,【强制】避免后续代码维护困难,请勿超过 3 层。 正例:超过 3 层的 if-else的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现,其中卫语句示例如下:

public void today() {
    if (isBusy()) {
        System.out.println(“change time.”);
        return;
    }

    if (isFree()) {
        System.out.println(“go to travel.”);
        return;
    }

    ......
}  
复制代码
  • 卫句是个好的编程习惯,减少嵌套,代码整洁,容易阅读

【强制】在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。说明:不要在方法体内定义:Pattern pattern = Pattern.compile(“规则”);
【强制】用户请求传入的任何参数必须做有效性验证。说明:忽略参数校验可能导致: 正则输入源串拒绝服务 ReDoS

最后,防止NPE空指针这种运行时异常,养成习惯,注意对象是否可能为空,建议采用Optional类处理

欢迎关注我的微信公众号

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值