《java并发编程实战》 第一 二章 线程安全性

第一章 简介

注册CSDN后的第一篇博客,ヾ(◍°∇°◍)ノ゙ 标识为黄色为暂时还不能好好理解

提到的概念

原文《java Concurrency in Pratice》翻译的中文无力吐槽,生涩难懂,按自己理解做的笔记,有理解不当感谢指正

  1. 线程安全性 ,“永远不发生糟糕的事情”,说的什么骚话,就是描述能正常工作的可靠程度
  2. 线程安全性问题 ,提到的例子有多线程调用一个getValue()方法,value++产生的竞态条件;
  3. 线程安全性问题原因有 :a、常见的原因有某个线程安全的类一直通过加锁来保护其状态,随后又对这个类添加了一些没通过锁保护的新变量、没正确加锁来保护现有状态变量的新方法,疏忽导致。
  4. 线程活跃性 ,“某件正确的事情最终会发生”,描述一个并发应用程序及时执行的能力
  5. 线程活跃性问题 ,描述不能及时执行时出现的问题,包括死锁、饥饿、活锁;
  6. 线程性能问题 包括多个方面,响应灵敏程度、吞吐率、资源消耗情况、可伸缩性

内容

  1. 并发编程会带来那些额外的性能开销 :a、线程调度器频繁切换线程,CPU时间都花在线程调度上。b、共享数据使用同步机制,而同步机制往往会一直编译器的优化,使内存缓存区中的数据无效,以及增加共享内存总线的同步流量
  2. 书中的自定义标注类的标注@NotThreadSafe线程不安全 @ThreadSafe线程安全 @Immutable表示类是不可变的,暗含了线程安全的含义 域和方法的标注@GuardedBy(lock)表明只有持有特定的锁才能访问这个域或者方法,lock表示在访问这个被标注的方法或者变量需要持有的锁,取值可有“lock”、“filedName”、“Class Name.filedName”,例如@GuardedBy(“this”) int a ;表明只有内置锁才能访问a。
  3. 原文如下实在是说的委婉,很简单的意思说的跟文言文一样,举个例子:Timer定时器可以控制任务稍后运行、或者运行一次、周期性运行。TimerTask将在Timer管理的线程中执行,若TimerTask访问应用程序其他线程other threads访问的数据,则TimerTask需要以线程安全方式访问数据,other threads也要采用线程安全方式访问该数据。总之就是多个线程访问共享数据,要保证每个线程都是安全的访问共享数据,最简单的方式就是把线程安全性封装在共享对象内部
    书中举了一组框架,每个框架都能创建多个线程同时在这些线程中会调用你编写的代码,因此 要保证编写的代码是线程安全的,这些线程访问共享数据过程也必须是线程安全的。
    例子1:Timer
    例子2:Servlet和JSP,Servlet用于部署网页应用程序以及分发来自HTTP客户端的请求,每个servlet、jsp都表示一个程序逻辑组件,在高吞吐率的网站中,多个客户端可能同时请求到一个Servlet,因此必须保证Servlet是线程安全的。Servlet通常会访问与其他Servlet共享的信息,例如ServletContext和HttpSession等容器也都必须是线程安全的。
    例子3:Swing:GUI应用程序的一个固有属性是异步性,用户在任何时刻GUI界面按下一个按钮应用程序都会及时响应。比如我按下我按下GUI中开灯的按钮,事件线程中会有一个事件处理器被调用以执行用户开灯操作,事件处理器去改变灯的状态lamp_state时,而lamp_state也同时被其他线程会同时访问,则事件处理器修改灯状态的过程必须采用线程安全的方式修改。

第二章 线程安全性

要编写线程安全的代码,核心在于对状态访问操作进行管理,特别是对共享(shared)和可变(mutable)的状态访问。

基础:并发的原子、有序、可见性

  1. 如果在设计类时候没有考虑并发访问情况,如何修复?a、在访问状态变量时采用同步 b、将状态变量修改为不可变的变量 c、不在线程之间共享该状态变量
  2. 一个类如何才能称为是线程安全的?
    就是并发环境和单线程环境都不会被破坏的类。

当多个线程访问某个类时,无论运行时无论这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能正常工作,则称这个类是线程安全的。

  1. 如何保证对象是线程安全的? 对象的可变状态访问采用同步机制来协同
  2. 无状态对象一定是线程安全的:以一个无状态的Servlet为例
    @ThreadSafe
    public class StatelessFactorizer implements Servlet{
    	public void service(ServletRequest req,ServletResponse resp){
    		BigInteger i = extractFromRequest(req); //请求中提取数值
    		BigInteger[] factors = factor(i);		//因数分解
    		encodeIntoResponse(resp,factors);		//封装响应
    	}
    }
    

