Java多线程

Java内存模型(JMM)

Java内存模型定义了程序中各种变量的访问规则。其规定所有的变量存储在主内存,线程均有自己的工作内存。

工作内存中保存被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作空间进行,不能直接读写主内存数据。操作完成后,线程的工作内存通过缓存一致性协议将操作完的数据刷回主内存。

as-if-serial

编译器等会对原始的程序进行执行重排序和优化,但是不管怎么重排序,其结果和用户原始程序预定输出结果一致。

happens-before八大原则

  • 程序次序规则:一个线程内写在前面的操作先行发生于后面的。
  • 锁定规则:unlock操作先行发生于后面对同一个锁的lock操作。
  • volatile规则:对volatile变量的写操作先行发生于后面的读操作。
  • 线程启动规则:线程的start方法先行发生于线程的每个动作。
  • 线程中断规则:对线程的interrupt()方法的调用先行发生于被中断线程的代码检测到的中断事件的发生。
  • 线程终止规则:线程中所有操作先行发生于对线程的终止检测。
  • 对象终止规则:对象的初始化先行发生于finalize方法。
  • 传递性规则:如果A操作先行发生于B操作,操作B先行发生于操作C,那么A操作先行发生于操作C。

as-if-serial和happens-before的区别

as-if-serial保证单线程程序的执行结果不变,happens-before保证正确同步的多线程程序的执行结果不变。

原子性操作

一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行,这就是原子性操作。

线程的可见性

可见性指当一个线程修改了共享变量时,其他线程能够立即得知修改,volatile,synchronized,final都能保证可见性。

线程的有序性

即虽然多线程存在并发和指令优化等操作,在本线程内观察该线程的所有执行操作是有序的。

volatile关键字的作用

  1. 保证变量对所有线程的可见性。

    当一个线程修改了变量值,新的值对于其他线程来说是立即可以得知的。

  2. 禁止指令重新排序优化。

    使用volatile变量进行写操作,汇编指令带有lock前缀,相当于一个内存屏障,编译器不会将后面的指令重新排序到内存屏障之前。

Java线程的实现方法

  1. 实现Runnable接口
  2. 集成Thread类
  3. 实现Callable接口

Java线程的状态

线程的状态有new,runnable,block,waiting,timed_waiting,terminated

  • NEW:新建状态,线程被创建且未启动,此时还没有调用start方法。
  • RUNNABLE:运行状态,表示线程正在JVM中执行,但是这个执行并不一定真的在跑,也可能在排队等待CPU资源。
  • BLOCK:阻塞状态,线程等待获取锁,锁还没有获得。
  • WAITING:等待状态,线程内run方法运行完语句,Object.wait()、Thread.join()方法调用也就会进入该状态。
  • TIMED_WAITING:限期等待,在一定时间后跳出状态。调用Thread.sleep(long)、Object.wait(long)、Thread.join(long)进入状态,其中这些参数代表等待时间。
  • TERMINATED:结束状态,线程调用完run方法进入该状态。

线程通信的方式

  1. volatile关键字修饰变量,保证所有线程对变量的访问的可见性。
  2. synchronized关键词,确保多个线程在同一时刻只能有一个处于方法或者同步块中。
  3. wait/notify方法
  4. IO通信

同步线程以及线程调度的相关方法

  • wait:使一个线程处于等待状态,并且释放所持有的对象的锁。
  • sleep:使一个正在运行的线程处于睡眠状态,是一个静态的方法,调用此方法需要处理InterruptedException异常。
  • notify:唤醒一个处于等待状态的线程,调用此方法的时候,不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且和优先级无关。
  • notifyAll:唤醒所有处于等待状态的线程,该方法并不是将对象锁给所有线程,而是让它们竞争,只有获得锁定线程才能进入就绪状态。
  • Java5中,通过Lock接口提供了显式的锁机制,Lock接口中定义了加锁lock和解锁unlock方法,增强了多线程编程的灵活性以及对线程的协调。

线程池

没有线程池的情况下,多次创建,销毁线程开销比较大。如果在开辟的线程执行完当前任务后执行接下来的任务,复用已经创建的线程,降低开销,控制最大并发数。

线程池创建线程时,会将线程封装成工作线程worker,worker在执行完任务后还会循环获取工作队列中的任务来执行。

