JAVA并发编程笔记整理

JAVA并发编程笔记整理

一、基础概念

1.1、线程和进程

操作系统在执行一个程序的时候会为其创建一个进程。一个进程里可以创建多个线程,线程是操作系统调度的最小单元。一个进程至少包含一个线程。

1.2、普通线程和守护线程(Daemon)

当普通线程执行完后守护线程才会结束。比如jvm,当虚拟机不存在非守护线程时,虚拟机才会退出。

1.3、线程优先级

java线程优先级通过priority变量来设置,默认是5;数字(1-10)越大优先级越高。是一种偏重计算,不能确保优先级高的就一定会被有限处理。只是大概率时间片的数量偏向优先级高的。

二、java并发编程机制和底层实现

2.1 Synchronized关键字

Synchronized关键字可以保证同一时刻只有一个线程可以执行某个方法或某段代码块。
对于普通同步方法,锁的是当前实例对象。
对于静态同步方法,锁的是当前类的Class对象。
对于同步方法块,锁的是Synchronized括号里配置的对象。
底层原理:Synchronized用的锁存在对象头里的Mark Word。会存储锁的标识和线程ID。当上锁时可以简单的理解为使用monitorenter和monitorexit指令分别插入到同步代码开始位置和同步代码结束处/异常处。
锁的升级和对比:有四种类型,无锁、偏向锁、轻量级锁、重量级锁。
在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获取,为了让线程获取锁的代价更低而引入了偏向锁。先简单测试下对象头里是否存储着指向当前线程的偏向锁,如果测试成功,则表示获取到锁,如果测试失败,则使用CAS替换Mark Word竞争锁。偏向锁的撤销需要等到其它线程竞争时才会释放。
当有其它线程发生CAS竞争锁时,会升级成为轻量级锁。此过程线程不会阻塞而是一直自旋(会消耗CPU资源)竞争锁。如果自旋一定次数还获取不到锁,锁就会膨胀成重量级锁。此时线程不再自旋而是阻塞等待唤醒,阻塞线程会加到一个队列等待锁释放时被唤醒,不会消耗CPU但响应时间会变慢。
总结:Synchronized可以保证同一时刻只有一个线程串行执行,因而可以保证原子性和可见性。但无法保证有序性,因为锁范围的内部代码存在指令重排的情况。要根据业务会出现那种类型锁的情况多来判断是否适合使用Synchronized关键字。

2.2 volatile关键字

