网站配色方案 对比色,网站建设与管理必修,做网站搜索推广点击率太低怎么办,建筑英才招聘网首页前言线程并发系列文章#xff1a;熟练掌握线程原理与使用是程序员进阶的必经之路#xff0c;网上很多关于Java线程的知识#xff0c;比如多线程之间变量的可见性、操作的原子性#xff0c;进而扩展出的Volatile、锁(CAS/Synchronized/Lock)、信号量等知识。有些文章只说笼统…前言线程并发系列文章熟练掌握线程原理与使用是程序员进阶的必经之路网上很多关于Java线程的知识比如多线程之间变量的可见性、操作的原子性进而扩展出的Volatile、锁(CAS/Synchronized/Lock)、信号量等知识。有些文章只说笼统的概念、有些文章深入底层源码令人迷失其中、有些文章只说了其中某个点没有提及内在的联系。基于以上原因本系列文章尝试由浅入深、系统性地分析、总结Java线程相关知识算是加深印象、夯实基础也算是抛砖引玉。若是相关文章对各位看官有所帮助幸甚至哉。通过本篇文章你将了解到1、进程与线程区别2、开启/停止线程3、线程的交互1、进程与线程区别程序与进程平时所说的编写一个程序/软件比如编写好一个APK这个APK可以直接传送给另一个设备安装这时候我们说发送给你一个程序/软件是个静态的单个文件/多个文件的集合。当安装好APK之后运行该APK该程序就被CPU执行了这时候我们称这个进程在运行了。因此进程是程序的动态表现也是CPU执行时间段的描述。image.png当然程序与进程也不是一一对应关系也就是说一个程序里可以fork()多个进程来执行任务。进程与线程CPU调度执行程序之前需要准备好一些数据如程序所在的内存区域程序需要访问的外设资源等程序运行过程中产生的一些中间变量需要临时存储在寄存器等。这些与进程本身关联的东西称之为进程上下文。由此引发的问题CPU在切换进程的过程中势必涉及到上下文的切换切换的过程会占用CPU时间。image.png通俗点理解就是进程1先被CPU调度执行执行了一段时间后调度进程2执行此时上下文就会切换成与进程2相关的。再考虑另一种情形一个程序里实现了A、B两个有关联的功能两者在不同的进程实现A进程需要与B进程交互该过程就是个IPC(进程间通信)。我们知道IPC需要共享内存或者陷入内核调用这些操作代价比较大。Android 进程间通信系列文章请移步Android IPC 看了都懂系列随着计算机硬件越来越强大CPU频率越来越高甚至还发展出多个CPU。为了充分利用CPU线程应运而生。进程被分为更小的粒度原本一个进程要执行A、B、C三个任务现在将这三个任务分别放在三个线程里执行。image.png可以看出CPU调度的基本单位就是线程。进程与线程关系1、进程与线程均是CPU执行时间段的描述。2、进程是资源分配的基本单位线程是CPU调度的基本单位。3、一个进程里至少有一个线程。4、同一进程里的各个线程可以共享变量它们之间的通信称之为线程间通信。5、线程可以看作粒度更小的进程。线程的优势1、开启新线程远比开启新进程节约资源并且更快速。2、线程间通信比IPC简单、快捷易于理解。3、符合POSIX规范的线程可以跨平台移植。2、开启/停止线程既然线程如此重要那么来看看Java中如何开启与停止线程。开启线程查看Thread.java源码可知Thread实现了Runnable接口因此需要重写Runnable方法:run()。#Thread.javaOverridepublic void run() {if (target ! null) {target.run();}}而线程开启后执行任务的方法即是run()。该方法里先判断target是否不为空若是则执行target.run()。#Thread.java/* What will be run. */private Runnable target;target为Runnable类型该引用可以通过Thread构造方法赋值。由此看就比较明显了要线程实现任务要么直接重写run()方法要么传入Runnable引用。继承Thread声明MyThread继承自Thread并重写run()方法static class MyThread extends Thread {Overridepublic void run() {System.out.println(thread running by extends...);}}private static void startThreadByExtends() {MyThread t2 new MyThread();t2.start();}生成Thread引用后调用start()方法开启线程。实现Runnable先构造Runnable再将Runnable引用传递给Thread。private static void startThreadByImplements() {Runnable runnable new Runnable() {Overridepublic void run() {System.out.println(thread running by implements...);}};Thread t1 new Thread(runnable);t1.start();}生成Thread引用后调用start()方法开启线程。停止线程线程开启后被CPU调度后执行run()方法该方法执行完毕线程正常退出。当然也可以在run()方法执行途中退出该方法(设置标记位满足条件即退出)该线程也将停止。若是run()方法里正在Thread.sleep(xx)、Object.wait()等方法可以使用interrupt()方法中断线程。private static void stopThread() {MyThread t2 new MyThread();t2.start();//中断线程t2.interrupt();//已废弃t2.stop();}3、线程的交互硬件层面先来看看CPU和主存的交互image.pngCPU运算速度远远高于访问主存的速度也就是说当CPU需要计算如下表达式int a a 1;首先从主存里拿到a的值访问主存的过程中CPU是等待状态当从主存拿到a的值后才进行运算。这个过程显然很浪费CPU的时间因此在主存与CPU之间增加了高速缓存顾名思义当拿到a的值后放到高速缓存下次再次访问a的时候先去看看缓存里是否有有的话直接拿到放到寄存器里最后按照一定的规则将改变后的a的值刷新到主存里。访问速度寄存器--高速缓存--主存CPU在寻找值的时候先找寄存器再到高速缓存最后到主存。你可能已经发现问题了如下代码int a 1;int a;线程A、线程B分别执行上述代码假设线程A被CPU1调度线程B被CPU2调度。线程A、B分别执行a 1此时CPU1、CPU2的高速缓存分别存放着a1当线程A执行a时发现高速缓存有值于是直接拿出来计算结果是a2。当线程B执行时同样的从高速缓存获取值来计算结果是a2。最后高速缓存将修改后的值回写的主存结果是a2。这样的结果不是我们愿意看到的CPU针对此种情况设计了一套同步高速缓存主存的机制MESI(缓存一致性协议)该协议约定了各个CPU的高速缓存间与主存的配合尽量保证缓存数据是一致的。但是由于StoreBuffer/InvalidateQueue的存在还需要配合Volatile使用。有关Volatile详细解析请移步真正理解Java Volatile的妙用软件层面由于寄存器、高速缓存的存在让我们有种感觉每个线程都拥有自己的本地内存。实际上JVM设计了JMM(Java Memory Model Java内存模型)image.png本地内存是个虚拟概念如下代码static Integer integer new Integer(0);public static void main(String args[]) {Thread t1 new Thread(new Runnable() {Overridepublic void run() {integer 5;}});Thread t2 new Thread(new Runnable() {Overridepublic void run() {integer 6;}});t1.start();t2.start();}integer 在主存中只有一份可能还存在于寄存器、高速缓存等地方这些地方对应的是本地内存。而不是每个线程又重新复制了一份数据。再看看一段代码static boolean flag false;static int a 0;public static void main(String args[]) {Thread t1 new Thread(new Runnable() {Overridepublic void run() {a 1; //1flag true; //2}});Thread t2 new Thread(new Runnable() {Overridepublic void run() {if (flag) { //3a 2; //4}}});t1.start();t2.start();}若是线程1先执行完线程2再执行结果没问题。若是两个线程同时执行由于//1 //2之间没有依赖关系编译器/处理器 可能会对//1 //2交换位置这就是指令重排。如此之后有可能执行顺序是2-3-4-1还有可能是其它顺序最终的结果是不可控的。线程交互的核心从上述的软件层面、硬件层面分析可知线程1、线程2、线程3各自的本地内存对其它线程是不可见的多个线程写入主存时可能会存在脏数据指令重排导致结果不可控。多线程交互需要解决上述三个问题这三个问题也是线程并发的核心1、可见性2、原子性3、有序性上述三者既是并发核心也是基础只有满足了三者线程并发的共享变量结果才是可控的。我们熟知的锁、Volatile等是针对三者中的某个或者全部提出的解决方案。互斥与同步互斥的由来要满足并发的三个条件想想该怎么做呢先来看看原子性既然多线程同时访问共享变量容易出问题那么想到的是大家排队来访问它当其中一个线程(A)在访问时其它线程不能访问并排队等待A线程执行完毕后等待中的线程再次尝试访问共享变量我们把操作共享变量的代码所在的区域称为临界区共享变量称为临界资源。//临界区{a 5;b 6;c a;}如上面的代码多个线程不能同时访问临界区。这种访问方式称为互斥。也就是说多个线程互斥地访问临界区可以实现操作的原子性。同步的由来临界区内的操作的共享变量在不同的线程可能有不一样的处理如下代码//伪代码int a 0;//线程1执行private void add() {while(true) {if (a 10)a;}}//线程2执行private void sub() {while(true) {if (a 0)a--;}}线程1、线程2都对变量a进行了操作两者都依赖a的值做一些操作。线程1判断如果a10则a需要自增线程2判断如果a0则a需要自减。线程1、线程2分别不断地去检查a的值看是否满足条件再做进一步操作这么做没问题但是效率太低。如果线程1、线程2检查到不满足条件先停下来等待当满足条件时由对方通知自己这样子就不用傻乎乎地每次跑去问a是多少了极大提升了效率。因此交互变成这样子//伪代码int a 0;//线程1执行private void add() {while(true) {if (a 10)a;else//等待并通知线程2}}//线程2执行private void sub() {while(true) {if (a 0)a--;else//等待并通知线程1}}这么说流程有点枯燥我们用个小比喻类比一下用小明表示线程1、小刚表示线程2小明要发一批集装箱先把箱子拿到库房外的空地上空地面积有限最多只能放10个箱子等待小刚过来拿货。1、刚开始小刚发现空地没货于是等待小明通知。小明发现没货开始放货。2、小明发现空地上还可以放箱子于是继续放。3、小明发现箱子已经放了10个空地占满了于是就休息下来不再放了并打电话告诉小刚我的货够了你快点过来拿货吧。4、小刚收到通知后过来拿货一直拿当发现货拿完之后就不再拿了并打电话告诉小明货拿完了你快放货吧。于是整个流程简述小明放了10个箱子就等待小刚拿小刚拿完之后通知小明继续放。值得注意的是上述是批量放了箱子再批量拿箱子并没有拿一个放一个。关于这个问题后面细说又因为小明、小刚都依赖于箱子的个数做事通过上面对互斥的分析我们知道需要将这部分操作包裹在临界区里进行互斥访问。我们把上面的交互过程称之为同步同步与互斥关系可以看出同步是在互斥的基础上增加了等待-通知机制实现了对互斥资源的有序访问因此同步本身已经实现了互斥。同步是种复杂的互斥互斥是种特殊的同步解释了互斥、同步概念那么该这么实现呢接下来系列文章将重点分析系统提供的机制是如何实现可见性、原子性、有序性的以及互斥、同步与三者的关系。下篇文章聊聊Unsafe的作用及其用法。您若喜欢请点赞、关注您的鼓励是我前进的动力持续更新中和我一起步步为营系统、深入学习Android