将任务派发给线程池时,会出现以下几种情况:

  • 核心线程池未满,创建一个新的线程执行任务。
  • 核心线程池已满,工作队列未满,将线程存储在工作队列。
  • 工作队列已满,线程数量小于最大线程数,就创建一个新线程处理任务。
  • 如果超过最大线程数,按照拒绝策略来执行任务。

线程池参数

  • corePoolSize:常驻核心线程数,超过该值后如果线程空闲会被销毁。
  • maximumPoolSize:线程池能够容纳同时执行的线程最大数。
  • keepAliveTime:线程空间时间,线程空闲时间达到该值后会被销毁,知道剩下corePoolSieze个线程为止,避免浪费内存资源。
  • workQueue:工作队列。
  • threadFactory:线程工厂,用来生产一组相同任务的线程。
  • handler:拒绝策略,有以下几种拒绝策略:
    • AbortPolicy:丢弃任务并且抛出异常。
    • CallerRunsPolicy:重新尝试提交任务。
    • DiscardOldestPolicy:抛弃队列里等待最久的任务并把当前任务加入队列。
    • DiscardPolicy:表示直接抛弃当前任务但是不抛出异常。

线程池创建方法

  • newFixedThreadPool:创建固定大小的线程池。
  • newSingleThreadExecutor:使用单线程线程池。
  • newCachedThreadPool:maximumPoolSize设置为Integer最大值,工作完成后会回收工作线程。
  • newScheduledThreadPool:支持定期及周期性任务执行,不回收工作线程。
  • newWorkStealingPool:一个拥有多个任务队列的线程池。

Executor框架

Executor框架的目的是将任务提交和任务如何运行分离开来的机制。用户不再需要从代码层考虑设计任务的提交运行,只需要调用Executor框架实现类的Execute方法就可以提交任务。产生线程池的函数ThreadPoolExecutor也是Executor的具体实现类。

Executor的集成关系

  • Executor:一个接口,其定义了一个接收Runnalbe对象的方法executor,该方法接收一个Runnable实例执行这个任务。
  • ExecutorService:Executor的子类接口,其定义了一个接收Callable对象的方法,返回Future对象,同时提供executor方法。
  • ScheduledExecutorService:ExecutorService的子类接口,支持定期执行任务。
  • AbstractExecutorService:抽象类,提供ExecutorService的执行方法的默认实现。
  • Executors:实现ExecutorService接口的静态工厂类,提供了一系列工厂方法用于创建线程池。
  • ThreadPoolExecutor:继承AbstractExecutorService,用于创建线程池。
  • ForkJoinPool:继承AbstractExecutorService,Fork将大任务分叉为多个小任务,然后让小任务执行,Join是获得小任务的结果,类似于map reduce。
  • ThreadPoolExecutor:继承ThreadPoolExecutor,实现ScheduledExecutorService,用于创建带定时任务的线程池。

线程池的状态

  • Running:能接受新提交的任务,也可以处理阻塞队列的任务。
  • Shutdown:不再接受新提交的任务,但可以处理存量任务,线程池处于Running时调用shutdown方法,会进入该状态。
  • Stop:不接受新任务,不处理存量任务,调用shutdownnow进入该状态。
  • Tydying:所有任务已经终止了,worker_count有效线程数为0。
  • Terminated:线程池彻底终止。在tidyiny模式下调用terminated方法会进入该状态。

阻塞队列

阻塞队列时生产者消费者的实现具体组件之一。当阻塞队列为空时,从队列中获取元素的操作将会被阻塞,当阻塞队列满了,往队列添加元素的操作将会被阻塞。具体实现有:

  • ArrayBlockingQueue:底层由数组组成的有界阻塞队列。
  • LinkedBlockingQueue:底层是由链表组成的有界阻塞队列。
  • PriorityBlockingQueye:阻塞优先队列。
  • DelayQueue:创建元素时可以指定多久才能从队列中获取当前元素。
  • SynchronousQueue:不存储元素的阻塞队列,每一个存储必须等待一个取出操作。
  • LinkedTransferQueue:与LinkedBlockingQueue相比,多一个transfer方法,即如果当前有消费者正等待接收元素,可以把生产者传入的元素立刻传输给消费者。
  • LinkedBlockingDeque:双向阻塞队列。

ThreadLocal

