文章目录
一、线程不安全的例子
我们先来看一个线程不安全的例子:
使用多线程来求10个10000的和,这个肯定结果应该是10*10000 =100000吧,接下来我们来验证一下:
实体类:
DataEntity.java
package com.atguigu.gulimall.providerconsumer.test.synchronizedTest;
/**
* 线程不安全的类,其中的方法都是线程不安全的。
* @author: jd
* @create: 2024-07-24
*/
public class DataEntity {
private int totalCount =0;
public void addCount(){ //synchronized
this.totalCount++;
}
public int getTotalCount(){
return totalCount;
}
}
多线程类 ThreadPoolTest.java
package com.atguigu.gulimall.providerconsumer.test.synchronizedTest;
import com.atguigu.gulimall.providerconsumer.test.RejectThreadPoolDemo;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author: jd
* @create: 2024-07-24
*/
public class ThreadPoolTest {
//线程池
static final ThreadPoolExecutor executor = new ThreadPoolExecutor(9, 16, 60L,
TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), Executors.defaultThreadFactory());
public static void main(String[] args) {
DataEntity dataEntity = new DataEntity();
//向线程池中提交10个任务,
for (int i = 1; i <= 10; i++) {
System.out.println("===>提交完第" + i + "任务,线程名称:"+Thread.currentThread().getName());
executor.execute(() -> handle(dataEntity));
}
while (executor.getTaskCount()!= executor.getCompletedTaskCount()){
}
System.out.println("====>总数量:"+dataEntity.getTotalCount());
}
private static void handle(DataEntity dataEntity) {
for (int j = 0; j < 10000; j++) {
dataEntity.addCount();
}
System.out.println("当前执行完的线程名称 Thread.currentThread().getName() = " + Thread.currentThread().getName()+"当前结果"+dataEntity.getTotalCount());
}
}
验证结果: 可以看到不是我们期待的结果,而且每次我运行多线程求和的结果还不一致,这就说明现在的求和操作是线程不安全的。
为什么会出现这个结果呢?
简单的说,这是主内存和线程的工作内存数据不一致,以及多线程执行时无序,共同造成的结果!
我们先简单的了解一下 Java 的内存模型,后期我们在介绍里面的原理!
如上图所示,线程 A 和线程 B 之间,如果要完成数据通信的话,需要经历以下几个步骤:
- 1.线程 A 从主内存中将共享变量读入线程 A 的工作内存后并进行操作,之后将数据重新写回到主内存中;
- 2.线程 B 从主存中读取最新的共享变量,然后存入自己的工作内存中,再进行操作,数据操作完之后再重新写入到主内存中;
如果线程 A 更新后数据并没有及时写回到主存,而此时线程 B 从主内存中读到的数据,可能就是过期的数据,于是就会出现“脏读”现象。
因此在多线程环境下,如果不进行一定干预处理,可能就会出现像上文介绍的那样,采用多线程编程时,程序的实际运行结果与预期会不一致,就会产生非常严重的问题。
那我们如何使之线程安全呢 ?因为我现在写的是单体应用,涉及不到分布式,所以可以使用 synchronized 同步锁来实现。
代码改造:
package com.atguigu.gulimall.providerconsumer.test.synchronizedTest;
/**
* 线程不安全的类,其中的方法都是线程不安全的。
* @author: jd
* @create: 2024-07-24
*/
public class DataEntity {
private int totalCount =0;
synchronized public void addCount(){ //这里添加同步锁
this.totalCount++;
}
public int getTotalCount(){
return totalCount;
}
}
运行结果: 结果和我们预想的是一样的,是10万
无论运行多少次,结果都一样正确。对比上面的结果,也可以发现,线程按顺利访问addCount方法,按照代码的先后顺序执行,一个线程执行完addCount方法后,另一个线程才接着执行addCount方法后,这样可以保证多个线程不会同时访问同一个共享数据。因而最终结果一致。
接下来介绍一下synchronized的原理、使用场景、方式
二、synchronized的原理
1、原理概述
synchronized是Java的一个关键字。来自官方的解释:Synchronized方法支持一种简单的策略,用于防止线程干扰和内存一致性错误:如果一个对象对多个线程可见,则对该对象变量的所有读或写操作都通过Synchronized方法完成。
Synchronized保证同一时刻有且只有一条线程在操作共享数据,其他线程必须等待该线程处理完数据后再对共享数据进行操作。此时便产生了互斥锁,互斥锁的特性如下:
互斥性:即在同一时刻只允许一个线程持有某个对象锁,通过这种特性来实现多线程协调机制,这样在同一时刻只有一个线程对所需要的同步的代码块(复合操作)进行访问。互斥性也成为了操作的原子性。
可见性:必须确保在锁释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程可见(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起数据不一致。
Synchronized是最基本的互斥手段,保证同一时刻最多只有1个线程执行被Synchronized修饰的方法 / 代码,其他线程 必须等待当前线程执行完该方法 / 代码块后才能执行该方法 / 代码块。
2、 同步代码块的实现原理
Synchronized是由JVM实现的一种实现互斥同步的一种方式,Synchronized同步代码块时,如果查看编译后的字节码,会发现,被Synchronized修饰过的程序块,在编译前后被编译器生成了monitorenter和monitorexit两个字节码指令,其中,monitorexit指令出现了两次。monitorenter指向同步代码块的开始位置,monitorexit指明同步代码块的结束位置。如图:
先看monitorenter指令。每个对象都是一个监视器锁(monitor)(不加 synchronized
的对象不会关联监视器),在虛拟机执行到monitorenter指令时,首先要尝试获取对象的锁,获取monitor的所有权:(1)如果monitor的进入数为0,表示这个对象没有被锁定,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
(2)如果线程已经占有该monitor,说明当前线程已经拥有了这个对象的锁,只是重新进入,则进入monitor的进入数加1;
(3)如果其他线程已经占用了monitor,则获取monitor的所有权失败,该线程进入阻塞状态等待,直到monitor的进入数为0,再重新尝试获取monitor的所有权;执行monitorexit指令的线程必须是对象锁所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者锁,就被释放了其他被这个monitor阻塞的线程可以尝试去获取这个
monitor 的所有权。
monitorexit指令出现了两次,第1次为同步正常退出释放锁,第2次为发生异步退出释放锁,也就是说获得锁的线程可以通过正常控制路径退出,或者在同步代码块中抛出异常来释放锁。
3、同步方法的原理
当Synchronize同步一个方法(既可以是普通方法,也可以是静态方法)时,通过反编译查看,被Synchronized修饰过的方法,在编译后这里面并没monitorenter和monitorexit,相对于普通方法,其常量池中多了
ACC_SYNCHRONIZED 标示符,如下图所示:
这其实是一种隐式的方式,JVM就是根据 “ACC_SYNCHRONIZED” 标示符来实现方法的同步的。
“ACC_SYNCHRONIZED”标志用来区分一个方法是否是同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED是否被设置,如果被设置,当前线程将会获取monitor,获取成功后才执行方法体,最后不管方法是否正常完成都会释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
三、synchronized的使用
1、同步代码块
使用Synchronize修饰代码块时,其作用域是被修饰的整个代码块里面的内容,作用对象是括号中的对象,这个作用对象可以是类,也可以是指定的对象。
1.1 作用对象是类的时候,作用的是类及该类的所有对象
实体类,SynchronizedUsage.class对象
package com.atguigu.gulimall.providerconsumer.test.synchronizedTest.testwo;
/**
* 需要加锁的实体对象
* 博客地址:https://blog.csdn.net/m0_67297362/article/details/129725405
* @author: jd
* @create: 2024-07-25
*/
public class SynchronizedUsage {
/**
* 同步代码块:锁住 锁的对象是类
*/
public void synchronizedCodelockClass(){
synchronized (SynchronizedUsage.class){
print();
}
}
// 代表当前线程正在执行的标识
public void print() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 5; i++) {
System.out.println("Hello " + name);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//同步代码块:锁住当前对象 this
public void synchronizedCodeLockThis(){
// 这里锁住的是当前对象,也就是当前谁调用这个方法的那个对象
synchronized (this){
print();
}
System.out.println(Thread.currentThread().getName()+"释放了synchronized(this)锁,阻塞线程可获取到锁");
}
//锁住当前对象之外的,新的object对象
public void synchronizedCodeLockObject2(){
Object object= new Object();
// 这里锁住的是当前new 的对象
synchronized (object){
print();
}
}
}
测试一:Synchronized(SynchronizedUsage.class) 作用对象是类的时候,作用的是类及该类的所有对象
//修改上述代码中main方法的调用
public static void main(String[] args) {
//测试修饰类.class
ThreadTest test = new ThreadTest();
test.testSynchronizedCodeLockClass();
//测试修饰this对象
// test.testSynchronizedCodelockObjct();
}
/**
* 调用结果:
* 可见,当锁住的是类的时候,虽然多个线程所关联的对象不一样,但这些对象同属SynchronizedUsage,
* 锁住的代码块只能在当前已获得锁的线程执行完毕之后,才能由下一个线程去获得锁然后执行代码块。
*
*/
//测试一 :测试Synchronized 修饰实体类 ,
// 则所有的对象,无论是几个线程中使用的是不是同一个对象去调用实例对象的方法,都只能是串行,不会并行,
public void testSynchronizedCodeLockClass() {
SynchronizedUsage s1 = new SynchronizedUsage();
SynchronizedUsage s2 = new SynchronizedUsage();
Thread thread1 = new Thread(()->{
System.out.println(Thread.currentThread().getName()+"启动");
s1.synchronizedCodelockClass();;
System.out.println(Thread.currentThread().getName()+"结束");
});
thread1.setName("甲线程");
thread1.start();
Thread thread2 = new Thread(()->{
System.out.println(Thread.currentThread().getName()+"启动");
s2.synchronizedCodelockClass();;
System.out.println(Thread.currentThread().getName()+"结束");
});
thread2.setName("乙线程");
thread2.start();
}
测试结果 :
如下图可见,由于锁住是的类对象,所以虽然现在两个线程中用的对象不是一个,一个s1,一个s2 (都是SynchronizedUsage 类的对象)都调用了SynchronizedUsage 类中的方法,而且这个方法用Synchronized修饰的是类.class对象,所以这两个线程中对SynchronizedUsage类中synchronizedCodelockClass()方法的调用是线程安全的。
1.2作用对象为对象
测试二:Synchronized(this) 作用对象为对象,锁住当前对象
//修改上述代码中main方法的调用
public static void main(String[] args) {
//测试修饰类.class
ThreadTest test = new ThreadTest();
//test.testSynchronizedCodeLockClass();
//测试修饰this对象
test.testSynchronizedCodelockObjct();
}
// 其中的Synchronized是绑定到了当前对象this上了,如果两个线程访问的对象是一个对象的话,而且访问的是同一个对象的加Synchronized(this)代码块中调用的方法,
//那这两个线程是线程安全的。 因为他们两个的对手是同一个,也就是this,所以谁先调用到对象的方法,谁先持有锁,
public void testSynchronizedCodelockObjct() {
SynchronizedUsage s3 = new SynchronizedUsage();
SynchronizedUsage s4 = new SynchronizedUsage();
Thread thread3 = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"启动");
s3.synchronizedCodeLockThis();
System.out.println(Thread.currentThread().getName()+"结束");
});
thread3.setName("丙线程");
thread3.start();
Thread thread4 = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"启动");
s3.synchronizedCodeLockThis();
System.out.println(Thread.currentThread().getName()+"结束");
});
thread4.setName("丁线程");
thread4.start();
}
测试结果:
可以看到我用的都是s3对象,是一个对象,在两个线程中分别访问这个对象,并调用这个对象的synchronizedCodeLockThis()方法,由于这个方法中有synchronized(this)修饰的代码块,所以当丙线程调用到s3的这个方法的时候,占有了锁,然后去调用print方法,输出五个hello 丙线程,但是为什么结果是五个连着的丙线程,之后没有直接输出,丙线程释放锁呢 ?是这样的,丙线程在执行print()方法中的第五次循环之后,会sleep(1000)睡眠一下,在这睡眠完,立即释放锁的时候,由于有被阻塞的任务(另外一个线程),所以另外一个任务立刻获取到了锁,然后开始输出第一个hello 丁线程,然后才输出丙线程释放锁。然后是一连串的四个,hello丁线程,到最后丁线程释放锁,然后结束。
如果大家不好理解,“System.out.println(Thread.currentThread().getName()+“释放了synchronized(this)锁,阻塞线程可获取到锁”);” 这句话的输出时机,我可以去掉synchronized(this){…}锁之后的提示,这样输出可能更好理解一些
测试三(1):Synchronized(this)** 作用对象为对象,不锁当前对象 ,而是锁住其他对象。
添加线程thread5 戊线程 **
public void synchronizedCodelockObjct2() {
Object obj = new Object();
// 这里锁住的是当前对象
synchronized(obj) {
System.out.println("锁的是object对象");
print();
}
测试调用代码:
public static void main(String[] args) {
//测试修饰类.class
ThreadTest test = new ThreadTest();
// test.testSynchronizedCodeLockClass();
//测试修饰this对象
test.testSynchronizedCodelockObjct();
}
// 其中的Synchronized是绑定到了当前对象this上了,如果两个线程访问的对象是一个对象的话,而且访问的是同一个对象的加Synchronized(this)代码块中调用的方法,
//那这两个线程是线程安全的。 因为他们两个的对手是同一个,也就是this,所以谁先调用到对象的方法,谁先持有锁,
public void testSynchronizedCodelockObjct() {
SynchronizedUsage s3 = new SynchronizedUsage();
SynchronizedUsage s4 = new SynchronizedUsage();
Thread thread3 = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"启动");
s3.synchronizedCodeLockThis();
System.out.println(Thread.currentThread().getName()+"结束");
});
thread3.setName("丙线程");
thread3.start();
Thread thread4 = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"启动");
s3.synchronizedCodeLockThis();
System.out.println(Thread.currentThread().getName()+"结束");
});
thread4.setName("丁线程");
thread4.start();
//和上面是不同的SynchronizedUsage对象,而且锁里面绑定是当前this,所以 戊线程 可以和丙丁线程并行。也就是说丙丁是线程安全的,但是戊线程和他们不是线程安全的。
Thread thread5 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "启动");
s4.synchronizedCodeLockThis();
System.out.println(Thread.currentThread().getName() + "结束");
});
thread5.setName("戊线程");
thread5.start();
}
测试结果
可以看到戊线程 的hello是冗杂在丙线程的hello中的,也就是这两个线程在执行中,锁定的当前对象不是同一个,所以丙、丁线程和任意一个都不是线程安全的!
测试三(2):修改代码,添加线程thread6 己线程
public static void main(String[] args) {
//测试修饰类.class
ThreadTest test = new ThreadTest();
// test.testSynchronizedCodeLockClass();
//测试修饰this对象
test.testSynchronizedCodelockObjct();
}
// 其中的Synchronized是绑定到了当前对象this上了,如果两个线程访问的对象是一个对象的话,而且访问的是同一个对象的加Synchronized(this)代码块中调用的方法,
//那这两个线程是线程安全的。 因为他们两个的对手是同一个,也就是this,所以谁先调用到对象的方法,谁先持有锁,
public void testSynchronizedCodelockObjct() {
SynchronizedUsage s3 = new SynchronizedUsage();
SynchronizedUsage s4 = new SynchronizedUsage();
Thread thread3 = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"启动");
s3.synchronizedCodeLockThis();
System.out.println(Thread.currentThread().getName()+"结束");
});
thread3.setName("丙线程");
thread3.start();
Thread thread4 = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"启动");
s3.synchronizedCodeLockThis();
System.out.println(Thread.currentThread().getName()+"结束");
});
thread4.setName("丁线程");
thread4.start();
//和上面是不同的SynchronizedUsage对象,而且锁里面绑定是当前this,所以 戊线程 可以和丙丁线程并行。也就是说丙丁是线程安全的,但是戊线程和他们不是线程安全的。
Thread thread5 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "启动");
s4.synchronizedCodeLockThis();
System.out.println(Thread.currentThread().getName() + "结束");
});
thread5.setName("戊线程");
thread5.start();
Thread thread6 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "启动");
s3.synchronizedCodeLockThis();
System.out.println(Thread.currentThread().getName() + "结束");
});
thread6.setName("己线程");
thread6.start();
}
测试结果:
丙线程启动
丁线程启动
Hello 丙线程
戊线程启动
Hello 戊线程
己线程启动
Hello 己线程
Hello 丙线程
Hello 己线程
Hello 戊线程
Hello 丙线程
Hello 戊线程
Hello 己线程
Hello 己线程
Hello 丙线程
Hello 戊线程
Hello 戊线程
Hello 丙线程
Hello 己线程
丙线程结束
己线程结束
戊线程结束
Hello 丁线程
Hello 丁线程
Hello 丁线程
Hello 丁线程
Hello 丁线程
丁线程结束
Process finished with exit code 0
解释:己线程虽然也是调用对象s3,但它锁住的对象是Object新对象的锁,跟丙、丁锁的对象不一致,所以它也可以跟戊、丙线程并发运。行。
2、同步普通方法
使用Synchronize修饰普通方法时,其作用域是整个方法,锁住的对象是当前对象:
class Test1{
public synchronized void test() {
}
}
//等价于
class Test1{
public void test() {
//锁的是当前对象
synchronized(this) {
}
}
}
在SynchronizedUsage类中添加2个同步方法和一个普通方法:
public synchronized void synchronizedMethod1() {
for (int i = 0; i < 3; i++) {
try {
System.out.println(Thread.currentThread().getName() + "同步方法1");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void synchronizedMethod2() {
for (int i = 0; i < 3; i++) {
try {
System.out.println(Thread.currentThread().getName() + "同步方法2");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//非同步方法,
public void unsafeMethod() {
for (int i = 0; i < 3; i++) {
try {
System.out.println(Thread.currentThread().getName() + "普通方法");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试类ThreadTest添加一个测试方法testSynchronizedMethod,测试同时访问多个同步方法,及同时访问同步方法和非同步方法。
//同步关键词修饰方法
// 从测试结果可以得到以下结论:
// 1.一个线程访问同一个对象的两个不同的同步方法,因为是同一个对象,synchronize方法加锁指向的this也是指向同一个(当前对象),所以会导致程序串行的执行(庚线程方法1、方法2串行执行);
public void testSynchronizedMethod() {
SynchronizedUsage s5 = new SynchronizedUsage();
SynchronizedUsage s6 = new SynchronizedUsage();
Thread thread7 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "启动");
s5.synchronizedMethod1();
s5.synchronizedMethod2();
System.out.println(Thread.currentThread().getName() + "结束");
});
thread7.setName("庚线程");
thread7.start();
}
测试结果 : 很清晰的可以看出,庚线程中 s5对象调用的两个方法都是相当于锁住了当前对象(Synchronized(this)),而且现在调用这两个方法使用的是一个对象,也就是this是一样的,所以会先执行完一个,在执行另外一个任务。【但是这其实不能称为线程不安全问题,因为是一个线程中的不同方法调用,这个只是为了让大家更好的理解 当Synchronized直接修饰普通方法时,和Synchronized(this)的作用是一样的】
修改代码,添加线程thread8
//同步关键词修改方法
// 测试类ThreadTest添加一个测试方法testSynchronizedMethod,测试同时访问多个同步方法,及同时访问同步方法和非同步方法。
//测试结论
// 从测试结果可以得到以下结论:
// 1.一个线程访问同一个对象的两个不同的同步方法,因为是同一个对象,synchronize方法加锁指向的this也是指向同一个(当前对象),所以会导致程序串行的执行(庚线程方法1、方法2串行执行);
public void testSynchronizedMethod() {
SynchronizedUsage s5 = new SynchronizedUsage();
SynchronizedUsage s6 = new SynchronizedUsage();
Thread thread7 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "启动");
s5.synchronizedMethod1();
s5.synchronizedMethod2();
System.out.println(Thread.currentThread().getName() + "结束");
});
thread7.setName("庚线程");
thread7.start();
Thread thread8 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "启动");
s5.synchronizedMethod1();
System.out.println(Thread.currentThread().getName() + "结束");
});
thread8.setName("辛线程");
thread8.start();
}
测试结果: 因为锁的是一个对象s5,所以这几个任务都是串行的,一个执行完再执行另外一个。
添加thread9 壬线程
Thread thread9 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "启动");
s6.synchronizedMethod1();
System.out.println(Thread.currentThread().getName() + "结束");
});
thread9.setName("壬线程");
thread9.start();
测试结论 :由于thread9 壬线程拿到s6对象和庚线程 、辛线程拿到的不是一个对象,但是调用的方法是对当前对象的锁,所以辛可以和其他的两个并行。
同时访问同步方法和非同步方法,非同步方法不会受到影响。
3、同步静态方法
作用域是整个方法,锁住的是当前类及该类的所有对象,其效果等效于在同步代码块中锁住类对象:
class Test2{
public synchronized static void test() {
}
}
//等价于
class Test2{
public static void test() {
//锁的是类对象,类对象只有一个
synchronized(Test2.class) {
}
}
}
在SynchronizedUsage中增加一个同步的静态方法:
/**
* 同步静态方法
*/
public static synchronized void synchronizedStaticmethod() {
for (int i = 0; i < 3; i++) {
try {
System.out.println(Thread.currentThread().getName() + "同步静态方法1");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void testSynchronizedStaticMethod() {
Thread thread10 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "启动");
SynchronizedUsage.synchronizedStaticmethod();
System.out.println(Thread.currentThread().getName() + "结束");
});
thread10.setName("癸线程");
thread10.start();
Thread thread11 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "启动");
SynchronizedUsage.synchronizedStaticmethod();
System.out.println(Thread.currentThread().getName() + "结束");
});
thread11.setName("子线程");
thread11.start();
}
测试结果 ,由于修饰的是类,所以这两个都会互相排斥,串行。
四、synchronized的不可中断性与可重入性
1、不可中断性
当锁被其他线程获得后,如果当前线程还想获得锁,那么只能进行阻塞等待,直到已获得该锁的线程释放锁再尝试去获得锁的所有权。
2、可重入性
当一个线程请求另一个线程持有的锁的时候,那么请求的线程会阻塞,这是synchronized的不可中断性;但是,当线程去获取自己所拥有的锁,那么会请求成功而不会阻塞,这就是锁的可重入性。
重入的原理是:为每个锁关联一个计数器和持有者线程,当计数器为0时候,这个锁被认为是没有被任何线程持有;当有线程持有锁,计数器自增,并且记下锁的持有线程,当同一线程继续获取锁时候,计数器继续自增;当线程退出代码块时候,相应地计数器减1,直到计数器为0,锁被释放;此时这个锁才可以被其他线程获得。
public class SynchronizedUsageChild extends SynchronizedUsage {
@Override
public synchronized void synchronizedMethod1() {
System.out.println("子类在同步方法中调用父类的同步方法,验证可重入锁");
super.synchronizedMethod1();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试类增加一个验证可重入性的方法:
public void testReentrantLock() {
SynchronizedUsageChild child = new SynchronizedUsageChild();
Thread t = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "启动");
child.synchronizedMethod1();
System.out.println(Thread.currentThread().getName() + "结束");
});
t.start();
}
Thread-0启动
子类在同步方法中调用父类的同步方法,验证可重入锁
Thread-0同步方法1
Thread-0同步方法1
Thread-0同步方法1
Thread-0结束
分析:当线程执行SynchronizedUsageChild实例中的synchronizedMethod1方法时获得SynchronizedUsageChild实例的锁(锁的持有者是线程,锁的对象是当前实例);SynchronizedUsageChild实例在synchronizedMethod1方法中调用super.synchronizedMethod1(),调用者依然是SynchronizedUsageChild实例(在子类中用关键字super调用父类方法,调用父类方法的是子类实例),再次获得的锁依然是SynchronizedUsageChild实例的锁。也就是说,两次调用synchronizedMethod1方法获得的锁都是子类SynchronizedUsageChild的实例,
如果没有重入机制,那么SynchronizedUsageChild对象在执行synchronizedMethod1方法时,会发生死锁,因为super.synchronizedMethod1()拿不到自己持有的锁,因为此时锁已经被占有,会导致线程不断等待,等待一个永远无法获得的锁。
五、 使用synchronized的注意事项
1、注意synchronized同步块的粒度
由于锁的不可中断性,如果在同步块中包含耗时任务,就会发生严重的堵塞。此时就要减小锁的粒度,尽量从synchronized块中分离耗时且不影响共享状态的操作,去优化代码执行时间,这样即使在耗时操作的执行过程中,也不会组织其他线程访问共享状态。
同时,由于请求和释放锁的操作需要性能开销,所以synchronized块不能分解得过于琐碎。
参考链接:
https://blog.csdn.net/m0_67297362/article/details/129725405
https://www.cnblogs.com/dxflqm/p/18022798