Java并发编程实战读书笔记

第一章 简介

为什么要在计算机中加入操作系统来实现多个程序的同时执行?

主要基于以下原因:
1.在一个程序等待过程中,同时运行其他程序,提高资源利用率。
2.公平性:使不同的用户和程序通过粗粒度的时间分片,共享计算机资源。
3.便利性:计算多个任务时,编写多个程序,比只编写一个程序计算所有任务简单。

线程有什么优势?

1.发挥多处理器的能力。
2.建模的简单性:通过使用线程,可以将复杂并且异步的工作流进一步分解为一组简单并且同步的工作流,每个工作流在一个单独的线程中运行,并在特定的同步位置进行交互。如Servlet,编写Servlet时不用考虑多少请求在同一时刻被处理。
3.异步事件的简化处理:如服务器应用程序接受来自多个远程客户端的套接字(Socket)连接时,给每个连接都分配各自线程并且使用同步I/O,不然,如果是单线程,读操作一直没有数据到来就会阻塞或者使用非阻塞I/O,但是非阻塞I/O复杂性高于同步I/O,容易出错。
4.响应更灵敏的用户界面:使用多线程将执行时间比较久的任务放进一个线程中,避免传统GUI应用程序用户的操作被卡住。

线程有什么风险?

1.安全性问题:在多线程中,操作的执行顺序是不可预测的;出现竞态条件,需要同步。
2.活跃性问题:一些死循环,或者死锁的问题。
3.性能问题:频繁的上下文切换操作;某些同步机制抑制某些编译器优化,使内存缓冲区中的数据无效,以及增加共享内存总线的同步流量。这些因素会带来的额外性能开销。

第二章 线程安全性

编写线程安全的代码,核心在于对共享的(Shared)和可变的(Mutable)状态操作进行管理,状态可以理解为是存储在状态变量(例如实例或静态域)中的数据吗,“共享”意味着变量可以由多个线程同时访问,“可变”意味着变量的值在其生命周期发生变化。

如何避免多个线程访问同一个可变的状态变量导致程序出错的问题

  • 不在线程之间共享该状态变量。
  • 将状态变量修改为不可变的变量。
  • 在访问状态变量时使用同步。

什么是线程安全性

  • 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
  • 可以将线程安全类认为是一个在并发环境和单线程环境中都不会被破坏的类。
  • 无状态对象一定是线程安全的。

原子性

原子性是指一个操作不能再分割,要么全完成,要么全失败,比如count++的操作,其实是一个“读取-修改-写入”的操作序列,当多个线程同时访问count时,一个线程可能处于读的阶段,一个线程处于写入的阶段,那么这个count将不会准确,这种由于不恰当执行时序而出现不正确的结果是一种非常重要的情况,它有个正式的名字:竞态条件(Race Condition)

什么是“先检查后执行(Check-Then-Act)”

“先检查后执行”是最常见的的竞态条件类型,通过一个可能失效的观测结果来决定下一步的动作:首先观察到某个条件为真(例如文件X不存在),然后根据这个观察结果采用相应的动作(创建文件X),但事实上,在你观察到这个结果以及开始创建文件之间,观察结果可能变得无效(另一个线程在这期间创建了文件X),从而导致各种问题(未预期的异常、数据被覆盖、文件呗破坏等)。

复合操作

把“先检查后执行”和“读取-修改-写入”称为复合操作,为了确保线程安全性,需要将复合操作已原子方式执行。

加锁机制

synchronized是java的内置锁,相当于一种互斥体(互斥锁),意味着最多只有一个线程能持有这种锁,因为每次只有一个线程执行内置锁保护的代码块,因此,由这个锁保护的同步代码块会以原子方式执行。

什么是重入

如果某个线程试图获得一个由它自己持有的锁,那么这个请求就会成功。

锁保护状态

对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。

活跃性与性能

synchronized的方法每次只有一个线程可以执行,在负载多的时候会带来严重的性能问题,正确的使用synchronized需要在简单性(对整个方法进行同步)与并发性(对尽可能短的代码路径进行同步)之间的平衡。

第三章 对象的共享

可见性

当读操作和写操作在不同的线程中执行时,无法确保执行读操作的线程能适时地看到其他线程写入的值。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。

使用内置锁确保可见性

内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果,避免读到失效值。

使用Volatile变量

当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量时共享的,因此不会讲该变量上的操作与其他内存操作仪器重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

Volatile的局限性

Volatile只能确保可见性,不能确保原子性,加锁两者都能兼顾。

当且仅当满足以下所有条件,才应该使用volatile变量:

  • 当变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量值。
  • 该变量不会与其他状态变量一起纳入不变形条件中。
  • 在访问变量时不需要加锁。

发布与逸出

“发布(publish)”一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。而“逸出(Escape)”是指当某个不该发布的对象被发布。
逸出的例子:


//使内部的可变状态逸出(不要这么做)
class UnsafeStates {
  private String[] states = new String[]{
      "AK","AL" ...
  };
  public String[] getStates{} {return states;}
}
//隐式地使this引用逸出(不要这么做)
public class ThisEscape {
    public ThisEscape(EventSource source) {
        source.registerListener(
            new EventListener() {
                 public void onEvent(Event e) {
                     doSomething(e);
                 }
            });
    }
}

什么是线程封闭?有哪些线程封闭的方式?

当访问共享的可变数据时,通常需要使用同步。那么如果不共享数据就不需要同步了,如果仅在单线程内访问数据,就不需要同步,这种技术被称为线程封闭(Thread Confinement)
线程封闭的方式有

  • Ad-hoc线程封闭
    是指维护线程封闭性的职责完全有程序实现来承担。Ad-hoc线程封闭非常脆弱,因为没有任何一种语言特性,例如可见性修饰符或局部变量,能将对象封闭到目标线程上。在可能情况下,应使用更强的线程封闭技术。
  • 栈封闭
    栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象,也被称为线程内部使用或者线程局部使用,比Ad-hoc线程封闭更易于维护,也更加健壮。
  • ThreadLocal类
    维持线程封闭性的一种更规范方法是使用ThreadLocal,这个类能是线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。ThreadLocal对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。

不变性

不可变对象一定是安全的。
使用Final域和Volatile类型来发布不可变对象。

安全发布

任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这写对象时没有使用同步。


要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可

以通过以下方式来安全地发布:

1.在静态初始化函数中初始化一个对象引用。
2.将对象的引用保存到volatile类型的域或者AtomicReferance对象中。
3.将对象的引用保存到某个正确构造对象的final类型域中。
4.将对象的引用保存到一个由锁保护的域中。

线程安全库中的容器类提供了以下安全发布保证:
1.键值:Hashtable,synchronizedMap或者ConcurrentMap
2.某个元素:Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList、synchronizedSet
3.队列BlockingQueue、ConcurrentLinkedQueue

事实不可变对象、可变对象

如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么把这种对象称为“事实不可变对象”。
对象的发布需求取决于它的可变性:

  • 不可变对象可以通过任意机制来发布。
  • 事实不可变对象必须通过安全方式来发布。
  • 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来。

总结

在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:
线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。
保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象也包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。
未完待续。。。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值