ThreadLocal时线程共享变量,ThreadLocal有一个静态内部类ThreadLocalMap。其key是ThreadLocal对象,值是Entry对象,ThreadLocalMap是每个线程私有的。

  • set给ThreadLocalMap设置值。
  • get获取ThreadLocalMap。
  • remove删除ThreadLocalMap类型的对象。

存在的问题:

  • 对于线程池,由于线程池会重用Thread对象,因此与Thread绑定的ThreadLocal也会被重用,造成一系列问题。
  • 内存泄漏。由于ThreadLocal是弱引用,但Entry的value是强引用,因此当ThreadLocal被垃圾回收后,value依旧不会被释放,产生内存泄漏问题。

Java并发包下的unsafe类

对于Java而言,没有直接的指针组件,一般也不能使用偏移量对某块内存进行操作。这些操作相对来讲是安全的。Java有一个类将Unsafe类,这个类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题,这个类可以说是Java并发开发的基础。

Java中的乐观锁与CAS算法

对于乐观锁,开发者认为数据发送时发生并发冲突的概率不大,所以读操作前不上锁,到了写操作时才会进行判断,数据在此期间是否被其他线程修改。如果发生修改,那就返回写入失败。如果没有被修改,那就执行修改操作,返回修改成功。

ABA问题即解决方法

CAS算法是基于值来做比较的,如果当前有两个线程,一个线程将变量值由A改成B,再由B改回A,当线程开始执行CAS算法时,就很容易认为值没有变化,误以为读取的数据到执行CAS算法的期间,没有线程修改过数据。

juc包提供了一个AtomicStampedReference,即在原始的版本下加入版本号戳,解决ABA问题。

常见的Atomic类

在很多时候,我们需要的仅仅时一个简单的,高效的,线程安全的++或者--方案,使用syncthronized关键字和lock固然可以实现,但是代价较大,此时用原子类更加方便。

基本数据类型的原子类有:

  • AtomicInter:原子更新整型
  • AtomicLong:原子更新长整型
  • AtomicBoolean:原子更新布尔型

Atomic数据类型有:

  • AtomicIntegerArray:原子更新整型数组里的元素
  • AtomicLongArray:原子更新长整型数组里的元素
  • AtomicreferenceArray:原子更新引用类型数组里的元素

Atomic引用类型有:

  • AtomicReference:原子更新引用类型
  • AtomicMarkableReference:原子更新带有标记位的引用类型,可以绑定一个boolean标记
  • AtomicStampedReference:原子更新带有版本号的引用类型

FieldUpdater类型:

  • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器
  • AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器

Atomic类基本实现原理

以AtomicInteger为例:

方法getAndIncrement:以原子方式将当前的值加1,具体实现为:

  1. 在for死循环取得AtomicInteger里面存储的数值
  2. 对AtomicInteger当前的值加1
  3. 调用compareAndSet方法进行原子更新
  4. 先检查当前数值是否等于expect
  5. 如果等于则说明当前值没有被其他线程修改,则将值更新为next
  6. 如果不是会更新失败返回false,程序会进入for循环重新进行compareAndSet操作

CountDownLatch

CountDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。

是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,调用countDown方法,计数器的值就减1,当计数器的值为0时,表示所有线程都执行完毕,然后在等待的线程就可以恢复工作了。

只能一次性使用,不能reset。

CyclicBarrier

CyclicBarrier主要功能和CountDownLatch类似,也是通过一个计数器,使一个线程等待其他线程各自执行完毕后再执行。但是其可以重复使用(reset)。

Semaphore

Semaphore即信号量。

Semaphone的构造方法参数接收一个int值,设置一个计数器,表示可用的许可证数量即最大并发数。使用acquire方法获得一个许可证,计数器减一,使用release方法归还许可,计数器加1,如果此时计数器值为0,线程进入休眠。

Exchanger

Exchanger类可用于两个线程之间交换信息。可简单地将Exchanger对象理解为一个包含两个格子的容器,通过Exchange人方法可以向两个格子中填充信息,线程通过exchange方法交换数据,第一个线程执行exchange方法后会阻塞等待第二线程执行该方法。当两个线程都到达同步点时这两个线程就可以交换数据,当两个格子均被填充时,该对象会自动将两个格子的信息交换,然后返回给线程,从而实现两个线程的信息交换。

ConcurrentHashMap

