1.多线程典型应用场景

1.场景1:工作量太大,需要多人一块儿干,以缩短工期
比如要完成书稿校对工作。显然一个人校对太慢了,那就多叫几个人吧!每个人分一个章节,同时进行校对,速度一下就上来了。如下
图所示:

如果程序需要重复执行一段逻辑,每次执行又互不影响,那么你可以考虑多线程,每个线程执行总量的一部分,最后再把每个线程执行的结果合并

2.场景2:实现分工协作

饭店一般有收银员、厨师、传菜员、清洁员。每个人各司其职,大家配合工作。
每种角色的员工只关心自己的输入和输出,比如厨师的输入就是客户的点菜单,输出就是饭菜。而厨师的输入则是上个环节收银员的输出。这样做的好处是每个人专注自己的工作,有助于效率的提升
每个环节都有缓冲,订单的列表就是一个缓冲,调节两个环节速率的不匹配及不稳定。

3.场景3:排队的同时,不耽误做其他事情
比如B超的流程优化,你先去B超排队取号,然后可以先去做其他项目的检查,当快轮到你的时候,短信通知你去做B超。
这其实就是java多线程中的Future模式。这种模式下,主线程不会因为一个耗时的业务操作而阻塞,主线程可以单起一个线程去处理耗时
的操作,主线程逻辑继续执行,等用到另外线程返回的数据时,再通过Future对象获取。

2.多线程的实现方式

1.继承Thread类
2.实现Runnable接口
3.使用FutureTask
4.使用Executor框架

3.线程安全(重难点)

争夺共享资源,需要加锁同步

4.看若兄弟,实如父子-----Thread和Runnable详解

1.继承Thread类

start方法调用了start0方法,start0是一个native方法(JNI方法),JNI(Java Native Interface)是java和其它语言的交互方式,
start0会调用run方法。

2.实现Runnable接口

Thread类的run方法:

    @Overridepublic void run() {if (target != null) {target.run();}}

这里的target被设置为你实现业务逻辑的Runnable实现。
java多线程使用了模板方法,Thread负责线程相关的逻辑,业务逻辑则被剥离出来让Runnable的实现类去实现。

5.线程什么时候开始真正执行?----线程的状态详解

深入Thread类—线程API精讲

1.sleep方法:使线程休眠指定的时间长度
2.yield方法: 在多线程中意味着本线程愿意放弃CPU资源,不过这只是给CPU的一个提示,当CPU资源不紧张时,则会无视yield提醒。
刚刚yield的线程是不会马上参与竞争获取CPU资源的。
3. currentThread方法:用于获取当前线程的实例。
4. setPriority方法:用于设置线程的优先级,当CPU资源紧张的时候,优先级高的线程获得CPU资源的概率会更大,范围在1-10.
5. interrupt方法:线程中断
将线程的标识位设置为true,可以触发sleep等方法抛出InterruptedException异常,并将标识位置为false
可以调用isInterrupted方法获取标识位状态
7. join方法:将主线程block住,加入其它线程执行。

集体协作,什么最重要?沟通!——线程的等待和通知

wait / notify概念,这两个方法都在Object类中

调用wait方法,线程会进入BLOCKING状态,并释放持有的锁。
调用notify方法,会唤醒wait的线程,让其继续往下执行。

使用wait / notify

wait和notify/notifyAll必须配合synchronized一起使用

wait/notify的代码示例如下:

synchronized (obj){//do something
obj.wait();
//continue to do something
}

假如此段代码在A线程中,这段代码会在执行一些逻辑之后把A线程放入obj对象的wait set中,并且A线程会释放持有的锁。

synchronized (obj){//do something
obj.nofity();
//continue to do something
}

执行notify会将obj对象wait set中的线程弹出。被弹出的线程A会在获取CPU资源后继续执行wait方法后面的逻辑。

notifyAll的使用

notifyAll唤醒所有在此对象的wait set上的线程。

有福同享,有难同当——原子性

线程安全:某个类,在多线程并发访问时,始终能够保证运行的正确性,那么这个类就是线程安全的。
原子性:不可分割。

并发编程的三大特性

