并发编程之线程安全性

一、什么是线程安全性

并发编程中要编写线程安全的代码,则必须对可变的共享状态的访问操作进行管理。

对象的状态就是存储在实例或者静态变量中的数据,同时其状态也包含其关联对象的字段,比如字典集合既包含自己的状态,

也包含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.过多的同步代码往往会导致活跃性问题和性能问题。在使用锁的时候,我们应该清楚我们的代码功能及执行时间,

无论是计算密集型操作还是阻塞型操作,如果锁定时间过长都会带来活跃性或者性能问题。

时间: 2024-10-27 00:44:40

并发编程之线程安全性的相关文章

并发编程初探-线程安全性

在Java并发编程中,对于线程安全是非常重要的,也是必须要考虑的一个问题.可以这么说,只要涉及到网络的,都必须考虑线程安全问题.好了,开始噼里啪啦地开始敲代码之前,我觉得有必要了解一些文绉绉的理论知识,因为这些理论知识是我们敲出来的代码是否是线程安全的一个依据. 当多个线程访问某个状态变量并且其中有一个线程执行写入操作的时候,必须考虑采用同步机制来协同这些线程对变量的访问,Java中的主要同步机制是关键字synchronized,它提供了一种独占的加锁方式,但"同步"这个术语还包括类型的变量,显

JAVA并发编程4_线程同步之volatile关键字

上一篇博客JAVA并发编程3_线程同步之synchronized关键字中讲解了JAVA中保证线程同步的关键字synchronized,其实JAVA里面还有个较弱的同步机制volatile.volatile关键字是JAVA中的轻量级的同步机制,用来将变量的更新操作同步到其他线程.从内存可见性的角度来说,写入volatile变量相当于退出同步代码块,读取volatile变量相当于进入同步代码块. 旧的内存模型:保证读写volatile都直接发生在main memory中. 在新的内存模型下(1.5)

Java并发编程:线程的同步

.title { text-align: center } .todo { font-family: monospace; color: red } .done { color: green } .tag { background-color: #eee; font-family: monospace; padding: 2px; font-size: 80%; font-weight: normal } .timestamp { color: #bebebe } .timestamp-kwd

Java并发编程:线程的创建

.title { text-align: center } .todo { font-family: monospace; color: red } .done { color: green } .tag { background-color: #eee; font-family: monospace; padding: 2px; font-size: 80%; font-weight: normal } .timestamp { color: #bebebe } .timestamp-kwd

【转】Java并发编程:线程池的使用

Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间. 那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务? 在Java中可以通过线程池来达到这样的效果.今天我们就来详细讲解一下Java的线程池,首先我们从最核心的ThreadPool

68:Scala并发编程原生线程Actor、Cass Class下的消息传递和偏函数实战解析及其在Spark中的应用源码解析

今天给大家带来的是王家林老师的scala编程讲座的第68讲:Scala并发编程原生线程Actor.Cass Class下的消息传递和偏函数实战解析 昨天讲了Actor的匿名Actor及消息传递,那么我们今天来看一下原生线程Actor及CassClass下的消息传递,让我们从代码出发: case class Person(name:String,age:Int)//定义cass Class class HelloActor extends Actor{//预定义一个Actor  def act()

19、Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition

Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权.因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去.因此,一般情况下,当队列满时,会让生产者交出对

Java 并发编程:线程间的协作(wait/notify/sleep/yield/join)

Java并发编程系列[未完]: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程:线程间的协作(wait/notify/sleep/yield/join) 一.线程的状态 Java中线程中状态可分为五种:New(新建状态),Runnable(就绪状态),Running(运行状态),Blocked(阻塞状态),Dead(死亡状态). New:新建状态,当线程创建完成时为新

Java并发编程:线程池的使用(转)

Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间. 那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务? 在Java中可以通过线程池来达到这样的效果.今天我们就来详细讲解一下Java的线程池,首先我们从最核心的ThreadPool