JDk7采用锁分段技术。首先将数据分成Segment数据段,然后给每个数据段配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其线程访问。

get除读到空值不需要加锁。该方法先经过一次再散列,再用这个散列值通过散列运算定位到Segment,最后通过散列算法定位到元素。

put必须加锁,首先定位到Segment,然后进行插入操作,第一步判断是否需要对Segment里的HashEntry数组进行扩容,第二步定位添加元素的位置,然后将其放入数组。

JDK8的改进:

  1. 取消分段锁机制,采用CAS算法进行值的设置,如果CAS失败在使用synchronized加锁添加元素
  2. 引入红黑树结构,当某个槽内的元素个数超过8且Node数组容量大于64时,链表转为红黑树。
  3. 使用了更加优化的方式统计集合内的元素数量。

Synchronized底层实现原理

Java对象底层都关联一个的monitor,使用synchronized时,JVM会根据使用环境找到对象的monitor,根据monitor的状态进行加解锁的判断。如果成功加锁就成为该monirot的唯一持有者,monitor在被释放前不能再被其他线程获取。

synchronized在JVM编译后会产生monirotenter和monitorexit这两个字节码指令,获取和释放monitor。这两个字节码执行都需要一个引用类类型的参数指明要锁定和解锁的对象,对于同步普通方法,锁时当前对象实例,对于同步静态方法,锁是当前类的Class对象,对于同步块中,锁是synchronized块中的对象。

执行monitorrenter指令时,首先尝试获取对象锁,如果这个对象没有被锁定,或当前线程已经持有锁,就把锁的计数器加1,执行monitorexit指令时会将锁计数器减1,一旦计数器为0,锁随即就被释放。

synchronized关键词的使用方法

  1. 直接修饰某个实例方法
  2. 直接修饰某个静态方法
  3. 修饰代码块

Java偏向锁

JDK1.6中提出了偏向锁的概念。该锁提出的原因是,开发者发现多数情况下锁并不存在竞争,一把锁往往是由同一个线程获得的。偏向锁并不会主动释放,这样每次偏向锁进入的时候都会判断该资源是否时偏向自己的,如果是偏向自己的则不需要进行额外的操作,直接可以进入同步操作。

其申请流程为:

  1. 首先需要判断对象的Mark Work是否属于偏向模式,如果不属于,那就进入轻量级的逻辑判断,否则进入下一步判断。
  2. 判断目前请求锁的线程ID是否和偏向锁本身记录里的线程ID一致。如果一致,继续下一步的判断,如果不一致,跳转到步骤4。
  3. 判断是否需要重偏向,如果不用的话,直接获得偏向锁。
  4. 利用CAS算法将对象的Mark Work进行更改,使线程ID部分换成本线程ID,如果更换成功,则重偏向完成,获得偏向锁,如果失败,则说明有多线程竞争,升级为轻量级锁。

轻量级锁

轻量级锁是为了在没有竞争的前提下减少轻量级锁出现并导致的性能消耗。

其申请流程为:

  1. 如果同步对象没有被锁定,虚拟机将在当前线程的栈帧中简历一个锁记录空间,存储锁对象目前Mark Work的拷贝。
  2. 虚拟机使用CAS尝试把对象的Mark Work更新为指向锁记录的指针。
  3. 如果更新成功即代表该线程拥有了锁,锁标志位将转变成00,表示处于轻量级锁定状态。
  4. 如果更新失败就意味着至少存在一条线程与当前线程竞争。虚拟机检查对象的Mark Work是否指向当前线程的栈帧。
  5. 如果指向当前线程的栈帧,说明当前线程已经拥有了锁,直接进入同步块继续执行。
  6. 如果不是说明锁对象已经被其他线程抢占。
  7. 如果出现两个以上的线程争用同一个锁,轻量级锁就不再有效,将膨胀为重量级锁,锁标志位改变为10。此时Mark Work存储的就是指向重量级锁的指针,后面等待锁的线程也必须阻塞。

锁优化策略

即自适应自旋,锁消除,锁粗化,锁升级等策略。

Java的自旋锁

线程获取锁失败后,可以采用这样的策略,可以不放弃CPU,不停的重试内重试,这种操作称为自旋锁。

自适应自旋锁

自适应自旋锁自旋次数不再人为设定,通常由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定。

锁粗化

锁粗化的思想就是扩大加锁范围,避免反复的加锁和解锁。

