文章目录
JUC并发
synchronized
锁对象
synchronized 有三种应用场景:
- 作用于非静态实例方法:对当前实例加锁,进入同步方法前,需获得当前实例的锁,此时它是一个
对象锁
。 - 作用于静态实例方法:对实例所属类加锁,进入静态同步方法前,需要获得当前类对象的锁,此时它是一个
类锁
。 - 作用于代码块:对括号中配置的对象加锁。
底层原理
- 1、管程(英语:
monitors
,也称为监视器)是一种程序结构,结构内的多个子程序(对象或模块〉形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件设备或一群变量。对共享变量能够进行的所有操作集中在一个模块中。(把信号量及其操作原语“封装”在一个对象内部) 管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。 - 2、Java中每个对象都继承自
Object
,而每个Object 的对象都关联一个monitor
,该monitor在C语言底层中对应一个ObjectMonitor
结构体,有_owner
属性记录持有该对象的线程,有_count
属性,记录该线程获取monitor锁的次数,同一时间只有一个线程能够持有对象的monitor
,这是Java中所有对象都可以成为锁对象,且可以重复获取锁的基础。
- 3、字节码标识:
- synchronized同步代码块实现使用的是
monitorenter
和monitorexit
指令来标识获取到对象的monitor
和释放对象的monitor
,每次monitorenter
进入,锁重入次数加1,每次monitorexit
退出,锁重入次数减1。 - synchronized同步方法底层使用
ACC_SYNCHRONIZED
访问标志,标识同步方法,并使用ACC_STATIC
区分该方法是否静态同步方法,即有ACC_SYNCHRONIZED
和ACC_STATIC
标识的是静态同步方法,只有ACC_SYNCHRONIZED
标识的是普通同步方法。静态同步方法会持有类对象的monitor
,而普通同步方法会持有实例对象的monitor
.
- synchronized同步代码块实现使用的是
synchronized锁升级
reentrantlock
公平锁和非公平锁
reentrantlock默认是非公平锁,主要有以下两点考虑:
- 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能让真正运行的线程先获取到锁,更充分的利用CPU的时间片,尽量减少CPU空闲状态时间。
- 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了。否则那就用公平锁,大家公平使用。
可重入锁 / 递归锁
可重入锁的优点在于,可以在一定程度上避免死锁,一个加了锁的方法,如果递归调用自身,此时使用不可重入锁,就会自己把自己锁住。
死锁
死锁产生条件
- 存在互斥访问的锁
- 已经获取锁不可被强行剥离
- 没有完全获取到所需要的锁时,会持有锁并等待。
- 存在循环等待锁释放
举一个MySQL中,由于两个事务的间隙锁互相兼容,而插入意向锁与间隙互斥导致死锁的案例:
- time1阶段:事务A加间隙锁,范围(20, 30)
- time2阶段:事务B加间隙锁,范围(20, 30)
- time3阶段:事务A尝试给id为25的位置加插入意向锁,但是发现事务B在(20,30)间设置了间隙锁,加锁失败,阻塞,等待事务B释放间隙锁。
- time4阶段:事务B尝试给id为26的位置加插入意向锁,但是发现事务A在(20,30)间设置了间隙锁,加锁失败,阻塞,等待事务A释放间隙锁。
事务A和事务B相互等待对方释放锁,满足了死锁的四个条件:互斥、占有且等待、不可强占用、循环等待,因此发生了死锁。
如何排查死锁?如果解决死锁?
命令行版:
- jps -l
列出所有Java进程号,找到发生死锁的进程,假如是1123 - jstack 1123
查看进程栈信息,就可以定位到死锁的情况
图形工具版:
使用jconsole工具,也可以定位正在运行的Java进程的死锁情况。
LockSupport与中断机制
中断机制
- 一个线程不应该由其他线程来强制中断或停止,而是应该山线程自己自行停止,自己来决定自己的命运。
- 在Java中没有办法立即停止一条线程,然而停止线程却非常重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的协商机制——中断,也即中断标识协商机制。
- 中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程请求这条线程中断,此时究竟该做什么需要你自己写代码实现。
- 每个线程对象中都有一个中断标识位,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。
中断相关的三大API
- interrupt():
- 当一个线程调用interrupt()时,如果线程处于正常活动状态,那么会将该线程的中断标志设置为true,仅此而已,被设置中断标志的线程将继续正常运行,不受影响。
即interrupt()仅是设置中断标识,并不实际中断线程,需要被调用的线程进行配合。
就像你让一个人闭嘴,但最终那人是否闭嘴,需要它的配合。 - 如果线程处于
被阻塞的状态
(例如sleep、wait、join等状态),在别的线程中调用当前线程对象的interrupt()方法,那么线程将立即退出被阻塞状态,中断状态标志位将被清除,并抛出一个InterruptedException异常。
- 对于不活动的线程,调用interrupt()没有任何影响。
- 当一个线程调用interrupt()时,如果线程处于正常活动状态,那么会将该线程的中断标志设置为true,仅此而已,被设置中断标志的线程将继续正常运行,不受影响。
- public boolean isInterrupted():检测当前实例的中断标志位是否被设置为中断。
- public static boolean interrupted():静态方法,会调用当前线程的isInterrupted(),并传入参数,清除中断标志位。
从源码中可以看到,实例方法默认是不清除中断状态的,而静态方法则会。
如何中断运行中的线程?
- 通过volatile关键字,进行线程间通信,线程每次都会读取volatile关键字最新的值。
public class demo1 {
private static volatile boolean isStop;
public static void main(String[] args) {
new Thread( () -> {
while(!isStop) {}
System.out.println(Thread.currentThread().getName() + "收到停止!");
},"A").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
}
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "发送停止信号!");
isStop = true;
}, "B").start();
}
}
- 通过AtomicBoolean 原子布尔型,
public class demo1 {
private static volatile boolean isStop;
private static AtomicBoolean flag = new AtomicBoolean(false);
public static void main(String[] args) {
new Thread( () -> {
// while(!isStop) {}
while(!flag.get()) {}
System.out.println(Thread.currentThread().getName() + "收到停止!");
},"A").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
}
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "发送停止信号!");
// isStop = true;
flag.set(true);
}, "B").start(
);
}
}
- 通过线程自带的中断API,线程2中使用线程1的interrupt()方法,而线程1中使用isInterrupted()方法来监听。
public class demo1 {
private static volatile boolean isStop;
private static AtomicBoolean flag = new AtomicBoolean(false);
public static void main(String[] args) {
Thread t1 = new Thread( () -> {
// while(!isStop) {}
// while(!flag.get()) {}
while (!Thread.currentThread().isInterrupted()) {}
System.out.println(Thread.currentThread().getName() + "收到停止!");
},"A");
t1.start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
}
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "发送停止信号!");
// isStop = true;
// flag.set(true);
t1.interrupt();
}, "B").start(
);
}
}
LockSupport
synchronized和reentrantlock的线程等待和线程唤醒有如下局限性:
- 对象的wait()和notify()方法必须要在同步代码块或者同步方法里边运行,且成对出现必须先wait后notify才ok。
- reentrantlock构建的condition的await()方法和signal()方法必须先获取到reentrantlock锁,才可以使用,且成对出现,必须先await()后signal();
基于synchronized和reentrantlock实现三个线程交替打印A、B、C:
package Interrupt;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class demo1 {
public static void main(String[] args) {
Data d = new Data();
for (int i = 0; i < 3; ++i) {
final int temp = i;
new Thread( () -> {
for (int j = 0; j < 10; ++j) {
d.print2(temp, (char) ('A' + temp));
}
}).start();
}
}
}
class Data {
private int flag = 0;
private final Object lock = new Object();
private final Lock lk = new ReentrantLock();
private final Condition cd = lk.newCondition();
void print(int i, char letter) {
synchronized (lock) {
try {
while (i != flag) lock.wait();
System.out.println(letter);
flag = (flag + 1) % 3;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.notifyAll();
}
}
}
void print2(int i, char letter) {
lk.lock();
new Object();
try {
while (i != flag) cd.await();
System.out.println(letter);
flag = (flag + 1) % 3;
cd.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lk.unlock();
}
}
}
LockSupport原理与优势
- LockSupport是concurrent包中一个工具类,不支持构造,提供了一堆static方法,比如park(),unpark()等。
- LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程LockSupport和每个使用它的线程都有一个许可(permit)关联。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累凭证。
- 当调用park():
- 如果有凭证,直接消耗掉这个凭证,然后正常退出。
- 如果无凭证,就必须阻塞等待凭证可用。
- 当调用unpark():
- 给指定线程增加一个凭证,但凭证最多只能有一个,不可累加。
LockSupport优势在于灵活性:
- LockSupport的park()和unpark()方法不需要在同步代码块内,或者持有锁。
- unpark()可以先于park()方法调用,不用担心线程间的执行的先后顺序,因为如果先执行unpark(),为指定线程提前赋予了凭证,那么该线程在调用park()时,直接低效已有的凭证。
下边是一个主线程等待子线程任务执行完毕,再唤醒主线程的例子:
注意在使用时,LockSupport的unpark()方法需要指定线程,因此第一步,我们需要获取到主线程md
public class demo1 {
public static void main(String[] args) {
Thread mt = Thread.currentThread();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("子线程执行完毕。去唤醒主线程");
LockSupport.unpark(mt);
}
}).start();
System.out.println("等待子线程执行完毕");
LockSupport.park();
System.out.println("主线程被唤醒");
}
}
JMM
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性
展开的。
happens-before(先行发生原则)
CAS
CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg
。
执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就对总线加锁,只有一个线程能加速成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,所以在多线程情况下性能会比较好。
Unsafe
Unsafe是CAS核心类,由于Java无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe类相当于一个后门,其内部方法都是native修饰的,可以像C的指针一样直接操作内存。
Unsafe类的compareAndSwapObject,相对于Java中调用的函数,多个两个与内存操作相关的属性,var1和var2,var1表示要操作的对象,而var2表示操作对象属性地址的偏移量。
以unsafe类实现的原子类Int为例:
当compareAndSwap()执行失败时,会陷入循环,即自旋,仅当执行成功时,跳出循环。
ThreadLocal
基本使用
ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。
常用方法:
设置初始化方法:
protected T initialValue()
每个线程在第一次访问这个值的get()方法,会返回这个初始化方法的结果。
JDK8,可以用withInitial() 简化上边的写法:
案例:多线程模拟买票统计
public class demo1 {
public static void main(String[] args) {
SaleData sd = new SaleData();
for (int i = 0; i < 5; ++i) {
new Thread(() -> {
int j = Math.abs(new Random().nextInt()) % 20;
while (j-- != 0) {
sd.sale();
}
System.out.println(Thread.currentThread().getName() + "卖出:" + sd.saleNum.get());
}).start();
}
}
}
class SaleData {
ThreadLocal<Integer> saleNum = ThreadLocal.withInitial(() -> 0);
public void sale() {
saleNum.set(saleNum.get() + 1);
}
}
使用规范
每个线程都有自己的线程栈,栈空间大小是有效的,因此必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,造成ThreadLocal变量累积,可能会影响后续业务逻辑和造成内存泄露问题。
尽量在try - finally 块进行回收。