Java高并发笔记

(一)线程安全性

原子性

提供了互斥访问,同一时刻只能有一个线程来对他进行操作:Atomic包、CAS算法、synchronized、Lock

  1. 主要由Atomic包实现,使用unsafe类(可以访问主内存非工作内存),基于CAS(CompareAndSwap)实现多线程原子操作:

    CAS:比较主内存中的值和当前工作内存中的值是否一致,若一致则由当前线程改为期望值,Atomic用一个do…while循环循环判断CompareAndSwap条件来实现。

  2. AtomicLong在只需要求得最后结果的时候可以用LongAddr替代

    因为Atomic类在线程竞争很大的时候,循环判断会使得CPU额外开销非常大,LongAddr通过将值分为多个节点计算,首先计算base值是否可以更改,如果可以则直接更改base值,否则更改其内部的Cell对象的值(实际上是对每个线程计算了一个哈希值,对对应位置线程的哈希桶填入值得过程,避免了CAS循环判断的过程),最后的结果由base值和Cell对象的值相加得到,在多线程计算结束之前的中途任意时刻获取LongAddr对象的值,都是不准确的,因为获取的时候,值可能已经被其他线程更新了。

可见性

一个线程对主内存的修改可以及时被其他线程观察到

  1. 导致共享变量在线程之间不可见的原因:
    • 线程交叉执行
    • 重排序、线程交叉执行
    • 共享变量更新后没有在工作内存与主内存之间及时更新
  2. 解决办法
    • 使用synchronized关键字:
      1. 线程解锁前,必须把共享变量的最新值刷新到主内存
      2. 线程加锁时,将清空工作内存中共享变量的值,让需要使用共享变量的时候从主内存中重新读取最新的值
    • 使用volatile关键字:
      1. 基于内存屏障
        • volatile变量写操作的时候,会在写操作后加一条store指令,将本地内存中的共享变量值刷新到主内存
        • volatile变量读操作的时候,会在写操作后加一条load指令,将主内存中的共享变量值刷新到工作内存
      2. 禁止重排序
        • volatile写:
          1. 前面插入StoreStore指令,禁止前面的普通写和volatile写重排序
          2. 后面插入StoreLoad指令,防止volatile写与之后可能出现的volatile读/写重排序
        • volatile读:
          1. 后面先插入LoadLoad屏障,禁止下面所有的普通读和volatile读重排序
          2. 再插入LoadStore屏障,禁止下面所有的写操作和volatile读重排序
      3. volatile可以作为一种线程通信机制使用(通过主内存通信)

有序性

一个线程观察其他线程中的指令执行顺序,由于指令重排序(JVM和CPU导致)的存在,该观察结果一般杂乱无序。

实现:volatilesynchronizedLock