volatile关键字用以修饰共享变量,确保共享变量能被准确和一致的更新。使用得当开销较小,因为它不会引起线程上下文切换和调度。
底层原理:由于CPU存在L1、L2或其它缓存,在多核多线程下,可能存在某个核缓存未及时更新到其它核的修改结果。使用volatile修饰的共享变量在执行指令期间,声言处理器的LOCK#信号(一般锁内存不锁总线),实现缓存一致性协议(MESI)的多核处理器通过嗅探在总线上传播的数据来检查自己的缓存值是不是过期,过期则将自己的缓存行设置为无效,重新从内存中读取数据。由此可见volatile关键字可以保证可见性。但为什么说volatile关键字也保证了有序性呢?这是因为LOCK#指令本身也实现了内存屏障,这也是JMM实现指令重排序的方式。内存屏障类型主要有四种:读读(LoadLoad)、写写(StoreStore)、写读(LoadStore)、读写(StoreLoad)。
volatile的使用优化:对于缓存行64位的处理器,可以通过追加字节使变量长度凑够一个缓存行。这样在缓存行失效时,不会导致缓存行内的其它变量跟着一起失效。
双重检测DCL问题:可以看我git的详细代码和解释。(https://github.com/Sunflower789/design_patterns/blob/main/singleton/src/main/java/lazy/SingletonLazy4.java
总结:volatile关键字可以保证可见性和有序性,但不能保证原子性。

2.3 原子操作(CAS)

原子操作是指“不可被中断的一个或一系列操作”。
底层原理:java实现原子操作的原理是CAS(Compare and Swap),实质就是执行处理器的CMPXCHG指令。
实现方法:java.util.concurrent.atomic包下有许多API可以直接使用,而且是线程安全的。既可以更新基础类型,也可以更新引用类型或数组。
ABA问题:最早和最后的操作是同一个数值,会错误的以为没有设置成功过B状态。可以增加版本号字段或时间字段进行区分。
总结:CAS可以保证原子性,而且开销小。但要避免长时间的CAS操作或阻塞,会消耗CPU资源。

2.4 AQS队列器

队列同步器AbstractQueuedSynchronizer是用来构建锁或其它同步器的基础框架。它使用一个int成员变量表示同步状态,内置一个FIFO队列来放置线程资源。如果线程CAS获取到同步状态,则正常执行;如果获取失败,则新建节点(Node包含等待状态、前后驱节点、线程引用等)加入队列,此时线程相当于“死循环”阻塞。等获取到同步状态的线程执行完毕释放状态时,唤醒第一个节点去自旋竞争同步状态。
底层原理:CAS+volatile

三、java内存模型JMM

Java线程之间的通信由Java内存模型控制,简称JMM。JMM决定一个线程对共享变量的写入何时对另一个线程可见。线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。
JMM实现了happens-before等抽象规则,使得程序员只需要理解抽象的概念而不需要理解底层实现。

四、实操

4.1 线程状态

线程主要有以下状态:

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

线程状态.jpg
线程的创建是由父线程来进行空间分配的。调用start()方法即是告诉虚拟机只要线程规划器空闲,应立即启动线程。线程的启停方法有suspend(),resume(),stop(),不过这些方法都已经过期,不推荐使用。可以使用线程中断Thread.interrupted()或boolean变量来结束线程。不过要注意,如果线程会抛出InterruptedException异常,则虚拟机会先将该线程的中断标识位清除,然后抛出异常,此时调用isInterrupted()方法会返回false。详细代码见git: https://github.com/Sunflower789/concurrency/tree/main/shutdown

4.2 线程间通信

线程间通信为的是多个线程能够相互配合完成工作。实现方法通常是:共享内存,锁,管道通信。
1、可以利用对象锁来实现线程通信,详见git地址:https://github.com/Sunflower789/concurrency/blob/main/notify/src/main/java/Notify1.java
2、可以利用共享内存和volatile关键字保证可见性这一特点来实现,详见git地址:https://github.com/Sunflower789/concurrency/blob/main/notify/src/main/java/Notify2.java
3、可以利用管道通信(本质还是内存共享)来实现,详见git地址:https://github.com/Sunflower789/concurrency/blob/main/notify/src/main/java/Piped.java

4.3 多线程并发工具类

4.3.1 计数器CountDownLatch

CountDownLatch并发工具类是比较常用的。常用于等待多任务执行完成。具体的使用方法和例子可以看我git上的代码:https://github.com/Sunflower789/concurrency/blob/main/concurrencyutils/src/main/java/CountDownLatchTest.java
底层原理:其实底层还是基于AQS的,只不过不需要使用到队列。由源码可知,new CountDownLatch(count)会在类中创建内部类Sync集成AbstractQueuedSynchronizer父类,将count的值在构造函数里赋值给state变量。线程执行完任务后调用countDownLatch.countDown()方法会首先会循环调用tryReleaseShared()方法直到将state减1;只要state不为0,就执行doReleaseShared()方法,此时AQS队列是没有node的会直接返回true。最后调用countDownLatch.await()方法便是调用doAcquireSharedNanos()方法,循环执行:tryAcquireShared(1)方法(此时队列节点p == head == null),判断变量state==0,是直接返回true,不是判断是否超时。

4.3.2 同步屏障CyclicBarrier

让一个线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会打开,所有被屏障拦截的线程才会继续执行。所以需要特别注意的是:如果屏障个数设置大于线程到达屏障数,所有线程会一直阻塞不会自动释放掉的。CyclicBarrier多用于汇总计算结果等场景。具体的使用方法和例子可以看我git上的代码:https://github.com/Sunflower789/concurrency/blob/main/concurrencyutils/src/main/java/CyclicBarrierTest.java
底层原理:是一个计数类,利用ReentrantLock加锁进行减1操作,直到计数为0时释放所有阻塞线程。cyclicBarrier.wait()方法本身会阻塞线程。

4.3.3 信号量Semaphore

信号量是用来控制同时访问特定资源的线程数量。通常可以用于流量控制,特别是公用资源有限的应用场景,比如数据库连接。具体的使用方法和例子可以看我git上的代码:https://github.com/Sunflower789/concurrency/blob/main/concurrencyutils/src/main/java/SemaphoreTest.java
底层原理:是一个非公平锁。

4.3.4 交换者Exchanger

Exchanger用于线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,两个线程交换数据,将本线程生产出来的数据传递给对方。常用场景有:遗传算法,用于校对工作。具体的使用方法和例子可以看我git上的代码:https://github.com/Sunflower789/concurrency/blob/main/concurrencyutils/src/main/java/ExchangerTest.java
底层原理:使用线程内部变量ThreadLocal维护一个前后节点为其它线程的列表,调用exchange方法会有一些算法判断,决定是阻塞等待指定线程唤醒还是发送数据唤醒其它线程。

4.3.5 Future,CompletableFuture

Future接口可以接收异步结果,FutureTask是其实现类之一。CompletableFuture是封装的任务编排工具,具体常见使用方法可以参考我的git代码:https://github.com/Sunflower789/concurrency/tree/main/future

4.4 线程池

线程池是提前预申请的一批线程组合。目的是减少频繁创建线程多带来的性能损耗,同时也可以提高响应速度。其工作流程如下图所示:
da20237def1ca44d0d4e5cca7ea3328.jpg
1、判断核心线程是否都在执行任务,如果不是则创建新的工作线程来执行任务。反之下个流程。
2、判断工作队列是否已满,如果没有满则提交新任务到队列。反之下个流程。
3、判断是否所有线程都处于工作状态(最大线程数),如果没有则创建新的工作线程来执行任务。反之则交给饱和策略来处理这个任务。

4.4.1 线程池ThreadPoolExecutor的使用

线程池的使用主要就是创建、提交任务和关闭。创建重点在于corePoolSize、maximumPoolSize、runnableTaskQueue、RejectExecutionHandler四个参数的设置。一般对于CPU密集型任务配置尽可能小的线程(比如core数+1),对于IO密集型可以尽可能多的配置线程数(比如2*core数)。(可以通过Runtime.getRuntime().availableProcessors()来获取设备CPU数。)队列一般选择有界阻塞队列(LinkBlockingQueue、ArrayBlockingQueue),如果需要考虑优先级可以使用PriorityBlockingQueue队列,但要注意,如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不会执行。拒绝策略提供了四种:AbortPolicy–直接抛出异常(主线程会结束掉);CallerRunsPolicy–调用者所在线程执行任务;DiscardOldestPolicy–丢弃队列里最近一个任务,并执行当前任务;DiscardPolicy–不处理,丢弃掉。也可以实现RejectExecutionHandler接口来自定义拒绝策略,比如记录日志或存储被拒绝的任务。
线程池提交任务比较简单,就两个方法:execute()和submit()。前者用于提交不需要返回值的任务。后者用于提交需要返回值的任务(API有函数重载既支持Callable也支持Runable),线程池会返回一个future对象,通过get()方法可阻塞获取返回值。
关闭线程池使用shutdown()和shutdownNow()方法。它的原理是遍历线程池中的工作线程,然后逐个调用interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。前者会先设置线程池状态为SHUTWDOWN,然后才执行中断操作,所以正在执行的任务会正常执行结束(包括队列里的)。后者会先设置线程池状态为STOP,则直接中断线程,返回任务队列。(线程池的状态有5中:RUNING,SHUTWDOWN,STOP,TIDYING,TERMINATED)当线程池处于SHUTWDOWN并处理完所有任务后会变成TIDYING,当线程处于STOP并中断结束所有工作线程后也会变成TIDYING。处于TIDYING状态的线程池会执行钩子函数terminated()后变成TERMINATED,此时线程池就不能再提交任务了。
具体的使用方法和例子可以看我git上的代码:https://github.com/Sunflower789/concurrency/blob/main/threadpool/src/main/java/ThreadPoolTest.java

4.4.2 Executor框架

主要是封装下层线程池的任务调度,而更关注上层任务的提交。其核心接口是Executor和ExecutorService。ThreadPoolExecutor和ScheduledThreadPoolExecutor都实现了ExecutorService接口。其优点还包括提供了一些便捷创建特殊线程池的方法(SingleThreadPoolExecutor,FixThreadPool,CacheThreadPool等)但是其缺点也是明显的,没有自定义线程池名称,不利于异常排查,有些特殊线程池的默认创建使用了无界队列使用不当容易造成OOM。这也是为什么有些开发团队会要求直接使用ThreadPoolExecutor而不使用Executor的原因。
Executor封装进Springboot
1、直接使用ThreadPoolExecutor创建bean对象,参数通过配置文件注入。被bean容器管理的线程池在shutdown后只有核心线程并不会销毁。
2、在上一个方法的基础上,重写AsyncConfigurer接口,可以实现@Async注解使用线程池。这样可以方便线程池统一异常处理。具体例子可以参考我git上的代码:https://github.com/Sunflower789/concurrency/tree/main/async_use/src/main。这里要注意@Async注解是否生效的问题(可以网上搜下有很多答案,也可以自测)。不要再@Async标注的方法上也标注@Transactional注解,但可以在调用的方法上使用@Transactional。

4.5 Lock接口和锁

为了更灵活的使用锁,提供了Lock接口。常用API如下:

方法说明
lock()获取锁,成功则从方法返回,失败则阻塞。
tryLock()非阻塞获取锁。成功返回true,失败返回false。

简单使用可以参考git上代码:https://github.com/Sunflower789/concurrency/tree/main/lock

4.5.1 可重入锁ReentrantLock

在原有AQS的基础上,增加变量记录当前线程和重入次数。便实现可重入锁。

4.5.2 公平锁和非公平锁

公平锁就是锁的获取顺序按照FIFO队列的绝对顺序。非公平锁的获取则是哪个阻塞线程自旋设置同步状态成功获得。因此非公平锁的效率高些。但是公平锁所有线程都可以得到资源,不会出现非公平锁可能存在某线程一直抢不到资源的情况。可重入锁的AQS默认是使用非公平方式竞争资源。

4.5.3 读写锁

其本质就是AQS的同步状态允许被多个读线程共享,写线程则还是独占。(其实是两把锁或者说内部是两个“上锁”的状态对象)

4.6 并发容器

4.6.1 concurrencyHashMap

本质是将HashMap进行分组。拆分成多个HashEntry组成的Segment,多个Segment组成一个数组。不同Segment之间是互相不上锁的,但是同一个Segment多个线程同时修改是上锁的。

4.6.2 concurrencyLinkedQueue

使用CAS多次定位尾(头)节点,只有确定自己拿到的指针是指向尾(头)节点时,才进行入队或出队。

4.7 工作窃取ForkJoin

ForkJoin将大人物拆分成多个子任务再合并结果集。采用工作窃取算法来实现,快线程可以从慢线程里拿取任务执行。一个工作线程对应一个工作队列。被窃取线程会从工作队列的头部拿任务执行,窃取线程从队列尾部拿任务执行。适用于大任务的拆分,不适用于有IO的操作,多用于复杂的长时间计算任务。
详细使用例子可以参考git:https://github.com/Sunflower789/concurrency/tree/main/forkjoin

参考文献

1、《Java并发编程的艺术》–方腾飞、魏鹏、程晓明
2、其它

  • 8
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值