锁从客观上分为悲观锁和乐观锁。
CAS
操作实现的,java.util.concurrent.atomic
包下的原子变量。CAS(compare and swap)比较交换
是一种更新的原子操作,比较当前值和传入值是否一样,一样则更新,否则则失败。syschronized
,AQS
框架下的锁则是先尝试CAS
乐观锁获取锁,如果获取不到,才会转为悲观锁,如ReentrantLock
。在Java中主要有两种锁加锁机制:
syschronized
关键字修饰java.util.concurrent.Lock
,Lock是一个接口,有很多实现类比如ReentrantLock
。public class VolatileTest {
public static void main(String[] args) {
final VT vt = new VT();
Thread thread01 = new Thread(vt);
Thread thread02 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException ignore) { }
vt.sign = true;
System.out.println("vt.sign = true 通知 while (!sign) 结束!");
}
});
thread01.start();
thread02.start();
}
}
class VT implements Runnable {
public boolean sign = false;
@Override
public void run() {
while (!sign) {
}
System.out.println("你坏");
}
}
上面的代码是两个线程同时操作一个变量,程序希望当sign
在线程Thread01被操作vt.sign = true
时,线程Thread02输出你坏
。
实际上这段代码永远不会输出你坏
,而是一直处于死循环。这是为什么呢?接下来我们一步步讲解验证。
我们把sign
关键字加上volatile
关键字。
public volatile boolean sign = false;
这个时候会输出你坏
。
volatile
关键字是Java虚拟机提供的最轻量级锁的同步机制,作为一个修饰符出现,同来修饰变量,不含括局部变量,用来保证对所有线程可见性。
无volatile
关键字修饰时内存变化
当没有volatile
关键字修饰的时候,Thread01对变量进行操作,Thead02并不会拿到最新值。
有volatile
关键字时内存变化
当有volatile
关键字修饰的时候,Thread01对变量进行操作时,会把变量的变化强制刷新到主内存,Thread02获取值时,会把自己内存的sign值过期掉,从主内存读取最新的。
volatile
关键字底层是通过lock指令实现可见性的,lock指令相当于一个内存屏障,保证以下三点:
volatile
关键字会控制被修饰的变量在内存操作的时候会主动把值刷新到主内存,JMM会先将线程对应的CPU内存设置过期,从内存读取最新值。volatile
关键字是通过内存屏障防止指令重排,volatile
的内存屏障在读写的时候在前后各添加一个Store
屏障来保证重新排序时不会把内存屏障后面的时候指令排序到内存屏障之前。volatile
不能解决原子性,如果需要解决原子性需要synchronized
或者lock
。synchronized
关键字主要有以下三种使用方式:
修饰实例方法,作用于当前实例加锁,进入同步代码前要获取当前实例的锁。
public class SynchronizedTest implements Runnable{
private static int i = 0;
public synchronized void getI(){
if (i % 1000000 == 0) {
System.out.println(i);
}
}
public synchronized void increase() {
i++;
getI();
}
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
increase();
}
System.out.println(i);
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
SynchronizedTest synchronizedTest = new SynchronizedTest();
executorService.execute(synchronizedTest);
executorService.execute(synchronizedTest);
executorService.shutdown();
}
}
最后结果输出:
1000000
1556623
2000000
2000000
上述代码中,创建两个线程同时操作同一个共享资源i
,且increase()
、get()
方法加了synchronized
关键字,表示当前线程的锁是实例对象,因为传入线程都是synchronizedTest
对象实例是同一个,所以最终结果肯定能输出2000000
,如果我们换种方式,传入不同对象,代码如下:
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
SynchronizedTest synchronizedTest01 = new SynchronizedTest();
SynchronizedTest synchronizedTest02 = new SynchronizedTest();
executorService.execute(synchronizedTest01);
executorService.execute(synchronizedTest02);
executorService.shutdown();
}
输出如下:
1002588
1641267
1848269
最终肯定不是期望的200000
,因为synchronized
修饰方法锁的是当前实例,传入不同对象实例线程是无法保证安全的。
修饰静态方法,作用于当前类对象加锁,进入同步方法前要获取当前类对象的锁。
public class SynchronizedTest implements Runnable{
private static int i = 0;
public synchronized static void getI(){
if (i % 1000000 == 0) {
System.out.println(i);
}
}
public synchronized static void increase() {
i++;
getI();
}
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
increase();
}
System.out.println(i);
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
SynchronizedTest synchronizedTest01 = new SynchronizedTest();
SynchronizedTest synchronizedTest02 = new SynchronizedTest();
executorService.execute(synchronizedTest01);
executorService.execute(synchronizedTest02);
executorService.shutdown();
}
}
输出如下:
1000000
1649530
2000000
2000000
上述代码和第一段代码差不多,只不过increase()
、get()
方法是静态方法,且也加上了synchronized
表示锁的是当前类对象,虽然我们传入不同的对象,但是最终结果是会输出200000
的。
修饰语代码块,指定加锁对象,给对象加锁,进入同步方法前要获取给定对象的锁。
public class SynchronizedTest02 implements Runnable{
private static SynchronizedTest02 synchronizedTest02 = new SynchronizedTest02();
private static int i = 0;
@Override
public void run() {
// 传入对象锁当前实例对象
// 如果是 synchronized (SynchronizedTest02.class) 锁当前类对象
synchronized (synchronizedTest02){
for(int j=0;j<1000000;j++){
i++;
}
}
}
public static void main(String[] args) throws Exception {
Thread thread01 = new Thread(synchronizedTest02);
Thread thread02 = new Thread(synchronizedTest02);
thread01.start();
thread02.start();
Thread.sleep(3000);
System.out.println(i);
}
}
上述代码用锁修饰代码块,传入的是对象表示锁的是当前实例对象,如果传入是类表示锁的是类对象。
原子性表示一个操作不可中断,要么成功要么失败。
synchroniezd
能实现方法同步,同一时间段内只有一个线程能拿到锁,进入到代码执行,从而达到原子性。
底层通过执行mointorenter
指令,判断是否有ACC_SYNCHRONIZED
同步标识,有表示获取monitor
锁,此时计数器+1,方法执行完毕,执行mointorexit
指定,此时计数器-1,归0释放锁。
可见性表示一个线程修改了一个共享变量的值,其它线程都能够知道这个修改。CPU缓存优化
、指令重排
等都可能导致共享变量修不能立刻被其他线程察觉。
synchroniezd
通过操作系统内核互斥锁实现可见性,线程释放锁前必须把共享变量的最新值刷新到主内存中,线程获取锁之前会将工作内存中共享值清空,从主内存中获取最新的值。
程序在执行时,有可能会进行指令重排,CPU执行指令顺序不一定和程序的顺序一致。指定重排保证串行语义一致
(即重排后CPU执行的执行和程序真正执行顺序一致)。synchronized
能保证CPU执行指令顺序和程序的顺序一致。
public class LazySingleton {
/**
* 单例对象
* volatile + 双重检测机制 -> 禁止重排序
*/
private volatile static LazySingleton instance = null;
/**
* instance = new LazySingleton();
* 1. 分配对象内存空间
* 2. 初始化对象
* 3. 设置instance指向刚分配的内存
*
* JVM和CPU优化, 发生了指令重排, 1-3-2, 线程A执行完3, 线程B执行第一个判断, 直接返回, 这个时候是 * 有问题的。
* 通过volatile关键字禁止重排序
* @return
*/
public static LazySingleton getInstance(){
if (null == instance) {
synchronized (LazySingleton.class){
if (null == instance) {
instance = new LazySingleton();
}
}
}
return instance;
}
}
synchronized
的有序性是保证线程有序的执行,不是防止指令重排序。上面代码如果不加volatile
关键字可能导致的结果,就是第一个线程在初始化的时候,设置instance执行分配的内存时,这个时候第二个线程进来了,有指令重排,在第一个判断的时候直接返回,就出错了这个时候instance可能还没初始化成功。
synchronized
是可重入锁,允许一个线程二次请求自己持有对象锁的临界资源。
public class SynchronizedTest03 extends A {
public static void main(String[] args) {
SynchronizedTest03 synchronizedTest03 = new SynchronizedTest03();
synchronizedTest03.doA();
}
public synchronized void doA() {
System.out.println("子类方法:SynchronizedTest03.doA() ThreadId:" + Thread.currentThread().getId());
doB();
}
public synchronized void doB() {
System.out.println("子类方法:SynchronizedTest03.doB() ThreadId:" + Thread.currentThread().getId());
super.doA();
}
}
class A {
public synchronized void doA() {
System.out.println("父类方法:A.doA() ThreadId:" + Thread.currentThread().getId());
}
}
上面代码正常输入如下:
子类方法:SynchronizedTest03.doA() ThreadId:1
子类方法:SynchronizedTest03.doB() ThreadId:1
父类方法:A.doA() ThreadId:1
最后正常的输出了结果,并没有发生死锁,说明synchronized
是可重入锁。
synchronized
锁对象的时候有个计数器,记录线程获取锁的次数,在执行完对应的代码后计数器就会-1,知道计数器清0释放锁。
在介绍锁的类型之前先说一下什么是markword
,markword
是java对象数据结构中的一部分,markword
数据在长度为32位和64位虚拟机(未开启压缩指针)中分别是32bit和64bit,它的最后两位bit是锁状态标志位,用来标记当前对象的状态,如下表示:
状态 | 标志位 | 储存内容 |
---|---|---|
无锁(未开启偏向锁) | 01 | 对象哈希码、对象分代年龄 |
偏向锁(开启偏向锁) | 01 | 偏向线程id、偏向时间戳、对象分代年龄 |
轻量级锁 | 00 | 指向轻量级锁指针 |
重量级锁 | 10 | 指向重量级锁指针 |
GC标记 | 11 | 空 |
偏向锁会偏向于第一个访问锁的线程,如果在运行过程中只有一个线程访问不存在多个线程争用的情况下,则线程是不需要触发同步的,这个时候就会给线程加一个偏向锁。如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁升级至轻量级锁。
UseBiasedLocking
是一个偏向锁检查, 1.6 之后是默认开启的, 1.5 中是关闭的,需要手动开启参数是 XX: UseBiasedLocking=false
。
偏向锁获取过程:
当锁是偏向锁的时候,在运行过程中发现有其他线程抢占锁,偏向锁就会升级成轻量级锁,其他线程会通过自旋的形式获取锁,不会阻塞,提高性能,缺点是循环会消耗CPU。
轻量级锁加锁过程:
Displaced Mark Word
。当偏向锁升级成轻量级锁时,其他线程会通过自旋的方式获取锁,不会阻塞,如果自旋n次都失败了,这个时候轻量级锁就会升级成重量级锁。
synchronized的执行过程: