Java 并发编程(一)浅谈线程安全

首先我们要弄清楚什么叫线程安全。

“线程安全”是指:当多个线程访问某个类时,不管运行环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

这里有三个关键点,

第一、“线程安全”问题是来源于多个线程访问的情况下,当个线程没有竞争便涉及到出现线程安全的问题。

第二、类的“线程安全”性不依赖于多线程的执行顺序。

第三、主调代码不需要同步或协同。某个类“线程安全”特性不依靠外部调用者。

那是不是在未进行同步或协同等同步手段处理之前,所有的类都不是线程安全的呢? 非也。

我们知道,对象分两种,有状态对象(Stateful Bean)和无状态对象(Stateless Bean)。

有状态对象就是有数据存储功能,就是有实例变量的对象 ,可以保存数据,是非线程安全的。

无状态对象就是没有实例变量的对象 .不能保存数据,是不变类,是线程安全的。

举个例子,在基于Servlet 的 Web 服务中,如果存在如下的一个 Servlet ,它没有成员变量,不能储存数据。不管有多少个线程使用它,每个线程都会在自己独立的 Java 栈,方法内所有的局部变量都是新建且独享的。它就是无状态对象,是线程安全的。

@ThreadSafe
public class StatelessFactorizer implements Servlet{
	public void service (ServletRequest req , ServletResponse resp){
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = factor(i);
		encodeIntoResponse(resp,factors);
	}
}

如果为它添加一个用来统计访问用户数的变量 count 时,如下代码,由于这个Servlet 会被多个线程访问,count 变量也会被共享。在对 count 进行自增操作的时候,由于 count++  实际上需要分两步执行,即 tem = count +1 ; count = temp。假设初始值 count =1; 有线程 A 和 B 访问它。当 A 取 count=1后,进行 count++ 之前,线程B接着取 count = 1; 那么 在A B 访问完成之后,count 结果为 2,这显然是错误的。那么这个Servlet
就变成了非线程安全的了。

@NotThreadSafe
public class StatelessFactorizer implements Servlet{
	private long count = 0;
	public long getCount(){
		return count;
	}
	public void service (ServletRequest req , ServletResponse resp){
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = factor(i);
		++count;
		encodeIntoResponse(resp,factors);
	}
}

由于不恰当的执行顺序而出现不正确的执行结果是一件很头疼的事情,它有一个正式的名字:竞态条件(Race Condition)。要避免竞态条件的问题,我们就要把自增这个动作变为原子操作。假设有两个操作 A 和 B ,如果从执行 A 的线程来看,当另一个线程执行B时,要么将B完全执行完,要么完全不执行B,那么 A 和 B 对彼此来说就是原子的。原子操作是指,对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是一个原子方式执行的操作。

为了实现count自增的原子性,我们可以使用 AtomicLong 类来实现它。

@ThreadSafe
public class StatelessFactorizer implements Servlet{
	private AtomicLong count = new AtomicLong(0);
	public AtomicLong getCount(){
		return count;
	}
	public void service (ServletRequest req , ServletResponse resp){
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = factor(i);
		count.incrementAndGet();
		encodeIntoResponse(resp,factors);
	}
}

当 Servlet 中只有一个状态变量的时候,可以通过线程安全的对象来管理 Servlet 的状态以维护 Servlet 的线程安全性。如果在 Servlet 中添加更多的状态,即使添加更多的线程安全状态变量也不能保证Servlet 的安全性。如下面代码。

@ThreadSafe
public class UnsafeCachingFactorizer implements Servlet {
	private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
	private final AtomicReference<BigInteger[]> lastFactors = new AtocmicReference<BigInteger[]>();

	public void service(ServletRequest req, ServletResponse resp) {
		BigInteger i = extractFromRequest(req);
		if (i.equals(lastNumber.get()))
			encodeIntReponse(resp, lastFactors.get());
		else {
			BigInteger[] factors = factor(i);
			lastNumber.set(i);
			lastFactors.set(factors);
			encodeIntoResponse(resp, factors);
		}
	}
}

这段代码本意是为因子分解增加一个缓存的功能。虽然这些原子引用本身都是线程安全的,但是由于存在竞态条件,这可能产生错误的结果。

在拥有多个状态变量的对象中,如果各个变量之间并不是彼此独立存在的(例如这里的 lastNumber 和 lastFactors 必须保证一致对应),而是其中某个变量的值会对其他变量的值产生约束。因此,当更新某一个变量的时候,需要在同一个原子操作中对其他变量同时进行更新。

内置锁

Java提供了一种内置的锁机制来支持原子性:同步代码块(synchronized block),同步代码块包含2个部分:1,作为锁的对象引用;2,该锁所保护的代码块

因此,我们可以对 UnsafeCachingFactorizer 的 service 进行同步,即声明为  public synchronized void service(ServletRequest req, ServletResponse resp) ;这样,所有对service 的访问都会按串行排列。但是这种方法过于极端,服务的响应性非常低,无法令人接受。这是一个性能问题,而不是线程安全问题。

内置锁是可重入锁。“重入”意味着锁的操作的粒度是“线程”而不是“调用”。重入的一种实现方法是,为每个锁关联一个获取数值和一个所有者线程。当计数值为0时,这个所就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM 将记下锁的持有者,并且将获取计数值置为1.如果同一个线程再次获取这个锁,计数值将被递增,而当线程退出同步代码块时,计数器会相应的递减。当计数值为0时,这个锁将被释放。

由于锁能使其保护的代码以串行形式来访问,因此就可以通过锁来构造一些协议以实现对共享状态的独占访问。一种常见的加锁约定是,将所有的可变状态封装在对象内部,然后通过对象的内置锁对所有访问可变状态的方法进行同步,使得在该对象上不会发生并发访问。对于每个包含多个变量的不变形条件,其中涉及的所有的变量都需要由同一个锁保护。

如果同步可以避免竞态条件问题,那么为什么不在每个方法声明时都是用关键字 synchromized ?事实上,如果不加区别的滥用,可能导致程序中出现过多的同步。此外,如果只是将每个方法都作为同步方法,例如 Vector ,那么并不能保证 Vector 上复合操作都是原子的:

if(!vector.contains(element))
			vector.add(element);

虽然 contains 和 add 等方法都是原子方法,但在上面这个“如果不存在则添加(put-if-absent)” 的操作中仍然存在竞态条件。虽然 synchronized 方法可以确保单个操作的原子性,反如果要把多个操作合并为一个复合操作,还需要额外的加锁机制。此外,正如之前将整个 service 方法同步的做法一样,将每个方法都都作同步还可能导致活跃性问题和性能问题。

幸运的是,通过缩小同步代码块的作用范围,我们很容易做到既保证servlet 的并发性,同时又维护线程安全性。要确保同步代码块不要过小,并且不要将本应是原子的操作拆分到多个同步的代码块中去,从而在这些操作的执行过程中,其他线程可以访问共享状态。

下面,我们将修改为使用两个独立的同步代码块,每个同步代码块都只包含一小段代码。其中一个同步代码块负责保护判断是否只需返回缓存结果的“先检查后执行”操作序列,另一个同步代码块则负责确保对缓存的数值和引述分解结果进行同步操作。此外,我们还重新引入了"命中计数器“,添加了一个”缓存命中“计数器,并且在第一个同步代码块中更新这两个变量。由于这两个计数器也是共享可变状态的一部分,因此必须在所有访问它们的位置上都是用同步。位于同步代码块之外代码将以独占方式来访问局部(位于栈中)变量,这些变量不会在多个线程间共享,因此不需要同步。

public class CachedFactorizer implements Servlet {
	private BigInteger lastNumber;
	private BigInteger[] lastFactors;
	private long hits;
	private long cacheHits;

	// 也可以将 hits 和 cacheHits 声明为 volatile 来实现可见性
	public synchronized long getHits() {
		return hits;
	}

	public synchronized double getCacheHitRatio() {
		return (double) cacheHits;
	}

	public void service(ServletRequest req, ServletResponse resp) {
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = null;
		synchronized (this) {
			++hits;
			if (i.equals(lastNumber)) {
				++cacheHits;
				//使用克隆,缩短同步代码块大小
				factors = lastFactors.clone();
			}
		}
		if (factors == null) {
			factors = factor(i);
			synchronized (this) {
				lastNumber = i;
				lastFactors = factors.clone();
			}
		}
	}
}
时间: 2024-10-25 15:07:04

Java 并发编程(一)浅谈线程安全的相关文章

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中,一个应用程序对应着一个JVM实例(也有地方称为JVM进程),一般来说名字默认是java.exe或者javaw.exe(windows下可以通过任务管理器查看).Java采用的是单线程编程模型,即在我们自己的程序中如果没有主动创建线程的话,只会创建一个线程,通常称为主线程.但是要注意,虽然只有一个线程来执行任务,不代表JVM中只有一个线程,JVM实例在创建的时候,同时会创建很多其它的线程(比如垃圾收集器线程). 由于Java采用的是单线程编

Java并发编程的艺术(六)——线程间的通信

