当多个线程访问某个实例对象的方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,得到的结果始终和单线程下所获得的结果是一样的,那么我们就可以说这个对象是线程安全的。
2. 多线程的核心概念- 原子性:一些业务操作要么同时执行成功要么同时失败
- 可见性:当某个线程修改某个共享资源时,其他线程能够立刻看到
- 有序性:程序执行的顺序按照代码的先后顺序执行。
1、第一种 : 互斥同步(synchronized和lock锁)
JUC学习笔记-01-Synchronized 与Lock 的区别
2、第二种方法就是:非阻塞同步(CAS)
因为使用synchronized的时候,只能有一个线程可以获取对象的锁,其他线程就会进入阻塞状态,阻塞状态就会引起线程的挂起和唤醒,会带来很大的性能问题,所以就出现了非阻塞同步的实现方法。
互斥同步里实现了 操作的原子性(这个操作没有被中断) 和 可见性(对数据进行更改后,会立马写入到内存中,其他线程在使用到这个数据时,会获取到最新的数据),那怎么才能不用同步来实现原子性和可见性呢?
CAS是实现非阻塞同步的计算机指令,它有三个操作数:内存位置,旧的预期值,新值,在执行CAS操作时,当且仅当内存地址的值符合旧的预期值的时候,才会用新值来更新内存地址的值,否则就不执行更新。
使用方法:使用JUC包下的整数原子类decompareAndSet()和getAndIncrement()方法
缺点 :ABA 问题 版本号来解决
只能保证一个变量的原子操作,解决办法:使用AtomicReference类来保证对象之间的原子性。可以把多个变量放在一个对象里。
3、第三种:无同步方案
线程本地存储:将共享数据的可见范围限制在一个线程中。这样无需同步也能保证线程之间不出现数据争用问题。经常使用的就是ThreadLocal类
ThreadLocal类 最常见的ThreadLocal使用场景为 用来解决数据库连接、Session管理等
public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }
get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,set()用来设置当前线程中变量的副本
remove()用来移除当前线程中变量的副本,initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法
4. 什么是CAS
CAS(compare and swap比较并交换)出现的原因是由于锁存在以下几个问题
1、多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题
2、一个线程持有锁会导致其它所有需要此锁的线程挂起直至该锁释放
CAS是一个无锁解决方案,更准确的是采用乐观锁技术,实现线程安全的问题。CAS有三个操作数----内存对象(V)、预期原值(A)、新值(B)。
CAS原理就是对对象进行赋值时,先判断原来的值是否为A,如果为A,就把新值B赋值到V对象上面,如果原来的值不是A(代表V的值放生了变化),就不赋新值。
核心是Unsafe类(在jdk中rt.jar中的sun.misc下的Unsafe类),由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。换句话说就是CAS其实是直接操作的地址中的内容。CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被打断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
6. CAS出现的问题1、自循环时间长,开销大
2、只能保证一个共享变量的原子操作
3、ABA问题–>原子引用解决
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么这个时间差类会导致数据的变化。–>狸猫换太子,长得一样但并非是同一个人
例子:线程1从内存中位置V中取出A,这时候线程2也从内存中取出A,改为B,再改为A,这时候线程1进行CAS操作发现仍为A,并进行CAS操作。尽管线程1的CAS操作成功,但不是代表这个过程是没有问题的。
同一时间段只有一个线程可以进行访问,保证了线程安全,但是并发量下降。
可以反复通过CAS进行比较,没有加锁,既保证了一致性,又提高了并发量。
- 公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队打饭。
- 非公平锁
是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者节现象。 - 可重入锁
在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,可重入锁最大的作用是避免死锁。ReentrantLock/Synchronized 就是一个典型的可重入锁 - 独占锁(写锁)
- 共享锁(读锁)
- 互斥锁:读锁的共享锁可以保证并发读是非常高效的,读写、写读、写写的过程是互斥的
- 自旋锁
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
- CountDownLatch(火箭发射倒计时)
- CyclicBarrier(集齐七颗龙珠召唤神龙)
- Semaphore信号量(停车位空闲才能进入)
阻塞的概念
阻塞和非阻塞是说,进程是否由运行态变成阻塞态,这时,操作系统将会将该进程挂在阻塞队列上,并选择其他就绪进程上CPU,所以,阻塞对当前进程是种时间和效率上的浪费。阻塞是说,当前操作会引起进程进入阻塞态,例如普通的打开文件操作,就会阻塞自己,直到内核返回。而非阻塞是否,当前操作不会引起进入阻塞态。
阻塞队列的概念
阻塞队列:从名字可以看出,他也是队列的一种,那么他肯定是一个先进先出(FIFO)的数据结构。与普通队列不同的是,他支持两个附加操作,即阻塞添加和阻塞删除方法。
如上图,线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素。而在这一系列操作必须符合以下规定:
- 阻塞添加:当阻塞队列是满时,往队列里添加元素的操作将被阻塞。
- 阻塞移除:当阻塞队列是空时,从队列中获取元素/删除元素的操作将被阻塞
阻塞队列的好处
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。在concurrent包发布以前,在多线程环境下,我们每个程序员都必须自己控制这些细节,尤其还要兼顾效率和线程安全,而这回给我们程序带来不小的复杂度。
ArrayBlockingQueue是一个基于数组结构的有界阻塞队列,此队列按FIFO原则对元素进行排序
linkedBlockingQueue是一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue
SynchronousQueue是一个不存储元素的阻塞队列,灭个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于



