一、线程安全性

一个对象是否是需要是线性安全的,取决于它是否需要被多个线程访问

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

1. 无状态对象

无状态对象不包含域,也不包含与其他类中域的引用,计算过程中的临时状态仅存在于线程栈中的局部变量上,并且只能由正在执行的线程访问。访问无状态对象的线程不会影响另一个访问同一个对象的线程的计算结果,因为这两者之间没有共享关系。因此,无状态对象是线程安全的。

public class A{

public void service(){

}

}

2. 原子性

原子性从字面意思上看就是,要么全部都做,要么全部都不做。具有原子性的操作是线程安全的,例如 i = 1;不具有原子性的操作不是线程安全的,例如i++。因为i++实际上分为三步,读取i,将值加1,写回i。

3. 竞态条件

当某个计算的正确性取决于多个线程的交替执行顺序时,那么就会发生竞态条件,也就是说,正确的结果取决于运气。竞态条件和原子性相关,或者说,之所以代码会发生竞态条件,就是因为代码不是以原子方式操作的,而是一种复合操作。

例如:

public class A {

private static A instance = null;

public static A getInstance(){

if(instance == null){

instance = new A();

}

return instance;

}

}

这里会存在竞态条件(先检查后执行)。假设线程B1和B2同时执行getInstance,B1看到instance为空,因此他会进入到new A()的操作创建A的实例。B2同时也要判断instance是否为空,此时的instance是否为空,却取决于不可预测的时序(包括线程的调度方式),以及B1要花多少时间来new一个A的实例。如果B1在new操作时,轮到B2线程被调度,那么此时B2判断的instance也为空,因此到最后,会出现两个A的实例。

同理对于i++也一样存在竞态条件(读取—修改—写入)。

解决:

对于这两种竞态条件,我们要避免它,就要保证它是原子方式执行,即在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量。

i++这种情况可以使用concurrent.atomic包实现原子操作,而先检查后执行这种竞态条件则可以通过加锁来实现同步。

4. concurrent.atomic包实现原子操作

原子操作类主要用于高并发环境下的程序处理,这些处理主要有:

基本类:AtomicInteger, AtomicLong,AtomicBoolean

引用类型:AtomicReference

数组类型:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

像上面提到的递增,就可以用原子类来实现

class B {

private AtomicInteger count = new AtomicInteger(0);

public void inc(){

count.incrementAndGet();

}

}

二、加锁机制

1.内置锁

Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized),同步代码块包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。

对于前面先检查后执行的竞态条件,可以通过加锁来实现线程安全

public class A {

private static A instance = null;

public static synchronized A getInstance(){

if(instance == null){

instance = new A();

}

return instance;

}

}

谈到锁,就要谈到双重加锁机制:

public class A {

private static A instance = null;

private byte[] lock = new byte[0];

public static A getInstance(){

if(instance == null){

synchronized (lock){ //1

if(instance == null){ //2

instance = new A();//3

}

}

}

return instance;

}

}

双重加锁的理念是这样的:

1. 线程 1 进入 getInstance() 方法。

2. 由于 instance 为 null,线程 1 在 //1 处进入 synchronized 块。

3. 线程 1 被线程 2 预占。

4. 线程 2 进入 getInstance() 方法。

5. 由于 instance 仍旧为 null,线程 2 试图获取 //1 处的锁。然而,由于线程 1 持有该锁,线程 2 在 //1 处阻塞。

6. 线程 2 被线程 1 预占。

7. 线程 1 执行,由于在 //2 处实例仍旧为 null,线程 1 还创建一个 Singleton 对象并将其引用赋值给 instance。

8. 线程 1 退出 synchronized 块并从 getInstance() 方法返回实例。

9. 线程 1 被线程 2 预占。

10. 线程 2 获取 //1 处的锁并检查 instance 是否为 null。

11. 由于 instance 是非 null 的,并没有创建第二个 Singleton 对象,由线程 1 创建的对象被返回。

按理论来说,这是完美的。双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是这些习语失败的一个主要原因

所以不要使用双重锁定!

2.内置锁的特性

(1)自动获得和释放:

