1、ArrayList、HashMap线程不安全
2、Collections.synchronizedList() 生成线程安全的同步容器,底层使用synchorized
3、Java提供的同步容器还有Vector、Stack和Hashtable,基于synchronized实现
同步容器、并发容器
并发容器:List、Map、Set和Queue
1、JDK并发容器
1、List
ArrayList和vector内部都是使用数组实现的,区别:ArrayList非线程安全;Vector线程安全
LinkedList内部使用链表实现,非线程安全
1.1、CopyOnWriteArrayList【高效读取】
原理:修改数组元素的时候会复制出一个新数组,对新数组进行操作,将指针指向新数组
写入不会阻塞读取操作、写入和写入之间需要同步等待
迭代器是只读的
内部有一个ReentrantLock lock
public boolean add(E e) {
final ReentrantLock lock = this.lock; // 加锁
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1); // 复制一个新数组出来
newElements[len] = e; // 新添加元素放在新数组里
setArray(newElements); // 将数组指向新数组,数组使用volatile修饰,修改完保证读取可见
return true;
} finally {
lock.unlock();
}
}
hashMap是非线程安全的,如果想使用线程安全的HashMap
2.1、Collections.synchronizedMap()
Map<Integer, String> map = Collections.synchronizedMap(new HashMap<Integer, String>()) //生成一个SynchronizedMap 的Map,构造函数包含一个HashMap
实现原理:所有的方法上面都有synchronized (mutex) {...} 每一次操作都需要获取mutex锁
private static class SynchronizedMap<K,V>
implements Map<K,V>, Serializable {
private static final long serialVersionUID = 1978198479659022715L;
private final Map<K,V> m; // Backing Map
final Object mutex; // Object on which to synchronize
.....
}
2.2、ConcurrentHashMap
key、value不能为null
实现原理:todo
32.3、ConcurrentSkipListMap【重点】
key、value不能为null
实现原理:底层是跳表的数据结构,类似于平衡树,但是与平衡树还是存在区别
跳表的数据结构:多层链表,最底层的链表维护了跳表内所有元素,每上面一层都是全部元素的子集,所有链表元素key都是排序的
详细描述跳表的插入删除:juejin.cn/post/684490…
3、Set
3.1、CopyOnWriteArraySet
3.2、ConcurrentSkipListSet
4、Queue
阻塞、非阻塞(Blocking):当队列满时入队操阻塞;当队列空时出队操作阻塞
单端(Queue)、双端(Deque):单端-队尾入队,队首出队;双端-对尾队首都可以出队入队
4.1、单端阻塞
包含:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、LinkedTransferQueue、PriorityBlockingQueue和DelayQueue
BlockingQueue,是一个接口
a、ArrayBlockingQueue:内部持有队列是数据,支持有界
存放对象的是对象数组final Object[] items
void put(E e) throws InterruptedException; // 压入元素并且通知notEmpty
boolean offer(E e); // 压入元素
E poll(); // 从队列头弹出元素,队列为空返回null
E take() throws InterruptedException; // 从队列头弹出元素,队列为空一直等待
取值:使用take()取元素时,若队列为空,线程在notEmpty的Condition上进入阻塞等待状态;取出元素后会唤醒在notFull
添加元素:put()添加元素时完成添加之后会执行signal()唤醒在notEmpty上的线程;如果队列已满线程则会在notFull的Condition上进入阻塞等待状态
b、LinkedBlockingQueue:内部持有队列是链表,支持有界
SynchronousQueue:不持有队列
LinkedTransferQueue:融合了LinkedBlockingQueue、SynchronousQueue的功能
PriorityBlockingQueue:支持按照优先级出队
DelayQueue:支持延时出队
4.2、双端阻塞
LinkedBlockingDeque
4.3、单端非阻塞【重点】
ConcurrentLinkedQueue:队列使用链表结构存储,在高并发环境中性能最好的队列。
实现原理:todo 难点
public boolean offer(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e);
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) {
//p是最后的节点
if (p.casNext(null, newNode)) {
// p,casNext() 成功将newNode添加到链表尾部
if (p != t) // p!=t 代表着此时t代表的还不是尾部节点,意味着别的线程可能已经t的指向变更了
casTail(t, newNode); // 将t指到最新的尾部节点
return true;
}
// CAS竞争失败,再次尝试
}
else if (p == q)
// We have fallen off list. If tail is unchanged, it
// will also be off-list, in which case we need to
// jump to head, from which all live nodes are always
// reachable. Else the new tail is a better bet.
p = (t != (t = tail)) ? t : head;
else
// Check for tail updates after two hops.
p = (p != t && t != (t = tail)) ? t : q;
}
}
4.4、双端非阻塞
ConcurrentLinkedDeque
2、无锁工具类
互斥锁与无锁的对比
无锁的实现原理:CAS(Compare And Swap)指令。只有当目前count的值和期望值expect相等时,才会将count更新为newValue
只有count=expect的时候才会操作count=newValue
自旋:循环尝试。重新读取最新的值并进行newValue的计算
Java原子类:AtomicLong、getAndIncrement()、addAndGet(n)方法内部就是基于CAS实现的
do {
// 获取当前值
oldV = xxxx;
// 根据当前值计算新值
newV = ...oldV...
}while(!compareAndSet(oldV,newV);
2.1、原子化基本数据类型
AtomicBoolean、AtomicInteger和AtomicLong
2.2、原子化的对象引用类型
AtomicReference、AtomicStampedReference和AtomicMarkableReference
AtomicStampedReference:解决ABA问题的思路--增加一个版本号维度
2.3、原子化数组类型
AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray
3、线程池
3.1、如何创建线程池
优势:避免了重复创建、销毁线程的动作
创建一个线程系统都做了哪些事情?todo
JDK提供了一套Executor框架
Executors是线程池工厂,支持快速创建线程池。ps:大厂现在不支持使用,因为Executors里面大部分使用的都是无界队列
ExecutorService newFixedThreadPool(int nThreads); // 生成线程数个数固定的线程池
ExecutorService newSingleThreadExecutor(); // 只有一个线程的线程池
ExecutorService newCachedThreadPool(); // 根据实际情况调整线程数的线程池
ScheduledExecutorService newSingleThreadScheduledExecutor(); // 单个线程
ScheduledExecutorService newScheduledThreadPool(int corePoolSize); //创建一个线程池,可以安排命令在给定延迟后运行,或定期执行[计划任务]。
ScheduledExecutorService:计划执行。
ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit); // 在给定的delay[unit]后对任务进行调度
ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit); // 以上一个任务的开始时间为起点,间隔period时间后,调用下一次任务
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit); // 上一个任务结束后,经过delay时间进行任务调度
3.2、线程池核心参数
线程池在创建的时候内部都是使用:ThreadPoolExecutor 进行创建的
ThreadPoolExecutor(
int corePoolSize, // 线程池保有的最小线程数
int maximumPoolSize, // 线程池创建的最大线程数
long keepAliveTime, // 空闲线程空闲的时间
TimeUnit unit, // 空闲线程空闲时间单位
BlockingQueue workQueue, // 工作队列
ThreadFactory threadFactory, // 自定义如何创建线程,创建线程的工厂类
RejectedExecutionHandler handler // 任务拒绝策略
)
ThreadFactory:线程池中创建线程的工厂
ThreadPoolExecutor提供的四种拒绝策略:
- CallerRunsPolicy:提交任务的线程自己去执行该任务。
- AbortPolicy:默认的拒绝策略,会throws RejectedExecutionException。
- DiscardPolicy:直接丢弃任务,没有任何异常抛出。
- DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。
这几个拒绝策略都实现了RejectedExecutionHandler接口,该接口有一个rejectedExecution方法,可以自定义拒绝策略
线程池执行过程中最重要的是接收异常并进行处理,因为某个任务出现异常会导致后面的任务都无法执行
try {
//业务逻辑
} catch (RuntimeException x) {
//按需处理
} catch (Throwable x) {
//按需处理
}
线程池的工作队列:
1、ArrayBlockingQueue 基于数组的有界阻塞队列
2、LinkedBlockingQueue 基于链表阻塞队列,可设置队列长度
3、SynchronousQueue 不存储元素的阻塞队列,先出后进
4、PriorityBlockingQueue 一个有优先级的无限阻塞队列
5、DelayQueue 一个任务定时周期的延迟执行的队列
创建线程池的时候,返回值是ExecutorService,ExecutorService继承Executor,Executor的方法包含以下几个:
void execute(Runnable command); // Executor接口的方法
// 提交Runnable任务
Future<?> submit(Runnable task); // ExecutorService方法,Runnable接口run()方法没有返回值
// 提交Callable任务
<T> Future<T> submit(Callable<T> task); // ExecutorService方法,Callable接口的call()方法带有返回值
// 提交Runnable任务及结果引用
<T> Future<T> submit(Runnable task, T result); // ExecutorService方法
Q:为什么是先添加队列而不是先创建最大线程?
- 队列作为一个有限的缓冲区,并且阻塞队列自带阻塞和唤醒功能,无任务执行时有take方法挂起,不占用cpu资源
- 创建新线程要获取全局锁,其他线程就要阻塞,影响效率
Q:线程池.execute()、submit() 有什么区别
execute:有返回值。只支持提交Runnable的任务
submit:无返回值。支持Runnable、Callable的任务,但是Runnable的返回值始终都是void;通过get()获取结果的时候catch异常
Q:如何评估合适的线程数量?
3.3、Fork/Join框架
JDK中提供了一个ForkJoinPool线程池
<T> ForkJoinTask<T> submit(ForkJoinTask<T> task); // 往forkjoin线程池提交一个forkjoin任务
1、forkjoin有四个submit方法,区别于线程池,多了一个ForkJoinTask的submit方法
2、forkjoin使用一个无锁的栈来管理空闲线程
3、空闲的线程会从繁忙的线程等待任务队列的尾部取出任务进行执行
forkjoin任务:支持分解和join等待的任务
abstract class RecursiveAction extends ForkJoinTask<Void> // 无返回值
abstract class RecursiveTask<V> extends ForkJoinTask<V> // 返回V类型
使用方式:可以自定义任务实现以上两个抽象类任务,重写compute()方法进行任务的计算执行逻辑。在compute()方法中可以通过任务.fork()方法分解子任务,最终循环所有任务,通过.join()方法获取所有任务的结果
public class ForkJoinTest extends RecursiveTask<Long> {
@Override
protected Long compute() {
List<ForkJoinTest> taskList = Lists.newArrayList();
for (int i = 0; i < 100; i++) {
// ... 一系列判断,创建个子任务
ForkJoinTest task = new ForkJoinTest();
task.fork(); // 子任务发起调用
}
for (ForkJoinTest task : taskList) {
task.join(); // 获取子任务结果
}
return null;
}
}
4、Java并发基础
进程:
线程:程序执行的最小单位,线程之间切换调度成本小
线程生命周期:Thread.State枚举
wait操作:runnable->waiting状态
线程调用sleep之后会进入什么状态
4.1、创建线程
1、继承Thread
public class MyThread extends Thread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
@Override
public void run() {
System.out.println("【Thread】Hello:" + Thread.currentThread().getName());
}
}
2、实现Runnable接口
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("【Runnable】Hello:" + Thread.currentThread().getName());
}
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
调用start的含义:告知Java虚拟机,县城规划期空闲时应立即启动调用了start()方法的线程
4.2、线程中断
B线程调用A线程的interrupt()方法对A进行中断。A线程有一个标识位属性,标记为已中断
A可以调用isInterrupted()判断是否被中断
Thread.interrupted()对当前线程中断标志位进行复位
许多抛出InterruptedException异常的方法,在抛出该异常前会进行中断标识位的清除,例如线程A执行操作紧接着调用了Thread.sleep(n),此时B中调用了A线程的interrupt()方法,然后调用A的Thread.interrupted()方法,此时会返回false
4.3、suspend、resume、stop
suspend():线程的暂停
resume():线程的回复
stop():线程的终止
接口已经过期了,不建议使用
4.4、正确的中断线程
1、线程显式设置标识位flag,通过设置一个cancel方法将flag设置为false,实现run()方法的中断
2、通过interrupt()方法
4.5、等待通知机制
等待通知是任意的Java对象都具备的,所以wait、notify是定义在Object上
使用wait()、notify()、notifyAll()的时候需要先调用对象A加锁,在线程中调用对象A的wait()、notify()、notifyAll()方法
4.6、ThreadLocal【重点】
1、是什么
ThreadLocal是一个线程的局部变量,只有当前线程可以访问,因为只有当前线程可以访问,所以是线程安全的。
2、什么情况下使用
在多线程操作同一个对象的时候,如果是只有一个实例,会造成调用实例时的阻塞等待,如果为每个线程都创建一个实例,线程内可用,一是不会出现线程不安全的问题;二是会提高多线程下的执行效率
- 事务操作情况下可以使用ThreadLocal存储线程事务信息
- 数据库连接的Session会话管理
3、使用方式
public class ThreadLocalTest {
private static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<>();
static class ThreadMy implements Runnable {
private int i = 0;
public ThreadMy(int i) {
this.i = i;
}
@Override
public void run() {
// 取出当前线程的ThreadLocal的实例
SimpleDateFormat curThreadTl = tl.get();
if (Objects.isNull(curThreadTl)) {
tl.set(new SimpleDateFormat("yyyyMMdd:HH:mm:ss"));
}
// 使用该实例执行业务逻辑
System.out.println("Thread i:" + i + tl.get().format(System.currentTimeMillis()));
}
}
public static void main(String[] args) {
// 创建线程池,解析日期
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
pool.execute(new ThreadMy(i));
}
}
}
4、实现原理
其中比较重要的几个方法分别是get()、set()
public T get() {
// 获取当前线程引用标识
Thread t = Thread.currentThread();
// 从Thread中取出ThreadLocalMap(存储的是线程所有的“局部变量”),ThreadLocalMap是一个Entry<k,v>结构
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
public void set(T value) {
// 取出当前线程的引用标识
Thread t = Thread.currentThread();
// 获取线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
// set 当前线程对应的value值
map.set(this, value);
else
createMap(t, value);
}
5、会导致内存泄露
原因:
正确做法:
4.7、守护线程
1、怎么设置
thread.setDaemon(true); // 在start()调用前设置守护为true
thread.start();
2、特征
- 用户线程执行完成之后,当前只有守护线程时程序会随之结束,即便守护线程还在
while(true) {...} - 垃圾回收线程就是一个守护线程
4.8、线程安全和synchorized
1、什么是线程安全
多个线程操作同一个共享变量的时候操作的结果与预期一致
哪个区域的空间存在线程安全的问题:堆
堆是进程、线程共有的空间。所有线程都可访问到进程中的堆空间,会造成线程安全问题
栈是线程独有的,保存运行状态和局部变量的,是线程安全的
2、volatile修饰的一定安全吗
不一定。volatile只能保证一个线程修改之后,其他线程可见,在两个线程同时对变量进行赋值的时候会产生冲突,thread1和thread2同时对变量a(此时是1)++,执行完之后a=2,实际预期应该3
3、synchorized的使用
- 给指定对象加锁。
- 作用于实例方法。相当于对当前实例加锁
- 作用于静态方法。相当于对当前类加锁
4、可重入锁【ReentrantLock】与synchorized的区别,可重入锁的优势:
- 可重入锁对逻辑控制灵活性远好于synchorized,显式的加锁释放锁,
lock()、unlock() - 中断响应。避免死锁,
reentrantLock.lockInterruptibly(),使用线程thread.interrupt()执行线程的中断 - 锁申请等待限时。避免线程长时间拿不到锁陷入无限时等待。
reentrantLock.tryLock(等待时长,计时单位),不带单位情况下如果获取不到锁立即返回false - 公平锁。synchorized锁是非公平的。可重入锁可设置公平锁,构造函数中设置fair属性为true,
new ReentrantLock(true)
公平锁:按照时间顺序,保证先到者先得,后到者后得
4.9、JDK并发包
4.9.1、Condition条件
1、什么场景下使用?
Condition条件是配合可重入锁使用的
2、如何使用?
- 可重入锁 ReentrantLock的newCondition()方法可以生成一个Condition
- await()方法:是当前线程进入等待,释放当前锁
- awaitUninterruptbily()方法:在await基础上,不会在等待过程中响应中断
- singal()方法:唤醒一个Condition等待队列中的一个线程。condition.singal() 唤醒线程的时候需要先持有锁才能执行singal方法,执行完成之后释放锁
3、有什么使用案例?
ArrayBlockingQueue的put和take方法
4.9.2、信号量Semaphore
1、作用?
线程之间协作的一种方式,信号量可以允许多个线程同时访问某一个资源。
synchorized、重入锁ReentrantLock都是一次只允许一个线程访问一个资源,线程之间串行。
2、如何使用?
// 构造函数
public Semaphore(int permits) { // permits:准入许可数,代表可进入的线程数
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
// 主要方法
public void acquire(){} // 尝试获取一个准入许可
public void acquireUninterruptibly(){} // 尝试获取一个准入许可,不响应中断
public boolean tryAcquire(){} // 尝试获取一个准入许可,成功返回true,失败返回false,不等待
public boolean tryAcquire(long timeout, TimeUnit unit){} // 同上一个,但是会有等待
public void release(){} // 释放一个许可,信号量上许可数-1
4.9.3、读写锁ReadWriteLock
1、概念
读写分离的锁,支持多个读线程同时访问共享资源,写与读,写与写还是互斥串行
有点:
- 降低了锁竞争
- 提高了读效率
2、使用方式
public class ReadWriteLockTest {
public static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
// 由可重入锁对象生成读写锁
static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static int sharedVariable = 0;
public int handleRead(Lock lock) {
try {
lock.lock();
Thread.sleep(1000);
System.out.println(Thread.currentThread() + " read value: "+ sharedVariable);
return sharedVariable;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return 0;
}
public void handleWrite(Lock lock) {
try {
lock.lock();
Thread.sleep(2000);
sharedVariable++;
System.out.println(Thread.currentThread() + "write value: " + sharedVariable);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
final ReadWriteLockTest readWriteLockTest = new ReadWriteLockTest();
// 读线程
Runnable readThread = new Runnable() {
@Override
public void run() {
// 使用读写锁的读锁,多个线程操作hanleRead的时候是并行的
readWriteLockTest.handleRead(readLock);
}
};
// 写线程
Runnable writeThread = new Runnable() {
@Override
public void run() {
// 使用读写锁的读锁,多个线程操作hanleWrite的时候是串行的
readWriteLockTest.handleWrite(writeLock);
}
};
long startCurTime = System.currentTimeMillis();
List<Thread> threadList = Lists.newArrayList();
// 开18个线程读取
for (int i = 0; i < 18; i++) {
Thread thread = new Thread(readThread);
thread.start();
threadList.add(thread);
}
for (int i = 18; i < 20; i++) {
Thread thread = new Thread(writeThread);
thread.start();
threadList.add(thread);
}
// 主线程等待所有子线程执行完成
for (Thread thread : threadList) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Progress end: " + (System.currentTimeMillis() - startCurTime));
}
}
4.9.3、倒计时器 CountDownLatch
1、含义
倒计数器:控制某个线程等待,让线程等待到倒计数结束再开始执行,一般是正常执行等待前置准备操作执行完成
2、使用实例
一个线程A在CountDownLatch等待,启动n个线程进行前置准备操作,每个执行完成之后调用CountDownLatch的countDown()方法,倒计数-1,线程A此时便可以继续执行
public class CountDownLatchTest implements Runnable {
final static CountDownLatch latch = new CountDownLatch(10);
@Override
public void run() {
// 线程执行操作
try {
Thread.sleep(1000);
System.out.println("【" + Thread.currentThread().getName() +"】" + " prepare ok!");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
CountDownLatchTest latchTest = new CountDownLatchTest();
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
// 开启10个线程
pool.submit(latchTest);
}
try {
latch.await(); // 等待在latch上的线程【此时是主线程】进入等待
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( "wait in CountDownLatch Thread 【" + Thread.currentThread().getName() +"】 is running...");
pool.shutdown(); // 关闭线程池
}
}
4.9.4、循环栅栏 CyclicBarrier
1、作用
循环栅栏。多线程并发控制工具。可以实现线程间的计数等待,与CountDownLatch相似,比CountDownLatch强大
栅栏的作用:等待指定个数的线程都到达时触发所有线程的任务执行,否则等待
可循环利用的栅栏:允许N波线程的等待和统一执行