当前位置: 首页 > 科技观察

《重磅开篇》形成完美的多线程世界观

时间:2023-03-12 12:06:02 科技观察

本文转载自微信公众号《飞天小牛》,作者飞天小牛。转载本文请联系飞天小牛公众号。很早就想写这篇文章,但一直不敢写。一方面,之前的知识储备不足。人们很困惑,不知道从哪里开始。而且对于我们学生群体来说,很少有机会接触到高并发的真实场景,自己敲代码的时候基本不会用到,所以我们大部分学生都是面向面对面的——面对学习。你问synchronized,bababa我可以说一堆,你问volatile我可以说一堆bababa,但是我总觉得几乎没有什么意义,就是这些知识点是分散的,没有这么一条线把它们很好的串联起来.所以今天我大胆的创建一个线程,站在小白的角度,说说多线程我们需要学习什么,以及按照什么顺序来学习,帮助大家建立一个比较完整的知识体系,形成正确的多线程知识体系。线程世界观。我基本上会沿着这个思路写下面的文章。然后,我还没有踏入工作岗位,也没有实际的高并发经验,所以只是纸上谈兵,知识还很浅。如果大家觉得有什么问题,欢迎在评论区或私聊指正。晚辈感激不尽(抱拳)。首先,要学习多线程,必须要知道什么是线程,包括线程的一些基本概念(比如上下文切换),所以说到线程,肯定离不开进程。OK,进程和线程这两个概念在操作系统的课程中其实已经接触过了。当然,并行与并发、同步与异步等基本概念也是默认学习的,所以还是需要学习一下Java。线程和操作系统线程有什么区别?另外,有一点很容易被大家忽视的是,一项技术的出现一定不是凭空捏造出来的,它一定是出于某种目的,在某个成熟的时候产生的。所以,你需要知道我们为什么要使用多线程,多线程的出现解决了哪些问题。掌握了以上这一步,我们称之为炼气,即炼气化气。初期需要一心一意,心平气和。打基础现在我们知道什么是线程了,那么Java中如何创建线程呢?为此你会接触到三种创建线程(Thread)的方式:直接使用ThreadThread+RunnableThread+Callable+FutureTask了解如何创建线程,我们翻一下Thread类的源码,你会发现它定义了Java线程的六种状态,也就是所谓的生命周期。它与操作系统中线程的五态模型有什么区别和联系??既然已经翻过了Thread源码,还有什么理由不去深究呢?接下来我们来了解Thread类为我们提供了哪些方法来控制线程,它们能做什么,以及它们如何影响线程的状态:start/runsleep/yieldjoin/join(longn)interruptsetDaemondaemonthread这个阶段学习,这是进入阶段之后的第一步,我们称之为基础建设。地基不牢,地动山摇。诚然,一个程序顺序运行多个线程是没有问题的,但是如果多个线程同时访问一个共享资源,就可能会出现不可预知的现象,也就是我们常说的线程安全问题。要了解这些问题的根源,我们需要对Java内存模型(JavaMemoryModel,JMM)有深入的了解。为此,我们将学习与线程安全密切相关的三大特性:1)原子性:一个操作是不可中断的,要么全部执行成功,要么全部执行失败(也可以说是提供互斥访问,并且在同一时间只有一个线程对数据进行操作)2)可见性:当一个线程修改共享变量时,其他线程可以立即知道修改一种重新排序指令序列以优化程序性能的手段。由于重排序的存在,在多线程环境下可能会导致程序执行的结果出现错误。那么编译器和处理器在重排序时会遵循什么原则呢?出于这个原因,您将了解数据依赖性和似串行。这里简单介绍一下这两个概念:Compiler和Processor在重排序时,会遵守数据依赖关系,不会改变两个有数据依赖关系的操作的执行顺序。as-if-serial语义的意思是:无论怎么重新排序,程序的执行结果都不会改变。编译器、运行时和处理器都必须遵守似串行的语义。其实可见性和有序性其实是两个矛盾的点。一方面,对于程序员来说,我们希望内存模型易于理解,易于编程。为此,JMM的设计者必须为程序员提供足够强的内存可见性保证,专业术语称为“强内存模型”。另一方面,编译器和处理器希望内存模型对它们的约束尽可能少,这样它们就可以做尽可能多的优化(比如重新排序)来提高性能。并且处理器的约束要尽可能放宽,专业术语叫“弱内存模型”。当然,对于这个问题,JMM的设计者找到了一个很好的平衡点,那就是happens-before,这是JMM的核心理念!理解happens-before是理解JMM的关键。知其然,知其所以然,这个阶段,我们称之为金丹。具体到Java语言层面,度姐是如何保证线程安全的呢?即如何保证原子性、可见性和有序性?(保证有序性上面已经讲过了,就是利用happens-before原则)。1)为了可见性,可以使用volatile关键字来保证。不仅如此,volatile还可以起到禁止指令重排的作用;2)对于原子性,我们可以使用java.util.concurrent.atomic包中的锁和原子类来保证。(给萌新解释一下,java.util.concurrent,简称J.U.C,是一个包,也变成了并发包,现在网上大部分博客都会直接说JUC,对萌新不是很友好),我们可以看看juc.atomic当然,atomic包下的这些原子操作类保证原子性最重要的原因是它们使用了CAS操作。因此,你需要先深入研究CAS,理解CAS存在的三个问题,然后再去深挖这些原子类的底层原理。另外,我们上面提到的锁这个话题,其实也是一个非常核心的知识点。在深入研究之前,需要了解各种锁的概念:悲观锁和乐观锁重量级锁和轻量级锁自旋锁偏向锁可重入锁和不可重入锁公平锁和非公平锁共享锁和排他锁另外,概念与锁相关的包括临界区、竞争条件等,这些都是你需要了解的。那么锁在Java中是如何实现的呢?早期的Java程序依赖于synchronized关键字来实现锁定功能。在我们掌握了synchronized的使用和底层原理之后,你也会接触到synchronized的wait/notify/notifyAll方法。JavaSE5之后,在并发包JUC中增加了Lock接口和相关实现类(放在java.util.concurrent.locks包下),同样可以用来实现锁功能。为什么要新增这样一个Lock接口及其相关的实现类呢?因为使用synchronized关键字会隐式获取锁,但是会将锁的获取和释放固化,即先获取再释放。当然,这种方式简化了同步的管理,但是扩展性不如所示的锁的获取和释放。例如,对于一个场景,手动获取和释放锁。先获取锁A,再获取锁B。当获取到锁B时,释放锁A,同时获取锁C。获取到锁C后,释放B,同时获取锁。D,等等。这种场景下如果使用synchronized关键字就没那么容易实现了,但是使用Lock就容易多了。它提供类似于synchronized关键字的同步功能,只是在使用时需要显式获取和释放锁。虽然缺少了隐式获取和释放锁的便利性,但是它具有很多synchronized关键字所不具备的获取锁和释放锁、可中断获取锁、超时获取锁的可操作性。另外,还有一点很重要!我们可以遍历一下实现了Lock接口的类,比如ReentrantLock(大部分文章会直接翻译成可重入锁),你会惊讶的发现它的代码并不多,基本上所有的方法都调用了里面的方法它的静态内部类Sync,Sync类继承了AbstractQueuedSynchronizer类(即大名鼎鼎的AQS,译作队列同步器,简称同步器)。AQS可以理解为构建锁和同步器(工具类)的框架。locks包中的各种锁以及我们接下来要学习的JUC中的工具类都是基于AQS实现的。OK,AQS本文就不多说了。上面我们提到了两个并发关键字,synchronized和volatile。其实还有一个,就是final。很多朋友可能还不知道。什么?final和并发是什么关系?当然,这些会在后续的文章中写到。这个阶段的知识很重要,而且相对来说知识点比较多,难度也比较大,所以我们称之为渡劫。大乘渡劫结束了。至此,大家对多线程的基本知识结构有了一定的了解,世界观也初步形成了。最后就是加固的过程。让我们看看J.U.C包中还有什么。(下图未全):JUC其实可以分为五类:锁框架(locks包)原子类(atomicpackage)并发集合线程池工具类后三类正是我们现阶段需要学习的。并发集合和线程池没什么好说的。他们的知识点比较集中,学习目标也很明确。在Internet上很容易找到组织良好的文章。那么就要学习常用的工具类了:CountDownLatchCyclicBarrierSemaphoreExchanger所谓的工具类必须要封装一些比较复杂的操作,这样我们才能轻松的完成这些操作。以CountDownLatch为例:多线程协作完成业务功能时,有时需要等待其他多个线程完成任务,主线程才能继续执行业务功能。在这样的业务场景下,通常可以使用Thread类的join方法,让主线程等待加入的线程执行完毕,主线程才能继续执行。Java并发工具类为我们提供了这样一个“倒计时”的工具类CountDownLatch,可以非常方便的完成这个业务场景。另外,还有一个重要的类,不知道怎么归类,就是ThreadLocal,江湖人称之为线程隔离技术,必须问高考点。OK,经过这个阶段的学习,多线程的世界观已经完全形成,我们称之为大乘,忘我的境界,全在自己的心中。