Java并发基础

复习准备秋招,内容有错的地方请各位看官评论区留言!不胜感激!

Java并发基础

要解决的三个问题

  • 原子性(synchronized)
  • 有序性(synchronized、volatile)
  • 可见性(synchroniezd、volatile、final)

线程

  • 新建线程四种方式:
    1.继承Thread类
    2.实现Runnable接口
    3.实现Callable接口
    4.使用Executors工具栏创建线程池创建线程

  • 线程状态:

  1.NEW:初始状态,线程被构建,但是还没有调用start( )方法
  2.RUNNABLE:运行状态,Java线程将操作系统中的就绪和运行两种状态统称为“运行中”
  3.BLOCKED:阻塞状态,表示线程阻塞于锁
  4.WAITING:等待状态,表示线程进入等待状态。进入该状态表示当前线程需要等待其他线程做出一些特定操作(通知或中断)
  5.TIME_WAITING:超市等待状态,与WAITING不同,可以在指定的时间自行返回
  6.TERMINATED:终止状态,表示当前线程已经执行完毕
  • 线程间通信

    1.同步,volatile/ synchronized关键字
    2.等待/通知机制——wait/notify
    3.管道通信

  • run()和start()

    run()方法用于执行线程的运行时代码
    start()方法用于启动线程

  • wait、sleep、yield

  • 线程安全概念

    指某个方法在多线程环境中被调用时,能过正确地处理多个线程之间的共享变量,使程序功能正常完成。

  • 同步

    同步是指程序中用于控制不同线程间操作发生相对顺序的机制

  • 线程调度算法:
    1.分时调度
    2.抢占式调度-Java使用

线程调度器优先选择优先级最高的线程运行。但若发生以下情况就会终止线程的执行:

1.线程体中调用了 yield() 方法,让出了对 CPU的占用
2.线程体中调用了 sleep() 方法使线程进入睡眠
3.线程由于 IO 操作收到阻塞
4.另一个优先级更高的线程出现
5.在支持时间片的系统中,该线程的时间片用完

volatile

  • 内存可见性

    • 1.通过Lock 前缀的指令实现。(缓存一致性协议)
    • 2.将当前处理器缓存行的数据写回到系统内存
    • 3.这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效

  • 禁止指令重排

    • 1.通过内存屏障实现(一组处理器指令,用于实现对内存操作的顺序限制)
    • 2.Java编译器会在生成指令序列时,在适当的位置会插入内存屏障来禁止处理器对指令的重排
    • 3.volatile 会在变量写操作前加入两个内存屏障,来保证前面的写指令和后面的读指令是有序的:
      普通写->StoreStore->volatile写->StoreLoad->普通读
    • 4.volatile 会在变量读操作后加两个指令,禁止后面的读和写重排序:
      volatile读->StoreLoad->StoreStore->普通读->普通写

  • 保证有序性,可见性

