SJIUFY
集合分类
存放单一元素的集合
由Collection 接⼝派生:
- List : 存储的元素有序、可重复。
- Set : 存储的元素⽆序、不可重复。
- Queue: 有队列特性,元素有序、可重复。
存放键值对的集合
由Map接口派生
- Map:存放键值对
2.线程安全的集合
- Hashtable
- ConcurrentHashMap
- Vector
- Stack
3.ArrayList 和 Vector 的区别
- ArrayList扩容1.5倍
- Vector扩容2倍
3.线程池参数
- corePoolSize:核心线程数
- maximumPoolSize:线程池中最大线程数
- BlockingQueue:阻塞队列
- keepAliveTime :多余空闲线程的存活时间
- TimeUnit:时间单位
- ThreadFactory:线程工厂
- RejectedExecutionHandler :拒绝策略
4.线程池的种类
-
newCachedThreadPool : 可缓存的线程池。
适用于并发执行大量短期的小任务。可能会创建大量线程,从而导致 OOM。
-
newFixedThreadPool : 创建固定数目线程的线程池。
适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,
尽可能的少的分配线程,即适用执行长期的任务。但是它不会拒绝任务,
在任务比较多的时候会导致 OOM。
-
newScheduledThreadPool : 支持定时及周期性的任务执行的线程池。
适合周期性执行任务的场景,需要限制线程数量的场景。
-
newSingleThreadPool : 单线程化的线程池。
适用于串行执行任务的场景,一个任务一个任务地执行。
在任务比较多的时候也是会导致OOM。
5.拒绝策略种类
- AbortPolicy :线程池默认策略,直接抛出拒绝执行的异常
- DiscardPolicy :直接静悄悄的丢弃这个任务,不触发任何动作
- DiscardOldestPolicy :丢弃任务队列中等待事件最长的,即最老的任务
- CallerRunsPolicy:只要线程池没有关闭,就由提交任务的当前线程处理
6. execute()方法和 submit()方法的区别
- execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
- submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过
Future
的 get()
方法来获取返回值
3.创建多线程的方法
-
继承Thread类
-
实现Runnable接口
-
实现Callable接口
-
线程池
4.Runnable相比Thread的优势
-
避免java单继承的限制
-
线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类
5.Runnable和Callable区别
- Callable接口方法是 call() ,Runnable的方法是 run()
- Callable接口call方法有返回值,支持泛型,Runnable接口run方法无返回值
- Callable接口 call() 方法允许抛出异常;而Runnable接口 run() 方法不能继续上抛异常
6.run和start方法的区别
- 当程序调用 start() 方法,将会创建一个新线程去执行 run() 方法中的代码。 run() 就像一个普
通方法一样,直接调用 run() 的话,不会创建新线程。
- 一个线程的 start() 方法只能调用一次,多次调用会抛出
java.lang.IllegalThreadStateException 异常。 run() 方法则没有限制。
7.线程6种状态和转换
- NEW: 初始状态,线程被创建出来但没有被调用
start()
。
- RUNNABLE: 运行状态,线程被调用了
start()
等待运行的状态。
- BLOCKED :阻塞状态,需要等待锁释放。
- WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
- TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
- TERMINATED:终止状态,表示该线程已经运行完毕。
![1667884358112](C:\Users\Zhang Songbo\AppData\Roaming\Typora\typora-user-images\1667884358112.png)
8.死锁
概念:两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象。若无外
力作用,它们都将无法推进下去。
四个条件:
- 互斥:一个资源每次只能被一个进程使用
- 请求与保持:一个进程因请求资源而阻塞时,不释放获得的资源
- 不剥夺:进程已获得的资源,在未使用之前,不能强行剥夺
- 循环等待:进程之间循环等待着资源
避免死锁:
- 互斥条件不能破坏,因为加锁就是为了保证互斥
- 一次性申请所有的资源,避免线程占有资源而且在等待其他资源
- 占有部分资源的线程进一步申请其他资源时,如果申请不到,主动释放它占有的资源
- 按序申请资源
9.Volatile相关
volatile
关键字可以保证变量的可见性,如果我们将变量声明为 volatile,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
作用:
-
保证可见性:一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
-
防止指令重排序:通过对变量进行读写操作时,插入内存屏障来实现。
插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命
令的必须后执行。对一个volatile字段进行写操作,Java内存模型将在写操作后插入一个写屏障指令,这个指令会把之前的写入值都刷新到内存。
注:不能保证原子性
10.Synchronized相关
1.用法
-
修饰普通方法:作用于当前对象实例
-
修饰静态方法:作用于当前类
-
修饰代码块:指定加锁对象,对给定对象加锁
2.作用
- 原子性:确保线程互斥的访问同步代码;
- 可见性:保证共享变量的修改能够及时可见;
- 有序性:有效解决重排序问题。
3.底层原理
修饰代码块:通过 monitorenter 和 monitorexit 指令实现同步。
monitorenter 指令指向同步代码块的开始位置,
monitorexit 指令指向同步代码块的结束位置。
当执行monitorenter 指令时,线程试图获取锁也就是获取 monitor 的持有权,
其内部包含一个计数器,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。
执行 monitorexit 指令后,将锁计数器设为0 ,表明锁被释放。
如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
修饰方法:用ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法, JVM 通过该访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
4.Synchronized关键字对于锁的优化(锁升级)
它默认采用偏向锁,然后再程序运行中始终是有一个线程去获取Synchronized的锁,java对象会记录线程的id,所以下次再获取Synchronized的锁的时候,只需要比较这个线程id就行。在运行过程中如果出现第二个线程去请求Synchronized锁的时候分两种情况。
在没有发生并发竞争锁的情况下,Synchronized就会自动升级为轻量级锁,这个时候第二个线程就会尝试自旋锁的方式来获取锁,因为很快就能拿到锁,所以第二个线程也不会阻塞。
出现两个线程竞争锁的情况,Synchronized就会升级为重量级锁,这个时候只有一个线程获取锁,另外一个线程会阻塞,等待第一个线程释放锁之后才能拿到锁。
11.volatile和synchronized的区别
- volatile 只能使用在变量上;而 synchronized 可以在类,变量,方法和代码块上。
- volatile 至保证可见性; synchronized 保证原子性与可见性。
- volatile 禁用指令重排序; synchronized 不会。
- volatile 不会造成阻塞; synchronized 会。
12.ReentrantLock 相关
可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全的,可以实现公平锁。
- lock(): 加锁, 如果获取不到锁就死等;
- trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁;
- unlock(): 解锁。
13.ReentrantLock和synchronized区别
- 使用synchronized关键字实现同步,线程执行完同步代码块会自动释放锁,而ReentrantLock需要手动释放锁。
- synchronized是非公平锁,ReentrantLock可以设置为公平锁。
- ReentrantLock上等待获取锁的线程是可中断的,线程可以放弃等待锁。而synchonized会无限期等待下去。
- ReentrantLock 可以设置超时获取锁。在指定的截止时间之前获取锁,如果截止时间到了还没有获取到锁,则返回。
- ReentrantLock 的 tryLock() 方法可以尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false
14.ThreadLocal相关
线程本地变量。当使用 ThreadLocal 维护变量时, ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程。
1.ThreadLocal原理
每一个Thread线程都会拥有自己的一个成员变量ThreadLocalMap,该变量默认为空(实际是ThreadLocal的静态内部类,只是Thread持有引用)。当往ThreadLocal存数据时,调用的是ThreadLocal的set方法,该方法先拿到当前线程的ThreadLocalMap,然后以当前threadLocal实例为key把数据存进该map中。当取数据时,一样拿到当前线程的ThreadLocalMap,并以当前threadLocal实例为key从里面拿出数据。
简而言之,存进ThreadLocal的数据,相当于存进了线程自己的变量中。因此如果要保证共享变量的隔离性,就把他放进ThreadLocal(相当于在线程中拷贝了一份),要用时,从ThreadLocal中拿出来即可(拿出当前线程的副本)
2.ThreadLocal内存泄漏的原因
每个线程都有⼀个 ThreadLocalMap 的内部属性,map的key是 ThreaLocal ,定义为弱引用,value是强引用类型。
垃圾回收的时候会⾃动回收key,而value的回收取决于Thread对象的生命周期。一般会通过线程池的方式复用线程节省资源,这也就导致了线程对象的生命周期比较长,这样便一直存在一条强
引用链的关系: Thread --> ThreadLocalMap --> Entry --> Value ,随着任务的执行,value就有可能越来越多且无法释放,最终导致内存泄漏。
解决⽅法:每次使⽤完 ThreadLocal 就调⽤它的 remove() ⽅法,手动将对应的键值对删除,从⽽避免
内存泄漏。
15.AQS
AQS,抽象队列同步器,定义了一套多线程访问共享资源的同步器框架,像平常用的 ReentrantLock/Semaphore/CountDownLatch都是依赖于它的 。
AQS使用一个 volatile 的int类型的成员变量 state 来表示同步状态,通过CAS修改同步状态的值。
当线程调用 lock 方法时 ,如果 state =0,说明没有任何线程占有共享资源的锁,可以获得锁并将state 加1。
如果 state 不为0,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待。
16.CAS相关
1.什么是CAS
CAS全称 Compare And Swap ,比较与交换,是乐观锁的主要实现方式。CAS在不使用锁的情况下实现多线程之间的变量同步。 ReentrantLock 内部的AQS和原子类内部都使用了CAS。
CAS算法涉及到三个操作数:
需要读写的内存值V。
进行比较的值A。
要写入的新值B。
只有当V的值等于A时,才会使用原子方式用新值B来更新V的值,否则会继续重试直到成功更新值。
以 AtomicInteger 为例, AtomicInteger 的 getAndIncrement() 方法底层就是CAS实现,关键代码是
compareAndSwapInt(obj, offset, expect, update) ,其含义就是,如果 obj 内的 value 和
expect 相等,就证明没有其他线程改变过这个变量,那么就更新它为 update ,如果不相等,那就会继
续重试直到成功更新值。
2. CAS存在的问题
- ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但
是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变
化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时
候都把版本号加一,这样变化过程就从 A-B-A 变成了 1A-2B-3A 。
JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,原子更新带有版本号的引用类
型。
- 循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
- 只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对
多个共享变量操作时,CAS是无法保证操作的原子性的。
Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在
一个对象里来进行CAS操作。
17.锁的分类
-
公平锁与非公平锁:按照线程访问顺序获取对象锁。 synchronized 是非公平锁, Lock 默认是非公平锁,可以设置为公平锁,公平锁会影响性能。
-
共享式与独占式锁:他们俩个的区别在于同一时刻独占式只能有一个线程获取同步状态,而共享式在同一时刻可以有多个线程获取同步状态。例如读操作可以有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其他操作都会被阻塞。
-
悲观锁与乐观锁:悲观锁,每次访问资源都会加锁,执行完同步代码释放锁, synchronized 和 ReentrantLock 属于悲观
锁。 乐观锁,不会锁定资源,所有的线程都能访问并修改同一个资源,如果没有冲突就修改成功并退出,否则就会继续循环尝试。乐观锁最常见的实现就是 CAS 。
悲观锁适合写操作多的场景。 乐观锁适合读操作多的场景,不加锁可以提升读操作的性能。
18.wait()和sleep()的异同点?
相同点:
- 使当前线程暂停运行,把机会交给其他线程
- 任何线程在等待期间被中断都会抛出 InterruptedException
不同点:
- wait() 是Object超类中的方法;而 sleep() 是线程Thread类中的方法
- 对锁的持有不同, wait() 会释放锁,而 sleep() 并不释放锁
- 唤醒方法不完全相同, wait() 依靠 notify 或者 notifyAll 、中断、达到指定时间来唤醒;而
sleep() 到达指定时间被唤醒
- 调用 wait() 需要先获取对象的锁,而 Thread.sleep() 不用
19.JUC包
- ReentrantLock
- Semaphore
- CountDownLatch
- CyclicBarrier
- CopyOnWriteArrayList
1.ReentrantLock:可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全的。
- lock(): 加锁, 如果获取不到锁就死等;
- trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁;
- unlock(): 解锁。
2.Semaphore:表示 “可用资源的个数”,用于控制同时访问特定资源的线程数量,控制并发线程数。假如是5,程序执行前用acquire()方法获得信号,则可用信号变为4,程序执行完通过release()方法归还信号量,可用信号又变为5。如果可用信号为0,acquire就会造成阻塞,等待release释放信号。
3.CountDownLatch:用于某个线程等待其他线程执行完任务再执行,与thread.join()功能类似。常见的应用场景是开启多个线程同时执行某个任务,等到所有任务执行完再执行特定操作,如汇总统计结果。
4.CyclicBarrier:同步屏障,用于一组线程互相等待到某个状态,然后这组线程再同时执行。
CyclicBarrier和CountDownLatch区别:
- CyclicBarrier可重用,计数器可以使用reset()方法重置,而CountDownLatch 不可重用,计数值为 0 该 CountDownLatch 就不可再用了。
- CountDownLatch用于某个线程等待其他线程执行完任务再执行。CyclicBarrier用于一组线程互相等待到某个状态,然后这组线程再同时执行
5.CopyOnWriteArrayList:相当于线程安全的ArrayList,使用了一种叫写时复制的方法,当有新元素add到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。
CopyOnWriteArrayList 中add方法添加的时候是需要加锁的,保证同步,避免了多线程写的时候复制出多个副本。读的时候不需要加锁,如果读的时候有其他线程正在向 CopyOnWriteArrayList 添加数据,还是可以读到旧的数据。
CopyOnWrite并发容器用于读多写少的并发场景。
4.ArrayList 与 LinkedList 区别
-
是否保证线程安全:
ArrayList 和 LinkedList 都不保证线程安全
-
底层数据结构:
ArrayList 底层使⽤的是 Object 数组
LinkedList 底层使⽤的是 双向链表
-
是否⽀持随机访问:
ArrayList 支持
LinkedList 不支持