上下文切换

CPU时间分片导致,任务间是切换,即任务从保存到再加载就是一次上下文切换。

上下文切换影响多线程的执行速度,如何减少上下文的切换:

1、无锁并发编程、2、CAS算法(atomic包使用CAS算法更新数据,不需要加锁)

3、使用最少线程 4、协程

死锁

多个锁资源之间互相等待彼此锁资源,导致程序之间无法释放锁资源也无法获取锁资源的现象。

避免死锁的常见方法:

1、避免一个线程同时获取多个锁

2、避免一个线程占用多个资源,尽量保证每个锁只占用一个资源

3、尝试使用定时锁(lock.trylock(timeout))

小总结

为何同个JVM模型中多个会出现数据不一致?

深究原因是当前操作系统中,多核多CPU情况下, 处理器不直接与内存进行通信,而是系统内存 数据读取到内部缓存(L1、L2、L3)后再进行操作,并且该操作时间未知。

内存屏障:一组处理器指令,实现对内存操作的顺序限制

缓存命中:如果进行高速缓存行填充操作的内存地址仍然是下次处理器访问的地址,则处理器从缓存中读取数据而不是内存。

synchronize

synchronize实现同步的基础:Java中每个对象都可作为锁;具体表现为三种状态

  • 对象是普通同步方法,锁是当前的实例对象
  • 对象是静态同步方法,锁是当前类的Class对象
  • 对于同步方法块,锁是synchronize括号里配置的对象

synchronize用的锁是存在Java的对象头Mark Work里。

Java中锁一共有4种状态,级别从低到依次为:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,这几种状态会随着竞争情况逐渐升级,锁可以升级但不可降级。
锁优缺点

处理器如何实现原子操作?

第一种机制是可通过总线锁保证原子操作,第二种机制是通过缓存锁定来保证原子性。

缓存锁定:内存区域被缓存到处理器缓存行中,并且在LOCK操作期间被锁定,当执行锁操作回写到内存时,处理器不进行总线锁定,而是修改内部的内存地址,并运行它的缓存一致性保证操作的原子性。
原因:缓存一致性会阻止同时修改由俩个以上的处理器缓存的内存区域数据,当其中处理器回写已被锁定的缓存行的数据时,会使缓存行无效。

Java中通过锁与循环CAS(compare and swap 比较与设置) 方式实现原子操作

CAS实现原子性操作的大三问题:1)、ABA问题 2)、循环时间长开销大 3)、只能保证一个共享变量的原子操作

如何解决CAS操作多个共享变量时原子性问题?
第一种可直接使用锁机制,第二种可使用JDK提供的AtomicReference类来保证引用对象之间的原子性,可把多个变量放入到一个对象里来进行CAS操作

JVM内部实现了很多锁机制,除了偏向锁,JVM实现锁的方式都使用循环CAS,即当一个线程进入同步块的时候使用循环CAS方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。

Java内存模型

线程之间如何通信与线程之间如何同步?
通信可通过1、共享内存隐式进行;2、消息传递显示进行。
同步是指程序中用于控制不同线程间操作发生相对顺序的机制。
Java并发采用的是共享内存模型,同步是显示进行的,通信是通过共享内存隐式进行的。

在Java中,所有实例域、静态域和数组元素都存储在堆内存中,堆内存是线程之间共享的,局部变量,方法定义参数和异常处理器参数都不会在线程间共享,因此不会有内存可见性问题。

Java线程之间的通信由Java内存模型(JMM)控制。JMM定义了线程与主内存之间的抽象关系:线程之间的共享变量存储在主内存,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。

线程间的通信模型是如何实现通信的?
线程A与线程B之间要实现通信,必须经历俩个步骤
1)线程A把本地内存A更新过的共享变量刷新到主内存中
2)线程B到主内存去读取线程A之间更新过的共享变量。
线程间的通信是通过主内存进行的。

线程间通信

线程间的通信必须经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,提供内存可见性保证。

重排序

