一、synchronized保证三大特性
synchronized保证原子性的原理
对num++;增加同步代码块后,保证同一时间只有一个线程操作num++;。就不会出现安全问题。
synchronized保证可见性的原理
synchronized保证可见性的原理,执行synchronized时,会对应lock原子操作会刷新工作内存中共享变 量的值。
synchronized保证有序性的原理
我们加synchronized后,依然会发生重排序,只不过我们有同步 代码块,可以保证只有一个线程执行同步代码中的代码。保证有序性。
二、synchronized的特性
可重入特性
意思就是一个线程可以多次执行synchronized,重复获取同一把锁。
/*
目标:演示synchronized可重入
1.自定义一个线程类
2.在线程类的run方法中使用嵌套的同步代码块
3.使用两个线程来执行
*/
public class Demo01 {
public static void main(String[] args) {
new MyThread().start();
new MyThread().start();
}
public static void test01() {
synchronized (MyThread.class) {
String name = Thread.currentThread().getName();
System.out.println(name + "进入了同步代码块2");
}
}
}
// 1.自定义一个线程类
class MyThread extends Thread {
@Override
public void run() {
synchronized (MyThread.class) {
System.out.println(getName() + "进入了同步代码块1");
Demo01.test01();
}
}
}
可重入原理
synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁.。在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁。可重入的好处
- 可以避免死锁
-
不可中断特性
什么是不可中断
一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第 二个线程会一直阻塞或等待,不可被中断。synchronized不可中断演示
public class Test {
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
// 1.定义一个Runnable
Runnable run = () -> {
// 2.在Runnable定义同步代码块
synchronized (obj) {
String name = Thread.currentThread().getName();
System.out.println(name + "进入同步代码块");
// 保证不退出同步代码块
try {
Thread.sleep(888888);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 3.先开启一个线程来执行同步代码块
Thread t1 = new Thread(run);
t1.start();
Thread.sleep(1000);
// 4.后开启一个线程来执行同步代码块(阻塞状态)
Thread t2 = new Thread(run);
t2.start();
// 5.停止第二个线程
System.out.println("停止线程前");
t2.interrupt();
System.out.println("停止线程后");
System.out.println(t1.getState());
System.out.println(t2.getState());
}
}
输出结果:Thread-0进入同步代码块 停止线程前 停止线程后 TIMED_WAITING BLOCKED
ReentrantLock可中断演示
public class Test {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
// test01();
test02();
}
// 演示Lock可中断
public static void test02() throws InterruptedException {
Runnable run = () -> {
String name = Thread.currentThread().getName();
boolean b = false;
try {
b = lock.tryLock(3, TimeUnit.SECONDS);
if (b) {
System.out.println(name + "获得锁,进入锁执行");
Thread.sleep(88888);
} else {
System.out.println(name + "在指定时间没有得到锁做其他操作");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (b) {
lock.unlock();
System.out.println(name + "释放锁");
}
}
};
Thread t1 = new Thread(run);
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(run);
t2.start();
System.out.println("停止t2线程前");
t2.interrupt();
System.out.println("停止t2线程后");
Thread.sleep(4000);
System.out.println(t1.getState());
System.out.println(t2.getState());
}
// 演示Lock不可中断
public static void test01() throws InterruptedException {
Runnable run = () -> {
String name = Thread.currentThread().getName();
try {
lock.lock();
System.out.println(name + "获得锁,进入锁执行");
Thread.sleep(88888);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(name + "释放锁");
}
};
Thread t1 = new Thread(run);
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(run);
t2.start();
System.out.println("停止t2线程前");
t2.interrupt();
System.out.println("停止t2线程后");
Thread.sleep(1000);
System.out.println(t1.getState());
System.out.println(t2.getState());
}
}
控制台输出:Thread-0获得锁,进入锁执行 停止t2线程前 停止t2线程后 java.lang.InterruptedException atjava.util.concurrent.locks.AbstractQueuedSynchronizer.tryAcquireNanos(AbstractQueuedSynchronizer.java:1245) at java.util.concurrent.locks.ReentrantLock.tryLock(ReentrantLock.java:442) at Test.lambda$test02$0(Test.java:24) at java.lang.Thread.run(Thread.java:748) TIMED_WAITING TERMINATED
三、synchronized简单原理
public class SyncTest {
public void syncBlock(){
synchronized (this){
System.out.println("hello block");
}
}
public synchronized void syncMethod(){
System.out.println("hello method");
}
}
使用javap对其进行反汇编,部分信息如下
{
public void syncBlock();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter // monitorenter指令进入同步块
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String hello block
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit // monitorexit指令退出同步块
14: goto 22
17: astore_2
18: aload_1
19: monitorexit // monitorexit指令退出同步块
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
public synchronized void syncMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED //添加了ACC_SYNCHRONIZED标记
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String hello method
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
synchronized修饰代码块时
monitorenter
每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获 取该monitor。 当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应 的monitor的所有权。其过程如下:
- 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为 monitor的owner(所有者)
- 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1
- 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直 到monitor的进入数变为0,才能重新尝试获取monitor的所有权。
monitorenter小结: synchronized的锁对象会关联一个monitor,这个monitor不是我们主动创建的,是JVM的线程执行到这个 同步代码块,发现锁对象没有monitor就会创建monitor,monitor内部有两个重要的成员变量owner:拥有 这把锁的线程,recursions会记录线程拥有锁的次数,当一个线程拥有monitor后其他线程只能等待
monitorexit
1.能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
- 执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出 monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个 monitor的所有权
monitorexit释放锁。 monitorexit插入在方法结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit。
总结:synchronized在修饰代码块时,是通过monitorenter 和 monitorexit来保证并发安全。
synchronized 修饰方法的的情况
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
不过两者的本质都是对对象监视器 monitor 的获取。
四、Lock Record
字面意思就是锁记录。通过对Java对象头的介绍可以看到锁信息也是存在于对象的mark word中的。当对象状态为偏向锁(biasable)时,mark word存储的是偏向的线程ID;当状态为轻量级锁(lightweight locked)时,mark word存储的是指向线程栈中Lock Record的指针;当状态为重量级锁(inflated)时,为指向堆中的monitor对象的指针。
Lock Record的结构
线程在执行同步块之前,JVM会先在当前的线程的栈帧中创建一个Lock Record,其包括一个用于存储对象头中的 mark word(官方称之为Displaced Mark Word)以及一个指向对象的指针。下图右边的部分就是一个Lock Record。