synchronized 有三种应用场景:
对象锁
。类锁
。monitors
,也称为监视器)是一种程序结构,结构内的多个子程序(对象或模块〉形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件设备或一群变量。对共享变量能够进行的所有操作集中在一个模块中。(把信号量及其操作原语“封装”在一个对象内部) 管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。Object
,而每个Object 的对象都关联一个monitor
,该monitor在C语言底层中对应一个ObjectMonitor
结构体,有_owner
属性记录持有该对象的线程,有_count
属性,记录该线程获取monitor锁的次数,同一时间只有一个线程能够持有对象的monitor
,这是Java中所有对象都可以成为锁对象,且可以重复获取锁的基础。monitorenter
和monitorexit
指令来标识获取到对象的monitor
和释放对象的monitor
,每次monitorenter
进入,锁重入次数加1,每次monitorexit
退出,锁重入次数减1。ACC_SYNCHRONIZED
访问标志,标识同步方法,并使用ACC_STATIC
区分该方法是否静态同步方法,即有ACC_SYNCHRONIZED
和ACC_STATIC
标识的是静态同步方法,只有ACC_SYNCHRONIZED
标识的是普通同步方法。静态同步方法会持有类对象的monitor
,而普通同步方法会持有实例对象的monitor
.锁会带来并发时的性能下降,Java8对synchd锁关键字做了锁升级的优化。
对象锁升级过程:
前25位unused,全0
后31位,存放哈希码,仅当有调用的时候才生成
1位unused
4位分代年龄,因此分代年龄最大只能是15,因为对象头中只有4个bit记录。
偏向锁标志位,无锁为0
锁标志位01.
当线程A第一次竞争到锁,通过操作修改MarkWord中的偏向锁线程ID、偏向锁标志位,使其从无锁状态升级为偏向锁。
如果不存在其他线程竞争,持有偏向锁的线程将永远不需要进行同步。
偏向锁的出现是为了解决在一个线程执行同步时,提高性能,线程的id被记录到对象头,如果id符合,则不需要从用户态切换到内核态去进行加锁解锁的过程。
由于偏向锁的维护成本比较高,Java15废除了偏向锁。最终就是,JDK 15 之前,偏向锁默认是 enabled,从 15 开始,默认就是 disabled,除非显示的通过 UseBiasedLocking 开启
轻量级锁:多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有线程阻寒,轻量级锁本质是自旋锁。
自旋锁自旋的有最大的自旋次数,如果自旋达到一定次数,会将自旋锁升级为重量级锁,自旋次数取决于自适应自旋锁机制。自适应自旋锁的大致原理:
JIT即时编译器(JIT compiler,just-in-time compiler)
下边的代码,每次new一个对象,对其加锁,实际上加锁完全无用,JIT会对它优化,发生锁消除。
锁粗化就是加大锁的锁定范围,如下边的代码,多个同步代码块相连,锁对象相同,JIT编译器会执行锁粗化,用一个更大范围的synchronized,代替多个同步代码块,避免多次加锁、释放锁。
reentrantlock默认是非公平锁,主要有以下两点考虑:
如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了。否则那就用公平锁,大家公平使用。
可重入锁的优点在于,可以在一定程度上避免死锁,一个加了锁的方法,如果递归调用自身,此时使用不可重入锁,就会自己把自己锁住。
举一个MySQL中,由于两个事务的间隙锁互相兼容,而插入意向锁与间隙互斥导致死锁的案例:
事务A和事务B相互等待对方释放锁,满足了死锁的四个条件:互斥、占有且等待、不可强占用、循环等待,因此发生了死锁。
命令行版:
图形工具版:
使用jconsole工具,也可以定位正在运行的Java进程的死锁情况。
即interrupt()仅是设置中断标识,并不实际中断线程,需要被调用的线程进行配合。
就像你让一个人闭嘴,但最终那人是否闭嘴,需要它的配合。被阻塞的状态
(例如sleep、wait、join等状态),在别的线程中调用当前线程对象的interrupt()方法,那么线程将立即退出被阻塞状态,中断状态标志位将被清除,并抛出一个InterruptedException异常。
从源码中可以看到,实例方法默认是不清除中断状态的,而静态方法则会。
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();
}
}
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(
);
}
}
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(
);
}
}
synchronized和reentrantlock的线程等待和线程唤醒有如下局限性:
基于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的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(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性
展开的。
CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg
。
执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就对总线加锁,只有一个线程能加速成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,所以在多线程情况下性能会比较好。
Unsafe是CAS核心类,由于Java无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe类相当于一个后门,其内部方法都是native修饰的,可以像C的指针一样直接操作内存。
Unsafe类的compareAndSwapObject,相对于Java中调用的函数,多个两个与内存操作相关的属性,var1和var2,var1表示要操作的对象,而var2表示操作对象属性地址的偏移量。
以unsafe类实现的原子类Int为例:
当compareAndSwap()执行失败时,会陷入循环,即自旋,仅当执行成功时,跳出循环。
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 块中,try中使用,finally中进行remove清除。
public class demo1 {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(10);
SaleData sd = new SaleData();
try {
for (int i = 0; i < 10; i++) {
threadPool.submit(() -> {
int r = Math.abs(new Random().nextInt() % 15);
for (int j = 0; j < r; ++j) {
sd.sale();
}
System.out.println(Thread.currentThread().getName() + "卖出:" + sd.saleNum.get());
});
}
} finally {
// 必须在用完后remove
sd.saleNum.remove();
threadPool.shutdown();
}
}
}
class SaleData {
ThreadLocal<Integer> saleNum = ThreadLocal.withInitial(() -> 0);
public void sale() {
saleNum.set(saleNum.get() + 1);
}
}
Thread中有一个ThreadLocalMap的成员,
ThreadLocalMap的每个Entry以ThreadLocal实例的弱引用为key,任务对象为value。
假设我们在一个方法中,设置一个threadlocal变量t1存入到Thread上,该变量是一个强引用,而在存放到ThreadlocalMap上的是一个弱引用。
public void func1() {
ThreadLocal<String> t1 = new ThreadLocal<>();
t1.set("1111");
}
为什么使用弱引用?
当func1执行完毕,栈帧销毁,那么强引用t1也就没有了。但是此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象,若这个key引用就是弱引用,就会导致key指向的threadlocal对象及指向的对象不能被gc回收,导致内存泄露。
而使用弱引用,每次gc时都会找到该弱引用对象,就不会发生内存泄露。
当gc发生后,存在ThreadlocalMap中的Entry的key会变为null,那么其对应的value就无法被访问,为解决该问题。threadlocal的get()、set()、remove方法,都会寻找key为null的脏Entry,然后对它进行删除。
对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据( Instance Data)和对齐填充(Padding) 。
对齐填充的目的是为了保证8个字节的倍数。
数组对象比Java对象在对象头会多个记录数组长度的字段。
对象头分为两部分:
在64位系统中,对象头中的对象标记(Mark Word)占用8个字节,类型指针占了8个字节,一共是16个字节。
Mark Word 默认存储对象的哈希code、分代年龄和锁标志位等信息,这些信息都是与对象自身定义无关的数据,所有MarkWord 被设计成一个非固定的数据结构,以便在极小的空间内存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说再运行期间MarkWord里的存储数据会随着的锁的标志位的变化而变化
类元信息在类加载的时候创建在元空间。(元空间在Java8以后的概念,用于存储类元信息、静态变量、常量池等)
每个实例对象都有指向它的类元信息的指针class pointer,虚拟机通过这个指针来确定这个对象是哪个类的实例。
AQS(AbstractQueuedSynchronizer,抽象队列同步器)整体就是一个抽象的FIFO队列,来完成资源获取线程的排队工作,并通过一个int类型的state表示持有锁的状态。
每次尝试持有锁,检查state是否为0,如果为0,则将state修改为1,表示持有了锁。
AQS是JUC的实现基石,JUC中的ReentrantLock、CountDownLatch、ReentrantReadWriteLock、Semaphore都是基于AQS实现的。
锁面向是锁的使用者,而AQS(抽象队列同步器)面向的是锁的设计者。
state变量是一个volatile修饰的int变量,如果为0,表示空闲,如果大于等于1,则表明锁被占用。
CLH队列:CLH是三个大牛名字缩写,队列本质是一种双端队列。
ReentrantLock中维护了Sync,FairSync,NonFairSync3个内部类。
调用Reentrant的lock方法时,实际上是调用sync的lock方法,sync进而调用fairSync或者NonFairSync的lock方法。
这里可以发现公平锁与非公平锁的第一个区别:
非公平锁: 调用lock时,首先就会执行一次CAS,如果失败,才会进入acquire方法 公平锁: 不会进行CAS抢占锁,直接进入acquire方法
具体tryAcquire方法交给子类来实现:
公平锁的tryAcquire()方法,在获取同步状态时多了一个限制条件:hasQueuedPredecessors(),判断等待队列中是否存在有效前驱节点,只有队列为空或者当前线程节点是AQS中的第一个节点,则返回false,代表后续可以去获取锁;否则其他情况都表示,已经有前驱节点,返回true代表后续不能获取锁!!