方法签名:方法名(参数类型...)
NPE:NULL POINTER EXCEPTION
编程规约
命名风格
- 【强制】类名使用UpperCamelCase风格,但DO / BO / DTO / VO / AO / PO等情形例外。
正例:ForceCode / UserDO / XmlService / TcpUdpDeal / TaPromotion / QrCode
反例:forceCode / UserDo / XMLService / TCPUDPDeal / TAPromotion /QRCode - 【强制】常量命名全部要大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
正例:MAX_STOCK_COUNT /CACHE_EXPIRED_TIME
反例:MAX_COUNT / EXPIRED_TIME - 【强制】抽象类名使用 Abstrac 或 Base 开头;异常类名使用Exception结尾;测试类名以它要测试的类名开始,Test结尾。
- 【强制】POJO类中布尔类型的变量都不要加 is ,否则部分框架解析会引起序列化错误。
说明:在“建表规约”第一条,表达 是与否 的值采用 is_xxx 的命名方式,所以需要在<resultMap>(字段关系映射)设置从is_xxx到Xxx的映射关系。
反例:定义为基本数据类型Boolean isDeleted;的属性,它的方法名称也是 isDeled(),RPC框架在反向解析的时候,“误以为”对应的属性名称是 deleted,导致属性获取不到抛出异常。 - 【强制】包名统一使用小写,单数形式,类名有复数含义,则类名可以使用复数形式。
- 【推荐】如果模块、接口、类、方法中使用了设计模式,应在命名时体现出具体模式。
正例:public class OrderFactory; public class LoginProxy; public classResourceOberver; public class CakeDecorator; - 【推荐】接口类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的简洁性,并加上有效的额 Javadoc 注释,尽量不要在接口中定义变量,如果一定要定义变量,必须是和接口方法相关的,并且是整个应用的基础常量。
正例:接口方法签名:void commit();
接口基础常量:String COMPANY = "alibaba";(接口中默认public static final)
反例:public abstract void commit();
注意:JDK1.8 中接口允许有默认实现,default void commit(){System.out.println("默认实现");},实现类中不重写此方法,则调用默认实现方法,若有重写,则调用实现类重写的方法。 - 【参考】枚举类名建议带上 Enum 后缀,枚举成员名称需要全部大写,单词用下划线隔开。特殊的常量
说明:枚举其实就是特殊的常量类,且构造方法被默认强制为私有。
正例:类名:ProcessStatusEnum,成员名称:SUCCESS / UNKNOW_REASON - 【参考】各层命名规约:
1)Service / DAO 层方法:
获取单个对象方法用 get 为前缀。
获取多个对象方法用 list 为前缀,复数结尾,如:listObjects。
获取统计值的方法用 count 为前缀。
插入的方法用 save / insert 为前缀。
删除的方法用 remove / delete 为前缀。
修改的方法用 update 为前缀。
2)领域模型:
数据对象:xxxDO,xxx为数据表名。
数据传输对象:xxxDTO,xxx为业务领域相关的名称。
展示对象:xxxVO,xxx一般为网页名称。
POJO 是 DO / DTO / VO / BO 的统称,禁止命名成 xxxPOJO
常量定义
- ...
代码格式
- 【强制】在进行类型强制转换时,右括号与强制转化值之间不需要任何空格离开。
正例:long first = 10000000000L;
int second = (int)first + 2; - 【单行字符不超过120个】,超出则需要换行,换行时遵循如下原则:
1)第二行相对第一行缩进4个空格,从第三行开始,不在持续缩进。
2)运算符与下文一起换行。
3)方法调用的点符号与下文一起换行。
4)方法调用中的多个参数需要换行时,在逗号后进行。
5)在括号前不要换行。 - 【强制】IDE 的 text file encoding 设置为 UTF-8;IDE 中文件的换行符使用 UNIX 格式,不要使用Windows 格式。
- 【推荐】单个方法的总行数不超过80行。
说明:除注释外的方法签名、左右大括号、方法内代码、空行回车及任何不可见字符的总行数不超过80行。
正例:代码逻辑分清红花和绿叶,个性和共性,绿叶逻辑单独成为额外方法,使主干代码更加清晰;共性逻辑抽取成为共性方法,便于复用和维护。 - 【推荐】不同逻辑、不同语义、不同业务的代码之间插入一个空行分隔开来,以提升可读性。(没必要插入多个空行)
OOP规约
- 【强制】避免通过一个类的对象引用访问此类的静态成员变量或方法,造成无谓增加编译器解析成本,直接用类名来访问即可。
- 【强制】所有的复写方法,必须加 @Override 注解。
- 【强制】相同参数类型,相同业务含义,才可使用 Java 的可变参数,避免使用Object。
说明:可变参数必须放置在参数列表的最后(尽量不使用),【很多SQL拼接会使用】
正例:public List<User> listUsers(String type,Long... ids) {...} - 【强制】对外部正在调用或者二方依赖库的接口,不允许修改方法签名,以避免对接口调用方产生影响。若接口过时,必须加 @Deprecated 注解,并清晰说明采用的新街口或者新服务是什么。
- 【强制】不能使用过时的方法和类。
说明:java.net.URLDecoder 中的方法 decode(String encodeStr) 已经过时,应该使用双参数 decode(String source, String encode)。接口提供方既然明确是过时接口,那么有义务提供新接口;作为调用方,有义务去考证过时方法的新实现是什么。 - 【强制】Object 的 equals 方法容易抛出空指针异常,应使用常量或有值的对象来调用 equals。
正例:"test".equals(object)
反例:object.equals("test")
说明:推荐使用 java.util.Objects#equals (JDK7引入的工具类)。 - 【强制】所有整型包装类对象之间值的比较,全部使用 equals 。
说明:对于 Integer var = ? 在 -128 ~ 127 范围内的赋值,Integer 对象实在 IntegerCache.cache 中产生的,会复用已有对象,这个区间内的值可以直接用 “== ”进行判断,但是这个区间以外的数据在堆上产生,并不会复用已有对象! - 【强制】浮点数之间的等值判断,基本数据类型不能用 “==” ,包装数据不能用 equals 。
说明:浮点数采用“尾数 + 阶码”的编码方式,类似与科学计数法的“有效数字 + 指数”的表示方式。二进制无法精确表示大部分的十进制小数。
反例:float a = 1.0f - 0.9f; float b = 0.9f - 0.8f; if (a == b) { // 预期进入 //结局 false } Float x = Float.valueOf(a); Float y = Float.valueOf(b); if (x.equals(y)) { // false }
正例:
1)指定一个误差范围,两个浮点数的差值在此范围内,认为相等。
2)使用 BigDecimal 来定义,在进行浮点数的运算操作。 -
【强制】禁止使用构造方法 BigDecimal(double) 的方式把 double 的值转化为 BigDecimal 对象。
说明:BigDecimal(double) 存在精度损失的风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。
如:BigDecimal b = new BigDecimal(0.1f);实际的存储值为:0.100000001490116119384765625。
正例:优先推荐入参为 String 的构造方法,或使用 BigDecimal 的 valueOf 方法,此方法内部执行了 Double 的 toString,而 Double 的 toString 按 double 实际能表达的精度对尾数进行了截断。
BigDecimal good1 = new BigDecimal("0.1");
BigDecimal good2 = BigDecimal.valueOf(0.1); -
关于基本数据类型与包装数据类型的使用标准:
1)【强制】所有的 POJO 类属性必须使用包装数据类型。
2)【强制】RPC 方法的返回值和参数必须使用包装数据类型。
3)【推荐】所有的局部变量使用基本数据类型。
说明:POJO 类属性没有初始值,是要提醒使用者在需要使用时,必须自己显示的进行赋值,任何 NPE 问题,或者入库检查,都由使用者来保证。
正例:数据库的查询结果可能是 null ,因为自动拆箱,所以用基本数据类型接收有 NPE 风险
反例:比如显示成交总额涨跌的情况,即正负 x%,x 为基本数据类型,调用的RPC服务在调用不成功时,返回的是默认值,页面显示为 0%,这是不合理的,应该显示成中画线,所以包装数据类型的 null 值,能够表示额外的值,如远程调用失败,异常退出。 -
【强制】在定义 DO / DTO / VO 等 POJO类时,不要设定任何默认值。
反例:POJO类的createTime默认值为 new Date();但是这个属性在数据提取时并没有置入具体值,在更新其他字段时,又附带更新了此字段,导致创建时间被修改成当前时间。 -
【强制】当序列化类新增属性时,请不要修改 serialVersionUID 字段,以避免反序列化失败;如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。
说明:注意 serialVersionUID 不一致会抛出序列化运行异常。 -
【强制】POJO 类必须写 toString 方法,如果继承了另一个 POJO类,加上 super.toString()。
说明:便于排查问题查看属性 -
【强制】禁止在 POJO 类中,同时存在对应属性 xxx 的 isXxx() 和 getXxx() 方法。
说明:框架在调用属性 xxx 的提取方法时,并不能确定哪种方法一定是被优先调用到。 -
【推荐】当使用索引访问用 String 的 split 方法得到的数组时,需在最后一个分隔符后做有无内容的检查,否则会有抛 IndexOutOfBoundsException 的风险。(可先在字符串末尾加一个不包含分隔符的字符串,转化为数组后再截取)
说明:String str = "a,b,c,,,"; String[] ary = str.split(","); // 预期 6 ,实际 3 System.out.println(ary.length);
-
【推荐】循环体内,字符串的拼接,使用 StringBuilder 的 append 方法进行扩展(本质也是如此)。
说明:String 对象的字符串拼接,每次都会 new 一个 StringBuilder 对象,然后进行 append 操作,最后通过 toString 方法返回String。String str = "start"; for (int i = 0; i < 100; i++) { str = str + "hello"; } // 推荐做法 下 StringBuilder sb = new StringBuilder(str); for (int i = 0; i < 100; i++) { sb.append("hello"); } str = sb.toString();
-
【推荐】慎用 Object 的 clone 方法来拷贝对象。
说明:对象的 clone 方法默认是浅拷贝,若想实现深拷贝,需要重写 clone 方法来实现域对象的深度遍历式拷贝。 -
【推荐】类成员与方法访问控制从严
1) 如果不允许外部直接通过 new 来创建对象,那么构造方法必须限制为 private。
2) 工具类不允许有 public 或 default 构造方法。
3) 类非 static 成员变量并且与子类共享,必须限制为 protected;
4) 类非 static 成员变量并且仅在本类使用,必须限制为 private;
5) 类 static 成员变量如果仅在本类使用,必须限制为 private;
6) 若是 static 成员变量,必须考虑是否为 final;
7) 类成员方法只供类内部调用,必须限制为 private 。
8) 类成员方法只对继承类公开,限制为 protected 。
说明:对任何类、方法、参数、变量,严格控制访问范围。过于宽泛的访问范围,不利于模块解耦。
集合处理
- 【强制】关于 hashCode 和 equals 的处理,遵循如下原则:
1) 只要重写 equals,就必须重写 hashCode 。
2) 因为 Set 存储的是不重复对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须重写这两种方法。
3)如果自定义对象作为 Map 的键,那么必须重写 hashCode 和 equals 。
说明:String 重写了 hashCode 和 equals ,所以我们可以很愉快地将 String 对象作为 key 来使用。 - 【强制】使用 Map 的方法 keySet() / values() / entrySet() 返回集合对象时,不可以对其添加元素,否则会抛出 UnsupportedOperationException。
- 【强制】Collections 类返回的对象,如 emptyList() / singletonList() 等都是 immutable list(不可变的),不可以对其添加或删除元素。
说明:Arrays.asList() 方法则是返回一个内部类 ArrayList,没有重写 add(),remove() 等方法,调用也会抛出 UnsupportedOperationException 异常。
反例:某二方库的方法中,如果查询无结果,返回 Collections.emptyList() 空集合对象,调用方一旦进行了添加元素的操作,就会触发 UnsupportedOperationException 异常。 - 【强制】ArrayList 的 subList 结果不可强制转化为 ArrayList,否则会抛 ClassCastException
说明:ArrayList 的 subList 返回的是 ArrayList 的 内部类 SubList,只是 ArrayList 的一个视图,对于 SubList 子列表的所有操作最终会反映到原列表。
参考:ArrayList a = new ArrayList(); for (int i = 0; i < 10; i++) { a.add() } List a2 = a.subList(0,5); System.out.println(a2); // [0, 1, 2, 3, 4] a2.add(10); System.out.println(a2); // [0, 1, 2, 3, 4, 10] System.out.println(a); // [0, 1, 2, 3, 4, 10, 5, 6, 7, 8, 9]
- 【强制】在 subList 的场景中,高度注意对原集合元素的增加或删除,均会导致子列表的遍历、增加、删除产生 ConcurrentModificationException 异常。
- 【强制】使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入类型完全一样的数组,大小就是 list.size()。
说明:当使用 toArray 带参方法,入参分配的数组空间不够大时,toArray 方法内部将重新分配内存空间,并返回新数组地址;如果数组元素大于实际所需,下标为 [list.size()] 的数组元素将被置为 null ,其他数组元素保持原值,因此最好将方法入参数组大小定义为与集合数组个数一致。
正例:
反例:直接使用 toArray 无参方法存在问题。此方法返回值只能是 Object[] 类,若强转为其他类型,数组将出现 ClassCastException 错误。List<String> list = new ArrayList<>(); list.add("guan"); list.add("bao"); String[] array = new String[list.size()]; array = list.toArray(array); // array = (String[]) list.toArray(); System.out.println(array); for (int i = 0; i < array.length; i++) { System.out.println(array[i]); }
- 【强制】在使用 Collection 接口任何实现类的 addAll() 方法时,都要对输入的集合参数进行 NPE 判断。
说明:在 ArrayList # addAll 方法的第一行代码即 Object[] a = c.toArray();其中 c 为输入集合参数,如果为 null ,则直接抛出异常。 - 【强制】Arrays.asList() 把数组转换成集合时,不能使用其修改集合相关的方法,可查看上面第三条。
说明:asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组。修改原数组,list 会随之修改。
第一种情况:list.add("yangguanbao") 运行时异常。
第二种情况:如果 str[0] = "changed";,list.get(0) 也会随之修改,反之毅然。 - 【强制】泛型通配符 <? extends T> 用来接收返回的数据,此写法的泛型集合不能使用 add 方法,而 <? super T> 不能使用 get 方法,因为作为接口调用赋值时易出错。
说明:扩展说一下 PECS(Producer Extends Consumer Super) 原则:第一,频繁往外读取内容的,适合用 <? extends T>;第二,经常往里插入的,适合用 <? super T> 。 - 【强制】在无泛型限制定义的集合赋值给泛型限制的集合时,当使用集合元素时,需要进行 instanceof 判断,避免抛出 ClassCastException 异常。
说明:毕竟泛型是在 JDK5 之后才出现的,考虑到向前兼容,编译器允许非泛型集合与泛型集合互相赋值。
反例:// 泛型限制 List<String> generics = null; // 无泛型限制 List notGenerics = new ArrayList(); notGenerics.add("str"); notGenerics.add(1); generics = notGenerics; Object in; for (int i = 0; i < generics.size(); i++) { in = generics.get(i); if (!(in instanceof String)) { in = in.toString(); } System.out.println(in); } // 直接使用,由于不是 String 类型,报 ClassCastException System.out.println(generics.get(1));
- 【强制】不要在 foreach 循环里进行元素的 remove / add 操作。remove 元素使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
正例:
反例:ConcurrentModificationExceptionList<Integer> forList = new ArrayList(); for (int i = 0; i < 10; i++) { forList.add(i); } // 正例 Iterator<Integer> integerIterator = forList.iterator(); while (integerIterator.hasNext()) { Integer item =integerIterator.next(); if (item > 4){ integerIterator.remove(); } } System.out.println("forList:"); for (int i = 0; i < forList.size(); i++) { System.out.print("\t" + forList.get(i)); } // 反例:删除一个后,异常 try { // 对列表循环访问时异常 ConcurrentModificationException for (Integer i : forList) { if (i < 5) { forList.remove(i); } } }catch (Exception e){ System.out.println(); e.printStackTrace(); } System.out.println("forList:"); for (int i = 0; i < forList.size(); i++) { System.out.print("\t" + forList.get(i)); }
- 【强制】在JDK 7 及以上版本中,Comparator 要满足如下三个条件,不然 Arrays.sort,Collections.sort 会抛 IllegalArgumentException 异常。
说明:
1)x,y 的比较结果和 y,x的比较结果相反。
2)x > y,y > z,则 x > z。
3)x = y,则 x,z 比较结果和 y,z 的比较结果相同。
反例:下例中没有处理相等的情况,交换两个对象判断结果并不互相相反,不符合第一个条件,在实际使用中可能会出现异常。new Comparator<String>(){ @Override public int compare(String o1, String o2) { return o1.length() > o2.length() ? 1 : -1; } };
- 【推荐】在集合初始化时,指定集合初始值大小,避免不断发生扩容。
说明:HashMap 使用 HashMap(int initialCapacity) 初始化。
正例:initialCapacity =(需要存储的元素个数 / 负载因子)+ 1。注意负载因子(loader factor)默认为 0.75,如果暂时无法确定初始值大小,请设置为16(默认值)。
反例:HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素不断增加,容量被迫扩大 7 次,resize 需要重建 hash 表,这严重影响性能。 - 【推荐】使用 entrySet 遍历 Map 类集合K / V,而不是用 keySet方式遍历。
说明:keySet 其实遍历了两次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key 所对应的 value。如果是 JDK8,使用Map.forEach 方法。
正例:values() 返回的是 V 值集合,是一个 list 集合对象;
keySet() 返回的是 K 值集合,是一个 set 集合对象;
entrySet() 返回的是 K / V 值组合集合。Map<String,Object> map = new HashMap<>(16); map.put("k1","value1"); map.put("k2","value2"); map.put("k3","value3"); map.put("k4","value4"); map.forEach(new BiConsumer<String, Object>() { // lambda (key, value) -> { System.out.println("k = " + key + " , " + "v = " + value); } @Override public void accept(String s, Object o) { System.out.println("k = " + s + " , " + "v = " + o); } }); for (Map.Entry entry : map.entrySet()) { System.out.println("k = " + entry.getKey() + " , " + "v = " + entry.getValue()); }
- 【推荐】高度注意 Map 类集合 K / V 能不能存储 null 值的情况:
集合类 Key Value Super 说明 Hashtable 不允许为null 不允许为null Dictionary 线程安全 ConcurrentHashMap 不允许为null 不允许为null AbstracMap 锁分段技术(JDK8:CAS) TreeMap 不允许为null 允许为null AbstractMap 线程不安全 HashMap 允许为null 允许为null AbstractMap 线程不安全
- 【参考】合理利用集合的有序性(sorted)和稳定性(ordered),避免集合的无序性(unsorted)和不稳定性(unordered)带来的负面影响。
说明:有序性是指遍历的结果是按某种比较规则依次排列的。稳定性是指集合每次遍历的元素次序是一定的。如:ArrayList 是 order / unsort;HashMap 是 unorder / unsort;TreeSet 是 order / sort 。 - 【参考】利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List 的 container 方法进行遍历、对比、去重操作。
并发处理
- 【强制】获取单例对象需要保证线程安全,其中的方法也要保证线程安全。
说明:资源驱动类,工具类,单例工厂类都需要注意。 - 【强制】在创建线程或线程池时,请指定有意义的线程名称,方便出错时回溯。
正例:自定义线程工厂根据外部特征进行分组,比如来自同一机房的调用,把机房编号赋值给whatFeatureOfGroup。public class UserThreadFactory implements ThreadFactory { private final String namePrefix; private final AtomicInteger nextId = new AtomicInteger(1); // 定义线程组名称,使用 jstack 排查线程问题时,非常有帮助 public UserThreadFactory(String whatFeatureOfGroup) { namePrefix = "From User ThreadFactory's " + whatFeatureOfGroup + "-Worker-"; } @Override public Thread newThread(Runnable task) { String name = namePrefix + nextId.getAndIncrement(); Thread thread = new Thread(null,task,name,0); System.out.println(thread.getName()); return thread; } }
-
【强制】线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间及系统资源,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。 -
【强制】线程池不允许使用 Executors 创建,而是通过 ThreadPoolExecutor 的方式创建,这样的处理方式能让编写代码的工程师更加明确线程池的运行规则,规避资源耗尽的风险。
说明:
Executors 返回的线程池对象弊端如下。
1)FixedThreadPool 和 SingleThreadPool :允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
2)CachedThreadPool 允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,导致OOM。 -
【强制】SimpleDateFormat 是线程不安全的类,一般不要定义为 static,如果定义为 static ,必须加锁,或者使用 DateUtils 工具类。
正例:注意线程安全,使用 DateUtils。推荐如下处理。private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>(){ @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } };
说明:如果是 JDK8,,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat,官方解释:simple beautiful,strong,immutable,thread-safe。
参考:Instant、LocalDateTime与DateTimeFormatter -
【强制】必须回收自定义 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续的业务逻辑和造成内存泄露等问题。尽量在代码中使用 try-finally 块进行回收。
正例:class XXX { private static final ThreadLocal<Object> OBJECT_THREAD_LOCAL = new ThreadLocal<>(); method { OBJECT_THREAD_LOCAL.set("线程私有"); try { System.out.println(OBJECT_THREAD_LOCAL.get()); }finally { OBJECT_THREAD_LOCAL.remove(); } } }
-
【强制】在高并发场景中,同步调用应该去考量锁的性能消耗。能用无锁结构,就不用锁;能锁住块,就不锁住整个方法体;能用对象锁,就不要用类锁。
说明:使加锁的代码块工作量尽可能小,避免在锁代码块中使用RPC方法。 -
【强制】在多个资源、数据库表、对象同时加锁,需要保持一致的加锁顺序,否则可能会造成死锁。
说明:如果线程一需要对表A、B、C依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是A、B、C,否则可能出现死锁。 -
【强制】在 try 代码块之前调用 Lock 实现类的 lock() 方法,避免由于加锁失败,导致 finally 调用 unlock() 出现异常。
说明:在 lock() 中可能抛出 uncheck 异常,如果放在 try 代码块中,必然触发 finally 中的 unlock 方法,它会调用 AQS 的 tryRelease 方法。对未加锁的对象解锁会抛出 unchecked 异常,如 IllegalMonitorStateException,虽然都是加锁失败造成的程序中断,但是真正加锁出错信息可能被后者覆盖。
反例:Lock lock = new XxxLock(); preDo(); try{ // 无论加锁是否,都会执行解锁操作 lock.lock(); do(); }finally{ lock.unlock(); }
-
【强制】在并发修改同一记录时,为避免更新丢失,加锁。要么应用层加锁,要么在缓存层加锁,要么在数据库使用乐观锁,使用 version 作为更新标准。
说明:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数 >= 3。 -
【强制】对于多线程处理定时任务的情况,在 Timer 运行多个 TImeTask 时,只要其中之一没有捕获抛出的异常,其他任务便会自动终止运行。如果在处理定时任务时使用 ScheduleExecutorService,则没有这个问题。
-
【推荐】与资金相关的金融敏感信息,使用悲观锁策略。
-
【推荐】使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用 countDown 方法,线程执行代码注意 catch 异常,确保 countDown 方法被执行,避免主线程无法执行至 await 方法,直到超时才返回结果。
说明:子线程抛出异常堆栈,不能在主线程 try-catch到。 -
【推荐】避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一 seed 导致性能下降。
说明:Random 实例包括 java.util.Random 的实例或者 Math.random() 的方式。
正例:在 JDK7之后,可以直接使用 API ThreadLocalRandom,而在 JDK7 之前,需要编码保证每个线程有一个实例。 -
【推荐】在并发场景下,通过双重检查锁(double-checked-locking)实现延迟初始化的优化问题隐患,推荐解决方案中较为简单的一种(适用于 JDK5 及以上版本),即目标属性声明为 volatile。
反例:class LazyLoadDemo { private Helper helper = null; //双重检查 并 声明为 volatile public Helper getHelper() { if (helper == null) synchronized(this) { if (helper == null) { helper = new Helper(); } } } }
-
【参考】volatile 解决多线程内存不可见问题。对于一写多读,可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。如果是 count++ 操作,使用如下类实现:
AtomicInteger count = new AtomicInteger(); count.addAndGet(1);
JDK8 推荐使用 LongAdder 对象,它比 AtomicLong 性能更好(减少乐观锁的重试次数)。
-
【参考】HashMap 在容量不够进行 resize 时,由于高并发可能出现死链,导致CPU 飙升,可以使用其他数据结构或加锁来规避此风险。
-
【参考】ThreadLocal 对象使用 static 修饰。
说明:这个变量针对一个线程内所有操作共享,所以设置为静态变量,所有此类的实例共享此静态变量,在类第一次被使用时装载,只分配一块内存。所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。
控制语句
- 【强制】当 switch 括号内的变量类型为 String 并且此变量为外部参数时,必须先进行 null 判断。
- 【强制】在高并发场景中,避免使用“等于”判断作为中断或退出的条件。
说明:如果并发控制没有处理好,容易产生等值判断被“击穿” 的情况,应使用大于或小于的区间判断条件来代替。
反例:判断剩余奖品数量等于 0 时,终止发放奖品,但因为并发处理错误导致奖品数量瞬间变为负数,这样的话,活动无法终止。
注释规约
- ...
其他
- 【强制】在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。
说明:不要在方法体内定义:
Pattern pattern = Pattern.compile(规则); - 【强制】避免使用 Apache BeanUtils进行属性 copy。
说明:Apache BeanUtils 性能较差,可以使用其他方案,比如 Spring BeanUtils、Cglib BeanCopier,注意均是 浅拷贝。 - 【强制】不推荐使用 Math.random() 方法获取随机数,直接使用 Random 对象的 nextInt / nextLong .. 等方法。
- 【强制】获取当前毫秒数用 System.currentTimeMills(),而不是 new Date().getTime()。
说明:纳秒:System.nanoTime();JDK8中,针对统计时间等场景,推荐使用 Instant类。 - 【强制】在对日期格式化时,传入 pattern 中表示年份的统一用 小写 y。
说明:“yyyy” 表示当天所在年份,“YYYY” 表示本周(周日-周六)所在年份(JDK7之后),若本周跨年则返回的是下一年;注意:
“MM” - 月份;“mm” - 分钟;“hh” - 12小时制;“HH” - 24小时制