synchronized

  • 三种表现形式

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

  • JVM层面实现原理

    • 同步方法

      无需调用字节码指令来控制,它设置在方法调用和返回操作中,JVM可在方法常量池中的方法表结构中的ACC_SYNCHRONIZED 访问标志区分一个方法是不是同步方法。
      若方法设置了ACC_SYNCHRONIZED标识,执行线程将先获取moniter,然后再执行方法,最后在方法完成时,不论是否正常完成,释放moniter。

    • 同步代码块

      代码块的同步利用 moniterentermoniterexit 这两个字节码指令。分布位于同步块开始和结束的位置。
      JVM执行到moniterenter时,当前线程试图获取 moniter对象的所有权,如果未加锁或已经被当前线程持有,锁的计数器加1,执行到moniterexit时,计数器减1.当锁的计数器为0时,该锁就被释放了。
      如果获取moniter对象失败,则该线程进入阻塞状态。

  • Java对象头

    • 大小

      非数组对象:2字宽
      (1字宽=4byte=32bit)

      数组对象:3字宽

    • Mark Word

      存储对象的 hashcode、gc年龄分代、锁标志位

    • Class Meatdata Address

      存储到对象类型数据的指针

    • Array length

      数组的长度(如果当前对象是数组)

  • 锁升级((JDK1.6出现)锁可以升级但不可以降级,锁升级目的是为了提高获得锁和释放锁的效率)

    • 无锁

    • 偏向锁:
      当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS 操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word 里是否存储着指向当前线程的偏向锁。

      • 如果测试成功,则表示线程已经获得了锁
      • 如果测试失败,则再检查一些 Mark Word 中偏向锁的标识是否设置成 1(表示当前是偏向锁):
        如果没有设置,则使用CAS 竞争锁
        如果设置了,则尝试使用 CAS 将对象头的偏向锁指向当前线程
    • 轻量级锁:
      线程在执行同步块之前,JVM会先在 当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的 Mark Word 复制到锁记录中。然后线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向锁记录的指针。
      如果成功,当前线程获得锁
      如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁

    • 重量级锁:

      通过对象内部的 moniter 来实现的,moniter 本质是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。

原子操作

  • 概念

    不可被中断的一个或一系列操作

  • 处理器如何实现

    • 32位 IA-32处理器,处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存中读取或写入一个字节是原子的(即,当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址)
    • Pentium 6和最新的处理器保证单处理器对同一个缓存行里进行 16/32/64位的操作是原子的
  • 总线锁

    使用处理器提供的一个LOCK # 信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,该处理器就可以独占共享内存。

  • 缓存锁

    在同一时刻,我们只需保证对某个内存地址的操作是原子性的即可。但总线锁把CPU和内存之间的通信锁住了,这使得锁定期间其他处理器u不能操作其他内存地址的数据。所以总线锁的开销比较大。

    缓存锁定

    内存区域如果被缓存在处理器的缓存行中,并且在Lock 操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声明 LOCK # 信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性。

    不能使用缓存锁定的情况:

    • 当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,处理器会调用总线锁定
    • 有些处理器不支持缓存锁定。
    • 针对以上两个机制,可以使用处理器提供的Lock 前缀的指令来实现。被这些指令操作的内存区域就会加锁,导致其他处理器不能同时访问它。
  • Java实现原子操作

    • 1.使用循环CAS

      JVM的CAS 操作是利用了处理器提供的 CMPXCHG 指令实现的。
      从JDK1.5开始,JDK的并发包中提供了一些类来支持原子操作,AtomicXxxxx

    • 2.CAS存在的问题

      • ABA问题

        一个线程获取出数据的初始值是A,后续计划实施 CAS乐观锁,期望数据仍是A的时候,修改才能成功。此时另一个线程将数据修改成了B,又一个线程将数据修改回了A,第一个线程CAS乐观锁,发现数据仍然是A,则进行数据修改。上述过程中数据最后仍然是A,但过程中其实已经发生了变化,可能导致错误。

        解决方式:

        • 时间戳
        • AtomicStampedReference原子类,这个类的 compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以源自方式更新。
    • 3.循环时间长开销大

      自旋CAS 如果长时间不成功,会给CPU 带来非常大的执行开销。

      解决方式:

      • 如果JVM 能支持处理器提供的pause 指令,那么效率会提升。
    • 4.只能保证一个共享变量的原子操作

      对一个共享变量执行操作时,可以使用循环CAS 来保证原子性。但是对多个变量操作时,循环CAS 无法保证操作的原子性。这时可以用锁来实现。

      将多个共享变量合并成一个共享变量来操作。AtomicReference可以保证引用对象之间的原子性。

    • 5.使用锁机制来实现原子操作

是否锁住同步资源

乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。如:synchronized、ReentrantLock。

AbstractQueueSynchronizer(AQS)-队列同步器

概述

一个用来构建锁或其他同步组件的基础框架,它使用了一个int 成员变量表示同步状态,通过内置的 FIFO 队列来完成线程获取资源的排队工作。