执行代码时,为了提高性能,编译器与处理器常常会对指令进行重排序,重排序分为三类:编译器优化的重排序、指令级并行的重排序、内存系统的重排序。第一种属于编译器重排序,第二、三种为处理器重排序。

:baby_chick:top-背景知识:处理器使用写缓存区临时保存向内存写入的数据。写缓存区可以保证指令流水线持续运行,避免由于处理器停顿下来等待想内存写入数据而产生延迟。通过批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,减少对内存总线的占用。每个处理器上的缓冲区,仅仅对它所在的处理器可见。

什么情况下会出现指令重排序?什么是数据依赖性?重排序一定会导致程序不及预期?重排序对程序的影响是什么?
1、当数据存在依赖性的时候,编译器与处理器并不会对指令进行重排序。
2、对一个数据进行(写后读、写后写、读后写)操作时,如何重排序这俩个操作的执行顺序,则会对程序的执行结果产生影响,因此它们之间的操作顺序 存在数据的依赖性,这便是数据依赖性问题。
3、单线程情况下,处理器对数据依赖性的操作并不会进行指令重排序,因此不会存在程序运行不及预期的情况。
4、在多线程情况下,不同处理器和不同线程之间的数据依赖性不被编译器与处理器考虑,单线程语序可能被多线程破坏,因此多线程情况下会需要考虑重排序问题以及内存可见性问题,这也是程序猿进行多线程开发需要考虑的问题。

顺序一致性是一个理想化的模型,它的核心是每个线程对共享变量的修改都对其他线程变得立刻可见。(联系到最终一致性问题)

JMM无法保证顺序一致性,程序的整体执行是无序的,比如,在当前线程把写过的数据缓存到本地内存中,在没有刷新到主内存之前,这个写操作仅对当前线程可见;从其他线程的角度来观察,只有当前线程把本地内存写过的数据刷新到主内存之后,这个写操作才能对其他线程可见。这也是整体顺序不一致的原因。

happens-before

定义:指定俩个操作之间的执行顺序,这俩个操作可以在一个线程之间,也可以在不同线程之间。JMM通过happens-before关系向程序猿提供跨线程的内存可见性保证。如果A happens-before B,则A的执行结果将对B可见,且A执行顺序在B之前。

happens-before规则:

1)程序顺序规则 2)监视器规则 3)volatile规则 4)传递性 5)start规则 ** 6) **join规则

volatile

volatile的内存语意

可把一个volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步,这样子与一个普通变量用synchronize锁来进行读写进行同步的效果是相同。这是由于锁的happens-before规则保证了释放锁与获取锁的俩个线程的内存可见性,一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。

volatile变量自身具有如下特征

  • 可见性:一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性:对任意单一volatile变量的读/写具有原子性。但是类似volatile++这种复合操作不具有原子性

注意:对多个volatile变量的操作或类似volatile++这种复合操作,不具备原子性功能。

volatile写内存语意:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

volatile读内存的语意:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存读取共享变量。

volatile的内存语意的实现

volatile实现原理:通过合适的内存屏障禁止重排序,保证多线程之间读写的准确性。

:black_nib:由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特征可以确保对整个临界区代码的执行具有原子性,在功能上,锁比volatile更强大,在可伸缩性和执行性能上,volatile更有优势,如果想用volatile替代锁,需谨慎。

锁的内存含义

释放锁的内存语意:当线程释放锁时,JMM会把线程对应的本地内存中的共享变量刷新到主内存中。

获取锁的内存语意:当线程获取锁时,JMM会把线程对应的本地内存置为无效。

线程A释放锁,线程B获取锁,这个过程实质上是线程A通过主内存向线程B发送信息。

锁内存含义的实现

公平锁与非公平锁内存语意:

  • 公平锁与非公平锁释放时,最后都要写一个volatile变量status
  • 公平锁获取时,首先会去读volatile变
  • 非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile变量读和volatile写的内存语意。

锁释放-获取的内存语意实现至少有下面俩种方式

1、利用volatile变量的写-读所具有的的内存含义

2、利用CAS所附有的volatile读和volatile写的内存含义。