类似StatelessFactorizer这种没有成员变量,也不含对其他类中变量的引用,计算的临时状态仅存在线程栈的局部变量中可视为无状态对象。访问StatelessFactorizer的线程不会影响另一个访问同一个StatelessFactorizer的线程计算结果,因为两个线程没有共享的状态。
5. 如何理解并发中原子性、有序性、可见性?
原子性:原子象征着具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++;实际上是先读a的值,再a加上1得到一个值,最后将这个值付给a,a=a+1,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,不可分割,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
可见性:例如多个线程读写变量a,一个线程对a进行写操作,这个写操作的结果正是其他线程再去读a的结果,一个线程修改了共享变量后,其他线程能够立即得知这个修改。可见性的关键还是在对变量的写操作之后能够在某个时间点显示地写回到主内存,这样其他线程就能从主内存中看到最新的写的值。volatile,synchronized, 显式锁,原子变量这些同步手段都可以保证可见性。可见性底层的实现是通过加内存屏障实现的:1. 写变量后加写屏障,保证CPU写缓冲区的值强制刷新回主内存。2. 读变量之前加读屏障,使缓存失效,从而强制从主内存读取变量最新值。写volatile变量 = 进入锁,读volatile变量 = 释放锁

大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存
也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。举个简单的例子,比如下面的这段代码:
i = i + 1;
当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。本文我们以多核CPU为例。

比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?

可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。

最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。

可见性底层的原理是相当于破坏了CPU高速缓存。

有序性

首先解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。

但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:

// An highlighted block
int a = 10;    //语句1
int r = 2;    //语句2
a = a + 3;    //语句3
r = a*a;     //语句4

这段代码有4个语句,那么可能的一个执行顺序是:1 3 2 4
那么可不可能是这个执行顺序呢: 语句2 语句1 语句4 语句3 ?不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。
虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2
 
//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
有序性的语意有三层

  1. 最常见的就是保证多线程执行的串行顺序,volatile, final, synchronized,显式锁都可以保证有序性
  2. 防止重排序引起的问题
  3. 程序执行的先后顺序,比如Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为happens-before 原则

下面就来具体介绍下happens-before原则(先行发生原则):
happens-before 关系保证:如果线程 A 与线程 B 满足 happens-before 关系,则线程 A 执行动作的结果对于线程 B 是可见的。如果两个操作未按 happens-before 排序,JVM 将可以对他们任意重排序。
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作

锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作

volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。

传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。

线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作

线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生

线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行

对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

总之,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

竞态条件

最常见的竞态条件类型就是“先检查后执行”,即基于一种可能失效的观测结果做出判断或者执行某个计算。举的例子,你跟你女朋友约了印象城肯德基见面,你到了店里发现,女朋友不在,想起来印象城里还有一家肯德基,女朋友可能是去了另一家。你可以出门去另一家找女朋友,此时就是一种竞态条件,你刚出门,可能女朋友就从肯德基后门进来,你之前的观察结果就变得无效。

@NotThreadSafe
	public class LazyInitRace{
	private ExpensiveObject instance = null;
	public ExpensiveObject getInstance(){  //多线程执行判断instance是否为空已经不可靠
			if(instance == null){
				instance = new ExpensiveObject();
		}
		return instance;
	}
}

第二种竞态条件:“读取 - 修改 - 写入”,

@NotThreadSafe
	public class UnsafeCounting implements Servlet{
	private long count = 0;
	public void service(ServletRequest req,ServletResponse resp){  
			BigInteger i = extractFromRequest(req);
			BigInteger[] factors = factor(i);
			count ++;                     //常见竞态条件
			encodeIntoResponse(resp,factors);
		}
	}
}

可用原子变量类简单修复计算器的竞态条件

@ThreadSafe
	public class UnsafeCounting implements Servlet{
	private final AtomicLong count = new AtomicLong(0);
	public void service(ServletRequest req,ServletResponse resp){  
			BigInteger i = extractFromRequest(req);
			BigInteger[] factors = factor(i);
			count.incrementAndGet();                     //原子变量类能确保所有对计数器状态的访问操作是原子的
			encodeIntoResponse(resp,factors);
		}
	}
}

在无状态的类中添加一个状态时,如果该状态完全由线程安全的对象管理,则这个类仍然是线程安全的。
用AtomicLong以线程安全方式管理计数器的状态,那么是否可以通过AtomicReference来管理最近因数分解的数值与结构?以下面UnsafeCachingFactorizer 为例,AtomicReference是一种替代long类型整数的线程安全类。虽然引用的类是线程安全的,但是没有保证更新一个变量lastNumber 时,在同一个原子操作中lastFactors也同时更新。导致多个请求Servlet时,可能a线程刚更改了lastNumber,b执行到encodeIntoResponse();b得到的factors只有在 a b线程传入的req相同情况下才算正确。
要保证多个相关状态变量的一致性,就要在单个原子操作中更所有相关的状态变量。