同步器的设计是基于模板方法的,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。

同步器提供的方法基本分为三类:

  • 独占式获取与释放同步状态
  • 共享式获取与释放同步状态
  • 查询同步队列中的等待线程情况

核心思想

  • 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置成锁定状态。
  • 如果被请求的资源被占用,那么就需要一套线程阻塞、等待以及被唤醒时锁分配的机制,这个机制就是AQS 通过CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

基于AQS实现的类

  • ReentrantLock(可重入锁)(Lock 接口的一个实现类)

    【可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁,不会被自己阻塞。】

    • 重入性原理:

      1.在线程获得锁的时候,如果已经获取锁的线程是当前线程,则直接再次获取锁
      2.由于锁会被获取n 次,那么只有锁在被释放n 次之后,该锁才算是完全释放成功

    • ReentrantLock公平锁分析AQS

      private transient volatile Node head;
      private transient volatile Node tail;

      private volatile int sate;

      等待队列中每个线程被包装成一个Node实例。Node有四个属性:thread、waitStatus、pre、next

      ReentrantLock使用方式:
      ReentrantLock reentranLock = new ReentrtantLock(true);
      reentranLock .lock();
      reentranLock.lcok() 内部会调用Sync 类来管理锁

      Sync有公平锁(FairSync)和非公平锁(NonFairSync)
      针对公平锁:

      • 1.acquire(1) 争锁 --> tryAcquire() --> addWaiter --> acquireQueued(addWaiter(…))
      • 2.进入 parkAndChenkInterupt()
  • ReentranReadWeitrLock

    用一个int 型变量 state 表示锁,高16位表示读锁,低16位表示写锁。
    实现了读写分离,读锁是共享的,写锁是独占的。读和读之间不会互斥,读写,写读,写写之间会互斥。

    • 三个特性

      公平选择性:支持非公平(默认)和公平的锁获取方式。吞吐量:非公平> 公平
      重进入:该锁支持重进入
      锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成读锁

  • Semaphore

  • CountDownLatch

  • Future Lock

JUC包

并发容器

  • ConcurrentHashMap

  • CopyOnWriteArrayList

    一个在非复合场景下操作是线程安全的并发容器。

    优点:

    • 当多个迭代器同时遍历和修改这个链表时,不会抛出 ConcurrentModeficationException异常。在写入时,将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全的执行。适用于读多写少的场景

    缺点:

    • 在写操作时,需要拷贝底层数组,会消耗内存;若源数组的内容比较多时,可能导致young gc 或 full gc
    • 不能适用于实时读的场景,只能满足最终一致性,没法满足实时一致性
    • 在实际使用中可能没法保证C opyOnWriteArrayList 具体放多少数据,如果数据太多,每次set/add 都要重新复制数组,代价太高
  • ConcurrentLinkedQueue

  • 阻塞队列

    使用通知模式实现。

    通知模式:
    当生产者往满的队列中添加元素时会则是生产者,当消费者消费一个队列中的元素后,会通知生产者当前队列可用。

