总听组里几个大神说起线程安全问题。本来对“线程安全”这个定义拿捏得就不是很准,更令人困惑的是,大神们用这个词指代的对象不仅抽象而且千变万化。比如,我们的架构师昨天说:

平台的A功能不是线程安全的,所以我们要在上层应用中多做一层封装,让它变成一个独占式的功能。

啥?一个功能还能是线程安全的?

又比如,同事小谢有一次说:

这个变量我已经加了synchronized关键字去访问了,所以这个变量一定是线程安全的。

所以线程安全是用来说变量的?加了synchronized关键字就能保证线程安全了吗?

如果我们写着多线程代码,每天debug时把“线程安全”挂在嘴上,但却并不知道它的真正涵义,而且讨论问题时每个人的理解都不一样,岂不是笑话?

于是,出于对代码(qiang)负(po)责(zheng)的心理,去看了一下JCIP的第二章,发现Brian Goetz大神还是讲得很清楚的。

“线程安全”的指代对象

狭义上讲,“线程安全”修饰的是一个类。广义上讲,也可能是整个程序。

讨论线程安全问题时,应该关注的是“状态”,确切来说,是共享、可变的变量。一个类中的变量可以被多个线程访问,且可以做修改,在这种情况下,由于线程调度的顺序不定,或线程之间的执行产生了重叠,类变量的最终结果可能不同,这种情况叫做竞态条件(race condition)

如果把一个Servlet写成这样:

public class UnsafeServlet implements Servlet{
private long count = 0; //客户端访问计数器
public long getCount(){ return count; }
public void service(ServletRequest req, ServletResponse resp){
//实际处理……
count++;
}
}

由于count++这个操作不是原子的,多个线程调用service()方法时万一出现相互重叠,可能发生计数不准的情况,比如明明有两个客户端来访问,count的值却为1。因此UnsafeServlet这个类不是线程安全的。

既然竞态条件只发生在对共享、可变的变量的处理上,广义上,可以通过三种方法去避免竞态条件:

  • 取消多线程对变量的共享
  • 把变量设为不可变
  • 设置同步机制去控制对变量的访问

在Java中,“同步机制”主要指synchronized关键字提供的互斥锁,但也包括volatile关键字、显式锁和原子变量等。

另外,一个类对自己的状态封装得越好,越利于保证类的线程安全性。因为封装可以收紧对共享变量的访问,便于程序员进行代码维护。

一个无状态的类永远线程安全。

如果程序中只有线程安全的类,这个程序不一定线程安全;反过来说,线程安全的程序中的类也不一定全都是线程安全的。

“线程安全”的具体定义

一个类在多线程环境中,不管多线程的调用顺序如何、执行是否相互重叠等,类的表现始终正确。

“表现正确”是指遵循不变性和操作的后置条件

不变性是指类中一些状态应该遵循的规律。比如一个进行整数分解的Servlet,用两个类变量去缓存上一次处理的整数和分解因子。那么在任何一个线程访问时,这个整数和分解因子应该是互相匹配的。如果某个线程拿到的整数是7,而因子是2和4,那么这个类的不变性受到了侵犯。

后置条件是指类方法调用的结果。比如UnsafeServlet.service()每次被调用时,count应该增加1。如果两个线程先后调用这个函数而count没有按我们所期待的增加2,则这个类的后置条件受到了侵犯。

原子性

线程安全的前提是保证对共享变量操作的原子性。UnsafeServlet中的count++是一个复合操作。复合操作包括两类:

  • read-modify-write: 如count++
  • check-then-act: 如单例中的懒加载,先判断类实例是否为空,再创建实例

原子性是指某操作的执行不可打断。假如线程A正在做该操作,则线程B要等A做完以后才能进行该操作。在A看来,B要么没有开始,要么已经做完,没有操作的中间态。

要实现原子性,可以使用Java提供的内在锁。

内在锁

在Java中,每个对象都可以作为一把锁来保证同步机制,这个锁称为内在锁。一个线程只有进入synchronized代码块或synchronized方法时才能获取到对象的内在锁。synchronized方法提供的是这个方法属于的对象的内在锁。

内在锁有两个特征:

  • 互斥性:对于某个对象,同一时间只有一个线程能拿到它的内在锁。
  • 重入性:一个持有对象A内在锁的线程可以多次进入A保护的其他代码块。这个机制保证了“获取锁”这个动作是以线程为单位的。

用synchronized关键字修饰方法看起来简单粗暴,但可能极大地影响性能。假如我们把Servlet中的整个service方法做成synchronized的,实际上等于把本来应该并行处理的客户端访问做成了串行的,不仅浪费系统资源(多CPU得不到利用),还会降低对客户端的响应。所以,要仔细考虑同步块的粒度,在代码简洁性和程序性能之间找到平衡。

同步机制

设计一个类时,要考虑它的同步机制,即有哪些变量需要同步保护,用哪个锁进行保护,在什么样的粒度上保护等。

值得参考的几个原则是:

  • 对每个共享变量的所有访问都应该用同一把锁进行保护。最好用注释等方法标注清楚哪个变量被哪个锁保护,以便维护。
  • 涉及某一条不变性的的所有共享变量的操作要用同一把锁进行保护。
  • 进行长时间操作,比如network I/O时,尽量不要持锁。

