Java开发手册-4

Java开发手册-4

  • 编程规约
    • 并发处理
    • 控制语句

编程规约

并发处理

  1. 【强制】获取单例对象需要保证线程安全,其中的方法也要保证线程安全。
    说明:资源驱动类、工具类、单例工厂类都需要注意。

  2. 【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
    正例:自定义线程工厂,并且根据外部特征进行分组,比如,来自同一机房的调用,把机房编号赋值给whatFeatureOfGroup:

    public class UserThreadFactory implements ThreadFactory {
        private final String namePrefix;    
        private final Atomiclnteger nextld = new Atomiclnteger(1);  
        //定义线程组名称,在利用jstack来排查问题时,非常有帮助
        UserThreadFactory(String whatFeatureOfGroup) {      
            namePrefix = "FromUserThreadFactory's" + whatFeatureOfGroup + "-Worker-";   
        }
        @Overide    
        public Thread newThread(Runnable task) {        
            String name = namePrefix + nextld.getAndlncrement();        
            Thread thread = new Thread(null, task, name, 0, false);
            System.out.printIn(thread.getName());       
            return thread;  
        } 
    }
    
  3. 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
    说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者"过度切换"的问题。

  4. 【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方 式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
    说明:Executors返回的线程池对象的弊端如下:
    (1)FixedThreadPoolScheduledThreadPoolSingleThreadPool
    允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
    (2)CachedThreadPool:
    允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM
    【强制】SimpleDateFormat是线程不安全的类,一般不要定义为static变量,如果定义为static,必须加锁,或者使用 DateUtils 工具类。
    正例:注意线程安全,使用DateUtils。亦推荐如下处理:

private static final ThreadLocal<DateFormat> dateStyle = new ThreadLocal<DateFormat>() { 
   @Overide 
   protected DateFormat initialValue() { 
       return new SimpleDateFormat("yyyy-MM-dd");
   }
}

说明:如果是JDK8的应用,可以使用Instant代替DateLocalDateTime代替CalendarDateTimeFormatter代替SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe

  1. 【强制】必须回收自定义的ThreadLocal变量记录的当前线程的值,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代码中使用try-finally块进行回收。
    正例
         objectThreadLocal.set(userlnfo);
         try { 
            //...
         } finally {
            objectThreadLocal.remove();
        }
  1. 【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
    说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用RPC方法。

  2. 【强制】对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。
    说明:线程一需要对表A、B、C依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是A、B、C,否则可能出现死锁。

  3. 【强制】在使用阻塞等待获取锁的方式中,必须在try代码块之外,并且在加锁方法与try代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁。
    说明:lock方法与try代码块之间的方法调用抛出异常,无法解锁,造成其它线程无法成功获取锁。
    说明:如果 lock 方法在try代码块之内,可能由于其它方法抛出异常,导致在finally代码块中,unlock对未加锁的对象解锁,它会调用AQStryRelease方法(取决于具体实现类),抛出IIlegalMonitorStateException异常。
    说明:Lock对象的lock方法实现中可能抛出unchecked异常,产生的后果与说明二相同。
    正例

Lock lock = new XxxLock();
//…
lock.lock();
try {
    doSomething();
    doOthers();
} finally { 
    lock.unlock();
}

反例:

Lock lock = new XxxLock();
//…
try { 
   //如果此处抛出异常,则直接执行finally代码块
   doSomething();
   //无论加锁是否成功,finally代码块都会执行
   lock.lock();
   doOthers();
} finally {
   lock.unlock();
}
  1. 【强制】在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁. 锁的释放规则与锁的阻塞等待方式相同。
    说明:Lock 对象的unlock方法在执行时,它会调用AQStryRelease方法(取决于具体实现类),如果当前线程不持有锁,则抛出IllegalMonitorStateException异常。
    正例
  Lock lock = new XxxLock();
  //… 
  boolean isLocked=lock.tryLock();
  if (isLocked) {
  try { 
        doSomething();
        doOthers();
  } finally { 
        lock.unlock();
  }
}
  1. 【强制】并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用version作为更新依据。
    说明:如果每次访问冲突概率小于20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于3次。
  2. 【推荐】资金相关的金融敏感信息,使用悲观锁策略。
    说明:乐观锁在获得锁的同时已经完成了更新操作,校验逻辑容易出现漏洞,另外,乐观锁对冲突的解决策略有较复杂的要求,处理不当容易造成系统压力或数据异常,所以资金相关的金融敏感信息不建议使用乐观锁更新。
    正例: 悲观锁遵循一锁二判三更新四释放的原则。
  3. 【推荐】避免Random实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一seed导致 的性能下降。
    说明:Random实例包括java.util.Random的实例或者Math.random()的方式。
    正例:JDK7之后,可以直接使用 API ThreadLocalRandom,而在JDK7之前,需要编码保证每个线程持有一个单独的Random实例。
    16.【推荐】通过双重检查锁(double-checked locking),实现延迟初始化需要将目标属性声明为volatile型。
    正例:
public class LazylnitDemo {
    private volatile Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized(this) {
                if (helper == null) {
                    helper = new Helper();
                }
        return helper;
    }
    // other methods and fields... 
}
  1. 【参考】volatile解决多线程内存不可见问题对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。
    说明:如果是count++操作,使用如下类实现:
```java 
Atomiclnteger count = new Atomiclnteger();
count.addAndGet(1);
 ```

如果是`JDK8`,推荐使用`LongAdder`对象,比`AtomicLong`性能更好(减少乐观锁的重试次数)。
  1. 【参考】ThreadLocal 对象使用static修饰,ThreadLocal无法解决共享对象的更新问题。
    说明:
    这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。

控制语句

  1. 【强制】在一个switch块内,每个case要么通过continue/break/return等来终止,要么注释说明程序将继续执行到哪一个case为止;在一个switch块内,都必须包含一个default语句并且放在最后,即使它什么代码也没有。
    说明:注意break是退出switch语句块,而return是退出方法体。
  2. 【强制】switch括号内的变量类型为String并且此变量为外部参数时,必须先进行null判断。
    反例:
public class SwitchString { 
    public static void main(String[] args){ 
        method(null);
    }
    public static void method(String param) { 
        switch (param) { 
        //肯定不是进入这里
        case "sth":
            System.out.println("it's sth");
            break;
            //也不是进入这里
        case "null":
            System.out.printIn("it's null");
            break;
            //也不是进入这里
        default:
            System.out.printIn("default");
        }
    }
  1. 【强制】三目运算符condition ? 表达式1 : 表达式2中,高度注意表达式1和2在类型对齐时,可能 抛出因自动拆箱导致的 NPE异常。
    说明:以下两种场景会触发类型对齐的拆箱操作:
    (1)表达式 1 或表达式 2 的值只要有一个是原始类型。
    (2)表达式 1或 表达式 2 的值的类型不一致,会强制拆箱升级成表示范围更大的那个类型。
    反例:
 Integer a = 1;
 Integer b = 2;
 Integer c = null;
 Boolean flag = false; 
 // a*b的结果是int类型,那么c会强制拆箱成int类型,抛出NPE异常
 Integer result = (flag ? a * b : c);
  1. 【强制】在高并发场景中,避免使用"等于"判断作为中断或退出的条件。
    说明:如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件来代替。
    反例:判断剩余奖品数量等于0时,终止发放奖品,但因为并发处理错误导致奖品数量瞬间变成了负数,这样的话,活动无法终止。
  2. 【推荐】当方法的代码总行数超过10行时,return/throw 等中断逻辑的右大括号后需要加一个空行。
    说明:这样做逻辑清晰,有利于代码阅读时重点关注。
  3. 【推荐】表达异常的分支时,少用if-else方式,这种方式可以改写成:
if (condition) { 
    ...
    return obj;
} 
//接着写else的业务逻辑代码; 

正例:超过3层的if-else的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现,其中卫语句示例如下:

public void findBoyfriend(Man man) { 
    if (man.isUgly()) {
        System.outprintln("本姑娘是外貌协会的资深会员");
        return; 
    }
    if (man.isPoor()) {
        System.out.println(“贫贱夫妻百事哀”);
        return;
    } 
    if (man.isBadTemper()) {
        System.out.println("银河有多远,你就给我滚多远");
        return;
    }
    System.out.println("可以先交往一段时间看看");
}
  1. 【推荐】除常用方法(如 getXxx/isXxx)等外不要在条件判断中执行其它复杂的语句,将复杂逻辑判 断的结果赋值给一个有意义的布尔变量名,以提高可读性。
    说明:很多 if语句内的逻辑表达式相当复杂,与、或、取反混合运算,甚至各种方法纵深调用,理解成本非常高。如果赋值一个非常好理解的布尔变量名字,则是件令人爽心悦目的事情。
    正例:
//伪代码如下
final boolean existed = (file.open(fileName, "w") != null)&&() ||();
if (existed) { 
    ...
}

反例:

public final void acquire(long arg){
    if(!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg) {
        selfInterrupt();
    }
}
  1. 【推荐】不要在其它表达式(尤其是条件表达式)中,插入赋值语句。
    说明:赋值点类似于人体的穴位,对于代码的理解至关重要,所以赋值语句需要清晰地单独成为一行。
    反例:
 public Lock getLock(boolean fair) {
        //算术表达式中出现赋值操作,容易忽略count值已经被改变
        threshold = (count = Integer.MAX_VALUE) - 1;
        //条件表达式中出现赋值操作,容易误认为是sync==fair
        return(sync = fair) ? new FairSync():new NonfairSync();
 }
  1. 【推荐】避免采用取反逻辑运算符。
    说明:取反逻辑不利于快速理解,并且取反逻辑写法一般都存在对应的正向逻辑写法。
    正例:使用if (x<628)来表达x小于628。
    反例:使用if(!(x >= 628))来表达x小于628。
  2. 【推荐】公开接口需要进行入参保护,尤其是批量操作的接口。
    反例:某业务系统,提供一个用户批量查询的接口,API文档上有说最多查多少个,但接口实现上没做任何保护,导致调用方传了一个1000的用户id数组过来后,查询信息后,内存爆了。

你可能感兴趣的:(java,开发语言)