目录
摘要
这篇文章主要记录了JAVA并发编程中使用的主要类(包括它们的实现原理)和面试过程会被问到的一些概念。
什么是并发
- 并发就是指程序同时处理多个任务的能力。
- 并发编程的根源在多个任务情况下对访问资源的有效控制。
线程的几种状态
- NEW:创建了一个线程对象,还未调用其start方法
- RUNNABLE:处于运行状态的线程指的是正在虚拟机中执行的线程。他们可能等待操作系统的其他资源(处理器)
- BLOCKED:等待获取synchronized
- WAITING:Object.wait/Thread.join/LockSupport.park.处于改状态的线程需要等待其他线程执行一些特定的动作 (Object.notify()/Object.notifyAll())
- TIMED_WAITING: Thread.sleep/Object.wait(long)/Thread.join(long)/ LockSupport.parkNano /LockSupport.parkUntil.
- TERMINATED:执行完毕的线程.
锁的分类
AbstractQueuedSynchronizer(队列同步器)
为实现依赖于FIFO等待队列的阻塞锁和其他相关的同步组件提供基础框架。它里面有一个state变量表示同步状态。通过内置的FIFO队列(双向链表)完成资源获取线程的排队工作。
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
//等待队列的头节点
private transient volatile Node head;//一个空节点
//等待队列的尾节点
private transient volatile Node tail;//获取资源失败的线程都是插入到队尾,tail节点始终指向队列中的最后一个元素
//同步状态
private volatile int state;
}
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
}
ReentrantLock(可重入锁)实现分析
它基于AQS实现的可重入锁,提供了公平(默认)与非公平两种锁。还提供了:锁的超时等待、可中断的锁等待、公平性、以及实现非块结构的加锁、Condition。
公平与非公平锁区别??
公平锁中, 线程严格按照先进先出(FIFO)的顺序 获取锁资源。
非公平锁中, 拥有锁的线程在释放锁资源的时候, 当前尝试获取锁资源的线程可以和等待队列中的第一个线程竞争锁资源, 这就是ReentrantLcok中非公平锁的含义; 但是已经进入等待队列的线程, 依然是按照先进先出的顺序获取锁资源。
使用:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
while(条件判断表达式) {
condition.wait();
}
// 处理逻辑
} finally {
lock.unlock();
}
类的关系结构:
非公平锁的实现:
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))//当前锁未被占用
setExclusiveOwnerThread(Thread.currentThread());//设置持有锁的线程为当前线程
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
abstract static class Sync extends AbstractQueuedSynchronizer {
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { //锁被释放
//公平锁和非公平锁的主要区别在这里
if (compareAndSetState(0, acquires)) {//获取锁成功
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {//持有锁的线程再次获去锁
int nextc = c + acquires;
if (nextc < 0) // overflow 2147483647
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
class AbstractQueuedSynchronizer {
public final void acquire(int arg) {
if (!tryAcquire(arg) &&//尝试获取锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //将当前线程加入到等待队列,线程被阻塞
selfInterrupt();
}
}
公平锁的实现:
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && //判断等待队列中是否还有其他线程在等待资源
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
1 h == t 队列为空
2 h != t && (s = h.next) == null 意味着有一个线程尝试获取锁失败后,正在进行入队操作,而且 在AQS的enq()方法中, head=tail方法正好还没执行到, 此时队列被认为不空, 返回true
3 h != t && ( s.thread != Thread.currentThread()) 队列中第一个节点不是当前线程,即还有其他线程在排队等待资源。
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
tryLock():
不管Reentrant使用公平锁(FairLock)还是非公平锁(NonFairLock), ReentrantLock类的tryLock()方法都会调用Sync类的nonfairTryAcquire()方法,采取非公平的竞争策略.
线程池使用及实现原理
为什么使用线程池:
降低资源消耗:通过复用已存在的线程降低线程关闭的次数,尽可能降低系统性能损耗。
提高响应速度:通过复用线程省去创建线程的过程,因此整体上提升了系统的响应速度;
实现原理:
其实java线程池的实现原理很简单,说白了就是一个线程集合 workers(HashSet) 和一个任务阻塞队列 workQueue(BlockingQueue)。当用户向线程池提交一个任务时,线程池会先将任务放入workQueue中。workers 中的线程会不断的从 workQueue 中获取任务然后执行。当workQueue中没有任务的时候,worker就会阻塞,直到队列中有任务了就取出来继续执行。
下面就是提交任务后,线程池内部的执行流程:
submit: 接收Runnable/Callable参数,返回 Future. submit会将任务包装成FutureTask(RunnableFuture)然后调用 execute.在任务中发生异常,只有通过 Futrue的get操作才会抛出异常。
execute: 接受 Runnable参数, 在任务中发生异常,会直接在执行抛出异常导致线程退出。
Java基于ThreadPoolExecutor提供了几种不同类型的线程池,先介绍线程池的几个主要参数的作用:
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
corePoolSize:核心线程的数量
maximumPoolSize:线程池的最大线程数。当workQueue满了,不能添加任务的时候,这个参数才会生效
keepAliveTime:空闲线程存活时间。如果当前线程池的线程个数已经超过了corePoolSize,并且线程空闲时间超过了keepAliveTime的话,就会将这些空闲线程销毁.默认为1分钟.
什么时候退出? 从队列获取任务的时候,首先会根据当前的线程数判断要不要设置超时间.如果超过这个时间,队列还没有任务进来,就会返回null,线程退出。
workQueue:任务队列
threadFactory:创建线程的工程类。可以通过指定线程工厂为每个创建出来的线程设置更有意义的名字,如果出现并发问题,也方便查找问题原因。
handler:饱和策略。当workQueue已满,且线程数超过maximumPoolSize。
AbortPolicy: 直接拒绝所提交的任务,并抛出RejectedExecutionException异常;
CallerRunsPolicy:用调用者所在的线程来执行任务;
DiscardPolicy:不处理直接丢弃掉任务;
DiscardOldestPolicy:丢弃掉阻塞队列中存放时间最久的任务,执行当前任务
java提供几种不同类型的线程池:
==newCachedThreadPool: ==
ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue());
创建一个可缓存线程池。核心线程数为0且没有最大线程数的限制。任务队列采用的是 SynchronousQueue 同步队列:属于线程安全的BlockingQueue的一种,此队列设计的理念类似于"单工模式",对于每个put/offer操作,必须等待一个take/poll操作(否则返回false)。类似于我们的现实生活中的"火把传递".因为这种策略,最终导致队列中并没有一个真正的元素;
== 因此当提交任务的时候,如果没有空闲线程来处理则会创建一个新的线程来处理该任务,这样可能会导致创建大量的线程从而使系统资源耗光. ==
newFixedThreadPool:
创建一个固定工作线程数量的线程池.
ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue()).
核心线程数和最大线程数相等。采用的是无界队列。
newSingleThreadExecutor:
创建一个单线程化的Executor
ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue());
核心线程数和最大线程数都是1,所以保证了所提交的任务都是串行执行的。采用的是无界队列。
newScheduleThreadPool:
支持定时的以及周期性的任务的线程池,线程数是固定的(创建时指定);
ThreadPoolExecutor(corePoolSize, Integer.MAX_VALUE, 0, TimeUntil.NANOSECONDS, new DelayedWorkQueue());
采用DelayedWorkQueue来做任务延时;== 也是无界队列,因此除了核心线程数,不会创建非核心线程==
线程池的关闭
关闭线程池,可以通过shutdown和shutdownNow这两个方法。它们的原理都是遍历线程池中所有的线程,然后依次中断线程。shutdown和shutdownNow还是有不一样的地方:
shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行和未执行任务的线程,并返回等待执行的任务的列表;
shutdown只是将线程池的状态设置为SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
可以看出shutdown方法会让正在执行的任务继续执行完,而shutdownNow会直接中断正在执行的任务。调用了这两个方法的任意一个,isShutdown方法都会返回true,当所有的线程都关闭成功,才表示线程池成功关闭,这时调用isTerminated方法才会返回true。
ThreadLocal
ThreadLocal是一个本地线程副本变量工具类.为变量在每个线程中都创建一个副本,这样每个线程都可以访问自己内部的副本变量,各个线程之间互不干扰。
使用场景
One possible (and common) use is when you have some object that is not thread-safe, but you want to avoid synchronizing access to that object (I’m looking at you, SimpleDateFormat). Instead, give each thread its own instance of the object.
如果程序中有一些非线程安全的对象,但你有不希望通过同步的方式访问这些对象(SimpleDateFormat)。取而代之,给每个线程创建一个副本对象。比如:数据库连接管理,线程会话管理等场景
实现原理
从线程的角度来看,每个线程的内部都有一个ThreadLocalMap的的实例(相当于线程的局部变量空间,存储着线程各自的私有数据)。
ThreadLocalMap 内部使用 Entry数组,默认大小INITIAL_CAPACITY(16)。
Entry 包含线程本地对象(key)和线程的变量副本(value)。
hreshold:table大小的2/3,当size >= threshold时,遍历table并删除key为null的元素,如果删除后size >= threshold*3/4时,需要对table进行扩容.
内部数据结构
get()
set()
Tips
Hash冲突怎么解决:
和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
table扩容
如果table中的元素数量达到阈值threshold的3/4,会进行扩容操作,
ThreadLocalMap的问题
由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
如何避免泄漏
既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。
如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。
[Runnable/Callable/FutureTask(继承自RunnableFuture)]
Callable callable = () -> {
try {
return “callable”;
} catch (Exception ex) {
ex.printStackTrace();
}
return “error”;
};
FutureTask futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
System.out.println(System.currentTimeMillis() + " " + Thread.currentThread().getName() + " " + futureTask.get());
Future future = threadPoolExecutor.submit(callable);
System.out.println(System.currentTimeMillis() + " " + Thread.currentThread().getName() + " " + future.get());
FutureTask futureTask2 = new FutureTask<>(callable);
threadPoolExecutor.execute(futureTask2);
锁降级:
写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成读取锁,从而实现锁降级的特性。
*** 锁降级之后,写锁并不会直接降级成读锁,不会随着读锁的释放而释放,因此需要显示地释放写锁
** 不存在锁升级
应用:用于对数据比较敏感,需要在对数据修改之后,获取到修改后的值,并进行接下来的其他操作。private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
writeLock.lock();
readLock.lock();
System.out.println(“get read lock”);
writeLock.unlock();
readLock.unlock();
[StampedLock]
[线程间通信]
wait/notify/notifyAll
管道 PipedInputStream / PipedOutputStream
[Thread.join]
核心思想是
while(isAlive()) {
wait(0);
}
[ThreadLocal]
ThreadLocal num = ThreadLocal.withInitial(() -> 0);
[Condition ]
private Lock lock = new ReentrantLock();
private Condition notFullCondition = lock.newCondition();
private Condition notEmptyCondition = lock.newCondition();
notEmptyCondition.notifyAll();
notEmptyCondition.await();
=======原子类
AtomicBoolean AtomicInteger AtomicLong
DoubleAdder LongAdder 对Double,Long的原子更新性能进行提升
DoubleAccumulator LongAccumulator 支持自定义运算
AtomicIntegerArray AtomicLongArray AtomicReferenceArray 原子更新数组类型
AtomicIntegerFieldUpdater AtomicLongFieldUpdater 原子的更新属性
[并发容器]
CopyOnWrite Concurrent ConcurrentBlocking
不能在 CopyOnWrite 容器的迭代操作中使用remove操作;
[并发工具类]
[] LinkedBlockingDeque blockingDeque = new LinkedBlockingDeque();
blockingDeque.add(“a”); //添加一个元素,如果添加失败则立刻抛出异常
blockingDeque.offer(“b”);//添加一个元素,如果添加失败则立刻返回false 上面两个都调用 offerLast
blockingDeque.put(“c”); //添加元素,如果中间失败则会继续重试直到添加成功
blockingDeque.remove();//从队头取出元素,如果失败则立刻抛出异常
blockingDeque.poll();//从队头取出元素,如果失败则立刻返回 null
blockingDeque.take();//从队头取出元素,如果中间失败则会继续重试直到取得元素
[] Semaphore semaphore = new Semaphore(2); //控制并发数量-接口限流 基于AQS实现的
try {
semaphore.acquire();
System.out.println(System.currentTimeMillis() / 1000 + " : " + Thread.currentThread().getName() + " 开始执行");
} finally {
semaphore.release();
}
[] CyclicBarrier barrier = new CyclicBarrier(4); //一般用于一组线程相互等待至某个状态后,然后这一组线程再一起执行-可重复
//基于 ReentrantLock 和 Condition 实现的:当 status为非0时await 将处于阻塞状态,当状态为0时则唤醒所以阻塞在Condition的线程
barrier.await();
[] CountDownLatch countDownLatch = new CountDownLatch(2); //一般用于某个线程等待多个其他线程执行完之后,它才执行:不可重复
//基于AQS实现的
countDownLatch.countDown(); //当状态为0时,唤醒所有处于等待队里的阻塞线程
System.out.println(System.currentTimeMillis() / 1000 + " : " + Thread.currentThread().getName()+ " 等待");
countDownLatch.await(); //状态为0则马上返回
System.out.println(System.currentTimeMillis() / 1000 + " : " + Thread.currentThread().getName() + " 等待完毕");
[] Exchanger 线程间交换数据-必须成对出现
///
def gupao_并发编程原理():
aba 问题
CAS
CDN