@NotThreadSafe
public class UnsafeCachingFactorizer implements Servlet {
private final AtomicReference<BigInteger> lastNumber 
    = new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> lastFactors 
    = new AtomicReference<BigInteger[]>();
    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        if (i.equals(lastNumber.get()))
        encodeIntoResponse(resp, lastFactors.get());
        else {
            BigInteger[] factors = factor(i);
            lastNumber.set(i);
            lastFactors.set(factors);
            encodeIntoResponse(resp, factors);
        }
    }
}

上述中通过添加状态变量时,添加Servlet状态变量通过添加线程安全的对象,状态变量一多显然不合理。

内置锁

java提供了一种内置的锁机制来支持原子性,同步代码块,同步代码块的锁就是方法调用所在的对象。

synchronized (lock){
//访问或修改由锁保护的共享状态
}

每个java对象都可以当做一个实现同步的锁,这些锁被称为内部锁。在线程进入同步代码块之前会自动获得锁,并且在退出同步代码块时会自动释放锁,任何时候只有一个线程执行内置锁保护的代码块,因此由锁保护的同步代码块会以原子方式执行。

@ThreadSafe
public class SynchronizedFactorizer implements Servlet {
@GuardBy("this") private BigInteger lastNumber  ;
@GuardBy("this") private BigInteger[] lastFactors ;
    public synchronized void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        if (i.equals(lastNumber.get()))
        encodeIntoResponse(resp, lastFactors);
        else {
            BigInteger[] factors = factor(i);
            lastNumber = i;
            lastFactors = factors;
            encodeIntoResponse(resp, factors);
        }
    }
}

该servlet的service虽然可以保证同一时刻只有一个线程执行service方法,线程安全,但是多个客户端无法同时因数分解,导致相应性非常低,无法让人接受。

重入

内置锁是可以重入的,这个重入是指某个线程可以获得一个已经由它自己持有的锁。重入的具体实现原理是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为0,这个锁被认为是没有被任何线程持有,当线程请求一个未被持有的锁,JVM将记下锁的持有者,并且将计数器的值置为1.当同一个线程再次获取这个锁,计数值再次递增,当线程退出同步代码块时,计数器将会相应地递减。当计数值为0时,这个锁将会被释放。

public class Widget{
	public synchronized void doSomething(){
	.....
	}
}
public class LoggingWidget extends Widget{
	public synchronized void doSomething(){
		System.out.println("子类改写父类的synchronized方法然后调用父类中的方法");
		super.doSomething();//若内置锁不可重入,父类的方法会一直等待一个已经被子类获得的锁,出现死锁
	}
}

用锁来保护共享状态

一种常见的错误是认为,只有在写入共享变量时才会需要使用同步,然而事实并非如此。 如果使用同步来协调对某个变量的访问,那么在访问这个变量的所有位置上都要用同步。当获取与对象关联的锁时,并不能阻止其他线程访问该对象,某个线程在获得对象的锁之后,只能阻止其他线程获得同一个锁。
一种常见的加锁约定是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步。许多线程安全类中都使用这种模式,例如Vector和其他同步集合类。

如何保证线程活跃性与安全性

通过说笑同步代码块的作用范围,我们可以容易做到既确保Servlet的并发性,同时又维护线程的安全性。要确保同步代码块不要太小,并且不要讲本应是原子的操作拆分到多个同步代码块中。但是应尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,避免单个线程占用同步代码块太长时间。

@ThreadSafe
public class CachedFactorizer implements Servlet{
@GuardedBy("this") private BigInteger lastNumber;
@GuardedBy("this") private BigInteger[] lastFactors;
@GuardedBy("this") private long hits;
@GuardedBy("this") private long cacheHits;
public synchronized long getHits(){ return hists;}
public synchronized double getCacheHitRatio(){
	return (double) cacheHits / (double) hits;
}
public void service (ServiceRequest req,ServiceResponse 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();
		}
	}
	encodeIntoResponse(resp,factors);
}
}

对于单个变量实现原子操作来说,原子变量很有用。单由于已经采用同步代码块,再使用原子变量两种机制会带来混乱,性能上也没有什么好处。判断同步代码块的合理大小,需要在各种设计需求中进行权衡,包括安全性(这个需要必须满足)、简单性、性能。如果持有锁的时间过长,那么会带来活跃性问题。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值