多线程程序中,若是控制很差,常常会出现各类的问题,有时候问题在调试或者测试的时候就会暴露出来,最要命的是程序部署一段时间以后才出现各类怪异现象,感到头疼?须要补充Java并发的知识了。该读书笔记系列以《Java并发编程实战》为基础,同时会参考网络上一些其余的资料,和你们一块儿学习Java并发编程的各个方面。这方面也是笔者比较薄弱的地方,理解不对的地方请留言或者邮件指出,同时也欢迎讨论。邮箱:sunwei.pyw@gmail.com编程
线程安全性这章,将从以下几个方面入手,描述探讨:设计模式
一、什么是对象的状态安全
二、线程安全网络
三、无状态和有状态多线程
四、竞态条件并发
五、加锁机制ide
内置锁学习
锁的重入测试
六、用锁来保护状态this
七、小结
本章大部分是比较抽象的概念,不过不要紧,咱们将列举一些代码来逐一说明,这些概念上的认识,将会对之后理解有很大的帮助。
一、什么是对象的状态
书中指出,从非正式的意义上说,对象的状态是指存储在状态变量(实例或者静态域)中的数据,对象的状态可能包括其余的依赖对象的域,例如某个HashMap的状态不只存储在HashMap自己,还存储在许多Map.Entry中。在对象的状态中包含了任何可能影响其外部可见行为的数据。咱们能够简单地理解为对象的状态就是对象的值和属性,对于简单的类型,就是其值。线程安全的代码,核心就是要对状态访问操做进行管理,特别是“共享的”和“可变的”状态。共享意味着多个线程能够同时访问,而可变的意思就是在其生命周期内,其值能够改变。
若是一个变量处于方法内部,它并非共享的,由于每一个线程执行代码的时候,都会有各自的值存储在线程的局部变量内,其余线程是没法访问的。若是一个类变量被声明成final的,它的属性也没有提供可访问的修改方法,那么它的状态就是不可变的。
二、线程安全
线程安全的核心就是正确性,正确性的含义是,某个类的行为与其规范彻底一致。这里的规范能够理解为预期。
当多个线程访问某个类时,不须要添加任何的同步或者协同,这个类始终都能表现出正确的行为和结果,那么就称这个类是线程安全的。
能够这样理解,一个对象是否须要线程安全,取决于它是否被多个线程访问。这里指的是程序访问对象的方式,而不是对象要实现的功能。单线程访问任何状态都是安全的。若是多个线程访问一个未知线程安全的对象,就须要对访问方式进行控制。一个对象是不是线程安全的,指的是对象的实现自己就是线程安全的。这种状况下,不论对线程的访问是否作了控制,这个对象老是线程安全的。
线程安全的程序是否彻底由线程安全的类组成?答案是否认的,彻底由线程安全类构成的程序不必定就是线程安全的,而在线程安全类中也能够包含非线程安全的类。
因此线程安全其实就是在多线程环境下,类是否始终能够表现出正确的行为和结果,这个正确性实际上是咱们业务上的预期。
三、无状态和有状态
Servlet是多线程单实例的,这意味着多个多个请求共享一份Servlet对象,是一个典型的多线程访问的例子。Servlet分为无状态和有状态,分析以下的例子,本例子对书上的例子作了稍微的简化:
public class SafeServlet implements Servlet{ public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { String str1 = req.getParameter("param1"); String str2 = req.getParameter("param2"); res.getWriter().write(str1 + str2); } … }
此处省略了其余的一些方法,service方法从请求中获取到两个字符串,返回两个字符串的链接。
这个类是线程安全的,由于它是无状态的,它不包含任何域,也不包含任何其余类中域的引用,虽然全部线程都共享同一个SafeServlet实例,可是全部的线程都没有共享变量,每一个线程都各行其是,没有交集,也不会相互影响。
所谓有状态,就像是咱们在第一节提出来的,该对象存在着共享的变量,每一个线程均可以访问这个变量。
像接下来的这个Servlet,用来统计处理次数:
public class UnsafeServlet implements Servlet{ private long count = 0l; public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { String str1 = req.getParameter("param1"); String str2 = req.getParameter("param2"); res.getWriter().write(str1 + str2); count ++; } }
该Servlet是有状态的,不一样的线程调用service时,都会处理一个共享的变量:count。
count操做并非原子的,它能够分解为三个步骤:
读取count的值;
修改count的值;
写count的值;
这三步操做可能在多线程访问时彻底搞乱顺序。当线程A刚读取完,线程B也读取了值,可是线程A的执行时间片(https://zh.wikipedia.org/wiki/%E6%97%B6%E9%97%B4%E7%89%87)
完了,线程B理所固然地把count+1而且写入了count变量中。当线程A再次执行的时候,用到的count已是过时的了。
而且其结果状态依赖上一个线程的处理。这会引起一系列不正确的结果。
无状态的类都是线程安全的,有状态的类,若是想线程安全,须要作一些并发控制。UnsafeServlet中的count,若是类型改为AtomicLong类型,这样能够把count++操做变成原子性的,由于AtomicLong对加一操做作了并发控制。由此咱们能够想象,若是对状态的操做是原子性的,该对象也是线程安全的,固然方法不止这一种,后面将会提到。
四、竞态条件
这是维基百科的解释:https://zh.wikipedia.org/wiki/%E7%AB%B6%E7%88%AD%E5%8D%B1%E5%AE%B3
以上的UnsafeServlet中,因为不恰当的执行时序而出现的不正确的结果是很是典型的,咱们称之为“竞态条件”。当某个计算的正确性取决于多个线程的交替执行时序时,那么就会存在竞态条件。这种状况下是否返回正确的结果,彻底靠运气。究其根本缘由,就是可能基于一种已经失效的结果来作操做。
值得一提的是,设计模式:单例模式(https://zh.wikipedia.org/wiki/%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F),很容易被写成线程不安全的以下方式:
public class Singlton { private Object obj = null; public Object getInstance(){ if(obj == null){ obj = new Object(); } return obj; } }
这里包含一个竞态条件,它可能破坏这个类的正确性。不难分析,当两个线程都访问这个类想获取一个Object对象的时候,A、B都断定obj是null。A、B都会建立一个Object对象。那么他们可能返回不一样的对象。维基百科上给出了安全的处理方式,这里就再也不赘述了。
要避免竞态条件问题,就必须在某个线程修改该变量时,经过某种方式防止其余线程使用这个变量,从而确保其余线程只能在修改操做完成以前或者以后读取和修改状态,而不是在在修改这个状态的过程当中,也就是以前UnsafeServlet所描述的那样,将分步的操做复合成原子性的。实际状况中,尽量能使用现有的线程安全对象来管理类的状态,这样更容易验证和维护线程的安全性问题。
五、加锁机制
UnsafeServlet的描述中,在Servlet中添加一个状态变量时,可使用线程安全的对象来保证类的安全性,若是须要更多的状态变量时,是否只须要用线程安全的对象就能够保证线程安全了呢?不是这样的。
咱们将代码稍做修改,添加一个变量记录最后一次请求的参数:
一样,省略了其余方法的实现。
public class UnsafeServlet implements Servlet{ private AtomicLong count = new AtomicLong(0); private AtomicReference<String> lastParam1 = new AtomicReference<String>(); public long getCount(){return count.get();} public String getLastParam1(){return lastParam1.get(); } public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { String str1 = req.getParameter("param1"); String str2 = req.getParameter("param2"); res.getWriter().write(str1 + str2); count.incrementAndGet(); lastParam1.set(str1); } … }
然而这种方式并不正确,虽然咱们的两个变量都是原子性的,也是线程安全的,可是这个类中存在竞态条件。
在线程安全的定义中,要求多个线程之间的操做不管采用什么执行时序或者交替方式,都要保证结果正确。虽然使用了set操做是原子性的变量,可是service方法没法保证两次set操做总体是原子性的。
内置锁
Java提供了一种内置机制来支持原子性:同步代码块(synchronized)。同步代码块包括两个部分:一个做为锁的对象引用,一个做为由这个锁保护的代码块。静态的synchronized方法以Class对象做为锁。
每一个Java对象均可以用作一个实现同步的锁,这些锁被称为内置锁。
线程在进入同步代码块以前会自动得到锁,而且在退出同步代码块时自动释放锁。不管是经过正常的控制路径退出仍是经过代码块中抛出的异常退出,得到内置锁的惟一途径就是进入由这个锁保护的同步代码块或方法。
内置锁至关于一种互斥体,意味着当线程A进入同步代码块的时候,其余线程是没法进入代码块的,这样就能够保证代码块是原子性的。直到线程A释放锁,其余线程才能进入代码块。此处咱们能够把整个servie方法都同步起来:
public synchronized void service(ServletRequest req, ServletResponse res)
可是这样的效率过低,由于这就至关于说明只能按顺序来访问该serlvet,违背了咱们多线程访问的初衷,一样咱们也能够只将操做属性的操做同步起来:
synchronized(this){count.incrementAndGet();lastParam1.set(str1);}
重入
重入指的是一种机制,当某个线程请求一个由其余线程持有的锁时,发出请求的线程就会阻塞,而后因为内置锁是能够重入的,所以若是某个线程试图得到一个已经由它本身持有的锁,那么这个请求是会成功的。也就是说本身请求本身的锁,是能够成功的,这种机制避免了一些状况
下死锁的发生。
上面的代码中,子类改写了父类的synchronized方法,而后又调用父类中的方法,此时若是没有可重入的锁,那么这段代码将死锁。因为Widget和ChildWidget中doSth方法都是synchronized的,所以每一个doSth方法在执行前都会获取Widget上的锁,由于这个锁已经被持有,而线程将永远等待下去。
public class Widget { public synchronized void doSth(){ System.out.println("Parent Do Sth"); } } public class ChildWidget extends Widget{ @Override public synchronized void doSth() { super.doSth(); } }
六、用锁来保护状态
因为锁能够保护代码按串行的形式来访问,所以能够经过锁来构造一些协议以实现对共享状态的独占访问。共享状态的复合操做,如:递增、单例模式里先判断后建立对象等,都必须是以原子操做以免竞态条件的产生。仅仅将复合操做封装到一个同步代码块中是不够的,若是用同步来协调对某个变量的访问,那么全部访问这个变量的位置都须要使用同一个锁。
对象的内置锁与其状态之间没有内在的关联,当获取与其对象相关联的锁时,并不能阻止其余线程访问该对象,某个线程在得到对象的锁以后,只能阻止其余线程获取同一个锁。
之因此每一个对象都有一个内置锁,只是为了免去显式地建立锁对象,若是自行构造一个锁对象,那么久须要在程序中自始至终都使用它们。
每一个共享的可变的变量都应该只由一个锁在保护,从而使得维护人员知道是哪个锁。
一种常见的约定是,将全部的可变状态都封装在对象内部,而且经过对象的内置锁对全部访问可变状态的代码进行同步。
七、小结
本节主要讲述了以下几个点,非正式的说法对象的状态就是对象的值和属性,对象在多线程访问时,若是老是能返回正确的结果,那么这个对象就是线程安全的,无状态的类必定是线程安全的,若是对象中存在竞态条件,将会出现多线程访问数据正确性问题。Java提供了内置锁来支持对象的线程安全。线程能够获取本身的锁,这就叫重入,因为锁机制只能保证同一个锁不被不一样的线程持有,咱们用锁机制来保护对象的状态时,须要注意不变性条件中的每一个变量都要使用同一个锁来保护。