并发体系
线程
线程安全
线程安全性:当多个对象访问同一个对象时,如果不考虑这些线程运行环境的调度与交替执行,也不需要额外的同步,或者进行调用方任何其它协调操作。调用这个对象都可以获得正确的结果,那这个对象就是线程安全的。
线程实现:
- Runnable:函数没有返回值
- Callable:函数有返回值
- Future:对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果、设置结果操作。
- FutureTask:是Future也是Runnable,又是包装了的Callable
- Thread:代表JVM一个线程
线程状态
- NEW:至今尚未启动的线程处于该状态,通俗来讲,该状态是线程实例化后还从未执行start()方法的状态;
- RUNNABLE:正在java虚拟机中执行的线程处于这种状态;
- BLOCKED:受阻塞并等待某个监视器锁的线程处于这种状态;
- WAITING:无限期地等待另一个线程来执行某一特定操作的线程处于这种状态;
- TIMED_WAITING:等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态;
- TERMINATED:已退出的线程处于这种状态,线程被销毁。
方法:
- sleep: 暂停阻塞等待一段时间,时间过了就继续。注意这个是不释放“锁”的
- wait: 也是阻塞和等待,但是需要notify来唤醒。wait是需要释放“锁”的
- join: 在一个线程中调用other.join(),将等待other执行完后才继续本线程
- notify/notifyAll: 唤醒线程
- yield: 当前线程可转让cpu控制权,让别的就绪状态线程运行(切换),也会等待阻塞一段时间,但是时间不是由客户控制了
- interrupte: 打断线程,可代替过时方法stop
- setPriority: MIN_PRIORITY 最小优先级=1 , NORM_PRIORITY 默认优先级=5 ,MAX_PRIORITY 最大优先级=10
线程安全实现方法
互斥同步:
- synchronized关键字, Java 5 以前使用,独占锁是一种悲观锁,synchronized就是一种独占锁
- 锁:重量型,消耗内存较多,原子性,可见的,顺序:公平锁 、非顺序:非公平锁
非阻塞同步:
- volatile 变量:轻量级的线程同步,不会引起线程调度,提供可见性,但是不提供原子性
- CAS 原子指令:轻量级线程同步,不会引起线程调度,提供可见性和原子性
无锁方案:
synchronized与volatile:
- volatile是线程安全的轻量级实现,volatile性能比synchronized好,且volatile只能修饰于变量,synchronized可以修饰方法,以及代码块
- 多线程访问volatile不会发生阻塞,而synchronized会发生阻塞
- volatile能保证数据可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公有内存中的数据做同步。
##3 线程池
线程池的两个主要作用:
- 控制线程数量,避免因为创建大量的线程导致的系统崩溃
- 重用线程,避免频繁地创建销毁线程
Java 1.5引入Executor与ExecutorService:
- Executor: 提交普通的可执行任务
- ExecutorService: 在Executor的基础上增强了对任务的控制,同时包括对自身生命周期的管理
- ScheduledExecutorService: 在ExecutorService基础上,提供对任务的周期性执行支持
Executors,是生产Executor的工厂:
- 固定线程数的线程池:newFixedThreadPool
- 单个线程的线程池:newSingleThreadExecutor
- 可缓存的线程池:newCachedThreadPool
- 可延时/周期调度的线程池:newScheduledThreadPool
- Fork/Join线程池:newWorkStealingPool,在Java 1.7时才引入,其核心实现就是ForkJoinPool类
工作窃取算法
由于线程处理不同任务的速度不同,这样就可能存在某个线程先执行完了自己队列中的任务的情况,这时为了提升效率,我们可以让该线程去“窃取”其它任务队列中的任务,这就是所谓的工作窃取算法。ForkJoinPool是一种实现了工作窃取算法的线程池。
锁
乐观锁/悲观锁
乐观锁与悲观锁概念:
- 悲观锁:认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
- 乐观锁:认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。一般会使用“数据版本机制”或“CAS操作”来实现。
乐观锁与悲观锁使用场景:
- 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确
- 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升
数据版本机制
实现数据版本一般有两种,第一种是使用版本号,第二种是使用时间戳。
版本号方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
CAS
CAS全称Compare And Swap(比较与交换),在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。
CAS算法涉及到三个操作数:
- 需要读写的内存值 V
- 进行比较的值 A
- 要写入的新值 B
问题:
- ABA问题: CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。Java 1.5的实现是compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
- 循环时间长开销大: CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
- 只能保证一个共享变量的原子操作: 对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
独享锁/共享锁
- 独享锁:指该锁一次只能被一个线程所持有。ReentrantLock是独享锁,Synchronized是独享锁。
- 共享锁:指该锁可被多个线程所持有。ReadWriteLock其读锁是共享锁,其写锁是独享锁。
AQS
独享锁与共享锁也是通过AQS(AbstractQueuedSynchronized)来实现的,通过实现不同的方法,来实现独享或者共享。
AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch。AQS维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。
AQS定义两种资源共享方式:
- Exclusive:独占,只有一个线程能执行,如ReentrantLock
- Share:共享,多个线程可同时执行,如Semaphore/CountDownLatch
AQS支持中断、超时:
- 阻塞和非阻塞(例如tryLock)同步
- 可选的超时设置,让调用者可以放弃等待
- 可中断的阻塞操作
自旋锁/适应性自旋锁
- 自旋锁:指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
- 自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。
- 自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。
- 适应性自旋锁:自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
- 在自旋锁中 另有三种常见的锁形式:TicketLock、CLHlock和MCSlock。
公平锁/非公平锁
- 公平锁:指多个线程按照申请锁的顺序来获取锁。ReetrantLock通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
- 非公平锁:指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。Synchronized是非公平锁。
无锁/偏向锁/轻量级锁/重量级锁
后三种锁是指锁的状态,并且是针对Synchronized。
- 无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
- 偏向锁:指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
- 轻量级锁:是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
- 重量级锁:是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。
锁状态 |
存储内容 |
标识位 |
备注 |
无锁 |
对象的hashcode、对象分代年龄、是否是偏向锁(0) |
01 |
|
偏向锁 |
偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) |
01 |
偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。 |
轻量级锁 |
指向栈中锁记录的指针 |
00 |
|
重量级锁 |
指向互斥量的指针 |
10 |
|
整体的锁状态升级流程如下:
无锁 —> 偏向锁 —> 轻量级锁 —> 重量级锁
可重入锁/非可重入锁
- 可重入锁:又名递归锁,表示该锁能够支持 一个线程对资源的重复加锁,不会因为之前已经获取过还没释放而阻塞。ReentrantLock和synchronized都是重入锁。可重入锁的一个优点是可一定程度避免死锁。
- 非可重入锁:表示该锁能够 不 支持 一个线程对资源的重复加锁,同一线程重入会导致死锁。
原文
http://lanlingzi.cn/post/technical/2019/0922_java_base_2/