concurrent包的实现

由于CAS同时具有volatile读和volatile写的内存含义,因此Java线程之间的通信有都是通过CAS与volatile变量之间的读写(4种)来实现的。

:e-mail: AQS(java.util.concurrent.atomic包)非阻塞数据结构和原子变量类。

volatile读/写和CAS可以实现线程之间的通信,把这些特征整合在一起,就形成了整个concurrent包得以实现的基石

concurrent包结构

final域的内存语意

Java中final修饰的实际上是为了表示常量,那么常量就必须做到不可变(最终的),那么final修饰的常量最好进行初始化操作,那么为何做出初始化操作后,该常量就不可变呢,底层是如何做到的呢?这就需要了解final域的内存语意。

:aerial_tramway:String类型是final修饰的,因此String类型是最终的,是一个常量

final域中,编译器与处理器要遵循俩个重排序规则

  • 构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用对象,这俩个操作之间不能重排序
  • 初次读一个包含final域的对象的引用,与随后初次读这个final域,这来个操作不可重排序

top:普通变量在构造方法中进行初始化时,可能由于重排序,导致在初始化方法外才对该变量进行初始化。

线程

线程的状态

Java线程的生命周期为五种状态,分别为:新建、就绪、运行、阻塞、销毁

新建:刚使用new方法,new出来的线程则处于新建状态

就绪:调用线程的start方法,这时候线程处于等待CPU分配时间片段阶段,等待执行

运行:当就绪状态的线程获取CPU资源,便进入运行状态,run方法定义了线程的操作与功能。

阻塞:在运行状态,可能因为某些原因导致运行状态的线程变成阻塞状态,比如sleep()、wait()之后线程就处于阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify()、notifyAll()方法,唤醒的线程会再次进入就绪状态。

销毁:线程正常执行完毕后或者线程被强制性终止或出现异常,那么线程就会被销毁,释放资源。

线程生命周期

线程的中断:中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作,中断好比其他线程对该线程打了招呼,其他线程可通过调用该线程的interrupt()方法对其进行中断操作。

:japanese_ogre:中断并不是一种错误,是一种标识符(信号),编译时可忽略该标识,也可以利用该机制实现一些功能。中断操作是一种简便的线程间的交互方式。

线程间的通信

1、通过synchronize关键字修饰代码块或者volatile关键字修饰变量达到不同线程之间内存可见性问题,这是一中线程间的通信方式。

2、等待/通知机制

一个线程修改一个对象的值,另一个线程感知到变化,然后进行相应的操作,整个过程开始与一个线程,最终执行又是另一个线程,这是一种生产者-消费者模式。

探索:可通过一个线程轮训(休眠)某个变量是否发生改变来完成消费者工作,但是该操作开销大

等待/通知是object类的方法[ notify()、notifyAll()、wait() ],因此所有对象都具有等待/通知机制。

因此,等待/通知机制可用于俩个线程通过对象O来进行交互。

注意:使用notify()、notifyAll()、wait()时需要对调用对象加锁。

3、Thread.join()方法使用

如果线程A执行了Thread.join()方法,则含义是:线程A等待thread线程终止之后才从thread.join()返回。join()方法的逻辑结构与等待/通知一致,实际上是加锁-循环-处理逻辑3个步骤。

Java中的锁

锁是用来控制多个线程访问共享资源的方式。一个锁能防止多个线程并发访问共享资源(读写锁除外)

LOCK接口

lock接口提供了synchronize关键字类似的同步功能,在使用时是显示的获取锁和释放锁。

LOCK锁API
独占锁Mutex是一个自定义同步组件,它在同一时刻只允许一个线程占有锁。Mutex中定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放同步状态。

Mutex是一个不支持重进入的锁。而synchronized关键字隐式的支持重进入,比如一个synchronized修饰的递归方法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获得该锁,而不像Mutex由于获取了锁,而在下一次获取锁时出现阻塞自己的情况。

重入锁如何实现可重进入?
重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决以下两个问题。
1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
2)锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。