并发工具类

  • CountDownLatch

    允许一个或多个线程等待其他线程完成操作。

    join()也可以实现该功能。
    join():用于让当前线程等待join 线程执行结束。

  • CyclicBarrier

    同步屏障:让一组线程到达一个屏障(同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续执行。
    每个线程调用await 方法告诉 CyclicBarrier 自己已经达到屏障,然后当前线程被阻塞。

  • Semaphore

    控制同时访问特定资源的线程数量。

    可以用于流量控制。

  • Exchanger

    一个用于线程间协作的工具类。Exchanger 用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。

    两个线程通过exchange 方法交换数据,如果第一个线程先执行 exchange 方法,它会一直等待第二个线程也执行 exchange 方法,当两个线程都到达同步点时,这两个线程就可以交换数据。

    可以用于遗传算法、校对工作。

Lock接口

  • 功能

    提供了于 synchronized 关键字类似的同步功能,但在使用的时候需要显示地获取和释放锁。

  • 特性(synchronized不具备)

    • 尝试非阻塞地获取锁

      当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取,则成功获取锁并持有锁

    • 能被中断地获取锁

      与 synchronized 不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常会被抛出,同时锁会被释放

    • 超时获取锁

      在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回

  • 几个实现

    • ReentrantLock
    • ReadWriteLock
    • ReentrantReadWriteLock
    • Lock
  • Atomic类

线程池

目标

执行大量异步任务时能够提供较好的性能
线程池提供了一种资源限制和线程管理的手段

创建方式

  • Executor executor = Executors.newFixedThreadExecutor

  • newSingleThreadExecutor

    创建一个单线程的线程池

  • newFixedThreadExecutor

    创建固定大小的线程池

  • newCachedThreadExecutor

    创建一个可缓存的线程池

  • newScheduledThreadExecutor

    创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求

状态

  • RUNNING

    正常状态,接受新的任务,处理等待队列中的任务

  • SHUTDOWN

    不接受新的任务提交,但是会处理等待队列中的任务

  • STOP

    不接受新的任务提交,不再处理等待队列中的任务并且中断正在执行的任务

  • TIDYING

    所有的任务都被销毁了,workCount = 0,线程池在转换为TIDYING状态时,会执行钩子方法 terminated()

  • TERMINATED

    terminated() 方法结束后,线程池的状态就会变为TERMINATED

7个参数

  • corePoolSize

    当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的线程能够执行当前的新任务也会创建,等到需要执行的任务数大于线程池基本大小时就不再创建。
    线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁。

  • maximumPoolSize

    线程池中允许存在的工作线程的最大数量。
    如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。
    注意:如果使用了无界的任务队列,则这个参数没什么效果。

  • workQueue

    用于保存等待执行的任务的阻塞队列

    • ArrayBlockingQueue
    • LinkedBlockingQueue
    • PriorityBlockingQueue
  • threadFactory

    为线程池提供创建新线程的线程工厂

  • keepAliveTime

    空闲线程存活时间。如果当前线程池中的线程数量比corePoolSize大,并且没有新的任务提交,这些空闲线程能存活的最大时间

  • TimeUnit

    存活时间keepAliveTime的单位

  • rejectExecutionHandler

    拒绝策略。当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,线程池采取的拒绝策略

    • AbortPolicy

      抛出RejectedExecutionException来拒绝新任务的提交

    • CallerRunsPolicy

      只用调用者所在队列来运行任务

    • DiscardPolicy

      直接丢弃任务,不处理

    • DiscardOldPolicy

      丢弃最早的未处理的任务

工作流程

线程池工作流程

submit()和execute()区别

  • 接收参数

    submit() 可以执行 Runnable 和Callable 类型的任务

    execute() 只能执行 Runnable 类型的任务

  • 返回值

    submit() 可以返回持有计算结果的 Future 对象

    execute() 没有返回值

  • 异常处理

    submit() 方便异常处理

双重锁实现单例

  • new操作的过程

    1.为 instance 分配内存空间;
    2.初始化 instance;
    3.将 instance 指向分配的内存空间。

  • volatile的作用

    禁止指令重排
    保证内存可见性

  • 特点

    • 两个if判断

      • 第一次 if(instance == null):这个是为了代码提高代码执行效率,由于单例模式只要一次创建实例即可,所以当创建了一个实例之后,再次调用getInstance方法就不必要进入同步代码块,不用竞争锁。直接返回前面创建的实例即可。
      • 第二次if(instance ==null):这个校验是防止二次创建实例。
  • 代码

  public class Singleton {
  
      private static volatile Singleton instance = null;
      private Singleton(){}

      public static Singleton getInstance(){
          if(instance == null ) {
              synchronized (Singleton.class){
                  if (instance == null){
                      instance = new Singleton();
                  }
              }
          }
          return instance;
      }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值