1.原子性:所有操作要么全部成功,要么全部失败;
2.可见性:一个线程对变量进行了修改,另外一个线程能够立刻读取到此变量的最新值;
3.有序性:代码在执行阶段,不一定和你的编写顺序一致;

竞态条件

竞态条件:在多线程的情况下,由于多个线程执行的时序不同,而出现不正确的结果。

举个例子,你的室友中午要出去办事,可能赶不上下午第一节课。他拜托你,如果老师点名时他还没回来,帮他答一下到。下午第一节课果然老师点名了,眼看就要点到你的室友,你环顾了下四周,确认室友没有赶回来,然后紧张的等待老师点到室友的名字。老师又点了几个名字后,终于点到了你室友的名字。你故作镇定,沉稳、大方的喊了声:到!但令人尴尬的是,几乎同时,教室后面也传出了一声铿锵有力的到!你回头一看,就这几秒钟的时间,室友已经赶回了教室,从后门溜进来坐在了最后一排。这就是竞态条件,你观察到室友没有来上课的结果,在你替室友答到的时候已经失效了

我们可以通过加锁或者使用原子变量来解决。原子变量在 java.util.concurrent.atomic 包中,它提供了一系列的原子操作。

眼见不实——可见性

什么是可见性?

某个线程对共享变量进行了修改,其它线程能够立刻看到修改后的最新值。

CPU缓存模型

CPU用了L1,L2,L3,一共三级缓存,CPU会先从主存中复制数据到缓存,CPU在计算的时候就可以从缓存中读取数据,在计算完成后再把
数据从缓存更新回主存。这样在计算期间,就无须访问主存了,速度大大提升。

初识volatile关键字

后面介绍。

我们不管遇到什么难题,一定不能乱了阵脚,还是从分析问题入手,最终解决问题一定是基于你分析出的原因。而不是靠猜测和盲目乱试。

有序性

指令重排序

CPU为了提高运行效率,可能会对编译后代码的指令做一些优化,这些优化不能100%保证按照你编写的代码顺序执行,但是一定可以保证
执行的最终结果是一致的。

生活化联想举例:
星期六早上,你要去超市进行采购,你自己想买两斤小龙虾,你儿子和你说要一袋巧克力,然后你老婆说家里没有酱油了买一瓶,你妈又说买两根胡萝卜。那么你到了超市会死板的按照小龙虾、巧克力、酱油、胡萝卜的顺序去采购吗?当然不会,你肯定会大致规划好路线,从离超市入口最近的货架开始采购,避免走回头路。不管你采购的顺序如何,最终你肯定会保证所有人给你的需求全部实现。CPU 也是如此,虽然是机器,但它也会规划更为合理的执行方式,确保程序运行正确的情况下,提高效率。


采购顺序得到优化

单例中的有序性问题

public class Singleton {private static **volatile** Singleton instance; private Singleton (){}public static Singleton getSingleton() {if (instance == null) {                         synchronized (Singleton.class) {if (instance == null) {       instance = new Singleton();}}}return instance;}}

这边必须对变量instance加volatile关键字,因为 instance = new Singleton (); 这一行代码会被编译为三条指令:
1.正常指令执行顺序:
(1).为instance分配一块内存A
(2).在分配的内存A上初始化instance实例
(3).把内存A的地址赋值给instance变量

2.而编译器优化后可能会变成:
(1).为instance分配一块内存A
(2).把内存A的地址赋值给instance变量
(3).在分配的内存A上初始化instance实例


可以看出优化后第2步和第3步调换了位置。加入线程A正在初始化instance,此时执行完第2步,正要执行第3步。而线程B执行到if(instance==null)的判断,那么线程B就会直接得到未初始化好的instance,此时线程B使用此instance显然是有问题的。

JMM(Java Memory Model)

Java内存模型

JMM为程序中的所有操作定义了一定的规则,叫做Happens-Before,无论两个操作是否在同一个线程,如果要想保证操作A能看到操作B的
结果,那么A、B之间一定要满足Happens-Before规则。

Happens-Before规则

