一、线程安全性

一个对象是否需要是线程安全的,取决于它是否被多个线程访问。
当多个线程访问,并且其中有一个执行写入时,必须采用同步机制,Java中主要的同步关键字是 synchronized 独占加锁。但 “同步” 这个术语还包括 volatile 类型的变量,显式锁,原子变量。

1、线程安全的定义

线程安全: 核心正确性,即某个类的行为与其规范完全一致。
线程安全的类: 某个类在主调代码中不需要任何的同步或协调,这个类都表现正确的行为。(在类中封装了必要的同步机制)
无状态对象一定线程安全: 既不包含任何域,也不包含任何对其他类中域的引用。(大多Servlet都是线程安全,除非要保存一些信息)

2、原子性

竞态条件: 由于不恰当的执行时序而出现不正确的结果。当某个计算的正确性取决于多个线程交替执行时序时,就会发生竞态条件。
复合操作: 如 “先检查后执行”(类延时初始化),“读取-修改-写入” (自增)等操作。

要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。

原子性 : 一组语句作为不可分割的单元被执行。

3、加锁

每个Java对象都可以用作一个实现同步的锁,这些锁被称为 内置锁监视器锁,线程只有在进入同步代码块会自动获得锁,退出释放锁。
可重入 : 内置锁是可重入的,即一个线程可以多次进入同步代码块加同一个锁,获取锁的是 线程 不是 调用 。(重入一种实现为每个锁加计数器和关联线程,进入一次加一,退出一次减一,0时候代表已被释放,未加锁。)

对于可能被多个线程同时访问的变量,访问时都需要持同一个锁,称这个状态变量由这个锁保护。每个变量都应只有一个锁保护。
对于包含多个变量的不变性条件,其中涉及的所有变量都需要同一个锁保护。

一种常见加锁约定:将所有可变状态封装对象内部,通过对象内置锁对访问代码路径同步,使对象不会被并发访问。

4、活跃性与性能

过多的同步与锁会带来活跃性(即性能)问题。所以尽可能在正确性前提下缩小锁的粒度。设计时需要在 性能、简单性和安全性 等需求之间权衡。

通常在简单性和性能之间存在着制约因素,一定不能盲目为了性能牺牲简单性。(会给维护带来不变,而且可能破坏安全性)
执行长时间计算或者无法快速完成的操作时(网络I/O、控制台I/O),一定不要持有锁。

二、对象的共享

之前介绍如何同步避免多线程同一时刻访问数据,现在介绍如何 共享和发布 对象。
synchronized 除了实现原子性或者确定 “临界区(Critical Section)”,还有另一个重要方面:内存可见性。

1、可见性

可见性是种复杂的熟悉,因为可见性总会违背我们的直觉。
重排序 : 为了提高执行速度,编译器、处理器以及运行时都会对操作的执行顺序进行调整,符合一定规则的代码不会被重排序,后面详细介绍。
失效数据: 缺乏同步的程序中可能会产生失效数据,并且一个线程可能获得变量的最新值,而另一个线程获得失效值。
最低安全性: 至少失效值是初始化值或者之前线程设置的值,而不是一个随机值。(但有例外,JVM允许64位的读写操作分解为两个32位的操作,所以并不是原子的。所以多线程使用共享可变的long和double等类型必须要保护)

加锁的含义不仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读写操作的线程都必须在同个锁上同步

2、Volatile变量

提供了一种稍弱的同步机制

  1. 会有内存屏障,不会将改变量操作与其它内存操作重排序。
  2. 不会被缓存在寄存器或者对其它处理器不可见的地方,因此读取时总是最新值。

仅当 volatile 变量能简化代码的实现以及对同步策略验证时,才应该使用。volatile 变量正确使用方式包括:确保它们自身状态的可见性,确保它们所引用对象状态的可见性,以及表示一些重要的程序生命周期事件的发生。
加锁机制即可保证可见性又可保证原子性,而volatile变量只能保证可见性。

当且仅当满足下列条件时,才应使用volatile变量:

  • 对变量量写入不依赖变量当前值,或者能确保只有单个线程更新变量。
  • 该变量不会与其它变量一起纳入不变性条件中。
  • 在访问变量时不需要加锁。

3、发布与逸出

