【多线程与并发】synchronized详解
在 java 语言中,保证线程安全性的主要手段是加锁,而 Java 中的锁主要有两种:synchronized 和 Lock,我们今天重点来看一下 synchronized 的几种用法:
类锁synchronized的使用
在开始前,让我们先记住使用synchronized是需要注意的几点:
对象锁方法锁形式:synchronized修饰普通方法,锁对象默认为this
public class ObjectSynchronizedUsage implements Runnable {
@Override
public void run() {
method();
}
public synchronized void method() {
System.out.println(Thread.currentThread().getName() + "开始");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
public static void main(String[] args) {
ObjectSynchronizedUsage instance = new ObjectSynchronizedUsage();
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
}
}
输出结果:
Thread-0开始
Thread-0结束
Thread-1开始
Thread-1结束
代码块形式:自己指定锁对象(this/自定义锁对象)
public class ObjectSynchronizedUsage implements Runnable {
@Override
public void run() {
//锁为this的同步代码快形式(本质与方法锁一样),两个线程使用同一把锁(this),Thread-1必须等到Thread-0释放掉该锁后,才能执行
synchronized (this) {
System.out.println(Thread.currentThread().getName() + "开始");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
}
public static void main(String[] args) {
ObjectSynchronizedUsage instance = new ObjectSynchronizedUsage();
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
}
}
输出结果:
Thread-0开始
Thread-0结束
Thread-1开始
Thread-1结束
public class ObjectSynchronizedUsage implements Runnable {
// 创建2把锁
Object block1 = new Object();
Object block2 = new Object();
@Override
public void run() {
synchronized (block1) {
System.out.println("block1锁:" + Thread.currentThread().getName() + "开始");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("block1锁:"+Thread.currentThread().getName() + "结束");
}
synchronized (block2) {
System.out.println("block2锁:" + Thread.currentThread().getName() + "开始");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("block2锁:"+Thread.currentThread().getName() + "结束");
}
}
public static void main(String[] args) {
ObjectSynchronizedUsage instance = new ObjectSynchronizedUsage();
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
}
}
输出结果:
block1锁:Thread-0开始
block1锁:Thread-0结束
block2锁:Thread-0开始 // 可以看到当第一个线程在执行完第一段同步代码块之后,第二个同步代码块可以马上得到执行,因为他们使用的锁不是同一把
block1锁:Thread-1开始
block2锁:Thread-0结束
block1锁:Thread-1结束
block2锁:Thread-1开始
block2锁:Thread-1结束
类锁synchronized修饰静态方法
示例1:
public class ClassSynchronizedUsage implements Runnable {
@Override
public void run() {
method();
}
//synchronized用在普通方法上,默认的锁就是this,当前实例
public synchronized void method() {
System.out.println(Thread.currentThread().getName() + "开始");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
public static void main(String[] args) {
ClassSynchronizedUsage instance1 = new ClassSynchronizedUsage();
ClassSynchronizedUsage instance2 = new ClassSynchronizedUsage();
// t1和t2对应的this是两个不同的实例,所以代码不会串行
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
}
}
输出结果:
Thread-0开始
Thread-1开始
Thread-1结束
Thread-0结束
public class ClassSynchronizedUsage implements Runnable {
...
// 将示例1中的method()方法改为静态方法,synchronized用在静态方法上,默认的锁就是当前所在的Class类,所以无论是哪个线程访问它,需要的锁都只有一把
public static synchronized void method() {
System.out.println("我是线程" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "结束");
}
...
}
输出结果:
Thread-0开始
Thread-0结束
Thread-1开始
Thread-1结束
synchronized原理加锁和释放锁的原理
我们从字节码的角度来看下synchronized实现细节。
从上一节我知道synchronized既可以作用于方法,也可以作用于代码块。但在实现上是有区别的。 比如如下代码whatsapp网页版,使用 synchronized 作用于代码块:
public class SynchronizedTest{
private int number;
public void test1(){
int i = 1;
synchronized(this){
number = i + 1;
}
}
}
使用javap命令反编译查看字节码:
> javap -verbose SynchronizedTest.class
得到如下信息:
Cgq2xl6X-COAC4hMAAEjW40t64s500.png
可以看到,编译而成的字节码中会包含monitorenter和monitorexit这两个字节码指令。
你可能已经发现了,上面的字节码中有1个monitorenter和2个monitorexit,这是因为虚拟机需要保证当异常发生时也能释放锁。因此2个monitorexit一个是代码正常执行结束后释放锁,一个是在代码执行异常时释放锁。
在看下synchronized 修饰方法有哪些区别:
public class SynchronizedTest{
public synchronized void test1(){
int i = 0;
i = i + 1;
}
}
使用javap查看上面方法编译后的字节码如下:
image-20230923213943899.png
从图中可以看出,被synchronized修饰的方法在被编译为字节码后,在方法的flags属性中会被标记为ACC_SYNCHRONIZED标志。当虚拟机访问一个被标记为ACC_SYNCHRONIZED的方法时,会自动在方法的开始和结束(或异常)位置添加monitorenter和monitorexit指令。
关于monitorenter和monitorexit,可以理解为一把具体的锁。在这个锁中保存着两个比较重要的属性:计数器和指针。
用一张图表示:
Cgq2xl6X-COAEskYAABd1Qkprak432.png
我们来看下monitorenter和monitorexit具体是如何工作的:
下图表现了Object,Monitor,SynchronizedQueue以及Thread状态之间的关系:
WechatIMG197.jpg
从上图可以看出任意线程对Object的访问,首先要获得Object的monitor,如果获取失败,该线程就会进入同步队列中,该线程状态变为BLOCKED。当monitor持有者释放后,在同步队列中的线程才会有机会重新获取monitor,才能继续执行。synchronized是可重入锁吗,及其实现原理?
其实在上一节中已经无意透漏出了答案——synchronized是可重入锁。
什么是可重入,可重入锁?
可重入:若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另一端代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调单个线程执行时重新进入同一个子程序仍然是安全的。(来源于维基百科)
可重入锁:又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。
示例:
public class ChildProgram extends SuperProgram {
public static void main(String[] args) {
ChildProgram childProgram = new ChildProgram();
childProgram.childSomeSthing();
}
public synchronized void childSomeSthing (){
superDoSomeSthing();
System.out.println("child do Something");
}
@Override
public synchronized void superDoSomeSthing() {
System.out.println("child do Something");
super.superDoSomeSthing();
}
}
public class SuperProgram {
public synchronized void superDoSomeSthing (){
System.out.println("super,doing something");
}
}
执行结果:
child do Something
super,doing something
child do Something
可以看到调用的三个方法均得到了执行。我们知道synchronized修饰普通方法时,使用的是对象锁,也就是ChildProgram对象。三个方法的锁都是ChildProgram对象。我们在子类中执行childSomeSthing方法时,获取了ChildProgram对象锁,然后在childSomeString时调用了重写父类的superDoSomeSthing方法whatsapp网页版,该方法的锁也是ChildProgram对象锁,然后在其中调用父类的superDoSomeSthing方法,该方法的锁也是ChildProgram对象锁。一个锁多次请求,而且都成功了,所以synchronized是可重入锁。
synchronized可重入锁的实现原理:
结合上一节中加锁和释放锁原理,不难理解:
执行monitorexit命令:
这就是synchronized的重入性,及在同一锁程中,每个对象拥有一个monitor计数器,当线程获取该对象锁后,monitor计数器就会+1,释放锁后就会将monitor计数器-1,线程不需要再次获取同一把锁。
保证可见性的原理:synchronized的happens-before关系
synchronized的happens-before规则,即监视器锁规则:对同一个监视器的解锁,happens-before于对该监视器的加锁。为进一步了解synchronized的并发语义,通过示例代码分析这条happens-before规则,示例代码如下:
public class MonitorTest{
private int a = 0;
public synchronized void writer(){//1
a++;//2
}//3
public synchronized void reader(){//4
int i = a;//5
}//6
}
在并发时,第5步操作中读取到的变量a是多少?这就需要通过happens-before规则来进行分析telegram中文版,示例代码的happens-before关系如下图所示:
WechatIMG198.jpg
上图中每一个箭头连接的两个节点就代表之间的happens-before关系:
黑色的是通过程序顺序规则推到出来。红色的为监视器规则推导而出:线程A释放锁happens-before线程B加锁。蓝色的则是通过传递性规则进一步推导的happens-before关系。
最终得到结论就是操作2 happens-before 5,通过这个关系我们可以得出以下:
根据happens-before的定义中的一条:如果A happens-before B,则A的执行结果对B可见。那么在上面的代码中,线程A先对共享变量a进行+1,由2 happens-before 5关系可知线程A的执行结果对线程B可见及线程B所读取到的 a的值为1。
©著作权归作者所有,转载或内容合作请联系作者