它可以指导你如何开发多线程的程序,而不至于陷入混乱之中。你所开发的多线程程序,如果想对共享变量的操作符合你所设定的顺序,那么需要依照Happens-Before规则来开发。
下面我们就来看一下Happens-Before规则:
1.程序顺序规则
如果程序A操作在B操作之前,那么线程中A操作将在B操作前执行。
2.上锁原则
不同线程对同一个锁的lock操作一定在unlock前。
3.volatile变量原则
对于volatile变量的写操作会早于对其的读操作。
4.线程启动原则
A线程中调用threadB.start()方法,那么threadB.start()方法执行会早于B线程中任何的动作。
5.传递规则
如果A早于B执行,B早于C执行,那么A一定早于C执行
6.线程中断规则
线程interrupt()方法的执行一定早于检测到线程的中断信号。
7.线程终结规则
如果线程A终结了,并且导致另外一个线程B中的ThreadA.join()方法取得返回,那么线程A中所有的操作都早于线程B在ThreadA.join()之后的动作。
8.对象终结规则
一个对象初始化操作肯定先于它的finalize()方法。

我们只有充分理解了Happens-Before原则,才能在编写多线程程序的时候,尽量避免数据的不一致性,让多线程程序按照我们设计的次序执行。

僵持不下——死锁

所谓的死锁,就是因为某种原因,达不到解锁的条件,导致某线程对资源的占有无法释放,其他线程会一直等待其解锁,而被一直block住。

死锁产生的原因

1.交叉死锁

public class DeadLock {private final String write_lock = new String();private final String read_lock = new String();public void read() {synchronized (read_lock) {System.out.println(Thread.currentThread().getName() + " got read lock and then i want to write");synchronized (write_lock) {System.out.println(Thread.currentThread().getName() + " got read lock and write lock");}}}public void write() {synchronized (write_lock) {System.out.println(Thread.currentThread().getName() + " got write lock and then i want to read");synchronized (read_lock) {System.out.println(Thread.currentThread().getName() + " got write lock and read lock");}}}public static void main(String[] args) {DeadLock deadLock = new DeadLock();new Thread(() -> {while (true) {deadLock.read();}},"read-first-thread").start();new Thread(() -> {while (true) {deadLock.write();}},"write-first-thread").start();}
}

DeadLock 类有一个读方法和一个写方法,读方法获取读锁后,又尝试获取写锁。而写方法获取写锁后,又尝试获取读锁。这种情况下,两个线程会互相等待对方的锁释放,从而形成了死锁。

2.内存不足
某系统内存 20M,两个线程正在分别执行任务,各自已经使用了 10M 内存。但是执行到一半时需要更大的内存,但是系统已经没有内存可供使用。那么两个线程都会等待对方执行完毕 时释放内存。这就造成了两个线程互相等待,从而形成死锁。

3.一问一答式的数据交换
所谓的一问一答式数据交换就是客户端发送请求,服务端返回响应。如果在交互过程中出现了数据的丢失,双方产生误解,以为对方没有收到消息,陷入等待之中。如果此时没有设置 timeout,就会造成互相的等待一直持续下去,从而形成死锁。

4.数据库锁
如果某个线程对数据库表或者行加锁,但是意外导致没能正确释放锁,而其他线程则会等待数据库锁的释放,从而陷入死锁。

5.文件锁
某个线程获取文件锁后开始执行。但是执行过程中意外退出,而没能释放锁。那么其他等待该文件锁的线程将会一直等待,直到系统释放文件句柄的资源。

6.死循环
假如某个线程,由于编码问题,在对资源加锁后,陷入死循环,导致一致无法释放锁。

原子性轻量级实现——Atomic与CAS

Atomic 相关类在 java.util.concurrent.atomic 包中。针对不同的原生类型及引用类型,有 AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference 等。另外还有数组对应类型 AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray。

CAS是Campare and swap的缩写,翻译过来就是比较替换,CAS是乐观锁的一种实现,而Synchronized则悲观锁的一种实现。

悲观锁:认为每一次操作大概率会有其它线程并发,所以自己在操作前都要对资源进行锁定,这种锁定是排他的。悲观锁的
缺点是不但把多线程并行转化为串行,而且加锁和释放锁都会有额外的开支。

乐观锁:认为每一次操作大概率不会有其它线程并发,所以操作时不加锁,而是在对数据操作时比较数据的版本,和自己更新前取得的
版本一致才进行更新。

Atomic 源代码分析

