一、Java对象与锁
1、对象结构
2、对象头的 Mark Word
二、锁介绍
1、概念和种类
1、乐观锁
不加锁,在使用数据时判断数据是不是最新。常用CAS算法实现
2、自旋锁 与 适应性自旋锁
两者并不是锁,而是锁提供的处理方式。
自旋锁(JDK1.4):
阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。
如果代码的逻辑较简单,那让抢不到锁的线程不放弃CPU的执行时间,选择“稍等一下”,比线程阻塞和切换更加节约时间。
适应性自旋锁(JDK6):
自旋的时间(次数)不再固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
如果上次自旋成功获得锁,那虚拟机将允许自旋等待持续相对更长的时间。
如果对于某个锁,自旋很少成功获,那以后尝试获取这个锁时将可能直接阻塞线程。
3、无锁、偏向锁、轻量级锁、重量级锁
针对synchronized锁的
1.无锁、偏向锁
当无线程竞争时,为了减少资源开销,对资源不加锁,或者偏向某频繁访问的线程,两者的标识位都是'01'。
无锁没有对资源进行锁定,当竞争升级时锁会升级为偏向锁。
偏向锁
一个锁被某个线程访问频繁,那么该线程会自动获取锁,通过CAS替换MarkWord中的偏向锁标记和锁标记,并设置线程 ID。
2.轻量级锁
如果竞争再激烈些,偏向锁会升级为轻量级锁,其他线程会通过自旋的形式(while(...))尝试获取锁,不阻塞。
轻量级锁加锁:
1.在线程进入同步块时,如果同步对象锁状态为无锁状态(偏向锁为0,锁标志位为01),虚拟机先将在当前线程的栈帧中创建一个锁记录(Lock Record)空间,用于存储锁对象目前的 Mark Word 拷贝,
2.复制对象头中的 Mark Word 到锁记录(Lock Record)中.
3.复制完后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向LockRecord的指针,并将 Lock Record 里的 owner 指针指向对象头的 mark word.
4.如果1-3成功了,那么这个线程就拥有了该对象的锁,并将对象MarkWord的锁标记设置为 00,即表示此对象处于轻量级锁定状态.
5.如果1-3失败了,虚拟机首先会检查对象的Mark Word 是否指向当前线程的栈帧,不是则有多个线程竞争锁。进行自旋,等待占用锁的线程释放锁,通过 CAS 后即可立即获取锁,否则轻量级锁就要升级为重量。
轻量级锁释放:
轻量级锁释放锁(解锁)时,会使用 CAS 将之前复制在栈桢中的Displaced Mard Word替换回 Mark Word 中。
3.重量级锁
升级为重量级锁时,锁标志的状态值变为”10“,锁对象与一个monitor关联 mark word 指向 拥有锁线程的 monitor 的指针。
提供了排它互斥访问机制,保证在每个时间点上最多只有一个线程会执行同步方法。等待锁的线程都会进入阻塞状态。
阻塞或者唤醒一个线程时,要操作系统来调度,用户态转换到内核态比较耗时。
重量级锁加锁
重量级锁是通过对象内部的监视器(monitor)来实现,也就是通过ObjectMonitor 实现,ObjectMonitor.hpp 有几个重要属性:
_Owner:保存当前持有锁的线程;
_EntryList:等待获取锁的线程;
_WaitSet:调用 Object 的 wait()方法等待时,此时将该等待的线程保存到_WaitSet中;
_cxq:存储没有获取到锁的线程;
_recursions:记录重入次数;
当多个线程同时访问某段同步代码时:
1、首先会进入_EntryList 集合;
2、当线程获取到对象的 monitor 之后,就会进入_Owner 区域,并把ObjectMonitor对象的_Owner 指向为当前线程,并将_count + 1;
3、如果调用了释放锁(比如 wait()方法)操作,就会释放当前持有的monitor,即_owner= null,_count - 1,同时这个线程会进入到_WaitSet 列表中等待被唤醒;
4、如果当前线程执行完毕,则释放 monitor 锁,复位_count 的值(_count-1),不过此时不会进入_WaitSet 列表;
重量级锁释放
锁释放是同步代码块执行结束后触发,ObjectMonitor::exit;
1、ObjectMonitor 中持有的锁的_owner 对象置为 null;
2、从_cxq 队列中唤醒一个被挂起的线程: 根据 QMode 模式判断是从_cxq 还是 EntryList 中获取头节点的线程进行唤醒,通过ObjectMonitor::ExitEpilog 方法唤醒该节点封装的线程,唤醒操作最终由unpark完成:_cxq(竞争列表)EntryList(锁候选者列表)
3、被唤醒的线程重新竞争重量级锁,被唤醒的线程不一定能抢到锁,未抢到锁的线程将继续挂起,synchronized 是非公平锁;
4、公平锁 与 非公平锁
1.公平锁
多个线程按照申请锁的顺序入队列中排队来获取锁。
每个线程都会获得执行;
除了第一个线程外都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大,效率低,
2.非公平锁
多个线程加锁时直接尝试获取锁,获取不到去队尾等待。
可以减少唤起线程的开销,整体的吞吐效率高。
有的线程线程可能等很久才会获得锁,或者根本获取不到。
5、可重入锁(递归锁)
同一个线程对同一把锁在不释放的前提下,反复加锁不会导致线程的卡死,要保证 unlock() 的次数和 lock()一样的多。
当 state== 0 时锁是空闲的,大于零表示锁已经被占用,它的数值表示当前线程重复占用这个锁的次数。
线程在获取了锁之后,再次去获取了同一个锁,仅是把状态值累加。线程释放了一次锁,仅把状态值减了,线程 A 把此锁全部释放了,状态值减到 0 了,其他线程才有机会获取锁。
ReentrantLock 和 synchronized都是可重入锁,重入锁可一定程度避免死锁。
6、独享锁 与 共享锁
独享锁与共享锁是通过AQS来实现的
1.独享锁
也叫排他锁,该锁一次只能被一个线程所持有。
如果线程T对数据A加上排它锁后,其他线程不能再对A加任何类型的锁。
获得独享锁的线程可读、可改数据。
2.共享锁
可被多个线程所持有。
如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。
获得共享锁的线程只能读数据,不能修改数据。
2、CAS机制 与 AQS机制
1、CAS机制
CAS 全称是 Compare And Swap(比较再交换)
CAS算法有三个操作数,通过内存中的值(V)、预期原始值(A)、修改后的新值。
比较 A 与 V 是否相等
如果比较相等,将 B 写入 V
CAS只能保证变量的原子性,不能保证变量的内存可见性。CAS获取共享变量的值时,需要和volatile配合使用,来保证共享变量的可见性,否则会出现 ABA问题。
2、AQS机制
抽象的队列同步器。
AQS用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
CLH(Craig,Landin,and Hagersten)
虚拟的双向队列(即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
AQS 定义了两种资源共享方式 :独占式 (Exclusive)和共享式(Share)
独占式:只有一个线程能执行,具体的 Java 实现有 ReentrantLock。
共享式:多个线程可同时执行,具体的 Java 实现有 Semaphore和CountDownLatch。
三、锁的使用
1、synchronized
1、加锁
1、作用在方法上
修饰非静态方法(普通方法)
修饰静态方法
2、作用在代码块上(更细,更灵活)
synchronized(this|object) {...}
synchronized(类.class) {...}
2、释放锁
1、占有锁的线程执行完了该代码块,然后释放对锁的占有。
2、占有锁线程执行发生异常,此时JVM会让线程自动释放锁。
3、占有锁线程进入 WAITING 状态从而释放锁,例如在该线程中调用 wait() 方法等。
3、锁的范围
对象锁
1、两个线程同时访问同一个对象的同步方法,会互斥;
2、两个线程同时访问同一个对象的同步方法和非同步方法,不互斥;
3、两个线程同时访问两个对象的同步方法,不互斥;
4、两个线程同时访问两个对象的同步方法和非同步方法,不互斥;
类锁
1、两个线程同时访问同一个类的静态同步方法,会互斥;
2、两个线程同时访问不同类的静态同步方法,不互斥;
3、两个线程同时访问同一个类的静态同步方法和静态非同步方法,不互斥;
4、两个线程同时访问同一个类的静态同步方法和非静态同步方法,不互斥;
2、Lock
支持尝试获锁、超时获锁、中断获锁、公平锁等操作。
ReenTrantLock特点
1.独占锁
2.可重入锁。
3.支持公平和非公平,默认非公平锁
4.锁的过程是可中断的
5.可设置超时时间
6.支持多个条件变量Condition,即支持多个不同的等待队列,而synchronized只支持一个。
void lock(); 加锁,如果锁已经被别人占用了,就无限等待
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException; 尝试获取锁,等待timeout时间。同时,可以响应中断
void unlock(); 释放锁
boolean tryLock(); 不会进行任何等待,如果能够获得锁,直接返回true,如果获取失败,就返回false
lockInterruptibly(); 可以响应中断,lock方法会阻塞线程直到获取到锁
Condition newCondition(); 获取与lock绑定的通信组件。
Lock lock=new ReentrantLock(false); // 默认是false,设置为true则为公平锁。
try{
// 加锁
lock.lock
逻辑代码
}finally{
// 解锁
lock.unlock
}
3、volatile
1.关于内存和代码执行的概念
1.happens-before
(1).如果 A happens-before B,那 A 的执行结果将对 B 可见,且 A 的执行顺序排在 B 前。
(2).并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
2.指令重排序
在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。
一般分三种
编译器优化的重排序: 编译器在不改变单线程程序语义的前提下,重新安排语句的执行顺序;
指令级并行的重排序: 现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
内存系统的重排序: 由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。
3.as-if-serial
不管怎么重排序,单线程下的执行结果不能被改变。
编译器、runtime和处理器都必须遵守as-if-serial语义。
4.内存屏障
java编译器在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。
2.volatile特性
1.提供可见性,任何一个线程对其的修改将立马对其他线程可见,volatile属性不会被线程缓存,始终从主存中读取。
2.修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可立即得到修改后的值。
3.属性的读写操作都是无锁的,不能替代synchronized,没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上。
4.只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序。
5.提供了happens-before保证,对volatile变量v的写入happens-before所有其他线程后续对v的读操作。
6.可以在单例双重检查中实现可见性和禁止指令重排序,保证安全性。
7.可以使得long和double的赋值是原子的。
3、JUC 的 tools(工具类)
1、CountDownLatch(闭锁)
同步辅助类,在完成一组正在其他线程中执行的操作之前,它支持一个或多个线程一直等待
CountDownLatch(int count):count为计数器的初始值(对应的线程数)。
countDown(): 每调用一次计数器值-1,直到count被减为0,代表所有线程全部执行完毕。
getCount():获取当前计数器的值。
await(): 等待计数器变为0,即等待所有异步线程执行完毕。
boolean await(long timeout, TimeUnit unit): 此方法与await()区别:
1、此方法至多会等待指定的时间,超时后会自动唤醒,若 timeout 小于等于零,则不会等待
2、boolean 类型返回值:若计数器变为零了,则返回 true;若指定的等待时间过去了,则返回 false
使用案例
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class Test{
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(10);
for (int i=0; i<=10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 运行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
}
}).start();
}
System.out.println("等待子线程运行结束");
latch.await();
System.out.println("子线程运行结束");
}
}
2、CyclicBarrier(栅栏)
同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点 ,并且在释放等待线程后可以重用。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
// 声明一个加法计数器,并指定触发的条件线程
CyclicBarrier cyclicBarrier = new CyclicBarrier(10, () -> System.out.println("都已执行完."));
for (int i = 0; i < 10 ; i++) {
int atmp = i;
new Thread(() -> {
System.out.println("A-----执行完: "+ atmp);
try {
// 阻塞本线程,等待其他线程
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
for (int i = 0; i < 10 ; i++) {
int btmp = i;
new Thread(() -> {
System.out.println("B-----执行完: "+ btmp);
try {
// 阻塞本线程,等待其他线程
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
3、Semaphore(信号量)
计数信号量,它的本质是一个“共享锁“。维护了一个信号量许可集。可通过调用 acquire()来获取信号量的许可;
当信号量中无可用的许可时须等待,直到有可用的许可为止。线程可以通过release()来释放它所持有的信号量许可。
用于多个线程对多个共享资源的互斥使用,另一个是用于并发线程数的控制(限流)。
import java.util.concurrent.Semaphore;
public class SemaphoreTest {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 线程启动,,,,,,");
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
System.out.println(Thread.currentThread().getName() + " 线程结束。。。");
}
}).start();
}
}
}