并发编程之线程安全性
一、什么是线程安全性
并发编程中要编写线程安全的代码,则必须对可变的共享状态的访问操作进行管理。
对象的状态就是存储在实例或者静态变量中的数据,同时其状态也包含其关联对象的字段,比如字典集合既包含自己的状态,
也包含KeyValuePair。
共享即可以多个线程同时访问变量,可变即变量在其声明周期内可以发生变化。
代码线程安全性关注的是防止对数据进行不可控的并发访问。
是否以多线程的方式访问对象,决定了此对象是否需要线程安全性。线程安全性强调的是对对象的访问方式,而不是对象
要实现的功能。要实现线程安全性,则需要采用同步机制来协调对对象可变状态的访问。例如当修改一个可能会有多个线
程同时访问的状态变量的时候,必须采用同步机制协调这些线程对变量的访问,否则可能导致数据被破坏或者导致不可预
知的结果。
保证线程安全性的三种方式
不共享状态变量
共享不可变状态变量
同步对状态变量的访问和操作
面向对象的封装特性有利于我们编写结构优雅、可维护性高的线程安全代码。
当多个线程访问某个类时,其始终都能表现出正确的行为,那这个类就是线程安全的。类的正确性是由类的规范定义的,
其规范包含约束对象状态的不变性条件和描述对象操作结果的后验条件。例如Servlet规范规定Servlet在站点启动时或者
第一次请求访问时进行初始化,后续再次请求则不会进行初始化,因为Servelet会被多个线程访问,所以为了保证其线程
安全性,其只能是无状态的或者对状态访问进行同步。
package com.codeartist; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class HelloConcurrentWorldServlet */ @WebServlet("/HelloConcurrentWorldServlet") public class HelloConcurrentWorldServlet extends HttpServlet { private static final long serialVersionUID = 1L; /** * @see HttpServlet#HttpServlet() */ public HelloConcurrentWorldServlet() { super(); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub response.getWriter().append("Hello Concurrent World ! from codeartist! "); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub doGet(request, response); } }
二、原子性
如果我们在Servlet中新增一个统计访问次数的状态字段,会出现什么情况呢?
package com.codeartist; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class CountorServlet */ @WebServlet("/CountorServlet") public class CountorServlet extends HttpServlet { private static final long serialVersionUID = 1L; private long acessCount=0; /** * @see HttpServlet#HttpServlet() */ public CountorServlet() { super(); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub acessCount++; response.getWriter().append("Welcome your acess my Servelet ! ,you are " + acessCount+ " visitor."); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub doGet(request, response); } }
我们知道Servlet并不是线程安全的,其中acessCount++只是看起来像一个操作的紧凑语法,其本身并不是一个不可分割的
原子性操作。实际上其包含三个独立的操作:读取acessCount的值,将其值递增1,然后将计算结果存入acessCount。这是一个依
赖操作顺序的操作序列。如果两个请求同时读取acessCount的值,最终会导致丢失一次访问记录。
在并发编程中,这种由于执行时序导致不确定结果的情况,有一个更专业的称谓“竟态条件”。开发中常见的竟态条件就
是“先检查后执行操作”,即基于可能失效的检测条件决定下一步的操作,其中又以对象的延迟初始化比较多见
package com.codeartist; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class DelayInitExpensiveServlet */ @WebServlet("/DelayInitExpensiveServlet") public class DelayInitExpensiveServlet extends HttpServlet { private static final long serialVersionUID = 1L; private ExpensiveObject expensiveObject = null; public ExpensiveObject getExpensiveObject() { if(this.expensiveObject == null) { this.expensiveObject = new ExpensiveObject(); } return this.expensiveObject; } /** * @see HttpServlet#HttpServlet() */ public DelayInitExpensiveServlet() { super(); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub response.getWriter().append("Served at: ").append(request.getContextPath()); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub doGet(request, response); } }
如果有两个线程同时执行 getExpensiveObject,第一个线程判断 expensiveObject为null,第二个线程有可能判断也为null
或者已经初始化完成,这除了依赖线程的执行次序,同时也依赖与初始化ExpensiveObject需要的事件长短。
在上边的两个例子中,我们必须在某个线程操作状态变量的时候,通过某种方式限制其他线程只能在操作之前或者
完成之后操作状态变量。其实就是要求这些符合操作要具有原子性,比如acessCount++,我们可以将其委托给线程
安全的AtomicLong来管理,从而确保了代码的线程安全性。
package com.codeartist; import java.io.IOException; import java.util.concurrent.atomic.AtomicLong; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class AtomicLongCountorServlet */ @WebServlet("/AtomicLongCountorServlet") publicclass AtomicLongCountorServlet extends HttpServlet { privatestaticfinallongserialVersionUID = 1L; private AtomicLong acessCount = new AtomicLong(0); /** * @see HttpServlet#HttpServlet() */ public AtomicLongCountorServlet() { super(); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protectedvoid doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub this.acessCount.incrementAndGet(); response.getWriter().append("Welcome your acess my Servelet ! ,you are " + this.acessCount.get()+ " visitor."); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protectedvoid doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub doGet(request, response); } }
三、锁定机制
如果Sevlet中有多个相互关联的状态变量需要确保操作的时序怎么办呢?比如下边简单示意的转账代码。
package com.codeartist; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class TransformCash */ @WebServlet("/TransformCash") public class TransformCash extends HttpServlet { private static final long serialVersionUID = 1L; private CashAcount fromCashAcount ; private CashAcount toCashAcount ; /** * @see HttpServlet#HttpServlet() */ public TransformCash() { super(); // TODO Auto-generated constructor stub } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub // float cash =100; this.fromCashAcount.reduce(cash); this.toCashAcount.plus(cash); //response.getWriter().append("Served at: ").append(request.getContextPath()); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub doGet(request, response); } }
没错就是通过加锁对操作进行同步。java提供了Synchronized关键字来实现锁定机制,线程在进入同步代码块之前
会自动获得锁,并在推出代码的时候释放锁。此互斥锁只能同时由一个线程持有,其他线程只能等待或者阻塞,
因此可以确保复合操作的原子性。
package com.codeartist; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Servlet implementation class TransformCash */ @WebServlet("/TransformCash") public class TransformCash extends HttpServlet { private static final long serialVersionUID = 1L; private CashAcount fromCashAcount ; private CashAcount toCashAcount ; /** * @see HttpServlet#HttpServlet() */ public TransformCash() { super(); // TODO Auto-generated constructor stub } protected synchronized void transform() { float cash =100; this.fromCashAcount.reduce(cash); this.toCashAcount.plus(cash); } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub // transform(); //response.getWriter().append("Served at: ").append(request.getContextPath()); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO Auto-generated method stub doGet(request, response); } }
java内置锁除了互斥特性,为了避免死锁的发生,它还具有重入特性,即某个线程可以重复申请获取自己已经持有的锁。
重入意味者锁定操作的粒度是线程而不是调用,即会同时记录申请的线程和次数。例如下边在子类中重写并调用父类
的synchronized 方法。
package com.codeartist; publicclass synchronizedParent { publicsynchronizedvoid initSomething() { } } package com.codeartist; publicclass synchronizedChild extends synchronizedParent { publicsynchronizedvoid initSomething() { super.initSomething(); } }
四、加锁同步需要注意的问题
1.访问共享状态的符合操作,需要在访问状态变量的所有位置都需要使用同步,
并且每个位置都需要使用同一个锁。
2.对象内置锁并不阻止其他线程对此对象的访问,只能阻止其获取同一个锁,需要我们自己实现同步策略确保对共享状态
的安全访问。
3.将所有的可变状态都封装在对象内部,并通过对象内置锁对所有访问状态的代码进行同步,是一种常见的加锁策略。
但是有时并不能保证复合操作的原子性。
if(!array.contains(element)) { //比较耗费时间的业务操作 array.add(element); }
4.过多的同步代码往往会导致活跃性问题和性能问题。在使用锁的时候,我们应该清楚我们的代码功能及执行时间,
无论是计算密集型操作还是阻塞型操作,如果锁定时间过长都会带来活跃性或者性能问题。