公平锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平锁虽然可能造成线程“饥饿”,但极少线程切换,保证了更大的吞吐量。

独占锁Mutex和可重入锁ReentrantLock基本都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

Java并发容器和框架

ConcurrentHashMap与ConcurrentLinkedQueue

阻塞队列

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

阻塞队列常常用于生产者/消费者模式场景。

在阻塞队列不可用时,这来个附加操作提供了4种处理方式

阻塞队列操作方法

Java7提供了7种阻塞队列(数组、队列、链表等数据结构组成),分别为:

  • ArrayBlockingQueue:一个数组结构的有界阻塞队列
  • LinkedBlockingQueue:一个链表结构的有界阻塞队列(默认长度是Integer.MAX_VALUE)
  • PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列
  • DelayQueue:一个支持延时获取元素的无界阻塞队列,队列使用PriorityBlockingQueue实现。
  • SynchronousQueue:一个不存储元素的无界阻塞队列
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列
  • LinkedBlockingDeque:链表结构组成的双向阻塞队列

13个原子操作类

JDK1.5之后提供Java.util.conncurent.atomic(Atomic包)提供13个类,属于4种类型的原子更新操作,分别是原子更新基本类型、原子更新数组、原子更新引用、原子更新属性(字段),Atomic包基本都是使用Unsafe实现的包装类。

原子更新基本类

提供三个类: AtomicBoolean、AtomicInteger、AtomicLong

三个类基本提供一样的方法,以AtomicInteger

int addAndGet(int delta):以原子方式将输入的数值与实例中的值相加并返回结果。
boolean  compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
int getAndIncrement():以原子方式将当前值加1,这里返回的是自增前的值。
int getAndSet(int newValue):以原子方式设置newValue值,并返回旧值

那么如何原子性的更新char、float、double?

可利用CAS是实现。

原子更新数组

提供四个类:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray、Atomic

三个类主要提供原子方式更新数组里的整数。以AtomicIntegerArray为例

int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加。
boolean compareAndSet(int i,int expect,int update):如果当前索值等于预期值,则以原子方式将数组中的索引值设置为update值

原子更新引用

原子更新基本类型的AtomicInteger只能更新一个变量,如果需要更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供三个类:AtomicReference(原子更新引用类型)、AtomicReferenceFieldUpdate(原子更新引用类型里的字段)、AtomicMarkableReference(原子更新带有标志位的引用类型)。

原子更新字段类

如果需要原子地更新某个类 里的某个字段,就需要使用原子的更新字段类,Atomic包提供三个类:AtomicIntegerFieldUpdater(原子更新整型字段的更新器)、AtomicLongFieldUpdater(原子更新长整型字段的更新器)、AtomicStampedReference(原子更新带有版本号的引用类型)--该类将整数值与引用关联起来,可用于原子的更新数据和数据版本号,可以解决使用CAS进行原子更新时可能出现ABA问题。

Java中的并发工具类

JDK中提供了几个并发的工具类:CountDownLatch、CyclicBarrier、semaphore工具类提供了一种并发流程控制的手段,Exchanger工具类提供了在线程间交换数据的一种手段。

CountDownLatch

背景:join用于让当前线程等待join线程执行结束,其原理是不停检查join线程是否存活,如果join线程存活则让当前线程永远等待。

CountDownLatch可运行一个或多个线程等待其他线程完成操作,可以实现join的功能,并且具有更多功能(特定时间后不会继续阻塞该线程)。

CyclicBarrier

进行多个线程阻塞后再进行运行, 可以用于多线程计算数据,最后合并计算结果的场景。

semaphore

semaphore(信号量)是用来控制同时访问特定资源 的线程数量,它通过协调各个线程,以保证合理的使用公共资源。

Exchanger

Exchanger(交换者)是一个用于线程协作的工具类,Exchanger用于线程间的数据交换,它提供了一个同步点,在这个同步点,俩个线程可以交换彼此的数据。俩个线程通过exchange方法交换数据。

