首页 > 代码库 > [读书笔记]《Effective Java》第10章并发

[读书笔记]《Effective Java》第10章并发

第66条:同步访问共享的可变数据

  • 同步的意义。
  1. 正确地使用同步可以保证没有任何方法会看到对象处于不一致的状态中。
  2. 进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前所有的修改效果。
  • Java语言规范保证读或者写一个变量是原子的,除非这个变量的类型为long或者double。
  • 对于原子数据的读取,Java语言规范并不保证一个线程写入的值对于另一个线程将是可见的。
  • 对于共享的数据,即使数据是原子可读写的,也要使用同步。
  • 活动性失败:因为JVM的优化,部分代码无法执行。
 1 /**
 2  * 共享原子可读写的变量不使用同步访问.
 3  * Created by itlivemore on 17-7-1.
 4  */
 5 public class StopThread {
 6     private static boolean stopThread;
 7 
 8     public static void main(String[] args) throws InterruptedException {
 9         Thread backGroundThread = new Thread(new Runnable() {
10             @Override
11             public void run() {
12                 int i = 0;
13                 while (!stopThread) {
14                     System.out.println("i = " + i++);
15                 }
16             }
17         });
18         backGroundThread.start();
19 
20         TimeUnit.SECONDS.sleep(1);
21         stopThread = true;
22     }
23 }

书上写道这个程序不会停止,但是我在自己的机器上运行1秒后停止了。作者的解释是JVM对代码优化了。

while(!done) {
     i++;
}

优化成了

if(!done) {
     while(true) {
          i++;
     }
}

修正方法:

  1. 可以增加对变量stopThread的一个读方法和一个写方法,方法都加上synchronized,确保同步访问stopThread变量。
  2. 使用volatile修饰stopThread变量。volatile可以保证任何一个线程在读取该域的时候都将看到最近刚刚被写入的值。
  • 安全性失败:没有正确地使用同步导致没有达到预期的结果。
 1 **
 2  * 没有正确使用同步导致安全性失败
 3  * Created by itlivemore on 17-7-1.
 4  */
 5 public class SafetyFailure {
 6     private static volatile int nextSerialNumber = 0;
 7 
 8     /*生成唯一的序列号的方法。nextSerialNumber虽然声明为volatile,但是++操作不是原子性的,需要两个步骤:
 9     * 先读取值,然后再写入值。如果线程读取值之后被阻塞,另一个线程也读取值,将会导致两个线程读取的值一样*/
10     public static int generateSerialNumber() {
11         return nextSerialNumber++;
12     }
13 
14     /*修正方法一:增加synchronized来同步*/
15     public synchronized static int generateSerialNumber2() {
16         return nextSerialNumber++;
17     }
18 
19     /*修正方法二:使用AtomicLong*/
20     private static final AtomicLong nextSeriaNumber2 = new AtomicLong();
21 
22     public static long generateSerialNumber3() {
23         return nextSeriaNumber2.getAndIncrement();
24     }
25 }
  • 最佳办法是不共享可变的数据。要么共享不可变的数据,要么不共享,将可变数据限制在单个线程中。

 

