一. synchronized是什么?
synchronized是Java的一个关键字,synchronized依赖于JVM具体实现。
1.1 synchronized的作用
synchronized也称为同步锁。synchronized是一种独占锁。
synchronized的两大功能:
-
内存可见性
-
操作原子性
什么是内存可见性?
可见性
是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
可参考《
Java内存分级和指令重排序》
。volatile也可实现内存可见性。
什么是操作原子性?
所谓
原子操作
是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何上下文切换
(context switch: 换到另一个线程)。
原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱
(禁止重排序)
,也不可以被切割而只执行其中的一部分。
将整个操作视作一个整体是原子性的核心特征。
1.2 synchronized什么时候释放锁?
出现异常或代码块执行结束,自动释放锁。
因为异常导致线程结束,同步锁释放,引起等待线程执行了,
发生程序乱入
,出现意想不到的效果。
/**
* 测试synchronized出现异常释放锁
*
* @author zhouronghua
* @time 2021/6/28 5:03 下午
*/
public class TestSyncException {
/**
* 计算器
*/
private int count = 0;
/**
* 测试synchronized异常
*
* @author zhouronghua
* @time 2021/6/28 5:18 下午
*/
private synchronized void test() {
System.out.println(Thread.currentThread().getName() + " test start...");
count++;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 5) {
// 产生一个Exception
System.out.println(Thread.currentThread().getName() + "测试异常");
int i = 5 / 0;
}
System.out.println(Thread.currentThread().getName() + " test end");
}
public static void main(String[] args) {
TestSyncException testSyncThread = new TestSyncException();
for (int i = 0; i < 10; i++) {
new Thread(testSyncThread::test, "thread " + (i + 1)).start();
}
}
}
当count=5发生异常,当前线程结束,释放锁。后续等待队列的线程, 检测到同步锁释放,
开始执行。
thread 1 test start...
thread 1 test end
thread 10 test start...
thread 10 test end
thread 9 test start...
thread 9 test end
thread 8 test start...
thread 8 test end
thread 7 test start...
thread 7测试异常
thread 6 test start...
Exception in thread "thread 7" java.lang.ArithmeticException: / by zero
at com.joe.helloapp.thread.TestSyncException.test(TestSyncException.java:32)
at java.lang.Thread.run(Thread.java:748)
thread 6 test end
thread 5 test start...
thread 5 test end
thread 4 test start...
thread 4 test end
thread 3 test start...
thread 3 test end
thread 2 test start...
thread 2 test end
1.3 synchronized实现
字节码层级,synchronized是通过monitorenter和monitorexit指令实现的.
例如单例模式获取单例对象,
其中同步代码块,对应的字节码:
MONITORENTER
L0
LINENUMBER 18 L0
GETSTATIC com/joe/helloapp/Sington.sInstance : Lcom/joe/helloapp/Sington;
IFNONNULL L7
L8
LINENUMBER 20 L8
NEW com/joe/helloapp/Sington
DUP
INVOKESPECIAL com/joe/helloapp/Sington.<init> ()V
PUTSTATIC com/joe/helloapp/Sington.sInstance : Lcom/joe/helloapp/Sington;
L7
LINENUMBER 22 L7
FRAME APPEND [java/lang/Object]
ALOAD 0
MONITOREXIT
根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1;相应地,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。
synchronized锁定的是对象
,而不是代码块。
1.4 synchronized是可重入锁
可重入锁是指同一线程可以多次获得同一个锁。
若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的
。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,
可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的
。
同一线程在调用自己类中其他synchronized方法/块或调用父类的synchronized方法/块都不会阻碍该线程的执行,就是说
同一线程对同一个对象锁是可重入的
。
验证
package com.joe.helloapp.thread;
/**
* 测试synchronized可重入锁
*
* @author zhouronghua
* @time 2021/6/28 11:27 上午
*/
public class TestSyncReentrantLock {
/**
* 线程任务
* 说明:此处synchronized,锁的对象是this对象
*
* @author zhouronghua
* @time 2021/6/25 9:05 上午
*/
public synchronized void fun1() {
System.out.println(Thread.currentThread().getName() + " fun1 start...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 调用synchronized方法。因为synchronized是可重入锁,
* 同一线程再次访问synchronized方法,可重入,执行fun2。
*/
fun2();
System.out.println(Thread.currentThread().getName() + " fun1 end");
}
/**
* 线程任务2
*
* @author zhouronghua
* @time 2021/6/25 9:05 上午
*/
public synchronized void fun2() {
System.out.println(Thread.currentThread().getName() + " fun2 start...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " fun2 end");
}
/**
* main入口
*
* @param args 参数列表
* @author zhouronghua
* @time 2021/6/28 11:28 上午
*/
public static void main(String[] args) {
TestSyncReentrantLock testSyncReentrantLock = new TestSyncReentrantLock();
new Thread(testSyncReentrantLock::fun1, "thread 1").start();
}
}
1)进入fun1,synchronized对this加锁,对应的线程为thread1;
2)fun1中调用synchronized方法fun2,需要对synchronized对this加锁,此时fun1中已经对this加锁了,且此时访问的线程为thread1,是同一线程,满足可重入;
3)fun2执行完成,回到fun1继续执行
thread 1 fun1 start...
thread 1 fun2 start...
thread 1 fun2 end
thread 1 fun1 end
二. synchronized使用
1. synchronized修饰方法
synchronized修饰方法相当于对整个方法synchronized(this)处理,相当于this对象加锁。
测试多线程访问synchronized方法
/**
* 测试线程同步
*
* @author zhouronghua
* @time 2021/6/25 8:57 上午
*/
public class TestSyncThread {
/**
* 线程任务
* 说明:此处synchronized,锁的对象是this对象
*
* @author zhouronghua
* @time 2021/6/25 9:05 上午
*/
public synchronized void fun1() {
System.out.println(Thread.currentThread().getName() + " fun1 start...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " fun1 end");
}
/**
* 测试多线程访问同一个方法
*
* @author zhouronghua
* @time 2021/6/25 6:15 下午
*/
private static void testSyncLockMethod() {
TestSyncThread testSyncThread = new TestSyncThread();
for (int i = 0; i < 10; i++) {
new Thread(testSyncThread::fun1, "thread " + (i + 1)).start();
}
}
public static void main(String[] args) {
testSyncLockMethod();
}
}
1)线程1执行fun1;
2)线程2执行fun1,检测到已经加锁,进入任务队列等待;线程3执行fun1,检测已经加锁,如在等待队列队首;线程4~线程10也如此。
3)线程1执行完,释放锁,开始执行任务对列表中的等待任务线程10;
线程10执行完成,执行等待任务线程9;。。。
thread 1 fun1 start...
thread 1 fun1 end
thread 10 fun1 start...
thread 10 fun1 end
thread 9 fun1 start...
thread 9 fun1 end
thread 8 fun1 start...
thread 8 fun1 end
thread 7 fun1 start...
thread 7 fun1 end
thread 6 fun1 start...
thread 6 fun1 end
thread 5 fun1 start...
thread 5 fun1 end
thread 4 fun1 start...
thread 4 fun1 end
thread 3 fun1 start...
thread 3 fun1 end
thread 2 fun1 start...
thread 2 fun1 end
2. synchronized修饰静态方法
synchronized修饰方法相当于对整个方法synchronized(T.class)处理,相当于类的class对象加锁。
因为静态方法是锁定的class对象,
/**
* 测试synchronized静态方法
* 说明: synchronized静态方法相当于对使用到TestSyncStaticThread.class对象同步
*
* @author zhouronghua
* @time 2021/6/28 2:08 下午
*/
public class TestSyncStaticThread implements Runnable {
/**
* 测试多线程访问同一个synchronized静态方法
*
* @author zhouronghua
* @time 2021/6/28 2:05 下午
*/
private static void testStaticSyncLockMethod() {
for (int i = 0; i < 10; i++) {
new Thread(new TestSyncStaticThread(), "thread " + (i + 1)).start();
}
}
@Override
public void run() {
fun2();
}
/**
* 线程任务2
*
* @author zhouronghua
* @time 2021/6/25 9:05 上午
*/
public synchronized static void fun2() {
System.out.println(Thread.currentThread().getName() + " fun2 start...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " fun2 end");
}
public static void main(String[] args) {
testStaticSyncLockMethod();
}
}
虽然线程使用的是不同的TestSyncStaticThread对象,静态同步方法锁定的是TestSyncStaticThread.class对象,而TestSyncStaticThread.class对象是唯一的。因此这些线程需要同步执行。
thread 1 fun2 start...
thread 1 fun2 end
thread 10 fun2 start...
thread 10 fun2 end
thread 9 fun2 start...
thread 9 fun2 end
thread 8 fun2 start...
thread 8 fun2 end
thread 7 fun2 start...
thread 7 fun2 end
thread 6 fun2 start...
thread 6 fun2 end
thread 5 fun2 start...
thread 5 fun2 end
thread 4 fun2 start...
thread 4 fun2 end
thread 3 fun2 start...
thread 3 fun2 end
thread 2 fun2 start...
thread 2 fun2 end
3. synchronized锁定代码块
对代码块部分进行加锁处理。
三. synchronized锁定对象
synchronized选取的锁定对象,不能是String常量,Long和Integer。
四. synchronized锁升级过程
偏向锁–》自旋锁–》重量级锁
什么时候偏向使用自旋锁
自旋锁:通过CAS处理,占用CPU时间。线程数比较少的时候,操作时间端。
重量级锁:通过等待队列。线程比较多的时候,操作消耗时间长,使用重量级锁。
参考文献: