《Java并发变成实践》读书笔记---第二章 线程安全性

什么是线程安全性

要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问。从非正式的意义上来说,对象的状态是指存储在状态变量(例如实例或静态域)中的数据。“共享”意味着变量可以由多个线程同时访问,而“可变”则意味着变量的值在其生命周期内可以发生变化。所以编写线程安全的代码更侧重于如何防止在数据上发生不受控的并发访问。

如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式可以修复这个问题。

1.不在线程之间共享该状态变量

2.将状态变量修改为不可变的变量

3.在访问状态变量时使用同步

当设计线程安全的类时,良好的面向对象技术,不可修改性,以及明细的不可变性规范都能起到一定的帮助作用

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

下面给出一个无状态的Servlet的例子,一个线程安全的类,无状态的类是绝对线程安全的,而无状态的类就是指上述所说的不存在实例或者静态域。

public class StatelessFactorizer extends HttpServlet{

	@Override
	public void service(ServletRequest arg0, ServletResponse arg1)
			throws ServletException, IOException {
		// TODO Auto-generated method stub
		super.service(arg0, arg1);
	}

}

原子性

只要给无状态对象加上状态,这个对象就会变成非线程安全的,例如给上述的Servlet增加一个计数器。

public class UnsafeCountingFactorizer extends HttpServlet{

	private long count = 0;

	@Override
	public void service(ServletRequest arg0, ServletResponse arg1)
			throws ServletException, IOException {
		// TODO Auto-generated method stub
		super.service(arg0, arg1);
		count++;
	}

}

原因是这个计数器的自增不是一个原子性的操作,这是一个“读取-修改-写入”的操作序列,并且其结果状态依赖于之前的状态。

竞争条件

UnSafeCountingFactorizer存在竞争条件(count 成员变量),从而使得结果变得不可靠。当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。

当两个线程同时竞争修改counnt这个变量时,就是出现计时器出错的情况,因为当前线程的累加操作有赖于上一个线程的结果,当前线程需要观察上上一线程的结果值。这种观察结果失效就是大多数竞态条件的本质--基于一种可能失效的观察结果来作出判断或者执行某个计算。这种类型的竞态条件称为”先检查后执行“。

使用”先检查后执行“的一种常见情况就是延迟初始化。

public class LazyInitRace {

	private LazyInitRace instance = null;

	public LazyInitRace getInstance(){
		if(instance == null){
			instance = new LazyInitRace();
		}
		return instance;
	}

}

LazyInitRace中就产生了一个竞条件,instance的初始化会被重复执行,假定线程A和线程B同时执行getInstance,A看到instance为空,就会创建一个LazyInitRace,但是B也同时需要判读instance是否为空,要取决于不可预测的时序。

与大多数并发错误一样,竞态条件并不总会产生错误,还需要某种不恰当的执行时序,这就是多线程问题比较难定位原因。

复合操作

LazyInitRace和UnsafeCountingFactorizer都包含一组需要以原子方式执行(或者说不可分割)的操作。要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式阻止其他线程修改这个变量,从而确保其他线程只能在修改操作完成之前或者之后读取和修改状态,儿不是在修改状态过程中。

我们把CountFactorizer中的计数器的”读取-修改-写入“等操作称之为复合操作:包含了一组必须以原子方式执行的操作以确保线程安全性。下面我们使用java.util.concurrent包中包含的一些原子变量类来保证数值和对象引用上的原子状态转换来改造CountFactorizer,使得它变得线程安全。

public class CountFactorizer extends HttpServlet{

	private final AtomicLong count = new AtomicLong(0);

	@Override
	public void service(ServletRequest arg0, ServletResponse arg1)
			throws ServletException, IOException {
		// TODO Auto-generated method stub
		super.service(arg0, arg1);
		count.incrementAndGet();
	}
}

加锁机制

Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。对于LazyInitRace的非线程安全,我们可以通过加锁机制来保证其线程安全。在getInstance方法上加上synchronized修饰符。

public class LazyInitRace {

	private LazyInitRace instance = null;

	public synchronized LazyInitRace getInstance(){
		if(instance == null){
			instance = new LazyInitRace();
		}
		return instance;
	}

}

重入

每个锁都关联一个请求计数器和一个占有他的线程,当请求计数器为0时,这个锁可以被认为是unhled的,当一个线程请求一个unheld的锁时,JVM记录锁的拥有者,并把锁的请求计数加1,如果同一个线程再次请求这个锁时,请求计数器就会增加,当该线程退出syncronized块时,计数器减1,当计数器为0时,锁被释放。

public class Widget {
    public synchronized void doSomething() {
        ...
    }
}

public class LoggingWidget extends Widget {
    public synchronized void doSomething() {
        System.out.println(toString() + ": calling doSomething");
        super.doSomething();
    }
}

如果没有Java锁的可重入性,当一个线程获取LoggingWidget的doSomething()代码块的锁后,这个线程已经拿到了LoggingWidget的锁,当调用父类中的doSomething()方法的时,JVM会认为这个线程已经获取了LoggingWidget的锁,而不能再次获取,从而无法调用Widget的doSomething()方法,从而照成死锁。从中我们也能看出,java线程是基于“每线程(per-thread)”,而不是基于“每调用的(per-invocation)”的,也就是说java为每个线程分配一个锁,而不是为每次调用分配一个锁。

