JUC 基础知识汇总,了解并发编程的全貌
NEW:
Thread对象被创建出来了,但是还没有执行start方法。
javapublic static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
});
System.out.println(t1.getState());
}
RUNNABLE:
Thread对象调用了start方法,就为RUNNABLE状态(CPU调度/没有调度)
javapublic static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(true){
}
});
t1.start();
Thread.sleep(500);
System.out.println(t1.getState());
}
BLOCKED:
synchronized没有拿到同步锁,被阻塞的情况
javapublic static void main(String[] args) throws InterruptedException {
Object obj = new Object();
Thread t1 = new Thread(() -> {
// t1线程拿不到锁资源,导致变为BLOCKED状态
synchronized (obj){
}
});
// main线程拿到obj的锁资源
synchronized (obj) {
t1.start();
Thread.sleep(500);
System.out.println(t1.getState());
}
}
WAITING:
调用wait方法就会处于WAITING状态,需要被手动唤醒
javapublic static void main(String[] args) throws InterruptedException {
Object obj = new Object();
Thread t1 = new Thread(() -> {
synchronized (obj){
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
Thread.sleep(500);
System.out.println(t1.getState());
}
TIME_WAITING:
调用sleep方法或者join方法,会被自动唤醒,无需手动唤醒
javapublic static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(500);
System.out.println(t1.getState());
}
BLOCKED、WAITING、TIME_WAITING:都可以理解为是阻塞、等待状态,因为处在这三种状态下,CPU不会调度当前线程
TERMINATED:
run方法执行完毕,线程生命周期到头了
javapublic static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(1000);
System.out.println(t1.getState());
}
获取当前线程
javaThread thread = Thread.currentThread();
获取栈帧
javaStackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); Map<Thread, StackTraceElement[]> allStackTraces = Thread.getAllStackTraces();
获取线程名称
javaThread.currentThread().getName();
设置线程调度优先级
java中给线程设置的优先级别有10个级别,从1~10任取一个整数。
javaThread.currentThread().setPriority(1);
线程的让步
可以通过Thread的静态方法yield,让当前线程从运行状态转变为就绪状态。
javaThread.yield();
线程的等待和唤醒
可以让获取synchronized锁资源的线程通过wait方法进去到锁的等待池,并且会释放锁资源
可以让获取synchronized锁资源的线程,通过notify或者notifyAll方法,将等待池中的线程唤醒,添加到锁池中
notify随机的唤醒等待池中的一个线程到锁池
notifyAll将等待池中的全部线程都唤醒,并且添加到锁池
在调用wait方法和notify以及norifyAll方法时,必须在synchronized修饰的代码块或者方法内部才可以,因为要操作基于某个对象的锁的信息维护。
javasynchronized (MiTest.class) {
MiTest.class.wait();
MiTest.class.notifyAll();
}
线程的结束方式
interrupt方式
javapublic static void main(String[] args) throws InterruptedException {
// 线程默认情况下, interrupt标记位:false
System.out.println(Thread.currentThread().isInterrupted());
// 执行interrupt之后,再次查看打断信息
Thread.currentThread().interrupt();
// interrupt标记位:ture
System.out.println(Thread.currentThread().isInterrupted());
// 返回当前线程,并归位为false interrupt标记位:ture
System.out.println(Thread.interrupted());
// 已经归位了
System.out.println(Thread.interrupted());
// =====================================================
Thread t1 = new Thread(() -> {
while(!Thread.currentThread().isInterrupted()){
// 处理业务
}
System.out.println("t1结束");
});
t1.start();
Thread.sleep(500);
t1.interrupt();
}
JMM (Java Memory Model) 即为Java内存模型
Java内存模型规定了所有的变量都存储在主内存中,每条线程都有自己的工作内存,线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对该变量的所有读写操作都必须在工作内存中进行而不能直接读写主内存中的数据,不同的线程之间也无法访问对方工作内存中的变量,线程间变量值的传递都需要通过主内存来完成
原子性、可见性、有序性
作用于主内存的变量,它把一个变量标识为一条线程独占的状态
作用域主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
作用域主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
作用于工作内存的变量,它把read操作从主内存中得到的变量值放入到工作内存的变量副本中
作用于工作内存的变量,它把工作内存中的一个变量的值传给存储引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时会执行这个操作
作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令执行这个操作
作用于工作内存的变量,它把工作内存中的一个变量的值传送到主内存中,以便write操作使用
作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
是Java提供的一种轻量级的同步机制,用来确保将变量的更新操作通知到其他线程
可见性
通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的方式实现可见性的
有序性
禁止指令重排序优化 通过内存屏障指令保证处理器不会乱序
volatile 是通过 MESI 缓存一致性协议 来保证可见性的
首先 cpu 会根据共享变量是否带有 volatile 字段,来决定是否使用MESI协议保证缓存一致性。 如果有volatile,汇编层面会对变量加上Lock前缀,当一个线程修改变量的值后,会马上经过store、write等原子操作修改主内存的值(如果不加Lock前缀不会马上同步),为什么监听到修改会马上同步呢?就是为了触发cpu的嗅探机制,及时失效其他线程变量副本。
cpu总线嗅探机制监听到这个变量被修改,就会把其他线程的变量副本由共享S置为无效I,当其他线程在使用变量副本时,发现其已经无效,就回去主内存中拿一个最新的值。
使CPU缓存数据立即写会主内存(Volatile修饰的变量会带lock前缀) 触发总线嗅探机制和缓存一致性协议MESI来失效其他线程的变量
被 volatile 修饰的变量存在一个 lock 前缀的指令,lock 前缀指令实际上相当于一个内存屏障
ThreadLocal意为线程本地变量,用于解决多线程并发时访问共享变量的问题。
在多线程的场景下,当有多个线程对共享变量进行修改的时候,就会出现线程安全问题,即数据不一致问题。常用的解决方法是对访问共享变量的代码加锁(synchronized或者Lock)。但是这种方式对性能的耗费比较大。
在JDK1.2中引入了ThreadLocal类,来修饰共享变量,使每个线程都单独拥有一份共享变量,这样就可以做到线程之间对于共享变量的隔离问题。
javapublic class ThreadLocal<T> {
...
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
}
在使用的过程中,如果key的引用丢失,也就是 ThreadLocal 的引用丢失后,key会因为弱引用被GC回收,在当前线程内部的 ThreadLocalMap 中,就没有key在指向的value,value无法被获取,更无法被回收,导致value的值一直占用内存,造成内存泄漏。
解决
只需要在使用完毕ThreadLocal对象之后,及时的调用remove方法,移除Entry即可
CAS (Compare-and-Swap)就是比较并交换(本质是 乐观锁
)
CAS指令需要三个操作数,分别是变量的内存地址,用V表示,旧值用A表示,新值用B表示。CAS指令在执行时,当且当V符合A时,处理器才会用B更新V的值,否则就不更新。不管是否更新了V值,都会返回V的旧值,这个处理过程是一个原子操作,执行期间不会被其他线程中断
如果变量V初次读取的变量值是A值,在这段期间它的值曾经被改为B值,后来又被改为A值,那么CAS操作会误认为它从来没有被改变过,这个问题被称为 ABA
问题
解决办法是在每一个线程对变量进行赋值操作时加上版本号信息。
原子类中提供了 AtomicStampedReference 类
通过控制变量值的版本来保证CAS的正确性,可解决CAS的 ABA
问题
可以指定CAS自旋次数,如果超过次数,直接挂失败/挂起线程。(自旋锁、自适应自旋锁) 可以在CAS失败之后,把这个操作暂存起来,后面等待获取结果,将暂存的操作全部执行,再返回最后的结果
原子类基于CAS算法实现线程安全,无需加锁,性能高效。
原子操作类可分为五种类型: 基本数据类型原子类、数组类型原子类、引用类型原子类、对象的属性修改类型、高性能原子类
AtomicInteger、AtomicLong、AtomicBoolean
AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
AtomicReference、、AtomicReferenceArray、AtomicStampedReference
AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdate
注意:对象的属性修改类型都是抽象类,所以在使用之前必须使用静态方法newUpdater创建一个新的更新器,然后参数传入需要更新的类和属性,属性名。同时待修改的属性必须用public volatile关键字修饰。
DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder、Striped64
以LongAdder为例,引入的目的是解决高并发环境下AtomicLong的自旋瓶颈问题。 思想概述:采用分段的思想,将value值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,最后再把这些段的值相加得到最终的值
队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个volatile int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,它是大部分同步需求的基础。
同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的 方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些 模板方法将会调用使用者重写的方法。
重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。
- getState():获取当前同步状态。
- setState(int newState):设置当前同步状态。
- compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态 设置的原子性。
同步状态可以表示任意状态,例如:
ReentrantLock 中用它来标识所有者线程已经重复获取该锁的次数 Semaphore 用它来表示剩余的许可数量 FutureTask 用它来表示任务的状态(尚未开始、正在运行、已完成以及已注销)
在同步器类中还可以自行管理一些额外的状态变量:
ReentrantLock 保存了锁当前所有者的信息,能区分某个获取操作是重入的还是竞争的
同步器可重写的方法:
方法名称 | 描述 |
---|---|
protected boolean tryAcquire(int arg) | 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态 |
protected boolean tryRelease(int arg) | 独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态 |
protected int tryAcquireShared(int arg) | 共享式获取同步状态,返回大于等于0的值,表示获取成功,反之,获取失败 |
protected boolean tryReleaseShared(int arg) | 共享式释放同步状态 |
protected boolean isHeldExclusively() | 当前同步器是否在独占模式下被线程占用,一般该方法标识是否被当前线程所独占 |
同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放同步状态和查询同步队列中的等待线程情况。自定义同步组件将使用同步器提供的模板方法来实现自己的同步语义。
ReentrantLock、 ReentrantReadWriteLock和CountDownLatch等锁实际上是内部定义的类实现的,同步器是实现锁的关键,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁的实现又分为互斥锁与共享锁,分别对应同步器的独占式与共享式操作同步状态。
同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取 同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其 加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再 次尝试获取同步状态。
同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点,节点的属性类型与名称以及描述。
ReentrantLock底层是基于AQS实现的,通过AQS维护的 volatile 的 state 变量来实现锁操作的。 ReentrantLock实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。 与synchronized一样,ReentrantLock也提供了可重入的加锁语义。
本文作者:柳始恭
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!