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

简介: 笔记

二、对象的发布与逸出


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

发布(publish) 使对象能够在当前作用域之外的代码中使用

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

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

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

静态域逸出:

1.jpg

public修饰get方法:

2.jpg

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

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

3.jpg

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


2.1安全发布对象


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

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

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


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


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

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


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


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

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

  • 无状态(没有共享变量)
  • 使用final使该引用变量不可变(如果该对象引用也引用了其他的对象,那么无论是发布或者使用时都需要加锁)
  • 加锁(内置锁,显示Lock锁)
  • 使用JDK为我们提供的类来实现线程安全(此部分的类就很多了)
  • 原子性(就比如上面的count++操作,可以使用AtomicLong来实现原子性,那么在增加的时候就不会出差错了!)
  • 容器(ConcurrentHashMap等等…)
  • ……
  • …等等


3.2原子性和可见性


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


3.2.1原子性


在多线程中很多时候都是因为某个操作不是原子性的,使数据混乱出错。如果操作的数据是原子性的,那么就可以很大程度上避免了线程安全问题了!

  • count++,先读取,后自增,再赋值。如果该操作是原子性的,那么就可以说线程安全了(因为没有中间的三部环节,一步到位【原子性】~

原子性就是执行某一个操作是不可分割的

  - 比如上面所说的count++操作,它就不是一个原子性的操作,它是分成了三个步骤的来实现这个操作的~

  - JDK中有atomic包提供给我们实现原子性操作~

4.jpg

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

5.jpg图片来源: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,你见过我们说要加锁吗??我们所有的数据都是在方法(栈封闭)上操作的,每个线程都拥有自己的变量,互不干扰

6.jpg

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

在线程封闭上还有另一种方法,就是我之前写过的: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在内部做了优化的。但是我们如果是要自己设计不可变对象,是需要满足三个条件的。

7.jpg


3.5线程安全性委托


很多时候我们要实现线程安全未必就需要自己加锁,自己来设计

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

8.jpg

非常多的"工具类"供我们使用,这些在往后的学习中都会有所介绍的~~这里就不介绍了


四、最后


正确使用多线程能够提高我们应用程序的效率,同时给我们会带来非常多的问题,这些都是我们在使用多线程之前需要注意的地方。

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

可以发现的是,上面在很多的地方说到了:。但我没有介绍它,因为我打算留在下一篇来写,敬请期待~~~

书上前4章花了65页来讲解,而我只用了一篇文章来概括,这是远远不够的,想要继续深入的同学可以去阅读书籍~

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

操作系统第一篇【引论】

操作系统第二篇【进程管理】

操作系统第三篇【线程】

操作系统第四篇【处理机调度】

操作系统第五篇【死锁】

操作系统第六篇【存储器管理】

操作系统第七篇【设备管理】

参考资料:

  • 《Java核心技术卷一》
  • 《Java并发编程实战》
  • 《计算机操作系统-汤小丹》
目录
相关文章
|
30天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
20 3
|
30天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
19 2
|
30天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
30 2
|
30天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
34 1
|
1月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
48 1
C++ 多线程之初识多线程
|
30天前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
38 1
|
30天前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
25 1
|
2月前
|
数据采集 负载均衡 安全
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
本文提供了多个多线程编程问题的解决方案,包括设计有限阻塞队列、多线程网页爬虫、红绿灯路口等,每个问题都给出了至少一种实现方法,涵盖了互斥锁、条件变量、信号量等线程同步机制的使用。
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
|
1月前
|
存储 前端开发 C++
C++ 多线程之带返回值的线程处理函数
这篇文章介绍了在C++中使用`async`函数、`packaged_task`和`promise`三种方法来创建带返回值的线程处理函数。
48 6
|
1月前
|
存储 运维 NoSQL
Redis为什么最开始被设计成单线程而不是多线程
总之,Redis采用单线程设计是基于对系统特性的深刻洞察和权衡的结果。这种设计不仅保持了Redis的高性能,还确保了其代码的简洁性、可维护性以及部署的便捷性,使之成为众多应用场景下的首选数据存储解决方案。
41 1