多线程

11 篇文章 0 订阅

本系列目录

进程、线程、协程

  1. 程序:是为完成特定任务,用某种语言编写的一组指令的集合,即一段静态代码,静态对象。
  2. 进程:是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程,每个程序都有一个独立的内存空间
  3. 线程:是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行. 一个进程最少有一个线程,线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程
  4. 协程:协程是一种用户态的轻量级线程,协程的调度完全由用户控制。从技术的角度来说,“协程就是你可以暂停执行的函数”。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

协程与线程的区别:

  1. 一个线程可以多个协程,一个进程也可以单独拥有多个协程。
  2. 线程进程都是同步机制,而协程则是异步。
  3. 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。
  4. 线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。
  5. 协程并不是取代线程, 而且抽象于线程之上, 线程是被分割的CPU资源, 协程是组织好的代码流程, 协程需要线程来承载运行, 线程是协程的资源, 但协程不会直接使用线程, 协程直接利用的是执行器(Interceptor), 执行器可以关联任意线程或线程池, 可以使当前线程, UI线程, 或新建新程.。
  6. 线程是协程的资源。协程通过Interceptor来间接使用线程这个资源。

happens-before

如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。与程序员密切相关的 happens-before 规则如下。

  1. 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续
    操作。
  2. 监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
  3. volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个
    volatile 域的读。
  4. 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before
    C。

深入理解happens-before规则

并发与并行的区别是什么

你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。

你知道那些并发容器?

ConcurrentHashMap、 CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentLinkedQueue、ConcurrentLinkedDeque、ConcurrentSkipListMap、ConcurrentSkipListSet、ArrayBlockingQueue、LinkedBlockingQueue、 LinkedBlockingDeque、PriorityBlockingQueu、SynchronousQueue、LinkedTransferQueue、DelayQueue

什么是阻塞队列?阻塞队列常用的应用场景?

阻塞队列是一个支持两个附加操作的队列、这两个附加操作支持阻塞的插入和移除方法。
1、支持阻塞的插入方法:当队列满时、队列会阻塞插入元素的线程,直到队列不满。
2、支持阻塞的移除方法:当队列空时、获取元素的线程会等待队列变为非空。

应用场景:
常用于生产者和消费者场景、生产者是往队列里添加元素的线程、消费者是从队列里取元素的线程。阻
塞队列正好是生产者存放、消费者来获取的容器。

Java里有哪些阻塞队列

名称说明
ArrayBlockingQueue数组结构组成的,有界阻塞队列
LinkedBlockingQueue链表结构组成的,有界阻塞队列
PriorityBlockingQueue支持优先级排序,无界阻塞队列
DelayOueue优先级队列实现,无界阻塞队列
SynchronousOueue不存储元素,阻塞队列
LinkedTransferQueue链表结构组成,无界阻塞队列
LinkedBlockingDeque链表结构组成,双向阻塞队列

线程状态

jdk1.8中为线程设置了5个状态:

  1. NEW:新创建的线程,未调用start()方法
  2. RUNNABLE:可能是正在运行,也可能是在等待cpu进行调度,可以理解为READY(start())和RUNNING
  3. BLOCKED:一般是线程等待获取一个锁,来继续执行下一步的操作,例如使用synchronized修饰的代码块,等待获取锁的线程就是处于这种状态
  4. WAITING:调用以下方法进入这种状态:Thread.sleep(long)、Object.wait(long)、Thread.join(long)、LockSupport.parkNanos(long)、LockSupport.parkUntil()
  5. TERMINATED:线程执行结束之后的状态