每个java对象都可以隐式地扮演一个用于同步的锁的角色,这些内置的锁被称为内部锁或监视器锁,执行线程进入synchronized块之前自动获得锁,而无论是正常退出还是抛出异常,线程都会自动释放锁。因此获得内部锁的唯一途径是进入这个内部锁保护的同步块或方法。

(2)互斥性:

内部锁在java中扮演了互斥锁的角色,即至多只有一个线程可以拥有锁,没有获取到锁的线程只能等待或阻塞直到锁被释放,因此同步块可以线程安全地原子执行。

(3)可重入性:

可重入是指对于同一个线程,它可以重新获得已有它占用的锁。

可重入性意味着锁的请求是基于”每线程”而不是基于”每调用”,它是通过为锁关联一个请求计数器和一个占有它的线程来实现。

可重入性方便了锁行为的封装,简化了面向对象并发代码的开发,可以防止类继承引起的死锁,例子如下:

public class Widget {

public synchronized void doSomething(){

......

}

}

public class LoggingWidget extends Widget {

public synchronized void doSomething(){

System.out.println(toString() + “: calling doSomething”);

super.doSomething();

}

}

子类LoggingWidget覆盖了父类Widget中synchronized类型的doSomething方法,并调用了父类的中的同步方法,因此子类LoggingWidget和父类Widget在调用doSomething方法之前都会先获取Widget的锁,若内部锁没有可重入性,则super.doSomething的调用就会因为无法获得锁而被死锁。

三、内存可见性和指令重排序

四、发布与逸出

发布:发布一个对象的意思是,使对象能够在当前作用域之外的代码中使用。

1. 将一个引用存储到其他代码可以访问的地方;

2. 在一个非私有的方法中返回该引用;

3. 将该对象传递到其他类的方法中等。

public static Set secrets;

public void init(){

secrets = new HashSet();

}

当发布某个对象时,可能间接地发布其他对象。例如如果将一个Secret对象添加到集合secrets中,那么在发布secrets的同时,也会发布Secret对象,因为任何代码都可以遍历这个集合,并获得对Secret对象的引用。

逸出:当某个不应该发布的对象被发布时,这种情况就是逸出。

对象逸出会导致对象的内部状态被暴露,可能危及到封装性,使程序难以维持稳定;若发布尚未构造完成的对象,可能危及线程安全问题。

最常见的逸出是this引用在构造时逸出,导致this引用逸出的常见错误有:

在构造函数中启动线程:

当对象在构造函数中显式还是隐式创建线程时,this引用几乎总是被新线程共享,于是新的线程在所属对象完成构造之前就能看见它。

避免构造函数中启动线程引起的this引用逸出的方法是不要在构造函数中启动新线程,取而代之的是在其他初始化或启动方法中启动对象拥有的线程。

在构造方法中调用可覆盖的实例方法:

在构造方法中调用那些既不是private也不是final的可被子类覆盖的实例方法时,同样导致this引用逸出。

避免此类错误的方法时千万不要在父类构造方法中调用被子类覆盖的方法。

在构造方法中创建内部类:

在构造方法中创建内部类实例时,内部类的实例包含了对封装实例的隐含引用(深入理解 内部类),可能导致隐式this逸出。例子如下:

public class ThisEscape {

public ThisEscape(EventSource source) {

source.registerListener(new EventListener() {

public void onEvent(Event e) {

doSomething(e);

}

});

}

}

上述例子中的this逸出可以使用工厂方法来避免,例子如下:

public class SafeListener {

private final EventListener listener;

private SafeListener(){

listener = new EventListener(){

public void onEvent(Event e){

doSomething(e);

}

);

}

public static SafeListener newInstance(EventSource source) {

SafeListener safe = new SafeListener();

source.registerListener(safe.listener);

return safe;

}

}

六、线程封闭

当访问共享的可变数据时,通常需要同步。一种避免使用同步的方式就是不同享数据,这叫做线程封闭。java提供了一些机制来维持线程封闭性,例如局部变量和ThreadLocal类。