深入理解JVM里面的happens-before原则:

  1. 程序次序规则:一个线程内,线程内的代码是有序执行的
  2. 锁定规则:一个unLock操作先行发生于对同一个锁的Lock操作
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作(WAR)
  4. 传递规则:操作A先于B,B先于C,那么A先于C
  5. 线程启动规则:Thread对象的start方法线性发生于该线程的每个动作
  6. 线程中断规则:对线程的interrupt方法调用先行发生于被中断线程的代码检测到中断事件的发生
  7. 线程终结规则:线程中的所有操作都先行发生于线程的终止检测(Thread.join()Thread.isAlive()
  8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

(二)安全发布

原则

  1. 不可以直接把私有引用变量直接发布出去
  2. 对象的构造器执行之前,不可以发布对象

安全发布策略

  1. 在静态初始化函数中初始化一个对象引用

    • 饿汉模式:类加载就实例化单例对象(静态域、静态块)

    • 懒汉模式:

      public class SingletonExample4 {
          // 私有构造函数
          private SingletonExample4() {}
          // 1、memory = allocate() 分配对象的内存空间
          // 2、ctorInstance() 初始化对象
          // 3、instance = memory 设置instance指向刚分配的内存
      
          // JVM和cpu优化,发生了指令重排
      
          // 1、memory = allocate() 分配对象的内存空间
          // 3、instance = memory 设置instance指向刚分配的内存
          // 2、ctorInstance() 初始化对象
      
          // 单例对象
          private static volatile SingletonExample4 instance = null;
          // 静态的工厂方法
          public static SingletonExample4 getInstance() {
              if (instance == null) { // 双重检测机制        // B
                  synchronized (SingletonExample4.class) { // 同步锁
                      if (instance == null)	instance = new SingletonExample4(); // A - 3
                  }
              }
              return instance;
          }
      }
      

      可能发生上述情况中,线程A先开辟内存,还没按构造函数初始化,线程B就直接拿到了对象引用,导致短时间内的对象还未初始化就被拿去使用。使用volatile限制指令重排可以解决这个问题

    • 枚举模式:JVM保证构造器绝对只调用一次

  2. 对象引用设为保存到volatile类型域或者AtomicReference对象中

    Atomic类是通过自旋CAS操作volatile变量实现的,只要有volatile修饰,就无法重排指令!

  3. 对象引用保存到某个正确构造对象的final类型域中

  4. 对象引用保存到一个由锁保护的域

(三)线程安全策略

不可变对象

  1. 条件如下:

    1. 对象创建后其状态不可被修改
    2. 对象的所有域都是final类型
    3. 对象是正确创建的(对象创建期间不暴露其this指针)

    以上条件可以通过将类声明为final(不可继承),其所有成员变量均声明为final来实现,对变量不提供set方法,get方法不直接返回对象本身,而是返回对象深拷贝

  2. final关键字

    • 修饰类:不能被继承
    • 修饰方法:方法不会被子类重写
    • 修饰变量:基本数据类型(值不变)、引用类型变量(引用地址不变)
  3. 不可变对象

    Collections.unmodifiableXXXCollectionListSetMap都是不可变对象,仅仅是保留可变对象的视图,unmodifiableXXX不支持修改,但是会随着可变对象的变化而更改他自己的视图

线程封闭

  1. Ad-hoc

    程序控制实现,最糟糕

  2. 堆栈封闭

    多个线程访问方法中的局部变量,不会被共享,无并发问题。

  3. ThreadLocal线程封闭

    内部维护了一个Map,一个线程对应一个keyThreadLocal<T>仅对当前线程可见。

线程不安全的类与写法

  1. StringBuilder,其线程安全类为StringBufferStringBuffer所有方法都用了synchronized关键字来限制多线程访问
  2. SimpleDateFormat,多线程访问同一个对象会报异常
  3. ArrayListHashSetHashMapCollection
  4. 先检查再执行:if (condition(a)) { handle(a) };,两个线程同时判断通过,同时执行了,主要是因为没有保证原子性

同步容器

主要是采用synchronized关键字实现同步

  1. ArrayList->VectorStack

    VectorStack这两种都使用synchronized修饰方法来同步,如果不需要同步应该使用ArrayList,性能更高,但是这两个容器并不保证绝对的线程安全,因为在外部操作时的顺序可能引发越界访问。

  2. HashMap->HashTablekeyvalue不能为null

  3. Collections.synchronizedXXXListSetMap

一个线程在遍历同步容器,同时另一线程在修改同步容器时,应该使用synchronized关键字或者Lock或者使用CopyOnWriteXXX并发容器来替代同步容器

并发容器

  1. ArrayList->CopyOnWriteArrayList:写(修改)操作的时候先复制数组,再写入,将原本的引用指向新的内存,通常用于读远大于写的场景,否则会触发频繁GC;读操作时,都原数组不需要加锁,写操作会加锁防止复制出多个不同的数组;基于ReentrantLock实现原子操作(这个ArrayList不能太大,否则复制的时候开销很大)

  2. HashSet->CopyOnWriteArraySetTreeSet->ConcurrentSkipListSet

    前者也是基于ReentrantLock实现,后者基于Map集合,单步操作保证线程安全,批量操作不保证,因为批量操作是多次调用单步操作实现的,无法保证每个单步操作之间的线程安全性(只允许一个线程调用批量操作),且也不允许keyvaluenull

  3. HashMap->ConcurrentHashMapTreeMap->ConcurrentSkipListMap

    ConcurrentHashMap针对读操作做了大量优化,高并发场景下,性能很高;

    ConcurrentSkipListMap基于跳表,存取时间和线程数无关(支持并发度更高)

    JGnV10.png

    影响HashMap的性能主要有两个参数:初始容量、加载因子

    1. 初始容量:16
    2. 加载因子:0.75

    HashMap中的数据量超过容量*加载因子时,就会调用resize方法,把容量翻倍;

    JGMnnf.png

    如图,是单线程再哈希过程,使用HashMap在多线程情形下容易出现死循环

    JGlFSA.png

    ConcurrentHashMap基于分段(segment)锁来处理(JDK7)

    JGlaY4.png

    基于红黑树(JDK8)

    因为求hash的过程需要取模,而对于计算机而言,取模开销比位操作大得多,所以HashMap采用位操作实现取模,也就导致了HashMap的容量为2^n

    即使使用带参数的构造器传入不是2的幂的容量大小,它也会根据给出的参数计算出一个2的幂作为初始容量。

安全共享对象策略

  1. 线程限制:线程独占对象
  2. 共享只读:任何线程都不能修改
  3. 线程安全对象:对象内部是同步的
  4. 被守护对象:获取特定的锁后才能访问这个对象

(四)JUC之AQS

AQS接口

AbstractQueuedSynchronizer,简称AQS

底层使用双向链表Sync queue实现,当需要使用Condition的时候,会引入单向链表Condition queue

JSQXIf.png

使用及性质

  1. 使用Node实现FIFO队列,可用于构建锁或者其他同步装置的基础框架
  2. int表示状态(state成员变量),state表示获取锁的线程锁
  3. 通过继承来使用,通过实现acquire和release来管理状态
  4. 可同时实现排他锁和共享锁模式(独占控制、共享控制)

实现思路:

AQS内部维护了一个队列来管理锁,线程首先尝试获取锁,若获取失败,则把当前线程以及等待状态等信息包装成Node节点加入sync queue,不断循环尝试获取锁(只有当前节点为head的直接后继才会进行尝试),若失败则阻塞自己,直到被唤醒;当持有锁的线程释放锁的时候,会唤醒队列中的后继线程。

除了Node节点的这个FIFO队列,还有一个重要的概念就是waitStatus一个volatile关键字修饰的节点等待状态。在AQS中waitstatus有五种值:

  1. SIGNAL 值为-1、后继节点的线程处于等待的状态、当前节点的线程如果释放了同步状态或者被取消、会通知后继节点、后继节点会获取锁并执行(当一个节点的状态为SIGNAL时就意味着在等待获取同步状态,前节点是头节点也就是获取同步状态的节点)
  2. CANCELLED 值为1、因为超时或者中断,结点会被设置为取消状态,被取消状态的结点不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态。处于这种状态的结点会被踢出队列,被GC回收(一旦节点状态值为1说明被取消,那么这个节点会从同步队列中删除)
  3. CONDITION 值为-2、节点在等待队列中、节点线程等待在Condition、当其它线程对Condition调用了signal()方法该节点会从等待队列中移到同步队列中
  4. PROPAGATE 值为-3、表示下一次共享式同步状态获取将会被无条件的被传播下去(读写锁中存在的状态,代表后续还有资源,可以多个线程同时拥有同步状态)
  5. initial 值为0、表示当前没有线程获取锁(初始状态)

CountDownLatch

同时只能有一个线程去操作这个对象,其他线程一直处于阻塞状态(阻塞是没能拿到锁、等待是拿到了锁但是wait了)

JS8JYV.png

这个计数器不能被重置次数,拆分大任务为多个子任务时,可以采用这个类

Semaphore

可以控制并发量,提供acquirerelease方法,常用于仅能提供有限访问的资源:如数据库连接池

通过tryAcquire方法,使得超过semaphore并发量的内容可以被丢弃

CyclicBarrier

JSOnaT.png

允许一组线程相互等待,直到到达某个公共的屏障点(CommonBarrierPort),每有一个线程await,计数器+1。

CountDownLatch的区别:

  1. CyclicBarrier可用reset方法重置
  2. 前者描述一个或n个线程等待其他线程的关系,后者描述了多个线程相互等待的关系

ReentrantLock 与 Condition

Java主要分两类锁:

  1. synchronized修饰的锁
  2. JUC提供的锁——ReentrantLock
  3. 区别:
    • 后者可重入(拥有锁的计数器)
    • 锁的实现:前者依赖于JVM,后者依赖于JDK
    • 性能:引入偏向锁(自旋锁)后,前者效率接近后者
    • 功能:前者更简便,由编译器保证加锁和释放;后者要手工加锁和释放。后者比前者更灵活
    • 公平性:后者可以指定公平/非公平锁,前者只能是先获得先拥有
    • 后者提供Condition类,可以分组唤醒需要唤醒的线程;前者要么唤醒全部线程,要么随机唤醒一个线程
    • 后者提供能够中断等待锁的线程的机制(lock.lockInterruptibly()),如果当前线程没有被中断则锁定,否则抛出异常

ReentrantReadWriteLock在没有任何读写锁的情况下,才能获取写入锁。是悲观锁。

stampLock:提供乐观锁方式

Condition.await():线程在sync队列中的状态会变成Condition,会加入condition队列

public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        Condition condition = reentrantLock.newCondition();

        new Thread(() -> {
            try {
                reentrantLock.lock();
                System.out.println("wait signal");// 1
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("get signal");// 4
            reentrantLock.unlock();
        }).start();

        new Thread(() -> {
            reentrantLock.lock();
            System.out.println("get lock");// 2
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            condition.signalAll();
            System.out.println("send signal ~ ");// 3
            reentrantLock.unlock();
        }).start();
    }

上述代码的执行顺序如注释所表示:线程1首先获得reentrantlock,进入AQS队列,之后由于condition.await,在AQS队列中被视为放弃锁(但还未移除),进入condition队列;之后线程2获得reentrantlock,并调用condition.signalAll()将线程1从condition队列移除,当线程2释放了reentrantlock后,AQS队列将锁分配给线程1,线程1得以继续执行。

(五)JUC扩展

通常有两种方法使用多线程:

  1. 实现Runnable接口
  2. 继承Thread

但是这两种方式都无法获得任务执行的结果

Callable接口

是泛型接口,其call函数的返回值就是泛型参数类型,能够抛出异常

Future接口

是泛型接口

RunnableCallable任务,可以取消任务、查询任务状态(取消/完成)、获取结果等;

这个接口可以监视目标线程调用call的情况,调用Future接口的get方法时,就可以获得线程的执行结果

FutureTask类

其父类RunnableFuture实现了RunnableFuture两个接口 ,如果函数参数为Runnable类型,他会转换为Callable类型

这个类统一了RunnableCallable,十分方便

Fork/Join框架

核心:工作窃取算法

JPxcB4.png

窃取任务的线程永远从其他线程的队列尾部拿任务。

局限性:

  1. 只能通过Fork/Join同步
  2. 任务不能是IO
  3. 任务不能抛出/检查异常

核心:两个类

ForkJoinPool:提供工作线程、管理任务状态

ForkJoinTask:提供在任务中执行Fork、Join操作的机制

任务:必须是ForkJoinTask的子类

BlockingQueue接口

阻塞队列

JiA0Z6.png

队列满入队、队列空出队,都会造成阻塞;主要用在生产者、消费者模型

操作阻塞:put(o)take()

实现类如下:

  • ArrayBlockingQueue:大小有限,初始化就要指定,FIFO
  • DelayQueue:其中的元素必须实现Delayed接口(继承了Comparable接口),内部实现是锁、排序,常用于延迟关闭资源
  • LinkedBlockingQueue:可以指定大小,也可以不指定(使用默认最大值)
  • PriorityBlockingQueue:没有大小限制,允许插入null对象,迭代器并不保证按照优先级顺序迭代
  • SynchronousQueue:只允许插入一个元素,一个线程插入元素后就会被阻塞,直至被另一个线程消费,因此又称为同步队列。

(六)线程池

对比Thread类

new Thread弊端

  1. 每次new Thread需要新建对象,开销高
  2. 线程缺乏统一管理,可能无限制新建线程,相互竞争,有可能占用过多系统资源导致死机或者OOM(耗尽内存)
  3. 缺少更多的功能:如定期执行、线程中断

线程池的好处:

  1. 重用存在的线程,减少对象创建、消亡
  2. 可有效控制最大并发线程数,提高系统资源利用率,避免过多资源竞争、避免阻塞
  3. 提供定时执行、定期执行、单线程、并发数控制等功能

ThreadPoolExecutor类

corePoolSize:核心线程数量,线程数小于此,直接创建新线程。线程池内的线程数大于等于corePoolSize时,将任务放入workQueue等待。

maximumPoolSize:大于等于corePoolSize,小于此,只有当workQueue满才创建新线程

workQueue:阻塞队列,存储等待执行的任务,接收BlockingQueue类型参数

keepAliveTime:线程没任务执行时最多保持多久时间终止

threadFactory:线程工厂,用来创建线程

rejectHandler:当拒绝处理任务时的策略(直接抛异常、用调用者所在的线程来执行、丢弃队列中最旧的任务、直接丢弃任务)

线程池实例的状态

JZwJdH.png

Running:能接受新提交的任务、阻塞队列中的任务

ShutDown:不能接受新提交的任务,可以处理阻塞队列任务

Stop:不处理任何任务

TiDying:如果所有任务都终止了,有效线程数为0。

Terminated

常用方法

execute:提交任务,交给线程池执行

submit:提交任务,能够返回结果,相当于execute+Future

shutdown:关闭线程池,并等待任务执行完

shutdownNow:关闭线程池,不等待任务执行完

Executor框架接口

Executors.newCachedThreadPool:线程池长度超过了任务量,则回收

Executors.newCachedThreadPool:控制并发数

Executors.newScheduledThreadPool:定期执行

Executors.newSingleThreadExecutor:保证任务以指定顺序执行

(七)死锁(这个在OS里讲的很详细)

条件

  1. 互斥
  2. 请求和保持条件
  3. 非剥夺
  4. 循环等待
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值