Java 并发
基础问题
线程和进程的含义及区别
- 线程
操作系统调度的最小单元,轻量级进程 - 进程
操作系统资源分配的基本单位,操作系统在运行一个程序的时候,会为其创建一个进程。 一个进程里可以创建多个线程,这些线程都拥有各自的计数器,堆栈和局部变量等属性,并且能够访问共享的内存变量。
每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
线程的状态和变迁
Blocked 和 Waiting 的区别: 线程
Waiting
是在等待其他线程发出通知,如线程由于调用Object.wait() 进入Waitng
状态,等待其他线程发出Notify的通知;而线程Blocked
是线程正在等待一个同步锁,以进入sychronized同步块。
如何创建一个线程?
继承Thread类,实现Runable接口,Callable和Future
start() 和 run() 区别
如下图所示,start()方法只能调用一次,重复调用会抛出IllegalThreadStateException
,run()方法可以重复调用
为什么调用start()会执行run()方法
见下面JDK Thread start()中的注释;总结就是,start()方法会让JVM调用该Thread的run()方法,并在新线程上执行,即主线程调用start()过后返回并继续执行,新的线程将会被创建并用来执行run()方法
start()方法中call了一个native的start0()
native方法是指该方法的实现由非Java语言实现,如C或C++,不提供实现体,是个原生态方法
Java语言本身不能对操作系统底层进行访问和操作,但可以通过JNI(Java Native Interface)接口调用其他语言来实现对底层的访问,JNI是JDK的一部分
为什么不能直接调用run()方法
不会创建新的线程,该run()方法就是个普通的方法将在主线程执行,一般来说直接调用run()是一个bug或者失误
/**
* Causes this thread to begin execution; the Java Virtual Machine
* calls the <code>run</code> method of this thread.
* <p>
* The result is that two threads are running concurrently: the
* current thread (which returns from the call to the
* <code>start</code> method) and the other thread (which executes its
* <code>run</code> method).
* <p>
* It is never legal to start a thread more than once.
* In particular, a thread may not be restarted once it has completed
* execution.
*
* @exception IllegalThreadStateException if the thread was already
* started.
* @see #run()
* @see #stop()
*/
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
/**
* If this thread was constructed using a separate
* <code>Runnable</code> run object, then that
* <code>Runnable</code> object's <code>run</code> method is called;
* otherwise, this method does nothing and returns.
* <p>
* Subclasses of <code>Thread</code> should override this method.
*
* @see #start()
* @see #stop()
* @see #Thread(ThreadGroup, Runnable, String)
*/
@Override
public void run() {
if (target != null) {
target.run();
}
}
yield()方法有什么作用?
yield()方法只是让线程从RUNNING状态变为READY状态,但是也可能变为READY后又立刻被CPU调度器执行(变为RUNNING);如下图JDK注释说的,yield()只是告诉cpu调度器,我可以让出我的时间片,但是cpu调度器可以忽视这个消息;一般来说这个方法没有很多应该使用的场景,多是用于测试或者debug。
sleep()和yield()方法有什么区别?
sleep()让线程从RUNABLE状态变为TIMED_WAITING状态,让出CPU的时间片,不考虑线程优先级,yield()只会让优先级高的运行;sleep的线程不会丧失任何monitor的所有权
为什么sleep()和yield()方法是静态的?
sleep()和yield()都是谁调谁sleep/yield也都是自愿的,如果是实例方法会造成很多混乱,即我可以让你这个线程sleep
/**
* A hint to the scheduler that the current thread is willing to yield
* its current use of a processor. The scheduler is free to ignore this
* hint.
*
* <p> Yield is a heuristic attempt to improve relative progression
* between threads that would otherwise over-utilise a CPU. Its use
* should be combined with detailed profiling and benchmarking to
* ensure that it actually has the desired effect.
*
* <p> It is rarely appropriate to use this method. It may be useful
* for debugging or testing purposes, where it may help to reproduce
* bugs due to race conditions. It may also be useful when designing
* concurrency control constructs such as the ones in the
* {@link java.util.concurrent.locks} package.
*/
public static native void yield();
/**
* Causes the currently executing thread to sleep (temporarily cease
* execution) for the specified number of milliseconds, subject to
* the precision and accuracy of system timers and schedulers. The thread
* does not lose ownership of any monitors.
*
* @param millis
* the length of time to sleep in milliseconds
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public static native void sleep(long millis) throws InterruptedException;
sleep()和wait()方法有什么区别?
wait()是Object的实例方法,有几个重点:
-
当前线程必须持有该object的锁(monitor),配合
synchronized
使用,否则抛出IllegalMonitorStateException
异常 -
wait()方法使得当前线程阻塞,并释放该object的锁(monitor)
-
当前线程将等待其他线程调用该object的notify()或者notifyAll()的方法重新获得锁并继续执行下去
-
响应中断
-
wait()方法必须用在循环里面 在后面讲monitor的时候有补充
为什么wait()方法必须用在循环里面,即用while(condition not hold)而不是if(condition not hold):
当有多个线程由于相同condition not hold在wait()这行代码处阻塞等待持锁的时候,一旦condition change某个线程拿到锁了从wait()往下执行,又改变了condition,释放锁,由于另外个阻塞的线程是if判断,不再判断condition,拿到锁再处理时会发生错误。假设condition是array size > 0 而wait()之后会进行remove,那么就会出现IndexOutOfArray的问题
显而易见的相同点为:都使得当前线程阻塞了(TIMED_WAITING/WAITING状态)且都响应中断;不同点为:一个是Thread的静态方法,一个是Object的实例方法;wait()需要当前线程持有该object的monitor,sleep是当前线程自愿的行为;wait()调用后当前线程会释放monitor,sleep不会释放任何monitor;sleep自动唤醒,wait需要其他线程调用该object的notify和notifyAll()方法
/**
* Causes the current thread to wait until another thread invokes the
* {@link java.lang.Object#notify()} method or the
* {@link java.lang.Object#notifyAll()} method for this object.
* In other words, this method behaves exactly as if it simply
* performs the call {@code wait(0)}.
* <p>
* The current thread must own this object's monitor. The thread
* releases ownership of this monitor and waits until another thread
* notifies threads waiting on this object's monitor to wake up
* either through a call to the {@code notify} method or the
* {@code notifyAll} method. The thread then waits until it can
* re-obtain ownership of the monitor and resumes execution.
* <p>
* As in the one argument version, interrupts and spurious wakeups are
* possible, and this method should always be used in a loop:
* <pre>
* synchronized (obj) {
* while (<condition does not hold>)
* obj.wait();
* ... // Perform action appropriate to condition
* }
* </pre>
* This method should only be called by a thread that is the owner
* of this object's monitor. See the {@code notify} method for a
* description of the ways in which a thread can become the owner of
* a monitor.
*
* @throws IllegalMonitorStateException if the current thread is not
* the owner of the object's monitor.
* @throws InterruptedException if any thread interrupted the
* current thread before or while the current thread
* was waiting for a notification. The <i>interrupted
* status</i> of the current thread is cleared when
* this exception is thrown.
* @see java.lang.Object#notify()
* @see java.lang.Object#notifyAll()
*/
public final void wait() throws InterruptedException {
wait(0);
}
join()方法怎么使用?
- join()方法不是一个静态方法,是Thread类的实例方法
- 当前线程会等待t.join()的t线程执行完成后才继续执行
- 响应中断
/**
* Waits for this thread to die.
*
* <p> An invocation of this method behaves in exactly the same
* way as the invocation
*
* <blockquote>
* {@linkplain #join(long) join}{@code (0)}
* </blockquote>
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public final void join() throws InterruptedException {
join(0);
}
守护线程和用户线程有什么区别?
- 主线程结束后用户线程还会继续运行,JVM存活
- 如果没有用户线程,都是守护线程,那么JVM结束(所有的线程都会结束)
守护线程是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT线程都是守护线程。与之对应的是用户线程,用户线程可以理解为是系统的工作线程,它会完成这个程序需要完成的业务操作。如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出了。所以当系统只剩下守护进程的时候,java虚拟机会自动退出
什么是线程安全,有哪些线程安全程度?
操作系统可以保证每个进程只能访问分配给自己的地址空间。而每个进程中都有一块内存空间(堆内存)是公共的,是所有线程都能访问的,就像小区的公园一样。操作系统也会为每个线程分配自己的内存空间(栈内存),只有自己这个线程能访问。
线程的安全其实是内存的安全。
如何保证线程安全?
- 放到栈内存,例如局部变量。但是也就只有自己能访问了,当变量从(方法内的)局部变量变为(类的)成员变量的时候,也就是从栈内存到公共的堆内存,就可能会出现问题
- 人人有份。ThreadLocal,每个Thread都有自己的一个map存储变量。这些变量虽然整体是放到堆内存的,但是由于自己拷贝一份到自己的map,自己处理自己的,就像是本地的一样,也就安全了。
- 只能看不能摸。例如,常量或者只读变量。
- 制定规则,先入为主。例如可重入锁。
- 地广人稀,CAS。
操作系统的内存模型
操作系统的内存分为用户空间和内核空间。用户空间中的栈,存放函数调用过程中的参数和局部变量和返回值等,由操作系统管理,自动分配和回收,每一个函数执行的时候会在栈上占据一定空间,叫做栈帧。堆主要是存放通过调用malloc
或者new
动态分配的资源,由程序员分配和释放。JVM本身是一个进程,Java中所说的堆栈其实都是操作系统里的堆空间。
什么是中断
另外一个问题,我们应该
如何安全地结束一个线程?
- 使用退出标志,用一个while()循环,再设置一个boolean标志
- 使用中断
thread.interrupt()
将设置该线程中断位为true,包含两种,一种本质就是1,用while循环加上isInterrupted()来判断;一种通过catch InterruptedException来处理
interrupt()方法只是改变中断状态,不会中断一个正在运行的线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。这一方法实际完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程检查到中断标识,就得以退出阻塞的状态。
更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个 InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。如果线程没有被阻塞,这时调用 interrupt()将不起作用,直到执行到wait(),sleep(),join()时,才马上会抛出 InterruptedException
/**
* Interrupts this thread.
*
* <p> Unless the current thread is interrupting itself, which is
* always permitted, the {@link #checkAccess() checkAccess} method
* of this thread is invoked, which may cause a {@link
* SecurityException} to be thrown.
*
* <p> If this thread is blocked in an invocation of the {@link
* Object#wait() wait()}, {@link Object#wait(long) wait(long)}, or {@link
* Object#wait(long, int) wait(long, int)} methods of the {@link Object}
* class, or of the {@link #join()}, {@link #join(long)}, {@link
* #join(long, int)}, {@link #sleep(long)}, or {@link #sleep(long, int)},
* methods of this class, then its interrupt status will be cleared and it
* will receive an {@link InterruptedException}.
*
* <p> If this thread is blocked in an I/O operation upon an {@link
* java.nio.channels.InterruptibleChannel InterruptibleChannel}
* then the channel will be closed, the thread's interrupt
* status will be set, and the thread will receive a {@link
* java.nio.channels.ClosedByInterruptException}.
*
* <p> If this thread is blocked in a {@link java.nio.channels.Selector}
* then the thread's interrupt status will be set and it will return
* immediately from the selection operation, possibly with a non-zero
* value, just as if the selector's {@link
* java.nio.channels.Selector#wakeup wakeup} method were invoked.
*
* <p> If none of the previous conditions hold then this thread's interrupt
* status will be set. </p>
*
* <p> Interrupting a thread that is not alive need not have any effect.
*
* @throws SecurityException
* if the current thread cannot modify this thread
*
* @revised 6.0
* @spec JSR-51
*/
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
/**
* Tests whether the current thread has been interrupted. The
* <i>interrupted status</i> of the thread is cleared by this method. In
* other words, if this method were to be called twice in succession, the
* second call would return false (unless the current thread were
* interrupted again, after the first call had cleared its interrupted
* status and before the second call had examined it).
*
* <p>A thread interruption ignored because a thread was not alive
* at the time of the interrupt will be reflected by this method
* returning false.
*
* @return <code>true</code> if the current thread has been interrupted;
* <code>false</code> otherwise.
* @see #isInterrupted()
* @revised 6.0
*/
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
/**
* Tests whether this thread has been interrupted. The <i>interrupted
* status</i> of the thread is unaffected by this method.
*
* <p>A thread interruption ignored because a thread was not alive
* at the time of the interrupt will be reflected by this method
* returning false.
*
* @return <code>true</code> if this thread has been interrupted;
* <code>false</code> otherwise.
* @see #interrupted()
* @revised 6.0
*/
public boolean isInterrupted() {
return isInterrupted(false);
}
/**
* Tests if some Thread has been interrupted. The interrupted state
* is reset or not based on the value of ClearInterrupted that is
* passed.
*/
private native boolean isInterrupted(boolean ClearInterrupted);
interrupted和isInterrupted方法的区别?
如上图所示,interrupted()是线程的静态方法,返回当前线程的中断标记且会重置中断标记;isInterrupted()是实例方法,返回this的中断标记且不会重置中断标记。
可重入锁 & synchronized
synchronized 作用
- 原子性: 保证线程互斥访问同步代码
- 可见性: 保证对共享变量的修改及时可见,对一个对象
unlock
操作前,必须要将该对象同步到主存;而对一个对象lock
操作前会清空线程工作内存中的该对象的拷贝,使用此对象前会从主存中load - 有序性: 解决重排问题,一个
unlock
操作happen-before
于后面对同一个锁的lock
操作
synchronized 用法
- 作用在实例方法或者对象实例时,锁的就是对象实例
- 作用在静态方法时,锁的是对象的Class实例,因为Class数据存在于永久代,因此静态方法锁相当于该类的全局锁?
可重入锁和synchronized的主要区别
- 机制是不一样的:synchronized是java内置的关键字,在JVM层面实现,系统会监控锁的释放,且自动释放,同步执行完或发生异常将释放锁;lock是JDK代码实现的,需要手动在finally模块释放,并可以非阻塞地获取锁。
- 性能不一样:竞争激烈的情况下lock会比synchronized好;竞争不激烈的情况下synchronized性能好,且synchronized会从偏向锁-->轻量级锁-->重量级锁升级
- 场景范围不一样:使用lock,线程2等待线程1释放锁的时候,可以不用一直等待,synchronized做不到
- 总的来说,lock提供了比synchronized更多的功能:如下图PPT所示
- synchronized 同步格式
synchronized(需要一个任意的对象(锁)){
代码块中放操作共享数据的代码;
}
-
关于Lock
-
Lock其实只是一个接口
- ReentrantLock是唯一实现了Lock接口的类
可重入锁的实现?
先看JUC中的[AQS]
什么是synchronized的锁升级?
Java对象在内存中的存储结构包括:对象头,实例数据,填充数据;对象头包含Mark Word(hashCode, GC分代年龄,锁信息),Class Metadata address(指向对象类型数据的指针),Array Length(该对象为数组时,数组的长度)
-
偏向锁
当对象被创建出来的时候,就有了偏向锁的标志位“01”,但状态为0,即被创建的对象的偏向锁不生效;但是,当线程执行到临界区(critical section)的时候,此时会利用CAS操作将线程ID插入到Markword中,并修改偏向锁的标志位,即状态为0,且能知道是哪个线程拥有了偏向锁。
当此线程执行之后,若这个同步块代码又被进入了,则会:
- 判断当前线程是否与Markword当中的线程id一致,若一致则继续执行
- 若不一致,检查对象是否还是可偏向的,即偏向锁的状态
- 如果未偏向,则利用CAS竞争锁,也就是第一次获取锁时的操作
- 如果已经偏向了,可能需要重新偏向,或者大部分时候升级为轻量级锁
偏向锁存在的意义:大部分时候都是同一个线程进入同一个同步块,那么这个时候就不需要再有加锁解锁的开销;偏向锁实际上是偏向第一个拿到锁的线程。
-
升级为轻量级锁
-
锁撤销-有开销
- 在一个安全的点停止拥有锁的线程
- 遍历线程栈,存在锁记录的话,需要修复锁记录和Markword使其变成无锁状态
- 唤醒当前线程,升级成轻量级锁
-
轻量级锁:锁标志位为“00”,此时Markword bitfield被替换为指向LockRecord的指针,之前的bitfield将会复制到创建的这个LockRecord里面,LockRecord有个owner的指针指向对象
-
轻量级锁又分为自旋锁和自适应自旋锁
-
自旋锁:
当有线程竞争锁时,会在原地循环等待,直到锁被释放可以立刻获取锁,而这个原地循环会消耗CPU。适用于同步代码块执行很快的场景。经验表明,大部分同步代码块都执行很快。设置一个原地循环的最大限制次数,如果超过就升级为重量级锁。默认为10次,可以通过-XX:PreBlockSpin来进行更改。
-
自适应自旋锁:
即可动态调整自旋等待的次数而不是个固定值。
-
轻量级锁也被称为非阻塞同步,乐观锁
-
-
升级为重量级锁
- 重量级锁依赖对象内部monitor,monitor又依赖于操作系统的mutex锁,此时的标志位为“10”,bitfield将会指向mutex的指针
- 为什么重量级锁开销大?
- 此时,等待的线程会被阻塞,不会消耗CPU但是阻塞或者唤醒一个线程时都需要操作系统来帮忙,这就需要从用户态转换到内核态,这个转换很耗时。
- 重量级锁又被称为阻塞同步,悲观锁
volatile关键字
volatile是什么意思?原理是什么?
volatile是修饰变量的关键字,它修饰的变量可以保证 1. 可见性; 2. 禁止指令重排;可见性即,对volatile变量读的时候,一定读的是最新值(主存中的值),对volatile变量进行写后,会将该值写到主存并更新其他线程该值的缓存;禁止指令重排即当编译器遇到volatile修饰的变量的代码行时,会在指令序列中插入内存屏障以禁止指令重排,保证volatile变量之前的指令都执行完,其后的指令都还没有执行。
CAS的特点是什么?
监视器(Monitor)和Condition区别
操作系统在面对 进程/线程 间的同步的时候所支持的同步原语中,信号量(semaphore)和互斥量(mutex)是最重要的。在使用mutex进行并发控制时,要非常小心地控制其down和up的操作,否则将引起死锁。在此基础出现了更高层次的同步原语,使我们不需要亲自去操作变量进行阻塞和唤醒,这个更高级的同步原语就是monitor---也是编程语言在语法上提供的语法糖,如何实现属于编译器的工作,不是操作系统的范畴。
🌰先让我们来看一个wait()
和notify()
的经典例子
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
this.notifyAll();
}
public synchronized String getTask() {
//进入同步块,说明该线程已经获取了taskQueue对象的锁,若没获取则无法进入同步块
while (queue.isEmpty()) {
this.wait();
//该线程执行wait(),释放了taskQueue对象的锁,自己阻塞了(不往下面执行,该线程id进入taskQueue的waitQueue里面等待通知),但是不会阻塞其他线程addTask(),因为锁已经释放了
//直到其他线程调用了该taskQueue对象的notify()等方法
//由于是while循环,会再次判断queue是否为空,若不为空,则执行下面的remove操作
//若为空,那么继续到wait()这一行,线程id会进入taskQueue的synchronizedQueue里,竞争锁,拿到锁后重复上述操作;这里可以回答为什么wait()方法需要用在循环里,因为如果只是一个if判断,很有可能其他线程抢先remove了,一个空queue再执行remove会报错
}
return queue.remove();
}
}
要知道wait()
和notify()
主要是用来做线程之间通信的,主要解决“生产-消费”模式里的并发问题。如果只是解决锁竞争只需要synchronized
就好了。
-
monitor的基本元素
- 临界区:互斥进入临界区
- monitor对象和锁:monitor object有相应数据结构保存被阻塞的线程,和基于mutex的锁
- 条件变量和定义在monitor对象上的wait和notify操作
-
java中的每一个对象都有一个监视器(Monitor)
- Monitor锁在HotSpot虚拟机是用ObjectMonitor(C++)来实现的,其主要数据结构如下:
ObjectMonitor() { _header = NULL; _count = 0; //记录个数 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
-
使用synchronized关键字来圈定临界区,实现互斥的界限
-
monitor object:
synchronized需要指定一个对象与之关联,如果synchronized修饰的是实例方法,那么其实关联的就是this,如果修饰的是类方法,关联的对象就是this.class,这个关联的对象就是monitor object。
锁:
Java对象存储在内存中,分别分为三个部分:对象头,实例数据和对齐填充,在对象头中就保存了锁标识。
-
wait和notify的操作:
这些方法的实现是JVM内部基于C++实现的一套机制,原理如下图:总结就是,waitThread(即调用object.wait()方法的线程)先要获取object的锁(也就是为什么需要配合synchronized关键字使用),如果获取成功,执行到object.wait()这行代码处,该线程就放进了这个object的waitQueue里面,表示在等待中。如果获取失败,该线程将被放进synchronizedQueue中,想要继续尝试获取锁。
当调用object.notifyAll()的方法时,所有等在waitQueue里的线程将移动到synchronizedQueue里面去和那些waitThread没有获取锁的线程一起尝试获取锁。
一旦某个线程获取成功了,就继续往下执行。
-
总结:Monitor是一套机制,它界于操作系统和我们实际编程中间,提供并发编程的方式。
J.U.C.
AQS
AQS是什么?
AQS叫做队列同步器(AbstractQueuedSynchronizer),是用来构建锁和同步组件的基础框架,其主要数据结构如下:
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
-
AQS本身维护一个Node节点的双向链表队列,和一个表征同步状态的state字段
-
AQS有一个内部类Node,每个Node主要就是存一个Thread,Thread的状态,父节点prev和子节点next
static final class Node {
/** 共享状态节点 */
static final Node SHARED = new Node();
/** 独占状态节点 */
static final Node EXCLUSIVE = null;
/** 节点的同步状态
* 其值可以为 CANCELLED, SIGNAL, CONDITION, PROPAGATE
* CANCELLED: 当前线程由于等待超时了或中断而取消锁争抢,一旦线程进入CANCELLED状态,就不再进行其他状态流转
* SIGNAL: 当前线程释放锁或者取消锁争抢时,需要唤醒他的后续节点们,即当前节点为SIGNAL状态,代表后续节点需要被signal/唤醒
* CONDITION: 当前线程是等在某个condition队列里的,不能被当做同步队列节点里来争抢锁,除非状态变迁了。
* PROPAGATE: releaseShared需要被传播给其他节点
*/
volatile int waitStatus;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile Node prev;
volatile Node next;
volatile Thread thread;
/**
* Link to next node waiting on condition, or the special
* value SHARED. Because condition queues are accessed only
* when holding in exclusive mode, we just need a simple
* linked queue to hold nodes while they are waiting on
* conditions. They are then transferred to the queue to
* re-acquire. And because conditions can only be exclusive,
* we save a field by using special value to indicate shared
* mode.
*/
Node nextWaiter;
/**
* Returns true if node is waiting in shared mode.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
}
AQS独占式和共享式的含义?
-
独占式:同一时刻只有一个线程持有同步状态,如ReentrantLock的实现
独占式获取同步状态过程:
- 共享式:共享资源可以被多个线程同时占有,如ReadWriteLock,CountdownLatch
AQS的模板模式是什么?
- AQS只是一个基础框架,
tryAcquire(int arg)
,tryRelease(int arg)
,tryAcquireShared(int arg)
,tryReleaseShared(int arg)
,isHeldExclusively()
都没有具体实现,因为公平锁有公平锁的实现方式,非公平锁有非公平锁的实现方式等,需要子类去实现
简述并发工具Semaphore, CountdownLatch, CyclicBarrier的特点使用场景
-
CountdownLatch
,就是某个线程(超时)阻塞在await(time)
行,等其他线程全部countdown()
之后才会继续进行。初始化CountdownLatch
时有个counter,必须这么多的线程都countdown()
才可以。
用途:例如某个工作要等其他子项工作都处理完成后才能进行。一等多 -
Semaphore
是同一时刻只允许特定数量的线程运行,每个线程运行前去acquire()
一下许可证,如果acquire()
到就继续运行,许可证减一,运行完成后release()
一下许可证,让其他线程可以竞争。
用途: 控制并发度,或者某些珍贵资源同时只能被少数访问等等。令牌桶 -
CyclicBarrier
同步屏障,如果说CountDownLatch
是一个或多个线程等其他线程完成工作才得以进行,CyclicBarrier
就是一组线程他们在并发工作的时候,在某些地点,我们要求他们步调一致,或者达到某个状态才继续,否则他们会互相等待,直到最后一个线程到达该状态。等待的地方就调用await()
;
用途:似乎没有前两者那么多,我在工作中还没有用到过。但是这个场景还是挺常见的。多等多
通过题目来巩固:leetcode1195
利用wait()
和notifyAll()
利用cyclicBarrier
线程安全的集合有哪些?
ConcurrentHashMap
- HashMap不是线程安全的,在扩容的时候可能出现循环链表的情况导致get()操作CPU空转
- HashTable是线程安全的,但它所有操作都用了同步块
synchronized
性能较差 - JDK1.7的ConcurrentHashMap使用segments,使得每个segments对应一段数据,对这一个segment加锁
- JDK1.8的两个特点
- 取消segment, 使用
transient volatile HashEntry<K,V>[] table
对每个table元素加锁(更加减小了锁的粒度),实现对每行数据加锁。并发控制使用synchronized
+CAS操作 - 将原来table数组+单向链表的结构转变为table数组+单向链表+红黑树的结构
- 取消segment, 使用
CopyOnWriteArrayList
Copy-On-Write的思想很多地方都有。主要就是牺牲空间来节省时间获得性能,应用在很多读多于写的场景里。Copy-On-Write实现的就是读读不互斥,读写不互斥,只有写写会阻塞(同步等待)。
CopyOnWriteArrayList的所有可变操作如add
,set
都是通过拷贝一份原来数据副本,在副本上进行操作,最后再用副本替换原来的数据的方式。
源码如下:
//field,主要是一个锁和一个数组
final transient Object lock = new Object();
private transient volatile Object[] array;
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
get
操作:
public E get(int index) {
return elementAt(getArray(), index);
}
static <E> E elementAt(Object[] a, int index) {
return (E) a[index];
}
add
操作:
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}
BlockingQueue
参考资料
实现一个BlockingQueue的Leetcode,对Condition,BlockingQueue的理解都有很大帮助
BlockingQueue
顾名思义-阻塞队列。在之前讲wait()
,notify()
和monitor的时候,举了一个经典的例子TaskQueue
其实本质就是一个BlockingQueue
,也就是用于“生产-消费”的场景。阻塞的地方主要是,队列为空时,不能消费,直到队列有元素;队列满了以后,不能生产,直到队列有空余。BlockingQueue
只是一个接口,它对插入,删除元素提供了以下几种方式:
Throws exception | Special value | Blocks | Times out | |
---|---|---|---|---|
Insert | add(e) | offer(e) | put(e) | offer(e,time,unit) |
Remove | remove() | poll() - null |
take() | poll(time,unit) |
Examine | element() | peek() | not applicable | not applicable |
BlockingQueue
不接受null
值的插入。BlockingQueue
的实现都是线程安全的,但是批量操作时不一定是原子的。
其主要的实现方式有:
- ArrayBlockingQueue/LinkedBlockingQueue
最基础的功能,底层数组实现/链表实现。默认是非公平的(提高吞吐量),插入还是读取操作都要获取锁才能进行操作。ArrayBlockingQueue
一旦创建,容量固定。LinkedBlockingQueue
如果创建时不指定大小,默认是容量是Integer.MAX_VALUE
可以视为无界队列。
ArrayBlockingQueue
属性:
// 用于存放元素的数组
final Object[] items;
// 下一次读取操作的位置
int takeIndex;
// 下一次写入操作的位置
int putIndex;
// 队列中的元素数量
int count;
// 以下几个就是控制并发用的同步器,读线程排队在notEmpty,写线程排队在notFull
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
- SynchronousQueue
同步队列,没有堆积能力,只能生产一个消费一个 - DelayQueue/DelayedWorkQueue
- PriorityBlockingQueue
可以理解为PriorityQueue
的线程安全版本,可以重写compareTo()
方法实现内部元素顺序。
ConcurrentLinkedQueue
与阻塞队列对应, ConcurrentLinkedQueue
是一个线程安全的非阻塞队列。阻塞队列通过加锁实现,非阻塞队列可以通过CAS实现。这个容器性能比较好,内部实现比较复杂。主要使用CAS非阻塞算法实现的。
ConcurrentSkipListMap
跳表
跳表是一种空间换时间的思想,我们将一个单链表里的元素,拎出来一部分作为一级二级...索引,这样查询的时间复杂度就会从O(n)降到O(logn);
跳表里的数据都是有序的,插入数据并维护索引(以免因为分布不均使得查询退化成O(n))的时间复杂度也是O(logn)这里采取了一种近似处理,即索引随机分布:通过概率算法计算出这个元素要插入到几级索引中或者不需要插入新的索引:
跳表的工业使用:LSM Tree ->HBase,LevelDB, RocksDB
线程池
太熟悉就不多说了