线程封闭技术的一个常见应用是JDBC的Connection对象。JDBC规范不要求Connection对象时线程安全的,而要求连接池是线程安全的。线程聪哥线程池中获得一个Connection对象,并且用该对象来处理请求,使用完之后再返回给连接池。由于大多数请求(例如Servlet请求和EJB)都是单个线程采用同步的方式来处理,并且在Connection对象返回前,连接池不会再把它分配给其它线程,因此这种连接在处理请求时,把Connection对象封闭在线程中。

1. 栈封闭(局部变量)

栈限制是线程封闭的一种特例,只能通过局部变量才可以访问对象,局部使对象限制在执行线程中,存在于执行线程栈,其他线程无法访问这个栈,从而确保线程安全。(每一个线程都有一个工作内存,工作内存中班包括有栈,局部的基本类型变量是处于栈中,引用类型的引用处于栈中,而引用指向的对象处于堆中)。

栈限制的例子如下:

public int loadTheArk(Collection candidates){

SortedSet animals;

int numPairs = 0;

Animal candidate = null;

//animals被限制在本地方法栈中

animals = new TreeSet(new SpeciesGenderComparator());

animals.addAll(candidates);

for(Animal a : animals){

if(candidate == null || !candidate.isPotentialMate(a)){

candidate = a;

}else{

ark.load(new AnimalPair(candidate, a));

++numPairs;

candidate = null;

}

}

return numPairs;

}

指向TreeSet对象的唯一引用保存在animals中,而animals这个引用被封闭在局部变量中,因此封闭在线程本身的工作内存中,其它线程不能访问。如果发布了对集合的引用,那么线程的封闭性将被破坏,并且导致对象animals的逸出。

2. ThreadLocal类

ThreadLocal线程本地变量是一种规范化的维护线程限制的方式,它允许将每个线程与持有数值的对象关联在一起,为每个使用它的线程维护一份单独的拷贝。ThreadLocal提供了set和get访问器,get总是返回由当前线程通过set设置的最新值。

public static ThreadLocal num = new ThreadLocal(){

@Override

protected Integer initialValue() {

return 0;

}

};

public ThreadLocal getThreadLocal(){

return num;

}

public int getNextNum(){

num.set(num.get() + 1);

return num.get();

}

我们来看看ThreadLocal是如何做到对每一个线程都做到独立的副本的。

20161220165904004.png

在set方法中我们可以看到,ThreadLocalMap这个应该是关键,把ThreadLocalMap看成一个map。ThreadLocalMap是通过getMap(t)方法获得的,传入的t是当前线程,也就是说,ThreadLocalMap是与各自线程绑定的。此后,ThreadLocalMap通过set方法,把当前的ThreadLocal作为key,传入的值作为value保存在ThreadLocalMap中。ThreadLocal通过操作每一个线程特有的ThreadLocalMap对象,从而实现了变量访问在不同线程中的隔离。

3. 不可变对象

如果一个对象在创建后其状态就不能被修改,那么这个对象就称为不可变对象。

不可变对象需要满足下面条件:

1. 对象本身是final的(避免被子类化),声明属性为private 和 final

2. 不可变对象的状态在创建后就不能再改变,不要提供任何可以修改对象状态的方法 - 不仅仅是set方法, 还有任何其它可以改变状态的方法,每次对他们的改变都是产生了新的不可变对象的对象。

3. 不可变对象能被正确地创建(在创建过程中没有发生this引用逸出)。

4. 如果类有任何可变对象属性, 那么当它们在类和类的调用者间传递的时候必须被保护性拷贝

不可变对象一定是线程安全的,不需要任何同步或锁的机制就可以保证安全地在多线程之间共享。