《Java并发变成实践》读书笔记---第二章 线程安全性,布布扣,bubuko.com

时间: 2024-10-17 06:04:41

《Java并发变成实践》读书笔记---第二章 线程安全性的相关文章

Java并发编程实践读书笔记--第一部分 基础知识

目前关于线程安全性没有一个统一的定义,作者自己总结了一个定义,如下:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为,那么就称这个类是线程安全的. 在并发编程中,由于不恰当的执行时序而出现不确定的结果的情况被称为竞态条件(Race Condition).最常见的竞态条件就是“先检查后执行(Check-Then-Act)”操作,即通过一个可能已经失效的观察来决定下一步的动作.比较简单的例子就是两

Java并发编程实践(读书笔记) 任务执行(未完)

任务的定义 大多数并发程序都是围绕任务进行管理的.任务就是抽象和离散的工作单元.   任务的执行策略 1.顺序的执行任务 这种策略的特点是一般只有按顺序处理到来的任务.一次只能处理一个任务,后来其它任务都要等待处理.响应性很糟糕,吞吐量低.系统资源利用率低. 2.显示的为任务创建线程 为每个任务创建对应一个线程,响应快,系统资源利用路高.缺点是资源消耗量大,如果有大量任务要执行的话,系统迟早会因为无限制创建过多的线程而造成内存耗尽.特别当创建的线程数量远远大于系统的CPU核数,由于每一个核同一时

Java并发编程实践读书笔记(5) 线程池的使用

Executor与Task的耦合性 1,除非线程池很非常大,否则一个Task不要依赖同一个线程服务中的另外一个Task,因为这样容易造成死锁: 2,线程的执行是并行的,所以在设计Task的时候要考虑到线程安全问题.如果你认为只会在单任务线程的Executor中运行的话,从设计上讲这就已经耦合了. 3,长时间的任务有可能会影响到其他任务的执行效率,可以让其他线程在等待的时候限定一下等待时间.不要无限制地等待下去. 确定线程池的大小 给出如下定义: 要使CPU达到期望的使用率,线程池的大小应设置为:

Java并发编程实践读书笔记(3)任务执行

类似于Web服务器这种多任务情况时,不可能只用一个线程来对外提供服务.这样效率和吞吐量都太低. 但是也不能来一个请求就创建一个线程,因为创建线程的成本很高,系统能创建的线程数量是有限的. 于是Executor就出现 了. Executor框架 线程池的意义 线程创建太少了浪费服务器资源,另外线程创建多了又搞得服务器很累.两个极端的结果都是对外的吞吐量上不去. 所以线程是需要统一管理的,不能随便new Thread().start(). Executor提供了四种线程池. ExecutorServ

《Java并发编程实战》第二章 线程安全性 读书笔记

一.什么是线程安全性 编写线程安全的代码 核心在于要对状态访问操作进行管理. 共享,可变的状态的访问 - 前者表示多个线程访问, 后者声明周期内发生改变. 线程安全性 核心概念是正确性.某个类的行为与其规范完全一致. 多个线程同时操作共享的变量,造成线程安全性问题. * 编写线程安全性代码的三种方法: 不在线程之间共享该状态变量 将状态变量修改为不可变的变量 在访问状态变量时使用同步 Java同步机制工具: synchronized volatile类型变量 显示锁(Explicit Lock

《深入理解Java虚拟机》读书笔记---第二章 Java内存区域与内存溢出异常

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来.这一章就是给大家介绍Java虚拟机内存的各个区域,讲解这些区域的作用,服务对象以及其中可能产生的问题. 1.运行时数据区域 Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域. 1.1程序计数器 程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器.在虚拟机的概念模型中里,字

JAVA并发编程实战 读书笔记(二)对象的共享

<java并发编程实战>读书摘要 birdhack 2015年1月2日 对象的共享 JAVA并发编程实战读书笔记 我们已经知道了同步代码块和同步方法可以确保以原子的方式执行操作,但一种常见的误解是,认为关键之synchronized只能用于实现原子性或者确定临界区.同步还有另一个重要的方面:内存可见性. 1.可见性 为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制. 在没有同步的情况下,编译器.处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整.在缺乏足够同步的多线程程

[Java 并发] Java并发编程实践 思维导图 - 第二章 线程安全性

根据<Java并发编程实践>一书整理的思维导图.

[Effective Java 读书笔记] 第二章 创建和销毁对象 第一条

第二章  创建和销毁对象 第一条 使用静态工厂方法替代构造器,原因: 静态工厂方法可以有不同的名字,也就是说,构造器只能通过参数的不同来区分不同的目的,静态工厂在名字上就能表达不同的目的 静态工厂方法不用每次调用的时候都创建新的对象(其实是因为它是static的,所以只能用static的,所以是一早就创建了,不需要重复创建吧..),比如书中 Boolean.valueOf(boolean) 1 public static final Boolean TRUE = new Boolean(true