java并发编程(慕课网)

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方法:

    @Override
    public 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不能被回收。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值