第1章 简介
线程的优势:
①发挥多处理器的强大优势 ②建模的简单性 ③异步事件的简化处理④相应更灵敏的用户界面
线程带来的风险:
①安全性问②活跃性问题③性能问题
第2章 线程安全性
2.1什么是线程安全性
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
在线程安全的类中封装了必要的同步机制,因此客户端无需进一步采取同步措施。
无状态对象一定是线程安全的。
2.2原子性
2.2.1竞态条件
竞态条件:当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。换句话说,就是正确的结果要取决于运气。最常见的静态条件类型就是“先检查后执行(Check-Then-Act)”操作,即通过一个可能失效的观测结果来决定下一步的动作。
数据竞争:如果在访问非final类型的域时没有采用同步来进行协调,那么就会出现数据竞争。(JMM知识)
2.3加锁机制
2.3.1内置锁
Java提供了一种内置的锁机制来支持原子性synchronized
2.3.2重入
“重入”意味着获取锁的操作的粒度是“线程”,而不是“调用”。
如下面代码所示:如果没有“重入”,则产生死锁。
public class Widget{ public synchronized void doSomething(){ System.out.println("Widget..doSomething"); } } public class LoggingWidget extends Widget{ public synchronized void doSomething(){ System.out.println("LoggingWidget..doSomething"); super.doSomething(); } }
2.4用锁来保护状态
对于可能被多个线程同时访问的可变状态变量,在访问他的时候都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁来保护的。
每个共享和可变的变量都应该由一个锁来保护,从而使维护人员知道是哪一个锁。
对于每个包含多个变量的不变性条件,其中涉及的所所有变量都需要由一个锁来保护。
2.5活跃性与性能
通常,在简单性与性能之间存在相互制约因素。当实现某个同步策略时,一定不要盲目地为了性能而牺牲简单性(这可能破坏安全性)。
当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络I/O或者控制台I/O),一定不要持有锁。
第3章 对象的共享
我们已经知道了同步代码块和同步方法可以确保以原子的方式执行操作,但一种常见的误解是,认为关键字synchronized只能用于实现原子性或者确定“临界区(Critical Section)”。同步还有另一个重要的方面:内存可见性(Memory Visibility)。
3.1可见性(JMM知识)
如下面代码所示,明明flag已经改为true,为什么程序没有停止?因为主线程没有看到最新的flag的值
import java.util.concurrent.TimeUnit; /** * @author CBeann * @create 2020-03-26 13:22 */ public class NoVisibility { public static void main(String[] args) { ThreadDemo threadDemo = new ThreadDemo(); new Thread(threadDemo).start(); while (true) { if (threadDemo.isFlag()) { System.out.println("------"); break; } } } } class ThreadDemo implements Runnable { private boolean flag = false; @Override public void run() { try { TimeUnit.MILLISECONDS.sleep(200); } catch (Exception e) { e.printStackTrace(); } flag = true; System.out.println("flag=" + isFlag()); } public boolean isFlag() { return flag; } public void setFlag(boolean flag) { this.flag = flag; } }
解决办法
private volatile boolean flag = false;
3.1.3 加锁与可见性
加锁的含义不仅仅局限性互斥行为,还包括内存可见性。为了确保所有的线程都能见到共享变量的最新值,所有执行读操作或者写操作的线程必须在同一个锁上同步。
3.1.4 Volatile变量
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。
volatile变量的典型用法:
volatile Boolean flag; ... while(!flag){ doSomeThing(); }
加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。
3.2 发布和逸出
“发布(Publish)”一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。
当某个不应该发布的对象被发布时,这种情况就被称为“逸出(Escape)”。
如下面的代码所示,demo中的list被发布,而且逸出。
import java.util.ArrayList; import java.util.List; /** * @author CBeann * @create 2020-02-20 2:49 */ public class Start { public static void main(String[] args) { Demo demo = new Demo(); //调用getList方法发布Demo对象中的list对象 List<String> list = demo.getList(); //该对象逸出,因为list对象已经逸出它所在的作用域(Demo的是私有变量域) list.add("main-thread-add"); for (String s : demo.getList()) { System.out.println(s); } } } class Demo { private List<String> list = null; public Demo() { list = new ArrayList<>(); list.add("cbeann"); } public List<String> getList() { return list; } public void setList(List<String> list) { this.list = list; } }
3.3 线程封闭
当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步,这种技术称为线程封闭(Thread Confinement),它是实现线程安全性最简单方式之一。
3.3.2栈封闭
栈封闭是线程封闭的一种特例。在栈封闭中,只能通过局部变量表才能访问对象。
例如下面代码中的demo对象是一个局部变量,只要不发布,其它线程都无法获得该对象的引用。
public static void main(String[] args) { Demo demo = new Demo(); }
3.3.3 ThreadLocal类
把对象存在threadLocal对象中能实现不共享。因为它的key是thread,所以其它线程取不到数据。
ThreadLocal threadLocal = new ThreadLocal(); try { // public void set(T value) { // Thread t = Thread.currentThread(); // ThreadLocal.ThreadLocalMap map = getMap(t); // if (map != null) // map.set(this, value); // else // createMap(t, value); // } threadLocal.set(1); Object o = threadLocal.get(); } catch (Exception e) { e.printStackTrace(); }finally { threadLocal.remove();//最后要删除,否则容易出现内存泄露 }
3.4不变性
不可变的对象一定是线程安全的。
即使对象中所有的域都是final类型的,这个对象仍然是可变的,因为在final类型的域中可以保存对可变对象的引用。
如下面代码所示,demo里的list用final修饰,但是仍然可以添加。
public class Start { public static void main(String[] args) { Demo demo = new Demo(); demo.getList().add(1); for (Object o : demo.getList()) { System.out.println(o); } } } class Demo { private final List list = new ArrayList(); public List getList() { return list; } }
3.5 安全发布
3.5.6安全的共享对象
在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:
线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。
保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及发布的并且由某个特定的锁保护的对象。
第4章 对象的组合
对象的组合
在设计线程安全的类的过程中,需要包含以下三个基本要素:
- 找出构成对象状态的所有变量。
- 找出约束状态变量的不变性条件。
- 建立对象状态的并发访问管理策略。
线程安全的类组合的类不一定是线程安全的
//线程安全 class Demo { private Map map = new ConcurrentHashMap(); public void put(String key, Object val) { map.put(key, val); } } //线程不安全 class Demo { private Map map = new ConcurrentHashMap(); private Map map2 = new ConcurrentHashMap(); public void put(String key, Object val) { if (map.containsKey(key)){ map2.put(key, val); } } }
本章疑问
为什么书中说下面的一个线程不安全,一个线程安全?
课本提示:线程不安全的代码中说list保护的锁反正不是ListHelper的锁,大致意思是锁不同,反正我没看懂,看懂的可以在下面留言。
//线程不安全 class ListHelper<E> { public List<E> list = Collections.singletonList(new ArrayList<>()); public synchronized boolean putIfAbsent(E e) { boolean absent = !list.contains(e); if (absent) list.add(e); return absent; } }
//线程安全 class ImprovedList<T> implements List<T> { private final List<T> list; public ImprovedList(List<T> list) { this.list = list; } public synchronized boolean putIfAbsent(T x) { boolean contains = list.contains(x); if (contains) list.add(x); return !contains; } //还要实现size,isempty等方法 }
第5章 基础构建模块
5.1同步类容器
5.1.3隐藏迭代器
虽然加锁可以防止迭代器抛出ConcurrentModificationException,但是你必须要记住在所有对共享容器进行迭代的地方都需要加速。实际情况要更加复杂,因为在某些情况下,迭代器会隐藏起来。如下面代码所示。编辑器将字符串的连接操作转换为调用StringBuilder.append(Object),而这个方法又会调用容器的toString方法,标准容器的toString方法将迭代容器,并在每一个元素上调用toString来生成容器内容的格式化表示。
class HiddenIterator{ private final Set set = new HashSet(); public void addTenThings(){ Random random = new Random(); for (int i = 0; i < 10; i++) { set.add(random.nextInt()); } //存在隐藏迭代器 System.out.println("DEBUG: added ten elements to " + set); } }
容器的hashCode和equals等方法也会间接的执行迭代操作,当容器作为另一个容器的元素或键值时,就会出现这种情况。同样,containsAll、removeAll和retainAll等方法,以及把容器作为参赛的构造函数,都会对容器进行迭代。所有这些间接的迭代操作都可以抛出 ConcurrentModificationException。
5.2并发容器
通过并发容器来代替同步容器,可以极大的提高伸缩性并降低风险。
Map map = new HashMap();//不安全的容器 Map map1 = new Hashtable();//同步容器 Map map2 = new ConcurrentHashMap();//并发容器
5.3阻塞队列和生产者-消费者模式
在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具:它们能抑制并防止生产过度的工作项,使应用程序在负荷过载的情况下变的更加健壮。
5.5同步工具类
阻塞队列可以作为同步工具类,其它类型的同步工具类还包括信号量(Semaphore)、栅栏(Barrier)以及闭锁(Latch)。
5.5.1闭锁
闭锁是一种同步工具类。闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程可以通过,当到达结束状态时,这扇门会打开并且允许所有的线程通过。当闭锁到达结束状态后,将不会再改变状态,因此这扇门将永远保持打开状态。闭锁可以用来确保某些活动直到其它活动都完成后才继续执行。
public class CountDownLatchDemo { //测试10个线程并发执行需要多长时间 public static void main(String[] args) throws Exception { int threadNum = 10; CountDownLatch startGate = new CountDownLatch(1); CountDownLatch endGate = new CountDownLatch(threadNum); for (int i = 0; i < threadNum; i++) { new Thread(new Runnable() { @Override public void run() { try { startGate.await(); System.out.println("tun task..."); } catch (Exception e) { e.printStackTrace(); } finally { endGate.countDown(); } } }).start(); } long start = System.currentTimeMillis(); startGate.countDown(); endGate.await(); long end = System.currentTimeMillis(); System.out.println(end - start); } }
5.5.3信号量
计数信号量(Semaphore)用来控制同时访问某个特定资源的操作数量,或者同时执行某个操作的数量。计数信号量还可以用来实现某种资源池,或者对容器施加边界。当信号量的参数为1时,作用和排它锁相似。
public class SemaphoreDemo { public static void main(String[] args) throws Exception { //设置3个资源 Semaphore semaphore = new Semaphore(3); for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { String name = Thread.currentThread().getName(); try { semaphore.acquire(); System.out.println(name+"获得信号量"); System.out.println("do task..."); } catch (Exception e) { e.printStackTrace(); } finally { semaphore.release(); System.out.println(name+"释放信号量"); } } }, "threadID-" + i).start(); } } }
5.5.4栅栏
CyclicBarrier
总结
●可变变量是至关重要的。
所有的并发问题都可以归结为如何协调对并发状态的访问。可变状态越少,就越容易确保线程安全性。
●尽量将域声明为final 类型,除非需要它们是可变的。
●不可变对象一定是线程安全的。
不可变对象能极大地降低并发编程的复杂性。它们更为简单而且安全,可以任意共享而无须使用加锁或保护性复制等机制。
●封装有助于管理复杂性。
在编写线程安全的程序时,虽然可以将所有数据都保存在全局变量中,但为什么要这样做?将数据封装在对象中,更易于维持不变性条件:将同步机制封装在对象中,更易于遵循同步策略。
●用锁来保护每个可变变量。
●当保护同一个不变性条件中的所有变量时,要使用同一个锁。
●在执行复合操作期间,要持有锁。
●如果从多个线程中访问同一个可变变量时没有同步机制,那么程序会出现问题。
●不要故作聪明地推断出不需要使用同步。
●在设计过程中考虑线程安全,或者在文档中明确地指出它不是线程安全的。
●将同步策略文档化。
第6章 任务执行
6.1在线程中执行任务
6.1.3无限制创建线程的不足
线程生命周期的开销非常高。线程的创建与销毁不是没有代价的。线程的创建过程会需要时间,延迟处理的请求。
资源消耗。如果你已经拥有足够多的线程使所有的CPU处于忙碌状态,那么创建更多的线程反而会降低性能。
稳定性。在可创建线程的数量上存在一个限制,如果破坏了这些限制,那么很有可能出现OOM异常。
6.2 Executor框架
6.2.2 执行策略
每当看到下面这种形式的代码时:
new Thread(runnable).start();
并且你希望获得一种更加灵活的执行策略时,请考虑使用Executor来替代Thread。
6.2.3 线程池
6.3找出可利用的并行性
6.3.5 CompletionService: Executor与BlockingQueue
如果向Executor提交一组任务,并且希望计算完成后获得结果,那么你可以保留与每一个任务关联的Future,然后反复使用get方法,这种方法可行,但是繁琐。
如下面代码所示,我提交一5个执行随机时间的任务,当执行完毕后,completionService.take()就会返回执行完毕的那一个。
import java.util.Random; import java.util.concurrent.*; /** * @author CBeann * @create 2020-02-20 2:49 */ public class CompletionServiceDemo{ public static void main(String[] args) throws Exception { CompletionService completionService = new ExecutorCompletionService<Integer>(new Myexecutor()); Random random = new Random(); int threadNum = 5; for (int i = 0; i < threadNum; i++) { completionService.submit(new Callable() { @Override public Object call() throws Exception { int nextInt = random.nextInt() % 10;//10秒以内 if (nextInt <= 0) nextInt += 10; try { System.out.println("业务逻辑执行时间: " + nextInt); TimeUnit.SECONDS.sleep(nextInt); } catch (Exception e) { e.printStackTrace(); } return nextInt; } }); } int sum = 0; for (int i = 0; i < threadNum; i++) { Future take = completionService.take(); Integer o = (Integer) take.get(); sum += o; } System.out.println(sum); } } class Myexecutor implements Executor { @Override public void execute(Runnable command) { new Thread(command).start(); } }