多线程基础必要知识点!看了学习多线程事半功倍

前言

不当心就鸽了几天没有更新了,这个星期回家咯。在学校的日子要努力一点才行!html

只有光头才能变强java

回顾前面:编程

本文章的知识主要参考《Java并发编程实战》这本书的前4章,这本书的前4章都是讲解并发的基础的。要是能好好理解这些基础,那么咱们日后的学习就会事半功倍。c#

固然了,《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.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方法处理了之后才能够完成对应的操做

这就致使了:咱们完成一个小小的功能,使用了多线程的目的是想要提升效率,但如今没有把握得当,却带来严重的性能问题

在使用多线程的时候:更严重的时候还有死锁(程序就卡住不动了)。

这些都是咱们接下来要学习的地方:学习使用哪一种同步机制来实现线程安全,而且性能是提升了而不是下降了~

2、对象的发布与逸出

书上是这样定义发布和逸出的:

发布(publish) 使对象可以在当前做用域以外的代码中使用

逸出(escape) 当某个不该该发布的对象被发布了

常见逸出的有下面几种方式:

  • 静态域逸出
  • public修饰的get方法
  • 方法参数传递
  • 隐式的this

静态域逸出:

public修饰get方法:

方法参数传递我就再也不演示了,由于把对象传递过去给另外的方法,已是逸出了~

下面来看看该书给出this逸出的例子

逸出就是本不该该发布对象的地方,把对象发布了。致使咱们的数据泄露出去了,这就形成了一个安全隐患!理解起来是否是简单了一丢丢?

2.1安全发布对象

上面谈到了好几种逸出的状况,咱们接下来来谈谈如何安全发布对象

安全发布对象有几种常见的方式:

  • 在静态域中直接初始化public static Person = new Person();
    • 静态初始化由JVM在类的初始化阶段就执行了,JVM内部存在着同步机制,导致这种方式咱们能够安全发布对象
  • 对应的引用保存到volatile或者AtomicReferance引用中
    • 保证了该对象的引用的可见性和原子性
  • 由final修饰
    • 该对象是不可变的,那么线程就必定是安全的,因此是安全发布~
  • 由锁来保护
    • 发布和使用的时候都须要加锁,这样才保证可以该对象不会逸出

3、解决多线程遇到的问题

从上面咱们就能够看到,使用多线程会把咱们的系统搞得挺复杂的。是须要咱们去处理不少事情,为了防止多线程给咱们带来的安全和性能的问题~

下面就来简单总结一下咱们须要哪些知识点来解决多线程遇到的问题。

3.1简述解决线程安全性的办法

使用多线程就必定要保证咱们的线程是安全的,这是最重要的地方!

在Java中,咱们通常会有下面这么几种办法来实现线程安全问题:

  • 无状态(没有共享变量)
  • 使用final使该引用变量不可变(若是该对象引用也引用了其余的对象,那么不管是发布或者使用时都须要加锁)
  • 加锁(内置锁,显示Lock锁)
  • 使用JDK为咱们提供的类来实现线程安全(此部分的类就不少了)
    • 原子性(就好比上面的count++操做,可使用AtomicLong来实现原子性,那么在增长的时候就不会出差错了!)
    • 容器(ConcurrentHashMap等等...)
    • ......
  • ...等等

3.2原子性和可见性

何为原子性?何为可见性?当初我在ConcurrentHashMap基于JDK1.8源码剖析中已经简单说了一下了。不了解的同窗能够进去看看。

3.2.1原子性