public final int getAndAddInt(Object obj, long valueOffset, int var) {int expect;// 利用循环,直到更新成功才跳出循环。do {// 获取value的最新值expect = this.getIntVolatile(obj, valueOffset);// expect + var表示需要更新的值,如果compareAndSwapInt返回false,说明value值被其他线程更改了。// 那么就循环重试,再次获取value最新值expect,然后再计算需要更新的值expect + var。直到更新成功} while(!this.compareAndSwapInt(obj, valueOffset, expect, expect + var));// 返回当前线程在更改value成功后的,value变量原先值。并不是更改后的值return expect;}

三个入参,第一个 obj 传入的是 AtomicInteger 对象自己,第二个是 value 变量的内存地址,第三个则是要增加的值。
你现在一定很好奇 compareAndSwapInt 的方法是如何实现的。我们点开此方法后,可以看到是一个 native 方法,native 方法使用 C 语言编写。由于 JDK 并未开源,我们只能下载开源版本的 OpenJDK。
可以看到在 compareAndSwapInt 源代码的最后,调用了 Atomic::cmpxchg (x,addr,e)。这个方法在不同的平台会有不同的实现。不过总的思想如下:
1.判断当前系统是否为多核处理器;
2.执行 CPU 指令 cmpxchg,如果为多核则在 cmpxchg 加 lock 前缀。
可以看到最终是通过 CPU 指令 cmpxchg 来实现比较交换。那么 Lock 前缀起到什么作用呢?加了 Lock 前缀的操作,在执行期间,所使用的缓存会被锁定,其他处理器无法读写该指令要访问的内存区域,由此保证了比较替换的原子性。而这个操作过程称之为缓存锁定。

CAS的缺点

CAS最终通过CPU指令实现,把无谓的同步消耗降到最低,但是没有银弹,CAS也有些几个致命的缺点:
1.比较替换如果失败,则会一直循环,直至成功。这在并发量很大的情况下对CPU的消耗将会非常大。
2.只能保证一个变量自身操作的原子性,但多个变量操作要实现原子性,是无法实现的。
3.ABA问题

解释一下ABA问题:
假如本线程更新前取得期望值为A,和更新操作之间的这段时间内,其它线程可能把value改为了B又改回成A。而本线程更新时发现value和
期望值一样还是A,认为其没有变化,则执行了更新操作。但其实此时的A已经不是彼时的A了。
大多数情况下ABA不会造成业务上的问题,但是如果你认为对你业务有影响,则必须要解决。JDK提供了AtomicStampedReference 类,通过对Atomic包装的变量增加版本号,来解决ABA的问题,即使value还是A,但如果版本变化了,也认为比较失败。

让你眼见为实——volatile详解

被volatile修饰的变量,会确保值的变化被其它线程所感知,从而从主存中取得该变量最新的值。
volatile关键字可以用来修饰实例变量和类变量。被volatile修饰后,该变量可以获得以下特性:
1.可见性
任何线程对其修改,其它线程马上就能读取到最新值
2.有序性
禁止指令重排序

CPU为了提升速度,采用了缓存,因此造成了多个线程缓存不一致的问题,这也是可见性的根源。为了解决缓存一致性,我们需要了解缓存一致性协议,MESI协议是目前主流的缓存一致性协议。

volatile是如何保证可见性的?
lock addl $0x0,(%rsp)
lock的作用是在其有效的范围内锁住总线,从而执行该行代码线程所在的处理器能够独占资源。由于总线被锁定,开销很大的。所以新的CPU实现已经不会锁住总线,而是锁定变量所在的缓冲区域,从而保证了数据的可见性。
volatile是如何保证有序性的?
volatile的有序性是通过内存屏障。所谓内存屏障就是在屏障前的所有指令可以重排序,屏障之后的所有指令也可以重排序,但是重排序的时候不能越过内存屏障。也就是说内存屏障前的指令不会重排序到内存屏障之后,反之亦然。

volatile的局限性:
1.volatile的可见性和有序性只能作用于单一变量;
2.volatile不能确保原子性;
3.volatile不能作用于方法,只能修饰实例或者类变量;
4.volatile只能保证可见性,不能保证同步,同步只能靠synchronized或者其它加锁方式保证;

资源有限,请排队等候——synchronized的使用、原理及缺陷

synchronized的作用

举个例子:
我们去体检,每个人可以并行从家里到医院,并行拿表、填表,并行走到各个检查室门口。但是,一旦要做检查了,我们就需要在检查室门口排队。这是因为只有一个大夫做检查,大夫是共享资源。对共享资源的访问我们要保证同步,否则就会出现问题。

synchronized作用域中的代码为同步执行的,也就是并发的情况下,执行到对同一个对象加锁的synchronized代码块时,为串行执行。此外,synchronized可以确保可见性,在一个线程执行完synchronized代码后,所有代码中对变量值的变化都能立即被其它线程所看到。

synchronized的使用

1.选用一个锁对象,可以是任意对象;
2.锁对象锁的是同步代码块,并不是自己;
3.不同类型的多个Thread如果有代码要同步执行,锁对象要使用所有线程共同持有的同一个对象;
4.需要同步的代码放到大括号中。需要同步的意思就是需要保证原子性、可见性、有序性中的任何一种或多种。不要放不需要同步的代码进来,影响代码效率。

synchronized的原理

synchronized 的秘密其实都在同步对象上。就像上文所说,这个对象就是一个看门人,每次只允许一个线程进来,进门后此线程可以做任何自己想做的事情,然后再出来。此时看门人会吼一嗓子:没人了,可以进来啦!其它线程听到吼声,马上都冲了过来。但总有个敏捷值最高的线程先冲入门内,那么其它线程只好继续等待。
当一个线程获取了 monitor lock 后,其它线程如果运行到获取同一个 monitor 的时候就会被 block 住。当这个线程执行完同步代码,则会释放 monitor lock。在后一个线程获取锁后,happens-before 原则生效,前一个线程所做的任何修改都会被这个线程看到。

synchronized 使用的为非公平锁,如果你需要公平锁,那么不要使用 synchronized。可以使用 ReentrantLock,设置为公平锁。

线程作用域内共享变量——ThreadLocal

ThreadLocal使用场景

1.存储需要线程隔离的数据。比如线程执行的上下文信息,每个线程是不同的,但是对于同一个线程来说会共享同一份数据。
2.跨层传递参数。使用ThreadLocal后,在第一层把变量值保存到ThreadLocal中,在使用的层次方法中直接从ThreadLocal中取出,而不用作为参数在不同方法中传来传去,不过千万不要滥用ThreadLocal,它的本意并不是用来跨方法共享变量的。

如何使用ThreadLocal

public final class OperationInfoRecorder {private static final ThreadLocal<OperationInfoDTO> THREAD_LOCAL = new ThreadLocal<>();private OperationInfoRecorder() {}public static OperationInfoDTO get() {return THREAD_LOCAL.get();}public static void set(OperationInfoDTO operationInfoDTO) {THREAD_LOCAL.set(operationInfoDTO);}public static void remove() {THREAD_LOCAL.remove();}
}

1、static 确保全局只有一个保存OperationInfoDTO对象的ThreadLocal实例;
2、final 确保ThreadLocal的实例不可更改。防止被意外改变,导致放入的值和取出来的不一致。另外还能防止ThreadLocal的内存泄漏
3. 在使用完ThreadLocal变量后,手动remove掉,防止ThreadLocalMap中Entry一直保持对value的强引用。导致value不能被回收。

java并发编程(慕课网)相关推荐

  1. java 并发框架源码_某网Java并发编程高阶技术-高性能并发框架源码解析与实战(云盘下载)...

    第1章 课程介绍(Java并发编程进阶课程) 什么是Disruptor?它一个高性能的异步处理框架,号称"单线程每秒可处理600W个订单"的神器,本课程目标:彻底精通一个如此优秀的 ...

  2. java并发编程实践 读书笔记_Java - 并发编程实践(读书笔记)

    [注] 同步机制保证:1)原子性 2)内存可见性: Volatile变量只能保证:1)可见性: - 恰当的同步,同步的弱形式,确保对一个变量的更新以可预见的方式告知其他线程. [注] 用锁来协调访问变 ...

  3. Java并发编程实战_一线大厂架构师整理:java并发编程实践教程

    并发编程是Java语言的重要特性之一, 在Java平台上提供了许多基本的并发功能来辅助开发多线程应用程序.然而,这些相对底层的并发功能与上层应用程序的并发语义之间并不存在一种简单而直观的映射关系.因此 ...

  4. 【Java并发编程:volatile关键字之解析】

    Java并发编程:volatile关键字解析 - Matrix海子 - 博客园 在Java 5之前,volatile是一个备受争议的关键字:因为在程序中使用它往往会导致出人意料的结果.在Java 5之 ...

  5. 学习笔记:Java 并发编程①_基础知识入门

    若文章内容或图片失效,请留言反馈. 部分素材来自网络,若不小心影响到您的利益,请联系博主删除. 视频链接:https://www.bilibili.com/video/av81461839 视频下载: ...

  6. 《Java 并发编程的艺术》迷你书

    本文源自InfoQ发表的<Java 并发编程的艺术>电子书  作者:方腾飞  序言:张龙 免费下载此迷你书 推荐序 欣闻腾飞兄弟的<聊聊并发>系列文章将要集结成InfoQ迷你书 ...

  7. 【推荐】《Java 并发编程的艺术》迷你书

    本文源自InfoQ发表的<Java 并发编程的艺术>电子书  作者:方腾飞  序言:张龙 免费下载此迷你书 推荐序 欣闻腾飞兄弟的<聊聊并发>系列文章将要集结成InfoQ迷你书 ...

  8. java并发编程2-一起看Happens-Before 规则的前因后果

    ​ 上一章 可见性.原子性.有序性的追根溯源 我们了解到了java并发编程常见的3个问题,那么有问题肯定需要解决呀!这章我们聊聊如何解决可见性和有序性是怎么解决的--java内存模型. 什么是java ...

  9. Java并发编程初级-未使用Java并发包的时候

    转自Java并发编程网   http://ifeve.com/java_multi_thread_programming_1_2/ 本文介绍了Java原生的多线程技术(1.2),通过详细介绍wait和 ...