第67条:避免过度同步

  • 在同步区域之外被调用的外来方法被称作“开放调用”。“开放调用”可以避免死锁,增加并发性。外来方法运行时间不确定,如果放在同步区域内,会阻止其它线程对锁的获取,影响并发。
  • 应该在同步区域内做尽可能少的工作。获得锁,检查共享数据,根据需要转换数据,然后放掉锁。
  • 书中有个例子,这里简化下,大概意思是相同的。在用foreach遍历时调用了外部方法,外部方法中有remove操作,会抛异常。同样的,如果在外部方法开新的线程来获得原方法中相同的锁,外部方法要获得锁而不能得到锁,而遍历中又等待外部方法调用完成,则会导致死锁。
 1 /**
 2  * 同步区域中调用外来方法
 3  * Created by itlivemore on 17-7-1.
 4  */
 5 class Other {
 6     public static void remove(List<Integer> list, int i) {
 7         if (i == 2) {
 8             list.remove(new Integer(i));
 9         } else {
10             System.out.println(i);
11         }
12     }
13 }
14 
15 public class SynchronizedCall {
16 
17     static List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
18 
19     public static void foreachlist() {
20         synchronized (list) {
21             int i = 0;
22             for (Integer integer : list) {
23                 Other.remove(list, i++);
24             }
25         }
26     }
27 
28     public static void main(String[] args) {
29 //会有异常,因为遍历时删除
30         foreachlist();
31     }
  • 如果一个可变的类要并发使用,应该使这个类变成是线程安全的,通过内部同步,可以获得比从外部锁定整个对象更高的并发性。否则,就不要在内部同步。让客户在必要的时候从外部同步。Java违背了这条方针的类StringBuffer,StringBuffer几乎用于单线程,但是执行的是内部同步。

 

第68条:executorstask优先于线程

  • 原先使用Thread类,既充当工作单元,又是执行机制。现在两都分离,Executors是执行机制,Runnable和Callable是工作单元。
  • ExecutorService简单使用
 1 /**
 2  * ExecutorService的简单使用
 3  * Created by itlivemore on 17-7-1.
 4  */
 5 public class Executor {
 6     public static void main(String[] args) {
 7         // 创建
 8         ExecutorService executorService = Executors.newSingleThreadExecutor();
 9         // 执行任务
10         executorService.execute(new Runnable() {
11             @Override
12             public void run() {
13                 System.out.println(System.currentTimeMillis());
14             }
15         });
16         // 关闭
17         executorService.shutdown();
18     }
19 }
  • ExecutorService创建的几种类型及使用场景:
  1. Executor.newCachedThreadPool,被提交的任务没有排成队列,而是直接交给线程执行。如果没有线程可用,就创建一个新的线程。适用于小程序,轻载的服务器。
  2. Executor.newFixedThreadPool,提供了包含固定线程数目的线程池。适用于一个重负载的服务器。
  3. 为了最大限度控制,直接使用ThreadPoolExecutor。
  • Executors.newScheduledThreadPool()代替java.util.Timer。线程池executor支持多个线程,并且从抛出未受检异常的任务中恢复。

 

第69条:并发工具优先于waitnotify

  • Java.util.concurrent中更高级的工具分成三类:Executor Framework、并发集合以及同步容器。
  • 并发集合为了提供并发性,同步是在内部自己管理的。因此,并发集合中不可能排除并发活动,将它锁定没有什么用,只会使程序的速度变慢。
  • String.intern()方法。例:“abc”.intern(),通过equals方法来判断常量池中是否存在”abc“,如果存在,则不创建字符串对象,直接返回存在的字符串。如果不存在就创建对象,放到常量池中,返回该对象。
 1 /**
 2  * String.intern()方法
 3  * Created by itlivemore on 17-7-1.
 4  */
 5 public class StringIntern {
 6     public static void main(String[] args) {
 7         String str1 = "abc";
 8         String str2 = new String("abc");
 9         // 返回true,说明是同一对象
10         System.out.println(str1 == str1.intern());
11         // 返回的是false
12         System.out.println(str1 == str2);
13     }
14 }

String.intern()的参考:http://blog.csdn.net/hanrentanfei/article/details/1817402

  • 同步器是一些使线程能够等待另一个线程的对象,允许它们协调动作。如:CountDownLatch,Semaphore,CyclicBarrier,Exchanger。
  • 对于间歇式的定时,始终应该优先使用System.nanoTime,而不是使用System.currentTimeMills。System.nanoTime更加准确也更加精确,它不受系统的实时时钟的调整所影响。
  • 应该优先使用并发工具,而不是使用wait和notify。wait方法被用来使线程等待某个条件。它必须在同步区域内部调用,这个同步区域将对象锁在了调用wait方法的对象上。使用wait方法的标准模式:
// 使用wait方法的标准模式
synchronized(ojb) {
  while(condition) {
    obj.wait();
  }
}
  • 为了唤醒等待的线程,一般使用notifyAll而不是notify。因为notifyAll可以保证将会唤醒所有需要被唤醒的线程。可能也会唤醒其他一些线程,但这不会影响程序的正确性。

 

第69条:线程安全性的文档化

  • 类的线程安全性级别
  1. 不可变的:这个类的实例是不可变的。不需要外部的同步。例:String,Long和BigInteger。
  2. 无条件的线程安全:这个类的实例是可变的,但是这个类有着足够的内部同步,所以它的实例可以被并发使用,无需任何外部同步。例:Random和ConcurrentHashMap。
  3. 有条件的线程安全:除了有些方法为进行安全的并发使用需要外部同步之外,这种线程安全级别与无条件的线程安全相同。例:Collections.synchronized包装返回的集合,它们的迭代器要求外部同步。
  4. 非线程安全:这个实例是可变的。为了并发地使用它们,客户必须利用自己选择的外部同步包围每个方法调用(或者调用序列)。例:通用的集合实现ArrayList、HashMap等。
  5. 线程对立的:这个类不能安全地被多个线程并发使用,即使所有的方法调用都被外部同步包围。线程对立的根源通常在于,没有同步地修改静态数据。在Java平台类库中,线程对立的类或者方法非常少。例:System.runFinalizersOnExit方法,但已经被废除。
  • 对于有条件的线程安全类,必须指明哪个调用序列需要外部同步,还要指明为了执行这些序列,必须获得哪一把锁(极少的情况是指哪几把锁)。
  • 当一个类使用一个公有可访问的锁对象时,就允许客户端以原子的方式执行一个方法调用序列。客户端可以超时地持有公有可访问锁,发起拒绝服务攻击,导致其它客户端无法访问该对象。可以使用私有锁对象来解决这个问题,把锁对象封装在它所同步的对象中。
// 私有锁对象,防止拒绝服务攻击
private final Object lock = new Object();
public void foo() {
  synchronized(lock) {}
}
  • 私有锁对象只能用在无条件的线程安全类上。有条件的线程安全类不能使用,因为要说明调用方法序列时要获得哪把锁。私有锁对象模式特别适用于那些专门为继承而设计的类。

 

第69条:慎用延迟初始化

  •  对于延迟初始化,除非绝对必要,否则不要这么做。
  • 延迟初始化的几种方式
 1 /**
 2  * 延迟初始化的几种方式
 3  * Created by itlivemore on 17-7-1.
 4  */
 5 public class LazyInitialzation {
 6     /*同步访问方法的延迟初始化*/
 7     private Integer field1;
 8 
 9     synchronized Integer getField1() {
10         if (field1 == null) {
11             field1 = 1;
12         }
13         return field1;
14     }
15 
16     /*出于性能考虑对静态域使用延迟初始化*/
17     private static class FieldHolder {
18         static final Integer field = 2;
19     }
20 
21     static Integer getField2() {
22         return FieldHolder.field;
23     }
24 
25 
26     /*出于性能考虑对实例域使用延迟初始化,就使用双重检查模式*/
27     private volatile Integer field3; // 注意这里要声明为volatile
28 
29     Integer getField3() {
30         // 增加局部变量result是确保field只在已经被初始化的情况下读取一次
31         Integer result = field3;
32         if (result == null) {
33             synchronized (this) {
34                 result = field3;
35                 if (result == null) {
36                     field3 = result = 3;
37                 }
38             }
39         }
40         return result;
41     }
42 
43     /*延迟初始化一个接受重复初始化的实例域,使用单重检查模式*/
44     private volatile Integer field4;
45 
46     Integer getField4() {
47         Integer result = field4;
48         if (result == null) {
49             field4 = result = 4;
50         }
51         return result;
52     }
53 }

 

第69条:不要依赖于线程调度器

  • 线程调度器调度线程的策略在各个操作系统是不一样的,任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的。
  • 要编写健壮的、响应良好的、可移植的多线程应用程序,最好的办法是确保可运行线程的平均数量不明显多于处理器的数量。
  • 让每个线程做些有意义的工作,然后等待更多有意义的工作。线程不应该处于忙等的状态,即反复地检查一个共享对象,以等待某些事情发生。
  • 因为某些线程无法像其它线程那样获得足够的CPU时间,那么,不要企图通过调用Thread.yield来修正程序。
  • Java线程的优先级不可移植,通过调整线程的优先级来改善程序并不合理。

 

第70条:避免使用线程组

  • 线程组并没有提供太多有用的功能,而且它们提供的许多功能还都是有缺陷的。不要使用它们。

 

[读书笔记]《Effective Java》第10章并发