发布: 一个对象指一个对象,使该对象能在作用域之外的代码使用。
逸出: 当某个不应该被发布的对象发布时。
常见的发布:

  1. 将对象的引用保存在一个公有的静态变量中。
  2. 从非私有方法中返回一个引用,同样会发布返回的对象。(封装能使得对程序的正确性分析边为可行,并使得无意中破坏约束条件变得更难)
  3. 发布一个类内部的类实例,可能会隐式的使 this 引用逸出。
    public class ThisEscape{public ThisEscape(EventSource source){source.registerListener(new EventListener(){public void onEvent(Event e){dosomething(e);}});}}

内部 EventListener 实例发布时,外部封装的 ThisEscape 也逸出了。
在对象构建时,构造函数中启动一个线程,会使得 this 引用被新线程共享。在构造函数中调用一个可改写的实例方法时,同样会使得 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;}}

4、线程封闭

一种避免使用同步的方式就是不共享数据(如果仅在单线程中访问数据就不需要同步),这种技术被称为线程封闭,它是实现线程安全性的最简单方式之一。当某个对象被封闭在一个线程中时,这种用法将自动实现线程安全性。

(1)Ad-hoc线程封闭

维护线程封闭性的职责完全由程序实现来承担,这种线程封闭技术是脆弱的,应该尽少使用。

(2)栈封闭

栈封闭是线程封闭的特例,栈封闭中,只能通过局部变量访问对象。同步变量能够使对象更容易被封闭在线程中,局部对象的固有属性之一就是封闭在执行线程中。

(3)ThreadLocal 类

维持线程封闭一种更规范方法是使用 ThreadLocal 类,这个类能使线程中某个值与保存值对象关联起来,提供了get 与 set 等访问接口或方法,这些方法为每个使用改变量的线程都存一份独立的副本,因此 get 总是返回 set 设置的最新值。
ThreadLocal 通常用来防止对可变的单实例变量或全局变量共享。

    private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){public Connection initialValue(){return DriverManager.getConnection(DB_URL);}};public static Connection getConnection(){return connectionHolder.get();}

当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免每次执行都重新分配该临时对象,就可以使用这项技术。
但ThreadLocal 变量类似于全局变量,它能降低代码可重用性并在类间引入隐含的耦合性,因此使用时要小心。

5、不变性

不可变的对象一定是线程安全的。即使对象的所有域都是final类型,对象也不一定是不可变的,因为 final 类型的域中可以保存对可变对象的引用。
满足下列条件,对象次啊是不可变的。

  • 对象创建以后其状态就不能修改。
  • 对象所有域都是 final 类型。
  • 对象是正确创建的(创建期间,this引用没有逸出)

Final 域

通过对域声明为final,可以告诉其它人这个域是不可变的。

除非需要更高的可见性,否则应将所有的域都声名为私有域。除非需要某个域是可变的,否则应将其声明为 final 域。

6、安全发布

我们希望在多个线程之间共享对象,此时必须保证安全地进行共享。

(1)不正确的发布

//没有在足够同步的情况下发布对象
public Holder holder;
public void initialize(){holder = new Holder(42);
}

除了发布对象的线程外,其它线程可能看到的 Holder 域是一个失效值,因此将看到一个空引用或之前的旧值。如果没有足够的同步,那么在多个线程间共享对象会发生一些奇怪的事。

(2)不可变性与初始化安全性

Java内存模型为不可变对象的共享提供了一种特殊的初始化保证其安全性。
任何线程都可以不需要额外同步情况下安全地访问不可变对象,即使发布时对象没有使用同步。

(3)安全发布常用模式

安全发布对象,对象引用及其状态必须同时对其它线程可见,一个正确构造对象可以通过下列方式安全发布:

  • 在静态初始化函数中初始化一个对象引用。
  • 将对象的引用保存到 volatile 类型的域或 AtomicReferance 中。
  • 将对象的引用保存到某个正确构造对象的f inal 域中。
  • 将对象的引用保存一个由锁保护的域中。
  • 一些线程安全的容器也可以安全发布对象。(Hashtable、synchronizedMap、ConcurrentMap;Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList、synchronizedSet;BlockingQueue、ConcurrentLinkedQueue;Future、Exchanger 等)
public static Holder holer = new Holer(42);

使用静态初始化器发布一个静态构造对象。JVM在类初始化阶段执行静态初始化器,在JVM内部存在同步机制,因此这种方式初始化的任何对象都可以正确被发布。

