回顾前面:
多线程三分钟就可以入个门了!
Thread源码剖析
本文章的知识主要参考《Java并发编程实战》这本书的前4章,这本书的前4章都是讲解并发的基础的。要是能好好理解这些基础,那么我们往后的学习就会事半功倍。
当然了,《Java并发编程实战》可以说是非常经典的一本书。我是未能完全理解的,在这也仅仅是抛砖引玉。想要更加全面地理解我下面所说的知识点,可以去阅读一下这本书,总的来说还是不错的。
首先来预览一下《Java并发编程实战》前4章的目录究竟在讲什么吧:
第1章 简介
- 1.1 并发简史
- 1.2 线程的优势
- 1.2.1 发挥多处理器的强大能力
- 1.2.2 建模的简单性
- 1.2.3 异步事件的简化处理
- 1.2.4 响应更灵敏的用户界面
- 1.3 线程带来的风险
- 1.3.1 安全性问题
- 1.3.2 活跃性问题
- 1.3.3 性能问题
- 1.4 线程无处不在
ps:这一部分我就不讲了,主要是引出我们接下来的知识点,有兴趣的同学可翻看原书~
第2章 线程安全性
- 2.1 什么是线程安全性
- 2.2 原子性
- 2.2.1 竞态条件
- 2.2.2 示例:延迟初始化中的竞态条件
- 2.2.3 复合操作
- 2.3 加锁机制
- 2.3.1 内置锁
- 2.3.2 重入
- 2.4 用锁来保护状态
- 2.5 活跃性与性能
第3章 对象的共享
- 3.1 可见性
- 3.1.1 失效数据
- 3.1.2 非原子的64位操作
- 3.1.3 加锁与可见性
- 3.1.4 Volatile变量
- 3.2 发布与逸出
- 3.3 线程封闭
- 3.3.1 Ad-hoc线程封闭
- 3.3.2 栈封闭
- 3.3.3 ThreadLocal类
- 3.4 不变性
- 3.4.1 Final域
- 3.4.2 示例:使用Volatile类型来发布不可变对象
- 3.5 安全发布
- 3.5.1 不正确的发布:正确的对象被破坏
- 3.5.2 不可变对象与初始化安全性
- 3.5.3 安全发布的常用模式
- 3.5.4 事实不可变对象
- 3.5.5 可变对象
- 3.5.6 安全地共享对象
第4章 对象的组合
- 4.1 设计线程安全的类
- 4.1.1 收集同步需求
- 4.1.2 依赖状态的操作
- 4.1.3 状态的所有权
- 4.2 实例封闭
- 4.2.1 Java监视器模式
- 4.2.2 示例:车辆追踪
- 4.3 线程安全性的委托
- 4.3.1 示例:基于委托的车辆追踪器
- 4.3.2 独立的状态变量
- 4.3.3 当委托失效时
- 4.3.4 发布底层的状态变量
- 4.3.5 示例:发布状态的车辆追踪器
- 4.4 在现有的线程安全类中添加功能
- 4.4.1 客户端加锁机制
- 4.4.2 组合
- 4.5 将同步策略文档化
那么接下来我们就开始吧~
一、使用多线程遇到的问题
1.1线程安全问题
在前面的文章中已经讲解了线程【多线程三分钟就可以入个门了!】,多线程主要是为了提高我们应用程序的使用率。但同时,这会给我们带来很多安全问题!
如果我们在单线程中以“顺序”(串行-->独占)的方式执行代码是没有任何问题的。但是到了多线程的环境下(并行),如果没有设计和控制得好,就会给我们带来很多意想不到的状况,也就是线程安全性问题
因为在多线程的环境下,线程是交替执行的,一般他们会使用多个线程执行相同的代码。如果在此相同的代码里边有着共享的变量,或者一些组合操作,我们想要的正确结果就很容易出现了问题
简单举个例子:
- 下面的程序在单线程中跑起来,是没有问题的。
public class UnsafeCountingServlet extends GenericServlet implements Servlet { private long count = 0; public long getCount() { return count; } public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { ++count; // To something else... } }
但是在多线程环境下跑起来,它的count值计算就不对了!
首先,它共享了count这个变量,其次来说++count;
这是一个组合的操作(注意,它并非是原子性)
++count
实际上的操作是这样子的:
- 读取count值
- 将值+1
- 将计算结果写入count
于是多线程执行的时候很可能就会有这样的情况:
- 当线程A读取到count的值是8的时候,同时线程B也进去这个方法上了,也是读取到count的值为8
- 它俩都对值进行加1
- 将计算结果写入到count上。但是,写入到count上的结果是9
- 也就是说:两个线程进来了,但是正确的结果是应该返回10,而它返回了9,这是不正常的!
如果说:当多个线程访问某个类的时候,这个类始终能表现出正确的行为,那么这个类就是线程安全的!
有个原则:能使用JDK提供的线程安全机制,就使用JDK的。
当然了,此部分其实是我们学习多线程最重要的环节,这里我就不详细说了。这里只是一个总览,这些知识点在后面的学习中都会遇到~~~
1.3性能问题
使用多线程我们的目的就是为了提高应用程序的使用率,但是如果多线程的代码没有好好设计的话,那未必会提高效率。反而降低了效率,甚至会造成死锁!
就比如说我们的Servlet,一个Servlet对象可以处理多个请求的,Servlet显然是一个天然支持多线程的。
又以下面的例子来说吧:
public class UnsafeCountingServlet extends GenericServlet implements Servlet { private long count = 0; public long getCount() { return count; } public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { ++count; // To something else... } }
从上面我们已经说了,上面这个类是线程不安全的。最简单的方式:如果我们在service方法上加上JDK为我们提供的内置锁synchronized,那么我们就可以实现线程安全了。
public class UnsafeCountingServlet extends GenericServlet implements Servlet { private long count = 0; public long getCount() { return count; } public void synchronized service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { ++count; // To something else... } }
虽然实现了线程安全了,但是这会带来很严重的性能问题:
- 每个请求都得等待上一个请求的service方法处理了以后才可以完成对应的操作
这就导致了:我们完成一个小小的功能,使用了多线程的目的是想要提高效率,但现在没有把握得当,却带来严重的性能问题!
在使用多线程的时候:更严重的时候还有死锁(程序就卡住不动了)。
这些都是我们接下来要学习的地方:学习使用哪种同步机制来实现线程安全,并且性能是提高了而不是降低了~