多条线程之间有时需要数据交互,下面介绍五种线程间数据交互的方式,他们的使用场景各有不同. 1. volatile.synchronized关键字 PS:关于volatile的详细介绍请移步至:Java并发编程的艺术(三)--volatile 1.1 如何实现通信? 这两种方式都采用了同步机制实现多条线程间的数据通信.与其说是"通信",倒不如说是"共享变量"来的恰当.当一个共享变量被volatile修饰 或 被同步块包裹后,他们的读写操作都会直接操作共享内存,从而各个

Java并发编程 - 逐级深入 看线程的中断

最近有足够的空闲时间 去东看看西看看,突然留意到在Java的并发编程中,有关线程中断的,以前初学时一直没弄清楚的一些小东西. 于是,刚好把收获简单的总结一下,通过此文来总结记录下来. 从源码看线程的状态 在开始分析线程的中断工作之前,我们肯定要先留意一个点,那就是肯定是有开启,才会有与之对应的中断工作出现. 开启一个线程的工作,相信每个Javaer都烂熟于心.它很简单,new一个thread对象,然后调用start方法开启线程. 那么,一个好玩的问题就出现了:既然开启一个线程的步骤如此简单明了,

java并发编程实战手册(一)线程管理

本文主要是以知识点的形式对java多线程进行了解,学习java多线程的基础,本文参考书籍<java并发编程实战手册>,若有兴趣想研究跟高级的多线程思想,可以阅读<java并发编程实战>. 1.线程的创建和运行 java线程的创建有三种方式,可能大部分人只知道常用的两种: 1.继承Thread类,并且覆盖run()方法. 2.创建一个实现Runnable接口的类.使用带参数的Thread构造器来创建Thread对象. 3.使用Callable与Future来创建启动线程 1.创建Ca

Java并发编程、多线程、线程池…

Java多线程干货系列(1):Java多线程基础http://www.importnew.com/21136.html#comment-651146 40个Java多线程问题总结http://www.importnew.com/18459.html#comment-651217 Java线程面试题 Top 50http://www.importnew.com/12773.html Java并发编程:Thread类的使用http://www.cnblogs.com/dolphin0520/p/39

Java并发编程:如何创建线程

一.Java中关于应用程序和进程相关的概念 在Java中,一个应用程序对应着一个JVM实例(也有地方称为JVM进程),一般来说名字默认为java.exe或者javaw.exe(windows下可以通过任务管理器查看).Java采用的是单线程编程模型,即在我们自己的程序中如果没有主动创建线程的话,只会创建一个线程,通常称为主线程.但是要注意,虽然只有一个线程来执行任务,不代表JVM中只有一个线程,JVM实例在创建的时候,同时会创建很多其他的线程(比如垃圾收集器线程). 由于Java采用的是单线程编

Java并发编程:如何创建线程?

在前面一篇文章中已经讲述了在进程和线程的由来,今天就来讲一下在Java中如何创建线程,让线程去执行一个子任务.下面先讲述一下Java中的应用程序和进程相关的概念知识,然后再阐述如何创建线程以及如何创建进程.下面是本文的目录大纲: 一.Java中关于应用程序和进程相关的概念 二.Java中如何创建线程 三.Java中如何创建进程 一.Java中关于应用程序和进程相关的概念 在Java中,一个应用程序对应着一个JVM实例(也有地方称为JVM进程),一般来说名字默认为java.exe或者javaw.e

Java并发编程——Executor接口及线程池的使用

在如今的程序里,单线程的程序,应该已经比较少了,而Java语言是内置支持多线程并发的,大家都说Java语言内置支持多线程,非常非常的强大和方便,但一直没有深入研究jdk内concurrent包.今天就认真学习了一下java.util.concurrent包,发现jdk多线程编程果然是强大和方便.本文是学习java.util.concurrent包内线程池及相关接口的一些总结. 任务接口抽象 Runnable接口 在java.lang包内,为多线程提供了Runnable接口. public int

Java并发编程与技术内幕:线程池深入理解

林炳文Evankaka原创作品.转载请注明出处http://blog.csdn.net/evankaka 摘要: 本文主要讲了Java当中的线程池的使用方法.注意事项及其实现源码实现原理,并辅以实例加以说明,对加深Java线程池的理解有很大的帮助. 首先,讲讲什么是线程池?照笔者的简单理解,其实就是一组线程实时处理休眠状态,等待唤醒执行.那么为什么要有线程池这个东西呢?可以从以下几个方面来考虑:其一.减少在创建和销毁线程上所花的时间以及系统资源的开销 .其二.2将当前任务与主线程隔离,能实现和主