在多线程中不少时候都是由于某个操做不是原子性的,使数据混乱出错。若是操做的数据是原子性的,那么就能够很大程度上避免了线程安全问题了!

  • count++,先读取,后自增,再赋值。若是该操做是原子性的,那么就能够说线程安全了(由于没有中间的三部环节,一步到位【原子性】~

原子性就是执行某一个操做是不可分割的
- 好比上面所说的count++操做,它就不是一个原子性的操做,它是分红了三个步骤的来实现这个操做的~
- JDK中有atomic包提供给咱们实现原子性操做~

也有人将其作成了表格来分类,咱们来看看:

图片来源:https://blog.csdn.net/eson_15/article/details/51553338

使用这些类相关的操做也能够进他的博客去看看:

3.2.2可见性

对于可见性,Java提供了一个关键字:volatile给咱们使用~

  • 咱们能够简单认为:volatile是一种轻量级的同步机制

volatile经典总结:volatile仅仅用来保证该变量对全部线程的可见性,但不保证原子性

咱们将其拆开来解释一下:

  • 保证该变量对全部线程的可见性
    • 在多线程的环境下:当这个变量修改时,全部的线程都会知道该变量被修改了,也就是所谓的“可见性”
  • 不保证原子性
    • 修改变量(赋值)实质上是在JVM中分了好几步,而在这几步内(从装载变量到修改),它是不安全的

使用了volatile修饰的变量保证了三点

  • 一旦你完成写入,任何访问这个字段的线程将会获得最新的值
  • 在你写入前,会保证全部以前发生的事已经发生,而且任何更新过的数据值也是可见的,由于内存屏障会把以前的写入值都刷新到缓存。
  • volatile能够防止重排序(重排序指的就是:程序执行的时候,CPU、编译器可能会对执行顺序作一些调整,致使执行的顺序并非从上往下的。从而出现了一些意想不到的效果)。而若是声明了volatile,那么CPU、编译器就会知道这个变量是共享的,不会被缓存在寄存器或者其余不可见的地方。

通常来讲,volatile大多用于标志位上(判断操做),知足下面的条件才应该使用volatile修饰变量:

  • 修改变量时不依赖变量的当前值(由于volatile是不保证原子性的)
  • 该变量不会归入到不变性条件中(该变量是可变的)
  • 在访问变量的时候不须要加锁(加锁就不必使用volatile这种轻量级同步机制了)

参考资料:

3.3线程封闭

在多线程的环境下,只要咱们不使用成员变量(不共享数据),那么就不会出现线程安全的问题了。

就用咱们熟悉的Servlet来举例子,写了那么多的Servlet,你见过咱们说要加锁吗??咱们全部的数据都是在方法(栈封闭)上操做的,每一个线程都拥有本身的变量,互不干扰

在方法上操做,只要咱们保证不要在栈(方法)上发布对象(每一个变量的做用域仅仅停留在当前的方法上),那么咱们的线程就是安全的

在线程封闭上还有另外一种方法,就是我以前写过的:ThreadLocal就是这么简单

使用这个类的API就能够保证每一个线程本身独占一个变量。(详情去读上面的文章便可)~

3.4不变性

不可变对象必定线程安全的。

上面咱们共享的变量都是可变的,正因为是可变的才会出现线程安全问题。若是该状态是不可变的,那么随便多个线程访问都是没有问题的

Java提供了final修饰符给咱们使用,final的身影咱们可能就见得比较多了,但值得说明的是:

  • final仅仅是不能修改该变量的引用,可是引用里边的数据是能够改的!

就好像下面这个HashMap,用final修饰了。可是它仅仅保证了该对象引用hashMap变量所指向是不可变的,可是hashMap内部的数据是可变的,也就是说:能够add,remove等等操做到集合中~~~

  • 所以,仅仅只可以说明hashMap是一个不可变的对象引用
final HashMap<Person> hashMap = new HashMap<>();

不可变的对象引用在使用的时候仍是须要加锁

  • 或者把Person也设计成是一个线程安全的类~
  • 由于内部的状态是可变的,不加锁或者Person不是线程安全类,操做都是有危险的

要想将对象设计成不可变对象,那么要知足下面三个条件:

  • 对象建立后状态就不能修改
  • 对象全部的域都是final修饰的
  • 对象是正确建立的(没有this引用逸出)

String在咱们学习的过程当中咱们就知道它是一个不可变对象,可是它没有遵循第二点(对象全部的域都是final修饰的),由于JVM在内部作了优化的。可是咱们若是是要本身设计不可变对象,是须要知足三个条件的。

3.5线程安全性委托

不少时候咱们要实现线程安全未必就须要本身加锁,本身来设计

咱们可使用JDK给咱们提供的对象来完成线程安全的设计:

很是多的"工具类"供咱们使用,这些在日后的学习中都会有所介绍的~~这里就不介绍了

4、最后

正确使用多线程可以提升咱们应用程序的效率,同时给咱们会带来很是多的问题,这些都是咱们在使用多线程以前须要注意的地方。

不管是不变性、可见性、原子性、线程封闭、委托这些都是实现线程安全的一种手段。要合理地使用这些手段,咱们的程序才能够更加健壮!

能够发现的是,上面在不少的地方说到了:。但我没有介绍它,由于我打算留在下一篇来写,敬请期待~~~

书上前4章花了65页来说解,而我只用了一篇文章来归纳,这是远远不够的,想要继续深刻的同窗能够去阅读书籍~

以前在学习操做系统的时候根据《计算机操做系统-汤小丹》这本书也作了一点点笔记,都是比较浅显的知识点。或许对你们有帮助

参考资料:

  • 《Java核心技术卷一》
  • 《Java并发编程实战》
  • 《计算机操做系统-汤小丹》

若是文章有错的地方欢迎指正,你们互相交流。习惯在微信看技术文章,想要获取更多的Java资源的同窗,能够关注微信公众号:Java3y。为了你们方便,刚新建了一下qq群:742919422,你们也能够去交流交流。谢谢支持了!但愿能多介绍给其余有须要的朋友

文章的目录导航

相关文章
相关标签/搜索