JUC(Java并发编程)
八锁、线程池、ThreadLocal
Synchronized和Lock区别
语法层面
Synchronized是一个关键字,底层由C++编写 ;Lock是jdk的一个API
Synchronized退出同步代码块自动释放锁;Lock需要手动unlock()
功能层面
都是悲观锁,都互斥,都是同步锁,都可重入
Lock可获取等待状态,公平,可打断
Lock有多种实现方式
在没竞争或者竞争小的情况下Synchronized昨儿很多优化,偏向锁、轻量锁……
竞争激励的时候使用Lock
Condition是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是 Condition 接口默认提供的。而synchronized关键字就相当于整个 Lock 对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
Synchronized升级
无锁—偏向锁—轻量级锁—重量级锁
- 在实际的应用中,锁总是同一个线程持有,很少发生竞争,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。
- 那么只需要锁在第一次被拥有的时候,记录下偏向线程的ID,这样偏向线程就一直持有锁(后续这个线程进入和退出这段同步锁的代码块时,不需要再次加锁和释放锁),而是直接会去检查锁的MARDWORD里面是否放的自己线程的ID。
- 如果相等,表示偏向锁是当前线程的,就不需要再次尝试获取锁,直到竞争发生才释放锁。以后每一次同步,检查所的偏向线程ID是否与当前线程ID一致,如果一致直接进入同步,无需每次加锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
- 如果不等,表示发生了竞争,此时锁已经不是偏向于同一个线程了,这个时候会尝试使用CAS来替换MarkWord里面的线程ID为新线程ID(偏向锁只有遇到其他线程尝试竞争偏向锁的时候,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁的)
- 竞争成功,表示之前的线程不存在了,MardWord里面的线程ID为新的线程ID,锁不会升级,仍然为偏向锁;
- 竞争失败,这个时候可能需要升级为轻量级锁才可以保证线程间公平竞争锁
- 轻量级锁由偏向锁升级而来,当存在第二个线程竞争的时候偏向锁会升级为轻量级锁,竞争线程尝试CAS更新对象头失败,会等到全局安全点撤销偏向锁。偏向锁的撤销:
- 第一个线程需要在执行synchronized方法(处于代码块),他还没有执行完,其他线程来抢夺,该偏向锁就会被取消并出现锁升级,此时轻量级锁由原持有偏向锁的线程持有,继续执行代码块,而正在竞争的线程会进入自旋获得该轻量级锁。
- 第一个线程执行完成synchronized(退出同步块),则将对象头设置为无锁状态并撤销偏向锁,重新偏向。
- 重量级锁:基于进入退出Monitor管程对象进行的,编译时回有monitorenter和monitorexit指令
实例方法上锁
代码块上锁
ReentrantLock实现3线程交替打印
1 | /** |
可重入锁
同步代码块例子:从头到尾锁的都是object,外层已经获取锁了,中层内层也可以直接进入。
对于ReentrantLock:注释掉内层的unlock后:仍然可以执行完程序,但是计数器没有清零,所以拿两个线程跑的时候就会出现阻塞现象。moniterexit计数器-1,只有减到0才会释放。
ArrayList的并发修改异常以及解决方法
并发修改异常:
1 | List<String> list = new Vector(); //JDK1.0 古老的实现类 用的synchronized,效率低 |
SynchronizedList无需改变List类的子类的数据结构,就可以将它们转换成线程安全的类,而Vector不能。
SynchronizedList遍历时没有进行同步处理,Vector的遍历方法是线程安全的。
SynchronizedList可以指定锁定的对象,Vector的锁定范围是方法。
Fail Fast与Fail Safe
- Fail Fast
- 一旦发现遍历时有人修改就抛出异常
- 对于ArrayList,底层源码有两个参数:
- mpdCount : 遍历过程中list被修改的次数
- except :遍历开始前被修改的次数
- 不一致的话就报错
- Fail Save
- CopyOnWriteArrayList就是采用这种方式
- 读的时候遍历旧数组
- 插入就先copy出来,加在copy的末尾再copy回去
HashSet安全方法
1 | Set<String> set = Collections.synchronizedSet(new HashSet()); |
HashMap、ConcurrentHashMap、Hashtable
CAS
CAS即CompareAndSwap,翻译成中文即比较并替换。Java中可以通过CAS操作来保证原子性,原子性 就是不可被中断的一些列操作或者一个操作,简单来说就是一系列操作,要么全部完成,要么失败,不 能被中断。
AQS
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** 排他锁的标识 */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/**0:默认值
-1:表示线程已经准备好了,就等释放资源了
-2:在等待队列中,等待condition唤醒
-3:共享式同步状态获取将会无条件地传播
*/
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
/** waitStatus Node对象储存表示的对象 */
volatile int waitStatus;
/** 上一个节点 */
volatile Node prev;
/** 下一个节点 */
volatile Node next;
/** 当前Node绑定的线程 */
volatile Thread thread;
Node nextWaiter;
/** 返回前一个节点,如果为null就抛异常 */
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
}
node节点里有个变量非常重要!waitStatus
1:线程被取消
0:默认值1:表示线程已经准备好了,就等释放资源了
2:在等待队列中,等待condition唤醒
3:共享式同步状态获取将会无条件地传播
下面举例说明,以独占式的 ReentrantLock 为例, state 初始状态为0,表示未锁定状态。A线程进行 lock() 时,会调用 tryAcquire() 独占该锁并将 state+1 。此后,其他线程再调用 tryAcquire() 时 就会失败,直到A线程 unlock() 到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取 多少次就要释放多么次,这样才能保证state是能回到零态的。
非公平锁
1 | final void lock() { |
1 | public final void acquire(int arg) { |
1 | protected boolean tryAcquire(int arg) { |
常用的辅助类(必会)
CountDownLatch 减法计数器
1 | public static void main(String[] args) throws InterruptedException { |
CyclicBarrier 加法计数器
反应了等一组线程某个条件完成以后全部一起执行后续功能
1 | public static void main(String[] args) { |
Semaphore 信号量
6个车 三个停车位 轮流等待车位
- 作用:多个共享资源互斥的使用并发限流,控制最大线程数打印结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3); //默认线程数 停车位个数
for (int i = 0; i < 6; i++) {
int temp = i;
new Thread(()->{
try {
semaphore.acquire();//得到,如果已经满了就等到释放为止
System.out.println(Thread.currentThread().getName() + "车进来了");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "车离开了");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();//释放,会将当前的信号量释放 + 1,然后唤醒等待的线程
}
}).start();
}
}1
2
3
4
5
6
7
8
9
10
11
12Thread-0车进来了
Thread-2车进来了
Thread-1车进来了
Thread-1车离开了
Thread-2车离开了
Thread-0车离开了
Thread-3车进来了
Thread-5车进来了
Thread-4车进来了
Thread-3车离开了
Thread-5车离开了
Thread-4车离开了
阻塞队列
阻塞
- 写入:如果队列满了,就必须阻塞等待
- 取出:如果队列是空的,必须阻塞等待生产
- 四组API
抛异常 有返回值,不抛出异常 阻塞等待 超时等待 添加 add offer put offer 移除 remove poll take poll 检测队列首 element peek / /
ConcurrentHashMap1.7 1.8底层实现原理
线程池
推荐使用 ThreadPoolExecutor 构造函数创建线程池
在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
另外,《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors 返回线程池对象的弊端如下(后文会详细介绍到):
FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
三大方法
1
2
3ExecutorService threadPool = Executors.newSingleThreadExecutor();//单线程
ExecutorService threadPool = Executors.newFixedThreadPool(5);//固定线程池大小
ExecutorService threadPool = Executors.newCachedThreadPool();//可伸缩的,遇强则强,遇弱则弱
我们可以创建三种类型的 ThreadPoolExecutor:FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
ThreadPoolExecutor类分析 (七大参数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25/**
* 用给定的初始参数创建一个新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
int maximumPoolSize,//线程池的最大线程数
long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
ThreadPoolExecutor 3 个最重要的参数:
- corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
- maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
- workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
ThreadPoolExecutor其他常见参数 :
- keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
- unit : keepAliveTime 参数的时间单位。
- threadFactory :executor 创建新线程的时候会用到。
- handler :饱和策略。关于饱和策略下面单独介绍一下。
四种拒绝策略(饱和策略)
- CallerRunsPolicy:由调用线程处理该任务
- AbortPolicy:丢弃任务 并抛出RejectedExecutionException异常 【 默认 】
- DiscardPolicy:丢弃任务,但是不抛出异常
- DiscardOldestPolicy:丢弃队列最前面的任务(被poll()出去),然后重新尝试执行任务
阻塞队列
- ArrayBlockingQueue、
- LinkedBlockingQueue、
- SynchronousQueue、
- PriorityBlockQueue。
ArrayBlockingQueue
是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序
ArrayBlockingQueue 是一个用数组实现的有界阻塞队列。
队列慢时插入操作被阻塞,队列空时,移除操作被阻塞。
按照先进先出(FIFO)原则对元素进行排序。
默认不保证线程公平的访问队列。
公平访问队列:按照阻塞的先后顺序访问队列,即先阻塞的线程先访问队列。
非公平性是对先等待的线程是非公平的,当队列可用时,阻塞的线程都可以争夺访问队列的资格。有可能先阻塞的线程最后才访问访问队列。
公平性会降低吞吐量。
LinkedBlockingQueue
一个基于链表结构的阻塞队列,此队列按 FIFO 排序元素,吞吐量通常要高于 ArrayBlockingQueue。静态工厂方法 Executors.newFixedThreadPool () 使用了这个队列。(newFixedThreadPool 用于创建固定线程数)
LinkedBlockingQueue 具有单链表和有界阻塞队列的功能。
队列慢时插入操作被阻塞,队列空时,移除操作被阻塞。
默认和最大长度为 Integer.MAX_VALUE,相当于无界 (值非常大:2^31-1)。
SynchronousQueue
一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 LinkedBlockingQueue,静态工厂方法 Executors.newCachedThreadPool 使用这个队列。(newCachedThreadPool 用于根据需要创建新线程)
我称 SynchronousQueue 为” 传球好手 “。想象一下这个场景:小明抱着一个篮球想传给小花,如果小花没有将球拿走,则小明是不能再拿其他球的。
SynchronousQueue 负责把生产者产生的数据传递给消费者线程。
SynchronousQueue 本身不存储数据,调用了 put 方法后,队列里面也是空的。
每一个 put 操作必须等待一个 take 操作完成,否则不能添加元素。
适合传递性场景。
性能高于 ArrayBlockingQueue 和 LinkedBlockingQueue。
PriorityBlockingQueue
一个具有优先级的无限阻塞队列。
PriorityBlockQueue = PriorityQueue + BlockingQueue
之前我们也讲到了 PriorityQueue 的原理,支持对元素排序。
元素默认自然排序。
可以自定义 CompareTo () 方法来指定元素排序规则。
可以通过构造函数构造参数 Comparator 来对元素进行排序。
最大线程到底如何确定(调优)
CPU密集型,几核就是几,可以保持CPU的效率最高
1
Runtime.getRuntime().availableProcessors();//获取电脑的CPU核数,运维电脑和本地不一样
- 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
IO密集型,判断程序中十分消耗IO的线程,大于这个数就行,一般设置为2倍
- 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
JMM模型(Java内存模型)
为了屏蔽系统之间的差异
Java 内存模型抽象了线程和主内存之间的关系,就比如说线程之间的共享变量必须存储在主内存中。Java 内存模型主要目的是为了屏蔽系统和硬件的差异,避免一套代码在不同的平台下产生的效果不一致。
在 JDK1.2 之前,Java 的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
- lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量 才可以被其他线程锁定。
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以 便随后的load动作使用。
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的 变量副本中。
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚 拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量, 每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随 后的write操作使用。
- write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的 变量中。
- 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内 存不接受,或者工作内存发起回写了但主内存不接受的情况出现。
- 不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回 主内存。
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存 中。
- 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或 a s s i gn ) 的 变 量 , 换 句 话 说 就 是 对 一 个 变 量 实 施 u s e 、 s t o r e 操 作 之 前 , 必 须 先 执 行 a s s i gn 和 l o a d 操 作 。
- 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执 行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
- 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量 前,需要重新执行load或assign操作以初始化变量的值。
- 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个 被其他线程锁定的变量。
- 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。
Volatile
volatile 关键字防止 JVM 的指令重排 ,保证变量的可见性,内存屏障保证有序性,但volatile不保证原子性
什么是指令重排:你写的程序,计算机并不是按照你写的那样去执行的
源代码–> 编译器的优化重排–> 指令运行也可能会重排–> 内存系统也会重排–>执行
处理器在进行指令重排的时候,考虑:数据之间依赖性问题
1 | int x = 1; |
可能造成的结果: a b x y 都是0
线程A | 线程B |
---|---|
x = a | y = b |
b = 1 | a = 2 |
正常的结果:x = 0; y = 0;
线程A | 线程B |
---|---|
b = 1 | a = 2 |
x = a | y = b |
诡异的结果:x = 2; y = 1;
volatile可以避免指令重排:
cpu中内存屏障作用:
- 保证特定的操作执行顺序
- 可以保证某些变量的内存可见性(利用这些特性 volatile实现了可见性)
单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
饿汉式
1
2
3
4
5
6
7
8public class Hungry {
private Hungry(){}
private final static Hungry hungry = new Hungry();
public static Hungry getInstance(){
return hungry;
}
}懒汉式(线程不安全)
1
2
3
4
5
6
7
8
9
10public class Lazy {
private Lazy(){}
private static Lazy lazy;
public static Lazy getInstance(){
if(lazy == null) {
lazy = new Lazy();
}
return lazy;
}
}懒汉式:双检锁/双重校验锁(DCL,即double-checked locking)
这里双重检测加锁是保证了操作原子性,只有一个线程能创建一个实例,其他线程无法创建第二个。volatile关键字是为了防止因为指令重排导致的多线程问题,有可能线程A创建一个实例,虚拟机只执行了分配空间,对象地址引用这两步,这是线程B过来发现对象已经被创建了,但是获取到的对象是还没有被初始化的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public class Lazy {
private Lazy(){}
private volatile static Lazy lazy;
//双重检测锁模式 懒汉式单例 DCL懒汉式
public static Lazy getInstance(){
if(lazy == null){
synchronized (Lazy.class){
if(lazy == null) {
lazy = new Lazy();//不是一个原子性操作
/**
* 1.分配内存空间
* 2.执行构造方法(初始化对象)
* 3.对象指向空间
*
* 指令重排 132
* A线程没问题,B指向空间发现不为null,直接return 但是此时lazy还没有完成构造
因此需要用volatile
*/
}
}
}
return lazy;
}
}登记式/静态内部类
1
2
3
4
5
6
7
8
9public class Lazy {
private static class SingletonHolder {
private static final Lazy INSTANCE = new Lazy();
}
private Lazy (){}
public static final Lazy getInstance() {
return SingletonHolder.INSTANCE;
}
}
Atomic 原子类
CAS :比较当前工作内存中的值和主内存的值,如果是期望的,就操作,否则就一直操作。
缺点:
- 自旋锁,会耗时
- 一次性只能保证一个共享变量的原子性
- ABA问题底层是一个自旋锁
1
2
3
4
5
6
7
8
9
10public class CompareAndSetTest {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
// 如果是期望的值2020,则更新为2021,否则不更新
atomicInteger.compareAndSet(2020,2021);
atomicInteger.getAndIncrement(); //+1
System.out.println(atomicInteger.get()); //2022
}
}
- AtomicBoolean: 原子更新布尔类型。
- AtomicInteger: 原子更新整型。
- AtomicLong: 原子更新长整型。
- AtomicReference: 原子更新引用类型。
- AtomicReferenceFieldUpdater: 原子更新引用类型的字段。
- AtomicMarkableReferce: 原子更新带有标记位的引用类型,可以使用构造方法更新一个布尔类型的标记位和引用类型。
- AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
- AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
- AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。
各种锁的理解
公平锁、非公平锁
公平锁:非常公平,不能够插队,必须先来后到
非公平所:非常不公平,可以插队(默认都是非公平)
1 | /** |
可重入锁(递归锁)
- synchronized锁
- Lock锁
自旋锁CAS
之前AtomicInteger里又提到过:
1 | public final int getAndAddInt(Object var1, long var2, int var4) { |
自己写一个自旋锁
1 | class Spinlock { |
死锁
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
1 | public class DeadLockDemo { |
输出:
1 | Thread[线程 1,5,main]get resource1 |
死锁的四个必要条件
- 互斥条件:一段时间内某个资源只能由一个线程占用
- 请求和保持条件:线程至少保持了一个资源,但又提出了新的资源要求,该新资源被其他线程占有,此时请求阻塞,但又对自己持有的资源不放
- 不剥夺条件:线程获得的资源在未释放以前不能被其他线程剥夺占有
- 环路等待条件:发生死锁时,必然存在一个线程资源环形链,A等B,B等C,C等A。
ThreadLocal
- 线程并发:在多线程并发场景下
- 传递数据:可以通过ThreadLocal在同一线程不同组件中传递公共变量
- 线程隔离:每个线程变量都是独立的,不会相互影响
JDK8优点:
- 每个Map存储的Entry变少(避免hash冲突)
- Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存的使用
重要方法声明:
方法声明 | 描述 |
---|---|
protected T initialValue() | 返回当前线程局部变量的初始值 |
public T get() | 设置当前线程绑定的局部变量 |
public void set(T value) | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
ThreadLocal中ThreadLocalMap数据结构和关系
ThreadLocal的key是弱引用,为什么
如何保证线程的顺序执行
1 | public class FIFOThreadExample { |
1 | import java.util.concurrent.ExecutorService; |
1 | public class TicketExample2 { |
1 | public class TicketExample3 { |