1. 什么是线程安全
当多个线程访问某个实例对象的方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,得到的结果始终和单线程下所获得的结果是一样的,那么我们就可以说这个对象是线程安全的。
2. 多线程的核心概念
- 原子性:一些业务操作要么同时执行成功要么同时失败
- 可见性:当某个线程修改某个共享资源时,其他线程能够立刻看到
- 有序性:程序执行的顺序按照代码的先后顺序执行。
3. 线程与进程的通信方式
1)多线程间通信方式:
- 共享变量
- Lock/Condition机制
- 管道
- wait/notify机制
2)进程与进程间通信
- 管道(Pipe):管道可用于具有亲缘关系进程间的通信,允许一个进程和另一个与它有共同祖先的进程之间进行通信。命名管道(named pipe):命名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关 系 进程间的通信。命名管道在文件系统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建。
- 信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送 信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数)。
- 消息(Message)队列:消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺
- 共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
- 内存映射(mapped memory):内存映射允许任何多个进程间通信,每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它。
- 信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
- 套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。
3. 如何实现线程安全
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的值放生了变化),就不赋新值。
5. CAS的底层原理
核心是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问题–>原子引用解决
7. 什么是ABA问题
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么这个时间差类会导致数据的变化。–>狸猫换太子,长得一样但并非是同一个人
例子:线程1从内存中位置V中取出A,这时候线程2也从内存中取出A,改为B,再改为A,这时候线程1进行CAS操作发现仍为A,并进行CAS操作。尽管线程1的CAS操作成功,但不是代表这个过程是没有问题的。
8. 为什么要用CAS而不是synchronized?
同一时间段只有一个线程可以进行访问,保证了线程安全,但是并发量下降。
可以反复通过CAS进行比较,没有加锁,既保证了一致性,又提高了并发量。
9. 公平锁、非公平锁、可重入锁、递归锁、自旋锁
- 公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队打饭。
- 非公平锁
是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者节现象。 - 可重入锁
在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,可重入锁最大的作用是避免死锁。ReentrantLock/Synchronized 就是一个典型的可重入锁 - 独占锁(写锁)
- 共享锁(读锁)
- 互斥锁:读锁的共享锁可以保证并发读是非常高效的,读写、写读、写写的过程是互斥的
- 自旋锁
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
10. JUC的三大辅助类
- CountDownLatch(火箭发射倒计时)
- CyclicBarrier(集齐七颗龙珠召唤神龙)
- Semaphore信号量(停车位空闲才能进入)
11. 什么是阻塞队列
阻塞的概念
阻塞和非阻塞是说,进程是否由运行态变成阻塞态,这时,操作系统将会将该进程挂在阻塞队列上,并选择其他就绪进程上CPU,所以,阻塞对当前进程是种时间和效率上的浪费。阻塞是说,当前操作会引起进程进入阻塞态,例如普通的打开文件操作,就会阻塞自己,直到内核返回。而非阻塞是否,当前操作不会引起进入阻塞态。
阻塞队列的概念
阻塞队列:从名字可以看出,他也是队列的一种,那么他肯定是一个先进先出(FIFO)的数据结构。与普通队列不同的是,他支持两个附加操作,即阻塞添加和阻塞删除方法。
如上图,线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素。而在这一系列操作必须符合以下规定:
- 阻塞添加:当阻塞队列是满时,往队列里添加元素的操作将被阻塞。
- 阻塞移除:当阻塞队列是空时,从队列中获取元素/删除元素的操作将被阻塞
阻塞队列的好处
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。在concurrent包发布以前,在多线程环境下,我们每个程序员都必须自己控制这些细节,尤其还要兼顾效率和线程安全,而这回给我们程序带来不小的复杂度。
12. 阻塞队列
ArrayBlockingQueue是一个基于数组结构的有界阻塞队列,此队列按FIFO原则对元素进行排序
LinkedBlockingQueue是一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue
SynchronousQueue是一个不存储元素的阻塞队列,灭个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于
13. ThreadLocal
ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本量。
ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
14. ThreadLocal和synchronized的区别
ThreadLocal和Synchonized都用于解决多线程并发访问。
但是ThreadLocal与synchronized有本质的区别:
1、Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
2、Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
一句话理解ThreadLocal,向ThreadLocal里面存东西就是向它里面的Map存东西的,然后ThreadLocal把这个Map挂到当前的线程底下,这样Map就只属于这个线程了。
public class ThreadLocaDemo {
private static ThreadLocal<String> localVar = new ThreadLocal<String>();
static void print(String str) {
//打印当前线程中本地内存中本地变量的值
System.out.println(str + " :" + localVar.get());
//清除本地内存中的本地变量
localVar.remove();
}
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
public void run() {
ThreadLocaDemo.localVar.set("local_A");
print("A");
//打印本地变量
System.out.println("after remove : " + localVar.get());
}
},"A").start();
Thread.sleep(1000);
new Thread(new Runnable() {
public void run() {
ThreadLocaDemo.localVar.set("local_B");
print("B");
System.out.println("after remove : " + localVar.get());
}
},"B").start();
}
}
A :local_A
after remove : null
B :local_B
after remove : null
15. ThreadLocal的原理和使用场景
Thread与ThreadLocal与ThreadLocalMap的关系
16. ThreadLocal的底层原理
参考链接:https://blog.csdn.net/u010445301/article/details/111322569
17. synchronized的自旋锁、偏向锁、轻量级锁和重量级锁
18. 线程池FixedThreadPool用的阻塞队列是什么
19. 谈谈你对AQS的理解。AQS如何实现可重入锁?
AQS(AbstractQueueSynchronizer:抽象队列同步器)
- AQS是一个Java线程同步的框架,是JDK中很多锁工具的核心实现框架。
- 在AQS中,维护了一个信号量state和一个线程组成的双向链表队列。其中,这个线程队列就是用来给线程排队的,而state就像是一个红绿灯,用来控制线程排队或者放行的。
- Java并发包下很多API都是基于AQS来实现的加锁和释放锁等功能的,AQS是Java并发包的基础类。
比如说ReentrantLock、ReentrantReadWriteLock底层都是基于AQS来实现的
ReentrantLock与AQS的关系
ReentrantLock内部包含了一个AQS对象,也就是AbstractQueuedSynchronizer类型的对象。
这个AQS对象就是ReentrantLock可以实现加锁和释放锁的关键性的核心组件。