锁消除

锁消除是一种更为彻底的优化,在编译时,Java编译器对应用上下文进行扫描,去除不可能存在共享资源竞争的锁。

Lock和ReentrantLock

Lock接口是Java并发包的顶层接口。

可重入锁ReentrantLock是Lock最常见的实现,与synchronized一样可重入,ReentrantLock在默认情况下是非公平的,可以通过构造方法指定公平锁。一旦使用了公平锁,性能会下降。

AQS

AQS(AbstrackQueuedSynchronizer)抽象的队列式同步器。

AQS是将每一个请求共享资源的线程封装成一个锁队列的一个节点(Node),来实现锁的分配。

AQS是用来构建锁或者其他同步组件的基本框架,它使用一个volatile int state变量来作为共享资源。如果线程获取资源失败,则进入同步队列等待。如果获取成功就执行临界区代码,释放资源时会同步同步队列中的等待线程。

子类通过继承同步器并实现它的抽象方法getState,setState和compareAndSetState对同步状态进行更改。

AQS获取独占锁,释放独占锁原理

获取:acquire

  1. 调用tryAcquire方法安全的获取线程同步状态,获取失败的线程会被构造同步节点并通过addWaiter方法加入到同步队列的尾部,在队列中自旋。
  2. 调用acquireQueued方法使得该节点以死循环的方式获取同步状态,如果获取不到则阻塞。

释放:release

  1. 调用tryRelease方法释放同步状态
  2. 调用unparkSuccessor方法唤醒头节点的后记节点,使后继节点尝试重新获取同步状态

AQS获取共享锁,释放共享锁原理

获取锁:acruireShared

  1. 调用tryQcquireShared方法尝试获取同步状态,返回值不小于0表示能获取同步状态。

释放:releaseShared

  1. 释放,并唤醒后续处于等待状态的节点。

线程池类型

  1. newCachedThreadPool可缓存线程池,可设置最小线程数和最大线程数,线程空闲1分钟后自动销毁。
  2. newFixedThreadPool指定工作线程数量的线程池。
  3. newSingleThreadExecutor单线程Executor。
  4. newScheduleThreadPool支持定时任务的指定工作线程数量线程池。
  5. newSingleThreadScheduledExecutor支持定时任务的单线程Executor

synchronized关键字作用

保证只有一个线程可以获取对象的锁,并执行代码块,其他线程不能在该线程指定代码块时执行。

三色标记法

三色标记法时垃圾回收器CMS和G1使用的标记算法,该方法把对象分为三种颜色:

  1. 白色,该对象未被访问。
  2. 灰色,该对象已经被访问,但是该对象引用的其他对象没有被访问。
  3. 黑色,该对象和引用的其他对象均被访问。

因此,三色标记法来说,所有对象可以看作由白色集合,灰色集合,黑色集合组成,通过这种别处方法的访问过程如下:

  1. 初始所有对象均在白色集合。
  2. 将GC root直接引用的对象移动至灰色集合。
  3. 从灰色集合中取出一个对象,将该对象引用的白色集合对象,移动至灰色集合。
  4. 移动完成后,将该对象移动至黑色集合。
  5. 重复3-4操作。

wait和sleep区别

在等待时,wait会释放锁,而sleep一直持有锁,wait通常被用于线程间的交互,sleep通常用于暂停程序执行。

sychronized和volatile区别

一旦一个变量被volatile修饰了之后,就有了两层语义:

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对于其他线程来说是立即可见的。
  • 禁止执行指令重排序
    • volotile的本质是告诉JVM当前变量在寄存器中的值是不确定的,需要从主存中读取。
    • sychronized则是锁住当前变量,只有当前线程可以访问该变量,其他线程被阻塞。
    • volatile只能作用在变量级别,sychronized可以使用在变量、方法和类级别。
    • volatile只能实现变量的修改可见性,并不能保证原子性;sychronized则可以保证变量修改的可见性和原子性。
    • volatile不会造成线程的阻塞,sychronized可能造成线程的阻塞。
    • volatile标记的变量不会被编译器优化;sychronized标记的变量可以被编译器优化。

启动一个线程使用run还是start

启动一个线程使用start方法,使线程所代表的虚拟处理机处于可以运行的状态,具体什么时候运行,取决于JVM的调度,run方法是线程启动进行回调的方法。