wait、sleep、yield、join、interrupt

  1. Object.wait:线程会释放掉它所占有的锁,从而使别的线程有机会抢占该锁。
    当前线程必须拥有当前对象锁,否则会抛出IllegalMonitorStateException异常。唤醒当前对象锁的等待线程使用notify或notifyAll方法,也必须拥有相同的对象锁,否则也会抛出IllegalMonitorStateException异常。wait()和notify()必须在synchronized函数或synchronized block中进行调用。如果在non-synchronized函数或non-synchronized block中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。
  2. Thread.Sleep:在指定时间内使当前线程进入BLOCKED状态,不会释放锁
  3. Thread.yield:作用是让步,不会释放锁。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!
  4. Thread.join:等待调用join方法的线程结束,再继续执行。
  5. Thread.interrupt:改变中断状态而已,它不会中断一个正在运行的线程。这一方法实际完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程就得以退出阻塞的状态。更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个 InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。如果线程没有被阻塞,这时调用 interrupt()将不起作用,直到执行到wait(),sleep(),join()时,才马上会抛出InterruptedException。interrupt()方法的简单理解

线程间通信方式

  1. 使用volatile关键字:volatile保证了被修饰的变量对所有线程的可见性
  2. 使用Object类的wait() 和 notify() 方法,wait和 notify必须配合synchronized使用,wait方法释放锁,notify方法不会释放锁直到代码执行完
  3. 使用JUC工具类 CountDownLatch,基于AQS框架,相当于也是维护了一个线程间共享变量state
  4. 使用 ReentrantLock 结合 Condition
  5. 基本LockSupport实现线程间的阻塞和唤醒

参考:线程间通信的几种实现方式

主存和工作内存交互时虚拟机保证的天然原子性操作有哪些

lock(锁定)、unclock(解锁)、read(读取)、load(载入)、use(使用)、assign(赋值)、store(储存)、write(写入)

  1. 虚拟机未将lock、unlock直接开放给用户,但是提供了更高层次的字节码指令monitorenter、monitorexit来隐式使用这两个指定,反映到Java就是synchronized关键字,因此synchronized修饰的代码块具备原子性
  2. 我们可以认为基本数据类型的访问读写是具有原子性的(long、double例外,但是大部分商用虚拟机都将它们读写当做原子性对待,平时在写long、double变量时不需要声明为volatile)

谈谈volatile关键字

当一个变量定义为 volaiile 之后,它将具备两种特性:

  1. 第一是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的,在各线程的工作内存中变量也存在不一致的情况,但是由于每次使用变量前都需要先刷新,执行引擎看不到不一致的情况,因此可以认为不存在不一致的情况。而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成,例如,线程 A 修改一个普通变量的值,然后向主内存进行回写,另外一条线程 B 在线程 A 回写完成了之后再从主内存进行读取操作,新变量值才会对线程 B 可见。

    不过,无法保证非原子性操作的变量线程安全,例如i++问题,对以下代码进行反编译:

    private static int i = 0;
    public static void increase() {
        i++;
    }
    

    反编译结果:

     public static void increase();
     Code:
        0: getstatic     #2                  // Field i:I
        3: iconst_1
        4: iadd
        5: putstatic     #2                  // Field i:I
        8: return
    

    getstatic将i的值取到操作栈顶时,volatile保证此时变量是正确的的,但是当执行iconst_1、iadd这些操作时,其它线程已经对i的值进行了修改,putstatic就会将较小的值同步回主内存

  2. 禁止指令重排:指令重排是指CPU在正确处理指令依赖情况以保证程序得出正确结果的前提下,不按程序规定的顺序将多条指令分开发给不同的电路单元处理。被volatile修饰的变量,会在赋值后多执行一步相当于添加内存屏障的操作,指令重排时不能将后面的指令重排到内存屏障之前。

synchronized关键字原理

程序编译后会在添加synchronized关键字代码块的前后分别添加monitorenter和monitorexit字节码指令,这两个指令都需要同一个reference类型的参数来指明要锁定和解锁的对象。执行monitorenter指令是就会尝试获取对象的锁。如果对象没有被锁定或者当前线程已经拥有对象的锁,就把锁的计数器加1,因此对同一个线程来说在synchronized中是可重入的,不会自己把自己锁死。相应的,在执行monitorexit指令时就将锁计数器减1,当计数器为0时释放锁。