(4)事实不可变对象、可变对象

  1. 任何线程都可以不需要额外同步安全使用被安全发布的事实不可变对象。
  2. 可变对象不仅在发布时需要同步,每次对象访问也需要同步来确保可见性。

对象发布需求取决于可变性:

  • 不可变对象可以任意机制发布
  • 事实不可变对象必须安全方式发布
  • 可变对象安全发布,并且需要是线程安全的或者由某个锁保护起来。

(5)实用策略

  • 线程封闭。 线程封闭对象只能由一个线程拥有,对象被封闭在线程中,并只能由这个线程修改。
  • 只读共享。 共享的只读对象(包括不可变对象和事实不可变对象)可以不需要额外同步由多个线程访问。
  • 线程安全共享。 线程安全的对象在其内部实现同步,多个线程可以对共有方法直接使用。
  • 保护对象。 被保护的对象只能通过持有锁来访问。保护对象包括封装在其它线程安全中的对象,以及已发布并由某个特定锁保护的对象。

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

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

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

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

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

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

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

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

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

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

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

  6. Java 并发编程艺术 读书笔记

    第 1 章 并发编程的挑战 1.1.3 如何减少上下文切换 减少上下文切换的方法有无锁并发编程.CAS 算法.使用最少线程和使用协程. 无锁并发编程.多线程竞争锁时,会引起上下文切换,所以多线程处理数 ...

  7. Java并发编程艺术----读书笔记(二)

    版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/a724888/article/details/64214595  java并发编程艺术2 jav ...

  8. JAVA并发编程艺术读书笔记(1,2章节)

    第一章 并发编程的挑战 为什么要使用并发编程? 主要是为了更有效地利用资源.即使是单核的CPU也可以多线程执行程序,多线程实际上是CPU分配时间片给各个线程,因为时间片非常短,所以看起来就像在同事执行 ...

  9. Java 并发编程实践 读书笔记四

    组合对象 设计线程安全的类 设计线程安全类的过程应该包括下面3个基本要素: 确定对象状态是由哪些变量构成的 确定限制状态变量的不变约束 制定一个管理并发访问对象状态的策略 对象状态从域讲,如果对象域都 ...

最新文章

  1. linux进程间通信:POSIX 消息队列 ----异步通信
  2. java中如何应对读改写场景
  3. 后端缓存的23个关键关注点
  4. B2B2C网站系统建设的常见误区
  5. SkipList 跳表
  6. python draw.text颜色_python wand:用draw.text()改变文本样式
  7. centos7 yum 安装lnmp
  8. [转]Webpack5(从入门到精通)
  9. 初识 Vue(11)---(Vue 中的事件绑定)
  10. Linux基础命令---cp
  11. python reportlab
  12. php加skplayer,织梦dedecms怎么整合添加ckplayer播放器支持flv,mp4等播放功能
  13. 【UAV】串级 PID 控制原理及应用
  14. DES加密,前端示例,Java示例,在线测试
  15. 流程判断-三目运算-for循环
  16. frida 挂钩_您必须知道的预提交挂钩
  17. (转)Apple Push Notification Services in iOS 6 Tutorial: Part 1/2
  18. 今天就给大家介绍一个抢购茅台的小技巧,不需要安装Python,不需要配置环境,就和你在电脑上安装使用QQ等软件一样
  19. [ 网络协议篇 ] IGP 详解之 OSPF 详解(一)--- 基础知识
  20. 中午午睡失眠,写了一些备忘

热门文章

  1. 【和63】王甲佳:实体“微地址”的遐想(微地址系列9-3)
  2. 初识遗传算法(二): 适应度地形与协同进化
  3. 技师计算机实际操作考试题库,技师操作题库_相关文章专题_写写帮文库
  4. 2022低压电工考试试题及在线模拟考试
  5. 树莓派桌面没有时间_三、屏幕连接树莓派无法显示桌面问题解决
  6. 回炉再造-多线程和进程的对比
  7. 案例的项目的需求分析和创新
  8. java腾讯字符串面试题_面试百度、阿里、腾讯,这134道Java面试题你会多少?
  9. php学生考勤在线请假系统,【腾讯智慧校园V1.66】最新版本发布,在线申请请假,老师在线批假;学生考勤导出;...
  10. kibana 修改Ico图标