读书计划
2017-04-19~2017-05-20读完
2017-04-19~2017-04-26 第一部分
2017-04-27~2017-05-04 第二部分
2017-05-05~2017-05-12 第三部分
2017-05-13~2017-05-20 第四部分
一.简介
早期的计算机没有操作系统,程序是通过编译后的二进制代码烧录到bios芯片中执行的,每次程序的运行都要人工干预;批量操作系统出来之后,多个程序可以预先都存储到硬盘中,由操作系统依次读取执行,单道批处理系统内存中只能存放一个执行中的程序,多道批处理系统支持多个程序都进驻内存,并在必要的时候(比如IO操作)切换到另一个程序执行;分时操作系统出来后,多个程序之间是分享CPU的时间片来并行执行(实际上一个时刻还是只有一个程序运行,由于切换快,看似同时运行);多核计算机出现后,程序实现了真正意义上的并行。多线程技术可以充分、公平的利用系统资源,同时提高程序的响应速度。然而,多线程技术也带来了一些的风险,比如安全性(正确性)和活跃性(比如死锁)等问题,本文将详细介绍如何高效、正确的使用多线程技术。
二.第一部分
2.1 第二章 线程安全性
2.1.1
线程安全是指程序对共享状态的操作不会因为多线程同时进行而使状态处于不一致的情况,如果状态是不可变的,那么对这个状态的访问永远是线程安全。
由于多线程执行时序的影响导致状态不一致的情况称为竞态条件,常见的竞态条件包括先检查后执行,读取-修改-写入等。需要借助原子操作来避免竞态条件产生的问题。
如果一个操作只涉及到一个状态变量,那么保证这个状态变量处于一个线程安全的类中即可(线程安全的类能保证操作这个状态的原子性)。但是如果一个操作中包含多个状态变量,仅仅保证每个状态变量都处于线程安全的类中是不行的,必须保证对多个状态变量的复合操作是一个整体的原子操作才行。
java中通常借助锁来实现原子操作,锁一般以同步方法和同步代码块两张方式存在,锁的类型有类锁和对象锁。当锁的计数器为0,代表没有被任何线程持有。锁是可重入的,锁是被线程持有,而不是被加锁的方法持有,所以一个线程中可以嵌套调用多个包含同一个锁的方法而不会发生死锁。每次该线程重入锁,锁计数器会加1,释放锁减1。如果一个操作执行时间很长,最好不要持有锁,否则会导致性能问题。
2.2 第三章 对象的共享
对象在多线程环境下访问,应该确保对象是线程安全的。通过锁可以保证操作的原子性,同时还可以保证内存可见性。内存可见性是指共享对象的状态在一个线程中改变后,能够使得其他线程都能获取到最新的该对象最新的状态。另外,编译器、jvm、处理器都可能对操作进行”重排序”,重排序可以保证单线程环境下结果的最终一致性,但是在多线程环境下,重排序可能带来奇怪的错误。
java提供了volatile关键字,用volatile关键字修饰的变量可以保证对变量的写入和读取保持内存可见性。即对volatile对象的写入会被立刻刷新到主内存,读取volatile对象也不会从线程的工作内存读取了,而是去主内存读取。同时volatile可以防止重排序,对volatile变量的读写、赋值等都禁止重排序。
synchronized可以同时保证原子性和内存可见性,而volatile只能保证内存可见性,所以volatile是一个弱性的同步。volatile的适用场景有比如判断任务状态while(!isStop),jdk的Thread类里的 private volatile int threadStatus = 0,单例模式的double check实现方式, instance需要volatile。当满足以下三个条件时,才应该考虑使用volatile:
a.对变量的写操作不依赖于当前值
b.该变量没有包含在具有其他变量的不变式中
c.变量的读操作不需要加锁
对象的发布指的是将在对象的当前作用域以外的地方可以访问该对象,比如将对象的引用保存到public static变量中;对象要被安全的发布,必须要在正确的完成了对象的构造之后才能被发布,常见的错误是在对象的构造函数中完成对其内部类的构造(内部类通过外部类.this可以访问外部类对象),以及在对象的构造函数中创建一个线程对象并且start,this对象会被新创建线程共享,可能还未被构造完成就被新创建并启动的线程使用了。当然,要想安全的发布对象,仅仅做到安全的构造是不够的。还需要使对象是线程安全的,通常有以下这些常用技巧来保证线程安全性:
a.线程封闭,一段时间内只有一个线程在操作对象,比如jdbc的connection对象本身不是线程安全的,但是通常一个servlet请求是一个线程处理的,数据库连接池为每个线程分配一个connection,线程用完后归还connection到数据库连接池,这种方式可以保证connection封闭在单个线程内,避免线程安全性问题。
b.栈封闭,方法内部new出来的对象都是线程安全的,因为只有当前线程的局部引用类型的变量可以访问这个对象。
c.ThreadLocal变量,通过将成员便利声明为ThreadLocal obj,可以在每个线程中都是获取这个obj变量的一个副本,各线程之间使用的是不同的副本,没有线程同步的问题。Thread类中包含ThreadLocal.ThreadLocalMap变量,ThreadLocalMap变量以ThreadLocal对象作为key,ThreadLocal变量相关的对象作为value.在线程执行的每个方法中都可以通过ThreadLocalMap获取到这个value,避免参数传递的问题,相当于线程内部的全局变量。但是这个方法也不能滥用,会降低代码复用性,增加耦合性。
d.不可变对象,在多线程中使用可变对象如果没有适当的同步,可能会使对象状态不一致。而不可变对象即使在多线程环境下,也是线程安全的,因为其状态不会发生变化,可以安全的共享。实现不可变对象需要满足一下几点:
d.1对象被安全的构造(构造函数中this不要溢出)
d.2所有的成员变量都是final类型的
d.3对象创建之后,其状态就不可变。
其中final类型的变量是不能被修改,但是如果final类型的变量是一个可变对象的引用,那么其指向的可变对象仍然是可能改变的。除非需要一个域是可变的,否则都应该将其声明为final,这是一个良好的编程习惯。另外,final类型的变量还可以保证初始化安全性,即变量肯定会在初始化后才会被其他线程访问。某些情况下,不可变对象可以用来保证线程安全,而不需要锁。
要想在多线程之间安全的共享对象,除了使用锁保持同步的方式以外,还可以使用不可变对象。如果需要发布可变对象,对象的发布和对象的状态必须同时对其他线程可见。有以下几种方式可以正确的发布对象。
a.静态初始化函数中初始化一个对象引用
b.用volatile修饰对象引用或者将对象引用保存到AtomicReference中
c.将对象引用保存到每个正确构造对象的final域中
d.将对象的引用保存到由锁保护的域中
将对象放入HashTable、ConcurrentMap等线程安全的容器中,可以保证对象被安全的发布,即使没有显式的使用同步。
如果对象从技术上看是可变对象,但是其状态在发布之后不会被改变,那么它被称为事实不可变对象,其只要被安全的发布,仍然是线程安全的。
2.3 第四章 对象的组合
为了设计线程安全的类,我们可以使用委托、组合等方式,在已有的线程安全的类的基础之上,构建我们自己的线程安全的类。设计线程安全的类的时候需要考虑到以下三个要素:
a.对象的所有状态
b对象的所有不变性条件
c.对象的并发访问管理策略
实例封装策略提倡将对象的状态封装在内部,不要暴露出去,只能通过对象的方法来访问,这样可以更方便的实现同步机制。
在单个的私有内部对象上加锁可以用于保护内部对象,private Object lock=new Object();synchronized(lock)
如果一个对象不包含多个状态组成的不变性条件,那么将每个状态都保持为线程安全就可以使得对象是线程安全的,这个是实现线程安全的委托机制;如果包含多个状态组成的不变性条件,那么仅仅使各个状态是线程安全的是不够的,还需要使用同步机制来保证对多个状态的操作是原子操作。
可以通过组合的方式来构造线程安全的类,如果一个类本身不是线程安全,我们可以将该类的对象通过组合的方式嵌入到一个新的类中,并在新的类中增加同步机制来控制对象的访问,从而使得新的类是一个线程安全的类。