如果一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange方法,当俩个线程都到达同步点时,这俩个线程就可以交换数据,将本线程产生的数据传递给对方

Java中的线程池

合理使用线程池能够带来三个好处:1、降低资源消耗;2、提高响应速率;3、提高线程的可管理性。

线程池的实现原理

ThreadPoolExecutor执行execute()方法的流程:

1、如果当前运行的线程少于corepoolSize,则创建新线程来执行任务(该步骤需要获取全局锁)

2、如果运行的线程等于多于corepoolSize,则将任务加入BlockingQueue

3、如果无法继续将任务加入BlockingQueue,则创建新的线程来处理任务(需要获取全局锁)

4、如果创建新线程将使当前运行的线程超出maximumpoolSize,任务将被拒绝,将调用策略。

线程池流程图

线程池中的线程执行任务分俩种情况

1、在execute()方法中创建一个线程时,会让这个线程执行当前任务。

2、这个线程执行完任务后,会反复从BlockingQueue获取任务来执行。

线程池的创建

可通过ThreadPoolExecutor来创建线程池,需要注意几个参数。

  • corePoolSize:核心线程数,当提交一个任务时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建新线程。等到需要执行的任务数大于核心线程数就不再创建。

  • runnableTaskQueue:保存等待执行的任务的阻塞队列

    • ArrayBlockingQueue:基于数据的FIFO队列
    • LinkedBlockingQueue:基于链表的FIFO队列,吞吐量通常比ArrayBlockingQueue高
    • synchronousQueue:不存储元素的阻塞队列
    • priorityBlockingQueue:具有优先级的无限阻塞队列
  • maximumPoolSize:线程池允许创建的最大线程数,如果队列满了,并且已创建的线程小于最大线程数,则线程池会再创建新的线程执行任务,值得注意的是,如果使用了无界的任务线程队列则这个参数没有意义。

  • ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。可使用开源框架guava提供的FactoryBuilder可以快速给线程池里的线程设置更有意义的名字。

  • RejectedExecutionHandle(饱和策略):线程池队列满后采用的策略,默认AbortPolicy

    • AbortPolicy:抛出异常
    • CallerRunsPolicy:只用调用者所在线程来执行任务
    • DiscardOldstPolicy:丢弃队列里最近的一个任务,并执行当前线程。
    • DiscardPolicy:不处理,丢弃
  • KeepAliveTime:线程池的工作线程空闲后,保持存活的时间。如果任务多,可调大时间提高利用率

向线程池提交任务

可使用俩种方式提交任务:execute()和submit()方法

  • execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功,execute()方法输入的任务是一个Runnable类的实例。
  • submit()方法用于提交需要返回值的任务,线程池会返回一个future类型的对象,通过这个future类型的对象可以判断任务是否执行成功,并且可通过get()获取返回值,get()方***阻塞当前线程直到任务完成。

Executor框架

Java的线程既是工作单元,也是执行机制,JDK5开始,把工作单元与执行机制分离开来,工作单元包括Runnable和Callable,而执行机制由Executor框架提供。Executor是基于生产者消费者模式的,提交任务的操作相当于生成者,执行任务的线程相当于消费者。Executor是一个接口,它将任务的提交与任务的执行分离开来。

Runnbale接口和Callable接口的实现类,都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。

任务的俩级调度模型

Executor框架结构

Executor框架主要由3大部分组成

  • 任务:包括被执行任务需要实现的接口,Runnbale接口或Callable接口
  • 任务的执行:包括任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口
  • 异步计算的结果:包括接口Future和实现Future接口的FutureTask类

Executor框架使用示意图

主线程创建Runnbale或Callable接口的任务对象,可把Runnbale或Callable对象executor或者submit提交给ExecutorService,如果ExecutorService.sumbit(...)将返回一个Future接口的FutureTask类,最后主线程可执行FutureTask.get()等待任务执行完成,也可执行FutureTask.cancel()取消任务执行。

通常用工具类Executors提供的方法进行创建;可使用工具类Executors来把一个Runnbale包装成Callable。