目录:
1. 线程的创建
2. 线程常用的方法yield()&&join()
sleep(),wait区别
wait()&¬ify
3. 返回值线程Callable和Future&FutureTask
4. 多线程同步锁synchronized&Lock
5. volite
6. AQS CAS
7.死锁监控
8.线程优化
9. 生产者消费者模型
正文:
1. 线程的创建
创建线程的3种方式:
1).实现Runnable接口的多线程例子
2).测试扩展Thread类实现的多线程程序
Thread有setName("MyThread");Runnable没有
3). 返回值的callable
2. 线程常用的方法
2.1 yield()&&join()
join&Yield:2者的作用相反,一个是加入(join),一个是让步(yield)
问题: 现在有 T1、T2、T3 三个线程,你怎样保证 T2 在 T1 执行完后执行,T3 在 T2 执行完后执?
yield(): 线程的让步是通过Thread.yield()来实现的。yield()方法的作用是:暂停当前正在执行的线程对象,并执行其他线程。
join(): Thread的非静态方法join()让一个线程B“加入”到另外一个线程A的尾部。B.join(),B优先执行(参加)
使用案例
2.2 wait() &¬ify
notify()和wait()是配套使用的
void notify() 唤醒在此对象监视器上等待的单个线程(唤醒线程) void notifyAll() 唤醒在此对象监视器上等待的所有线程。 void wait() 导致当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法。 (释放锁)
关于等待/通知,要记住的关键点是:必须从同步环境内调用wait()、notify()、notifyAll()方法。线程不能调用对象上等待或通知的方法,除非它拥有那个对象的锁。
wait()一般和sycronized一起用
使用案例:
2.3 sleep(),wait区别
使用案例:
3. 返回值线程Callable和Future&FutureTask
3.1 Callable: Callable与Runnable的功能大致相似,Callable中有一个call()函数,但是call()函数有返回值(有返回值)
3.2 Future: Future就是对于具体的Runnable或者Callable任务的执行结果进行 (执行结果)
3.3. Executor: Executor就是Runnable和Callable的调度容器
3.4 FutureTask: 是一个RunnableFuture<V>,而RunnableFuture实现了Runnbale又实现了Futrue<V>这两个接口
因此FutureTask既是Future、Runnable,又是包装了Callable( 如果是Runnable最终也会被转换为Callable ), 它是这两者的合体。
使用的实例:AsyncTask的源码分析
public class FutureTask<V> implements RunnableFuture<V> {
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
3.5 具体使用案例: 4者关系的2中组合:
第一种组合模式:Callable,FutureTask,Future的组合
Callable<Integer> callable = new Callable<Integer>() {
public Integer call() throws Exception {
return task(20);
}
};
FutureTask<Integer> future = new FutureTask<Integer>(callable);
new Thread(future).start();
第二种组合模式:ExecutorService ,Future,Callable
try {
ExecutorService mExecutor = Executors.newSingleThreadExecutor();
FutureTask<Integer> futureTask = new FutureTask<Integer>(
new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return task(20);
}
});
// 提交futureTask
/**FutureTask此时做为Runnable来用,Future是线程池的返回值*/
Future <?> futureTest=mExecutor.submit(futureTask);
// futureTest.get();
Log.d(TAG, "future result from futureTask : "
+ futureTask.get());
} catch (Exception e) {
e.printStackTrace();
}
4. 多线程同步锁synchronized&Lock
4.1 多线程为什么会有并发问题
1). Java 内存模型规定了所有的变量都存储在主内存中,每条线程有自己的工作内存。
2). 线程的工作内存中保存了该线程中用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。
3). 线程访问一个变量,首先将变量从主内存拷贝到工作内存,对变量的写操作,不会马上同步到主内存。
4). 不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行
4.2 为何要使用同步?
java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),
将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,
从而保证了该变量的唯一性和准确性。
同步和异步的区别
首先以一个常见的开发场景来区别一下同步和异步的区别,比如我们要获取一张网络图片并完成显示。在这个场景中我们需要开启两个线程,一个是子线程—即下载图片的线程;另外是主 UI 线程—即图片下载完成后进行显示的线程。针对这个场景分别用两幅实现的流程图来区分同步和异步。
4.3 线程的3种特性:
1).原子性:在一个操作中,CPU 不可以在中途暂停然后再调度,即不被中断操作,要么执行完成,要么就不执行。
2).可见性:多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
3.)有序性:程序执行的顺序按照代码的先后顺序执行。
4.4多线程同步的六种方式比较
4.4.1 synchronized
synchronized一直是解决线程安全问题,它可以保证原子性,可见性,以及有序性。
4.4.1.1 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块.
4.4.1.2 synchronized 线程执行互斥代码的六个过程:
(1)获得互斥锁;
(2)清空工作内存;
(3)从主内存中拷贝变量的最新值 到工作内存;
(4)执行代码;
(5)将更改后的共享变量的值刷新到主内存;
(6)释放互斥锁
4.4.1.3 synchronized 原理(监视器锁(Monitor)
synchronized 代码块是由一对儿 monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现,
Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。
synchronized开始的缺陷:
而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。
因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为“重量级锁”。
synchronized性能升级:
在 Java 6 之前,Monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。
现代的(Oracle)JDK 中,提供了三种不同的 Monitor 实现,也就是常说的三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。
所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。
锁升级:
关于锁类型与相关信息的信息都是存放在锁对象的对象头中 ,在了解偏向锁、轻量级锁、重量级锁之前,我们必须先认识一下什么是对象头!
自旋锁:
线程自旋说白了就是让cpu在做无用功,比如:可以执行几次for循环,可以执行几条空的汇编指令,目的是占着CPU不放,等待获取锁的机会。如果旋的时间过长会影响整体性能,时间过短又达不到延迟阻塞的目的。
偏向锁
当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操作,在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
轻量锁:
如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;
重量锁:
重量锁在JVM中又叫对象监视器(Monitor),它很像C中的Mutex,除了具备Mutex(0|1)互斥的功能,它还负责实现了Semaphore(信号量)的功能
否则,进一步升级为重量级锁(可能会先进行自旋锁升级,如果失败再尝试重量级锁升级)。
我注意到有的观点认为 Java 不会进行锁降级。实际上据我所知,锁降级确实是会发生的,当 JVM 进入安全点(SafePoint)的时候,会检查是否有闲置的 Monitor,然后试图进行降级。
synchronized锁升级原理:
大家对Synchronized的理解可能就是重量级锁,但是Java1.6对 Synchronized 进行了各种优化之后,有些情况下它就并不那么重,Java1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。
对象在虚拟机内存中的布局分为三块区域:对象头、实例数据和对齐填充;Java对象头是实现synchronized的锁对象的基础,一般而言,synchronized使用的锁对象是存储在Java对象头里
1). 比如只有一个线程访问的时候,是偏向锁;
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
当一个线程A访问加了同步锁的代码块时,会在对象头中存 储当前线程的id,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。
2). 当多个线程访问,但不是同时访问,这时候锁升级为轻量级锁;
在偏向锁情况下,如果线程B也访问了同步代码块,比较对象头的线程id不一样,会升级为轻量级锁,并且通过自旋的方式来获取轻量级锁。
3). 当多个线程同时访问,这时候升级为重量级锁。所以在并发不是很严重的情况下,使用Synchronized是可以的
如果线程A和线程B同时访问同步代码块,则轻量级锁会升级为重量级锁,线程A获取到重量级锁的情况下,线程B只能入队等待,进入BLOCK状态。
问题: synchronized锁方法和同步代码块区别
可以保证同步synchronized修饰的代码块 使用的锁是对象锁。
锁方法:也是锁对象,这个对象是不是同一个
同步代码块,锁对象,可以指定对象。
问题: 静态同步锁与普通同步锁的区别
普通的是锁住了对象
静态是锁住了类,类有多个实例的对象,synchronized修饰静态方法时,使用的锁是类对象的锁,Class。
等价于synchronized(this){}与synchronized(class){}。
使用总结:
1).方法锁(synchronized修饰方法)每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。从而有效避免了类成员变量的访问冲突。
2).对象锁(synchronized修饰方法或代码块)调用此对象的同步方法或进入其同步区域时,就必须先获得对象锁。如果此对象的对象锁已被占用,则需要等待此锁被释放。(方法锁也是对象锁)。好处:方法抛异常的时候,锁仍然可以由JVM来自动释放。
3).类锁(synchronized修饰静态的方法或代码块),使用字节码文件(即.class)作为锁。此类所有的实例化对象在调用此方法,共用同一把锁,我们称之为类锁。类锁是用来控制静态方法(或静态变量互斥体)之间的同步。
4.4.2 ReentrantLock (重入锁,有公平锁和非公平锁,默认是非公平锁)
ReentrantLock类是可重入、互斥、实现了Lock接口的锁
4.4.2.1 Lock底层实现原理:
Lock(ReentrantLock)的底层实现主要是Volatile + CAS(乐观锁),如果面试问起,你就说底层主要:AQS! AQS主要:靠volatile和CAS操作实现的。
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
...
}
ReentrantLock和 synchronized2者的终极比较:
1). 原理:
2). 高并发:Lock, 乐观锁
并发量不大:sychronize,悲观锁
3). Lock可以等待可中断,
ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
4). 使用场景:
请求并发量不大的情况下使用synchronized关键字。
Lock实现和synchronized不一样,后者是一种悲观锁,它胆子很小,它很怕有人和它抢吃的,所以它每次吃东西前都把自己关起来。而Lock呢底层其实是CAS乐观锁的体现
5). Lock灵活: Lock只能锁代码块,不能锁方法!
5.volatile
5.1 volatile让变量每次在使用的时候,都从主存中取。而不是从各个线程的“工作内存”。
对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的
5.2 volatile具备两种特性
1). 可见性
2). 禁止指令重排序优化。
比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。 2.重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变 比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系,所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。 重排序在单线程模式下是一定会保证最终结果的正确性,但是在多线程环境下,问题就出来了,来开个例子,我们对第一个TestVolatile的例子稍稍改进,再增加个共享变量a
5.3 使用场景:
1). 对于一个变量,只有一个线程执行写操作,其它线程都是读操作,这时候可以用 volatile 修饰这个变量。
如果读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。
2). 双重检查锁定(double-checked-locking)
public class TestInstance {
private static volatile TestInstance mInstance;
public static TestInstance getInstance(){ //1
if (mInstance == null){ //2
synchronized (TestInstance.class){ //3
if (mInstance == null){ //4
mInstance = new TestInstance(); //5
}
}
}
return mInstance;
}
举例:假如没有用volatile,并发情况下会出现问题,线程A执行到注释5 new TestInstance() 的时候,分为如下几个几步操作:分配内存初始化对象mInstance 指向内存
这时候如果发生指令重排,执行顺序是132,执行到第3的时候,线程B刚好进来了,并且执行到注释2,这时候判断mInstance 不为空,直接使用一个未初始化的对象。所以使用volatile关键字来禁止指令重排序。
5.4 volatile原理: (内存屏障)
针对处理器重排序:编译器在生成字节码指令时,通过在指令序列中插入内存屏障指令来禁止特定类型的处理器重排序,以实现volatile. 内存语义:volatile底层通过内存屏障指令实现
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将缓存的修改操作立即写到主内存
- 写操作会导致其它CPU中的缓存行失效,写之后,其它线程的读操作会从主内存读。
对内存操作顺序进行了限制。
- 硬件层的内存屏障分为两种:
Load Barrier
和Store Barrier
即读屏障和写屏障。 - 内存屏障有两个作用:
- 阻止屏障两侧的指令重排序;
- 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
JVM内存屏障插入策略:
在每个volatile写操作的前面插入一个StoreStore屏障;
在每个volatile写操作的后面插入一个StoreLoad屏障;
在每个volatile读操作的后面插入一个LoadLoad屏障;
在每个volatile读操作的后面插入一个LoadStore屏障。
具体来说就是:
- 对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;
- 对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障
问题: Volatile 修饰的变量在修改时具体如何实现可见性的?
首先在字节码中 针对Volatile修饰的变量会有一个 ACC_VOLATILE 标志:
- 可见性:当前线程修改后其他线程堆修改后的值立即可见,线程修改值之后立即写入到主存区,不用等到线程出栈的时候才同步到主存区。(线程工作区内存中保存的值要等待线程出栈时才写回主存区,在此之前其他线程对修改是不可见的)
volatile的字节码:
// 读-写顺序性,假设有一个被 ACC_VOLATILE 标志修饰的 volatile 字段 v
// 线程 1
getfield v // 读取字段 v 的值
iconst_2 // 将常量值 2 压入操作数栈
putfield v // 将操作数栈顶的值存入字段 v 中
// 线程 2
getfield v // 读取字段 v 的值
//写-读顺序性,假设有一个被 ACC_VOLATILE 标志修饰的 volatile 字段 v
// 线程 1
iconst_1 // 将常量值 1 压入操作数栈
putfield v // 将操作数栈顶的值存入字段 v 中
// 线程 2
getfield v // 读取字段 v 的值
5.4 volatile和synchronized两者之间比较
|
| 是否原子性 | 阻塞线程 | 原理 | 使用范围 |
| synchronized | 是 | 是 | monitor | 方法,类,变量 |
| volatile | 否 | 否 | 内存屏障 | 变量 |
volatile
synchronized
原子量: AomicInter
5.4. 1 AomicInter原理: volatile+ CAS
具体AomicInter实现:
现代的CPU提供了特殊的指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。
拿出AtomicInteger来研究在没有锁的情况下是如何做到数据正确性的。
private volatile int value;
首先毫无以为,在没有锁的机制下可能需要借助volatile原语,保证线程间的数据是可见的(共享的)。
这样才获取变量的值的时候才能直接读取。
public final int get() {
return value;
}
然后来看看++i是怎么做到的。
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
而compareAndSet利用JNI来完成CPU指令的操作。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
整体的过程就是这样子的,利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。
5.4.2 手动实现
如何把普通变量升级为原子变量?主要是AtomicIntegerFieldUpdater<T>类
AtomicRefenrence是干嘛的?
多线程并发总结:
- 当只有一个线程写,其它线程都是读的时候,可以用
volatile
修饰变量- 当多个线程写,那么一般情况下并发不严重的话可以用
Synchronized
,Synchronized在并发不严重的时候,比如只有一个线程访问的时候,是偏向锁;- 当多个线程访问,但不是同时访问,这时候锁升级为轻量级锁;
- 当多个线程同时访问,这时候升级为重量级锁。所以在并发不是很严重的情况下,使用Synchronized是可以的。不过Synchronized有局限性,比如不能设置锁超时,不能通过代码释放锁。
ReentranLock
可以通过代码释放锁,可以设置锁超时。- 高并发下,Synchronized、ReentranLock 效率低,因为同一时刻只有一个线程能进入同步代码块,如果同时有很多线程访问,那么其它线程就都在等待锁。这个时候可以使用并发包下的数据结构,例如
ConcurrentHashMap
,LinkBlockingQueue
,以及原子性的数据结构如:AtomicInteger
。
5.5 阻塞队列实现线程同步
问题:如何在两个线程之间共享数据
通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的
5.5.1 LinkedBlockingQueue使用场景
由于LinkedBlockingQueue实现是线程安全的,实现了先进先出等特性,是作为生产者消费者的首选,
5.5.2 核心方法
它用于阻塞操作的是put()和take()方法。
put()方法:类似于我们上面的生产者线程,容量达到最大时,自动阻塞。
take()方法:类似于我们上面的消费者线程,容量为0时,自动阻塞。
5.5.3 原理
LinkedBlockingQueue内部使用ReentrantLock实现插入锁(putLock)和取出锁(takeLock)
链表结构的阻塞队列,内部使用多个ReentrantLock
/** Main lock guarding all access */
final ReentrantLock lock = new ReentrantLock();
/** Condition for waiting takes */
private final Condition notEmpty = lock.newCondition();
/** Condition for waiting puts */
private final Condition notFull = lock.newCondition();
public LinkedBlockingQueue(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock putLock = this.putLock;
putLock.lock(); // Never contended, but necessary for visibility
try {
int n = 0;
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (n == capacity)
throw new IllegalStateException("Queue full");
enqueue(new Node<E>(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}
6. AQS CAS
6.1 CAS
CAS:是乐观锁,需要线程比较少的自旋
CAS:可以进行原子操作, 是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)
CAS,Compare and Swap即比较并交换,设计并发算法时常用到的一种技术,java.util.concurrent包全完建立在CAS之上,没有CAS也就没有此包,可见CAS的重要性。当前的处理器基本都支持CAS,只不过不同的厂家的实现不一样罢了。并且CAS也是通过Unsafe实现的,由于CAS都是硬件级别的操作,因此效率会比普通加锁高一些。
6.1.2 CAS算法
compare and swap(比较与交换)即比较-替换。假设有三个操作数:**内存值V、旧的预期值A、要修改的值B,
,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。
CAS算法涉及到三个操作数
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
6.1.3 CAS的原理:
就是自旋
6.1.4 CAS使用场景
当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。
6.1.5CAS缺点
CAS引发的ABA问题:
通过一个类,AtomicSampleReferece实现
CAS看起来很美,但这种操作显然无法涵盖并发下的所有场景,并且CAS从语义上来说也不是完美的,存在这样一个逻辑漏洞:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?如果在这段期间它的值曾经被改成了B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个漏洞称为CAS操作的"ABA"问题。
java.util.concurrent包为了解决这个问题,提供了一个带有标记的原子引用类"AtomicStampedReference",它可以通过控制变量值的版本来保证CAS的正确性。不过目前来说这个类比较"鸡肋",大部分情况下ABA问题并不会影响程序并发的正确性,如果需要解决ABA问题,使用传统的互斥同步可能回避原子类更加高效。
然后Compare and Swap的返回值是正确还是错误
6.1.6 和Synchronized比较
6.2. AQS
6.2.1 ReentrantLock实现的前提就是AbstractQueuedSynchronizer(AQS)队列同步器,简称AQS
是java.util.concurrent的核心,CountDownLatch、FutureTask、Semaphore、ReentrantLock等都有一个内部类是这个抽象类的子类。AQS是实现锁和其他同步组件的基础框架
6. 2.2. 干嘛用过的:
AQS是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理,线程的排队、等待与唤醒等底层操作。
6.2.3 数据结构
AQS实际上以双向队列的形式连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。
由于AQS是基于FIFO队列的实现,因此必然存在一个个节点,Node就是一个节点,Node有两种模式:共享模式和独占模式。
6. 2.4 原理
两个核心(同步状态和同步队列)
更改同步状态的三种核心方法
同步队列的结构、如何设置尾节点(需要CAS保证)和头结点(不需要CAS保证)
6.2.6 AQS中,自定义同步器需要重写哪些方法?
基础: AQS中5种指定的可重写方法的名称、作用,tryAcquire()的实现:compareAndSetState()。
进阶: 以实现锁为例,讲解如何使用同步器的实现自定义锁。
6.3 自旋锁
- 自旋锁(spinlock):是指尝试去获取锁的线程不会立即阻塞,而是采用循环的方式去获取锁,这样的好处是减少线程上下文切换消耗,缺点是循环会消耗CPU。
很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
手写自旋锁:
public class SpinLock {
private AtomicReference<Thread> cas = new AtomicReference<Thread>();
public void lock() {
Thread current = Thread.currentThread();
// 利用CAS
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread current = Thread.currentThread();
cas.compareAndSet(current, null);
}
}
public abstract class Lifecycle {
/**
* Lifecycle coroutines extensions stashes the CoroutineScope into this field.
*
* @hide used by lifecycle-common-ktx
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
AtomicReference<Object> mInternalScopeRef = new AtomicReference<>();
7.死锁监控
死锁怎么产生的?
死锁的解决办法?
1.破坏4个条件的一个,通过业务场景
2.控制线程的时序,尽量不要使用嵌套锁
3..避免在同步代码块中调用外部的同步方法。
4.在嵌套多层synchronized的代码块中,通过对象唯一值给对象锁进行排序,例如使用对象的hashCode值进行排序,使得每次获取锁的顺序是一致的。
5.用阻塞队列,信号量,countdownlatch。
死锁的列子:
1.A小孩有一把枪,要B的che
2.B有车,要A的抢
在Java层并没有相关API可以实现死锁监控,可以从Native层入手。
1). 获取blocked状态的线程
2). 获取该线程想要竞争的锁(native层函数)
3). 获取这个锁被哪个线程持有(native层函数)
4). 有了关系链,就可以找出造成死锁的线程
8.线程优化
koom对于线程的监控逻辑
9. 生产者消费者模型
生产者消费者模型四种实现方法
同步问题核心在于:如何保证同一资源被多个线程并发访问时的完整性。常用的同步方法是采用信号或加锁机制,保证资源在任意时刻至多被一个线程访问。Java语言在多线程编程上实现了完全对象化,提供了对同步机制的良好支持。在Java中一共有四种方法支持同步,其中前三个是同步方法,一个是管道方法。
(1)wait() / notify()方法
(2)BlockingQueue阻塞队列方法
(3)await() / signal()方法
(4)PipedInputStream / PipedOutputStream
(5)synchronized 使用。Java 多线程 -- 协作模型:生产消费者实现方式一:管程法
(6) synchronized 使用。 Java_多线程并发协作模型生产者消费者模式_信号灯法