理解以上概念以后,对多线程就有了一个好的基础,可以继续学习了。

[JCIP笔记] (二)当我们谈线程安全时,我们在谈论什么的更多相关文章

  1. 当我们说线程安全时,到底在说什么——Java进阶系列(二)

    原创文章,同步发自作者个人博客,转载请以超链接形式在文章开头处注明出处http://www.jasongj.com/java/thread_safe/ 多线程编程中的三个核心概念 原子性 这一点,跟数 ...

  2. [JCIP笔记] (三)如何设计一个线程安全的对象

    在当我们谈论线程安全时,我们在谈论什么中,我们讨论了怎样通过Java的synchronize机制去避免几个线程同时访问一个变量时发生问题.忧国忧民的Brian Goetz大神在多年的开发过程中,也悟到 ...

  3. JDK源码阅读-------自学笔记(二十五)(java.util.Vector 自定义讲解)

    Vector 向量 Vector简述 1).Vector底层是用数组实现的List 2).虽然线程安全,但是效率低,所以并不是安全就是好的 3).底层大量方法添加synchronized同步标记,sy ...

  4. NumPy学习笔记 二

    NumPy学习笔记 二 <NumPy学习笔记>系列将记录学习NumPy过程中的动手笔记,前期的参考书是<Python数据分析基础教程 NumPy学习指南>第二版.<数学分 ...

  5. qml学习笔记(二):可视化元素基类Item详解(上半场anchors等等)

    原博主博客地址:http://blog.csdn.net/qq21497936本文章博客地址:http://blog.csdn.net/qq21497936/article/details/78516 ...

  6. Emacs 笔记二

    Emacs 笔记二 Table of Contents 1. 前言 2. emacs基本操作(常用快捷键) 3. emacs模式讲解 4. emacs缓冲区 5. org mode 5.1. 列表 5 ...

  7. MyBatis笔记二:配置

    MyBatis笔记二:配置 1.全局配置 1.properites 这个配置主要是引入我们的 properites 配置文件的: <properties resource="db.pr ...

  8. 《CMake实践》笔记二:INSTALL/CMAKE_INSTALL_PREFIX

    <CMake实践>笔记一:PROJECT/MESSAGE/ADD_EXECUTABLE <CMake实践>笔记二:INSTALL/CMAKE_INSTALL_PREFIX &l ...

  9. jQuery源码笔记(二):定义了一些变量和函数 jQuery = function(){}

    笔记(二)也分为三部分: 一. 介绍: 注释说明:v2.0.3版本.Sizzle选择器.MIT软件许可注释中的#的信息索引.查询地址(英文版)匿名函数自执行:window参数及undefined参数意 ...

随机推荐

  1. Eclipse Web项目配置

    1.每次重开workspace都要重新配置一次 2.new web project之前配置 3.Windows-Preferences-(所有都要记得Apply) General   Maven P. ...

  2. Cassandra配置多节点集群以及使用雅虎YCSB压测Cassandra 3.11

    这几天在搭Cassandra集群以及对Cassandra的性能测试,步骤还挺多,记录一下. 关于Caaandra在服务器上配置多节点集群,可以参考一下文章: http://blog.csdn.net/ ...

  3. select函数的介绍和使用

    我们所使用的I/O模型一共有五种. 分别为阻塞I/O,非阻塞I/O,I/O复用,信号驱动I/O,异步I/O. 所谓I/O复用就是指管理多个I/O文件描述符,一般会使用(select,poll,epol ...

  4. emacs在windows下打开报错原因

    最开始实在是想不通,最开始我明明就能正常使用,后来发现不能用了,过了几天才回过神来,我路径中有中文,换了一个没有中文的路径后打开正常了.太低级的错误了嘛,却那么难发现. 这些数字就是识别不出来我的中文 ...

  5. php基础知识(三)---常用函数--2017-04-16

    常用函数如下:(红色为重点) 1.取字符串的长度 echo strlen("hello"); 2.echo strcmp("字符串1","字符串2&q ...

  6. kubernetes中kubeconfig的用法

    在开启了 TLS 的集群中,每当与集群交互的时候少不了的是身份认证,使用 kubeconfig(即证书) 和 token 两种认证方式是最简单也最通用的认证方式. 以kubectl为例介绍kubeco ...

  7. 2017/11/25 2D变换

    2D变换 一.盒模型解析模式 1.box-sizing:盒模型解析模式 1)content-box:标准盒模型(和css2一样的计算) 宽度和高度之外绘制元素的内边距和边框 width,height外 ...

  8. 神奇的Python

    不断学习新的知识,不断掌新的技能是一件非常有趣的事情,其实Python在我学习这门课之前从没听过,刚上第一节课老师给我们讲了一个它的应用比如可以筛选单词,定时放歌等,虽然感觉自己还没有真正理解这门课程 ...

  9. javascript中的Promise使用

    参考自: http://m.jb51.net/article/102642.htm 1.基本用法: (1).首先我们new一个Promise,将Promise实例化 (2).然后在实例化的promis ...

  10. Django+xadmin打造在线教育平台(六)

    九.课程章节信息 9.1.模板和urls 拷贝course-comments.html 和 course-video.html放入 templates目录下 先改course-video.html,同 ...