最新文章

  1. python3 +改进HTMLTestRunner.py 测试报告显示
  2. activemenu怎么拼 vue_Vue-el-menu使用,点击按钮跳转指定页面
  3. 路由怎么定位到当前页面的组件_Angular 重载当前路由
  4. 【面试】JAVA中交换值的三种方式
  5. 游戏AI探索之旅:从AlphaGo到MOBA游戏
  6. Codeforce - 920C- Swap Adjacent Elements 排序|思维
  7. dtm文件生成等高线 lisp_南方cass如何用图面高程点生成等高线
  8. Js事件对象EventUtil
  9. Atitit.软件按钮与仪表盘(13)--全文索引操作--db数据库子系统mssql2008
  10. java导出到txt_Java生成TXT文本并下载
  11. 【资源下载】SocketTool 下载 资源下载
  12. java hashmap value排序_按照Value对HashMap排序
  13. Directshow 采集-截屏和显示
  14. Win10电脑关机后立即自动重启怎么办
  15. 数据结构:八种数据结构大全
  16. 滴滴夜莺发布v3.3.0版本
  17. Fiddler抓部分app时网络连接失败
  18. 要做单片机课课设的快看过来1:KEIL安装以及C51环境搭建和Protues安装保姆教程
  19. 闲聊一下吧,发发牢骚,嘿嘿
  20. 蓝牙架构(6)—— 3 数据传输架构(3.1 核心传输载体)

热门文章

  1. iOS-UILabel多行显示文本内容、根据文本长短自动计算文本框高度
  2. 金仓数据库 KingbaseES SQL 语言参考手册 (8. 函数(二))
  3. Java程序员,面试阿里Java开发你必需要先理解的题库!
  4. 中国信通院推出了一个“APP签名服务系统,可防篡改、可追溯、第三方认证“的初步了解
  5. 有一段传奇 叫提高班
  6. 解决ImportError: /home/xxx/Software/anaconda3/lib/libstdc++.so.6: version `GLIBCXX_3.4.29‘ not found报错
  7. python修改自己的代码_python修改微信和支付宝步数的示例代码
  8. svg动画,渐变,阴影
  9. C/C++试题检测一下自己的编码技能
  10. App内测分发怎么做?蒲公英内测托管平台教程