竞态条件的赋值_《Java并发编程实战》读书笔记一:基础知识相关推荐

  1. java并发编程实战学习笔记之基础知识与对象的共享

    第二章:线程安全性 2.1 什么是线程安全性 可以被多个线程调用,并且在线程之间不会出现错误的交互 方法内的局部变量不需要保护,因为它存储在栈中,是每个线程独有的 2.2 原子性 一个共享变量可以定义 ...

  2. Java并发编程实战读书笔记

    Java并发编程 标签(空格分隔): 并发 多线程 基础 线程 在执行过程中,能够执行程序代码的一个执行单元,在Java语言中,线程有四种状态:运行,就绪,挂起,结束. 并发特性 原子性 一个操作不会 ...

  3. Java并发编程实战读书笔记(一)——线程安全性、对象共享

    一.线程安全性 一个对象是否需要是线程安全的,取决于它是否被多个线程访问. 当多个线程访问,并且其中有一个执行写入时,必须采用同步机制,Java中主要的同步关键字是 synchronized 独占加锁 ...

  4. Java并发编程实战读书笔记三

    第七章 取消和关闭 Java没有提供任何机制来安全的终止线程,虽然 Thread.stop 和 suspend 等方法提供了这样的机制,但由于存在着一些严重的陷,因此应该避免使用 7.1任务取消 7. ...

  5. Java并发编程实战读书笔记一

    第1章 简介 第2章 线程安全性 1个状态变量线程安全的模式 多个状态变量线程不安全的模式,在A线程lastNumbers.set和lastFactors.set之间B线程进行这两个set就出问题了, ...

  6. Java并发编程实战读书笔记二

    第五章 基础构建模块 5.1 同步容器类 5.1.1 同步容器类的问题 如下,如果list含有10个元素,线程A调用getLast的同时线程B调用deleteLast,那么getLast可能会报Arr ...

  7. java并发编程实践 读书笔记_Java - 并发编程实践(读书笔记)

    [注] 同步机制保证:1)原子性 2)内存可见性: Volatile变量只能保证:1)可见性: - 恰当的同步,同步的弱形式,确保对一个变量的更新以可预见的方式告知其他线程. [注] 用锁来协调访问变 ...

  8. 竞态条件的赋值_信号-sunshine225-51CTO博客

    一.基础知识信号产生的条件 a. 终端按键产生.如:ctrl+c(SIGINT信号),ctrl+\(SIGQUIT信号),ctrl+z(SIGTSTP信号)...... b. 系统命令和函数.如:ki ...

  9. Java并发编程实战_[Java并发编程实战] 简介

    并发简史 在早期不包含操作系统的计算机中,程序都是单一的串行程序,从头至尾只能执行一个程序,并且这个程序访问这个计算机的所有资源.然而,随着技术的发展,操作系统出现了.它使得计算机程序有了进程,线程的 ...

最新文章

  1. Codeforces Round #246 (Div. 2) B. Football Kit
  2. python做多元线性回归
  3. 贝叶斯软件genle教程_手把手教你用R的gemtc包对生存数据进行贝叶斯网状Meta分析...
  4. 【作业四】软件案例分析之必应词典
  5. 赛门铁克公告:解密Kneber恶意软件
  6. MFC拖拽文件到任意EDIT控件
  7. SAP Spartacus本地启动时的白屏问题分析
  8. uniapp开发实例github_跨端开发痛点?送你一款Vue最流行的跨端框架——uni-app
  9. 计算机网络课程实训方法,《计算机网络基本原理》实践环节指导一
  10. QT中QWidget、QDialog及QMainWindow的区别
  11. Java中的文件压缩
  12. Timer运行多个TimeTask
  13. python list倒序_python数据分析(1):内建序列函数
  14. mac ae 汉化
  15. 阿辉DirectX 11学习笔记一
  16. 数据挖掘实战:员工离职预测(训练赛)
  17. 串口编程之一: WIN32 API 中串口DCB 结构的介绍
  18. 程序员如何写好自己的简历,一位 5 年中大厂老哥跟你聊聊
  19. Gartner2022应用安全测试魔力象限
  20. springboot基础(72):Redisson分布式锁

热门文章

  1. 梯度下降回归SGDRegressor、岭回归(Ridge)和套索(Lasso)回归、套索最小角回归、ElasticNet回归、正交匹配追踪回归
  2. java判断自己活了多少天_用程序计算你活了多少天
  3. NCBI下载SRA数据的4种方法
  4. c语言怎么将n个数字存入到数组中_洛谷 || 拼数(C语言)
  5. signature=dff897e1da6b42a8e9483e18ff19fcde,Vídeo Institucional: Ingresse1
  6. flask 作为 three.js 的服务器
  7. python的assert作用
  8. 2.合并区间(LeetCode第56题)
  9. VBA最常用的基础代码、基础功能写法总结
  10. Linux下 curl 代理设置注意事项--curl proxy