多线程之不可变对象

66 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第12天,点击查看活动详情

1 日期转换的问题

SimpleDateFormat是线程不安全类,在对线程环境下,会出现错误.

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
     new Thread(() -> {
     try {
         log.debug("{}", sdf.parse("2022-08-07"));
     } catch (Exception e) {
         log.error("{}", e);
     }
     }).start();
}

出现 java.lang.NumberFormatException 或者出现不正确的日期解析结果.

19:10:40.859 [Thread-2] c.TestDateParse - {} 
java.lang.NumberFormatException: For input string: "" 
 at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) 
 at java.lang.Long.parseLong(Long.java:601) 
 at java.lang.Long.parseLong(Long.java:631) 
 at java.text.DigitList.getLong(DigitList.java:195) 
 at java.text.DecimalFormat.parse(DecimalFormat.java:2084) 
 at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162) 
 at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) 
 at java.text.DateFormat.parse(DateFormat.java:364) 
 at cn.cf.n7.TestDateParse.lambda$test1$0(TestDateParse.java:18) 
 at java.lang.Thread.run(Thread.java:748) 

2 解决方案

1 同步锁

加锁 ,保证每次只有一个线程去执行方法. 能解决问题,但是影响性能效率.

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 50; i++) {
     new Thread(() -> {
         synchronized (sdf) {
             try {
                 log.debug("{}", sdf.parse("1951-04-21"));
             } catch (Exception e) {
                 log.error("{}", e);
             }
         }
     }).start();
}

2 类不可变

如果一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改.

以Java8提供的新日期格式化类DateTimeFormatter.

DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
     new Thread(() -> {
         LocalDate date = dtf.parse("2022-08-07", LocalDate::from);
         log.debug("{}", date);
     }).start();
}

将对象置为不可变对象,是一种避免竞争的方式.

3 不可变设计

String类型就是不可变的,以其为例,整理不可变设计.

public final class String
 implements java.io.Serializable, Comparable<String>, CharSequence {
    
 /** The value is used for character storage. */
 private final char value[];
 /** Cache the hash code for the string */
 private int hash; // Default to 0
 
 // ...
 
}

其中使用final修饰了String对象, 且属性都是final的.

  • 属性用 final 修饰保证了该属性是只读的,不能修改
  • 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性

保护性拷贝

使用字符串时,也有一些跟修改相关的方法,比如 substring ,底层其实是同一个对象.

public String substring(int beginIndex) {
     if (beginIndex < 0) {
     throw new StringIndexOutOfBoundsException(beginIndex);
     }
     int subLen = value.length - beginIndex;
     if (subLen < 0) {
     throw new StringIndexOutOfBoundsException(subLen);
     }
     return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

发现其内部是调用 String 的构造方法创建了一个新字符串,再进入这个构造看看,是否对 final char[] value 做出 了修改:

public String(char value[], int offset, int count) {
     if (offset < 0) {
         throw new StringIndexOutOfBoundsException(offset);
     }
     if (count <= 0) {
         if (count < 0) {
             throw new StringIndexOutOfBoundsException(count);	
         }
         if (offset <= value.length) {
             this.value = "".value;
             return;
         }
     }
     if (offset > value.length - count) {
         throw new StringIndexOutOfBoundsException(offset + count);
     }
     this.value = Arrays.copyOfRange(value, offset, offset+count);
}

结果发现也没有,构造新字符串对象时,会生成新的 char[] value,对内容进行复制, 这种通过创建副本对象来避免共享的手段称为 保护性拷贝