乐观锁与悲观锁

  1. 悲观锁,正如其名,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。
  2. 乐观锁会假设整个数据处理过程中数据不会被修改,只有当操作提交操作时才检查数据是否被修改,如果发生冲突了就返回错误信息,反之提交成功。Java使用CAS实现乐观锁。

乐观锁出现的问题

ABA问题,假设有个变量a,线程1读到的值为2,然后进行修改3操作,线程b将a修改为4然后又改回为2,线程1提交时发现数据还是2,提交成功,这就是ABA问题,线程1读取了脏数据。
解决办法就是添加版本号,每次提交时获取最新版本号和之前版本号进行对比,一致就提交。
JUC包通过提供一个带有标记的原子引用类“AomicStampedReference”来解决ABA问题,它可以通过控制变量值的版本来保证CAS正确性,不过目前来说这个类比较鸡肋,大部分情况ABA问题不会影响并发正确性,要解决ABA问题改用互斥同步更高效

互斥同步和非阻塞同步

  1. 互斥同步:多个线程并发访问同一个数据时,保证同一时刻只被一个线程访问,是一种悲观并发策略。互斥同步手段:synchronized是原生语法的互斥锁;ReenterantLock是API层面的互斥锁。
  2. 非阻塞同步:这是一种基于冲突检测的乐观并发策略,先进性操作如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就采取其它补偿措施(例如:不断尝试直到成功),这种乐观的并发策略许多实现都不需要将线程挂起,因此被称为非阻塞同步(Non-Blocking Synchronization)。

    由于需要保证操作和冲突检测两个步骤具备原子性,如果依靠互斥同步就失去了意义,只能依靠硬件指令集的发展,硬件保证一个从语义上看起来需要多次操作的行为通过一条处理器指令就能完成,常用的指令有:

    1. 测试并设置(Test-and-Set)
    2. 获取并增加(Fetch-and-Increment)
    3. 交换(Swap)
    4. 比较并交换(Compare-and-Swap,CAS)
    5. 加载链接/条件储存(Load-linked/Store-Conditional,LL/SC)

CAS原理

CAS:Compare and Swap,即比较再交换。CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

ReenterantLock和synchronized关键字对比

ReenterantLock需要lock()和unlock()配合try catch finally使用,相比synchronized关键字ReenterantLock增加了以下高级功能:

  1. 等待可中断:正在等待的线程可以放弃等待,改为处理其他事情。
  2. 实现公平锁:多个线程等待一个锁是可以按照申请时间顺序依次获取锁,synchronized是非公平的,ReenterantLock默认是非公平锁,可以通过带boolean的构造函数使用公平锁
  3. 绑定多个条件:一个ReenterantLock可以同时绑定多个Condition对象,而synchronized中锁对象的wait()、notify()、notifyAll()可以实现一个隐含条件,如果要和多于一个条件关联就必须再加一个锁,ReenterantLock只需要多次调用newCondition()即可。
    性能方面,1.6之前单核synchronized性能高,多核ReenterantLock性能高。1.6之后对synchronized大大优化,它们性能基本持平synchronized甚至优之,所以现在性能不是选择ReenterantLock的理由

CopyOnWriteArrayList 原理

CopyOnWriteArrayList 读取是完全不用加锁的,并且更厉害的是:写入也不会阻塞读取操作,只有写入和写入之间需要进行同步等待,读操作的性能得到大幅度提升。
CopyOnWriteArrayList 类的所有可变操作(add,set等等)都是通过创建底层数组的新副本来实现的。当 List 需要被修改的时候,并不直接修改原有数组对象,而是对原有数据进行一次拷贝,将修改的内容写入副本中。写完之后,再将修改完的副本替换成原来的数据,这样就可以保证写操作不会影响读操作了。
摘自

ConcurrentHashMap原理

深入浅出ConcurrentHashMap1.8

Reactor线程模型

Reactor线程模型

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值