多线程之synchronized关键字
synchronized关键字
1、为什么需要同步器
多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源;这种资源可能是:
对象、变量、文件等。
共享:资源可以由多个线程同时访问
可变:资源可以在其生命周期内被修改
引出的问题:
由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问!
2、如何解决线程并发安全问题?
实际上,所有的并发模式在解决线程安全问题时,采用的方案都是序列化访问临界资源。即在同一时刻,只能有一个线程访问临
界资源,也称作同步互斥访问。
Java 中,提供了两种方式来实现同步互斥访问:****synchronized 和 Lock
同步器的本质就是加锁
加锁目的:序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问)
不过有一点需要区别的是:当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的
私有栈中,因此不具有共享性,不会导致线程安全问题。
synchronized内置锁是一种对象锁(锁的是对象而非引用),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可
重入的。
加锁的方式:
1、同步实例方法,锁是当前实例对象
2、同步类方法,锁是当前类对象
3、同步代码块,锁是括号里面的对象
synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码
块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低。当然,JVM内置锁在1.5
之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,,内置锁的并发性能已经基本与 Lock持平。
synchronized关键字被编译成字节码后会被翻译成monitorenter 和 monitorexit 两条指令分别在同步块逻辑代码的起始位置
与结束位置。
每个同步对象都有一个自己的Monitor(监视器锁),加锁过程如下图所示:
从这里我们可以得到一个信息,也就是说这些线层也在进行操作,此时在多核操作系统下,可能一直在CPU上来进行执行。
Monitor监视器锁
任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是
基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和
MonitorExit指令来实现。
monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行
monitorenter指令时尝试获取monitor的所有权,过程如下:
a. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor
的所有者;
b. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
c. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝
试获取monitor的所有权;
monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减
1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去
获取这个 monitor 的所有权。
monitorexit,指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁;
通过上面两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来
完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则
会抛出java.lang.IllegalMonitorStateException的异常的原因。
看一个同步方法
public class SynchronizedMethod {
public synchronized void method() {
System.out.println("Hello World!");
}
}
从编译的结果来看,方法的同步并没有通过指令 monitorenter 和 monitorexit 来完成(理论上其实也可以通过这两条指令来
实现),不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步的:
当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取
monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个
monitor对象。
两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通
过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切
换,对性能有较大影响。
什么是monitor?
可以把它理解为 一个同步工具,也可以描述为 一种同步机制,它通常被 描述为一个对象。与一切皆对象一样,所有的Java对象
是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把
看不见的锁,它叫做内部锁或者Monitor锁。也就是通常说Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的
是Monitor对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于
HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):
1 ObjectMonitor() {
2 _header = NULL;
3 _count = 0; // 记录个数
4 _waiters = 0,
5 _recursions = 0;
6 _object = NULL;
7 _owner = NULL;
8 _WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
9 _WaitSetLock = 0 ;
10 _Responsible = NULL ;
11 _succ = NULL ;
12 _cxq = NULL ;
13 FreeNext = NULL ;
14 _EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
15 _SpinFreq = 0 ;
16 _SpinClock = 0 ;
17 OwnerIsThread = 0 ;
18 }
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成
ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:
- 首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当
前线程,同时monitor中的计数器count加1;
- 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet
集合中等待被唤醒;
- 若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);
同时,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式
获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须
在同步代码块中使用。监视器Monitor有两种同步方式:互斥与协作。多线程环境下线程之间如果需要共享数据,需要解决互斥访问
数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。
3、练习
3.0 案例一
想要实现的功能是能够达到交替执行1-2-3这种效果,于是乎开始写出来这样的代码:
/**
* 验证交替打印
* 三个线程交替打印
*/
public class MyThread extends Thread {
private static volatile int i = 1;
public static void main(String[] args) {
MyThread t = new MyThread();
new Thread(()->{
while (true) {
synchronized (t) {
if (i != 1) {
try {
t.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序执行到了这里,那么说明i一定为1
t.notifyAll();
i=2;
System.out.println("11111");
}
}
}).start();
new Thread(()->{
while (true) {
synchronized (t) {
if (i != 2) {
try {
t.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序执行到了这里,那么说明i一定为1
t.notifyAll();
i=3;
System.out.println("22222");
}
}
}).start();
new Thread(()->{
while (true) {
synchronized (t) {
if (i != 3) {
try {
t.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序执行到了这里,那么说明i一定为1
t.notifyAll();
i=1;
System.out.println("33333");
}
}
}).start();
}
}
但是通过控制台打印,可以发现如下结果:
11111
22222
11111
22222
11111
22222
这种并没有达到我们想要的效果。
那么来分析一下:
当i !=1 的时候,那么当前i为2和3的时候就会进入到wait状态;三个线程又开始同时开始抢。但是要记住的一点是wait的特性,那就是第二次进来的时候,哪里睡得就从哪里开始来进行执行。如果线程抢到了,但是不是1,而是2,又因为wait是在哪里睡的,又会从哪里醒来,那么就会失去原来的判断,那么也就会导致没有顺序的出现。所以为了保证顺序,应该将上面的if都修改成while,代码如下所示:
public class MyThread extends Thread {
private static volatile int i = 1;
public static void main(String[] args) {
MyThread t = new MyThread();
new Thread(()->{
while (true) {
synchronized (t) {
while (i != 1) {
try {
t.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序执行到了这里,那么说明i一定为1
t.notifyAll();
i=2;
System.out.println("11111");
}
}
}).start();
new Thread(()->{
while (true) {
synchronized (t) {
while (i != 2) {
try {
t.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序执行到了这里,那么说明i一定为1
t.notifyAll();
i=3;
System.out.println("22222");
}
}
}).start();
new Thread(()->{
while (true) {
synchronized (t) {
while (i != 3) {
try {
t.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序执行到了这里,那么说明i一定为1
t.notifyAll();
i=1;
System.out.println("33333");
}
}
}).start();
}
}
即使线程从哪里睡得,当再次醒来的时候,执行完成之后又会来进行判断,如果判断不成立,那么依然会陷入到睡眠中去。如果判断成功,那么就会执行。
3.0、小结
1、需要注意这里的while的巧妙用法!
2、这里的notifyAll()和notify()方法这里是唤醒其他线程,但是在当前的同步块中如果没有执行完成,那么是不会释放掉锁的,等到notify或者是notifyAll在synchronized同步代码块中的代码执行完成之后,才会真正的释放掉锁;
3、wait方法可以查看之前讲解的。具体的可以分成为第一次和第二次(及其之后的特性),如果是第一次,多个线程在处于争抢监视器锁的时候,如果抢到了,那么将会进入到同步代码块中来执行;而没有抢到监视器的线程,将会进入到阻塞状态中,如果说抢到监视器的线程在执行代码的之后遇到了wait的执行,那么让出当前的监视器,然后大家一起进入到争抢状态。假设让出的监视器的线程又一次拿到了监视器,那么就会在原来阻塞的地方继续执行。
3.1、案例二
场景:三个线程,两个消费者,一个投食者。当消费者消费完的时候,投食者来进行投食
/**
* 三个线程,两个消费者,一个投食者。当消费者消费完的时候,投食者来进行投食
*/
public class MyThreadOne {
public static void main(String[] args) {
Food food = new Food();
// 两个动物线程
Thread t1 = new Thread(() -> {
synchronized (food) {
while (food.meat < 1) {
try {
food.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序执行到了这里,那么说明了food.meat一定是>=1的,因为有了一个while循环在这里来进行判断
System.out.println("大熊吃了一斤肉,还剩: " + food.meat + " 斤肉");
food.meat--;
System.out.println("大熊吃了一斤肉,还剩: " + food.meat + " 斤肉");
}
});
Thread t2 = new Thread(() -> {
synchronized (food) {
while (food.meat < 1) {
try {
food.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 程序执行到了这里,那么说明了food.meat一定是>=1的,因为有了一个while循环在这里来进行判断
System.out.println("小熊吃了一斤肉,还剩: " + food.meat + " 斤肉");
food.meat--;
System.out.println("小熊吃了一斤肉,还剩: " + food.meat + " 斤肉");
}
});
// 饲养员线程
Thread t3 = new Thread(() -> {
synchronized (food) {
while (food.meat < 1) {
food.meat++;
}
food.notifyAll();
System.out.println("添加了一斤肉片进去");
}
});
t1.start();
t2.start();
t3.start();
}
}
class Food {
volatile int meat = 1;
}
输出控制台打印:
大熊吃了一斤肉,还剩: 1 斤肉
大熊吃了一斤肉,还剩: 0 斤肉
添加了一斤肉片进去
小熊吃了一斤肉,还剩: 1 斤肉
小熊吃了一斤肉,还剩: 0 斤肉
从上面可以看到,我们可以自定义一个消费者和生产者来进行操作。那么看到了这里,一切似乎开始变得明朗起来。
案例三
/**
* @author lg
* @Description 三条线程交互打印1-100
* @date 2021/10/19 10:38
*/
public class DemoTwo {
/**
* 每个线程需要来进行打印的
*/
volatile int i;
/**
* 用来表示线程
* 1表示线程1
* 2表示线程2
* 3表示线程3
*/
volatile int j;
public static void main(String[] args) {
DemoTwo demoTwo = new DemoTwo();
// 需要注意的是这里的值需要和下面的if判断(demoTwo.i % 3 == 1)结合在一起来进行操作
demoTwo.i = 1;
demoTwo.j = 1;
new Thread(() -> {
while (true) {
synchronized (demoTwo) {
while (demoTwo.j != 1) {
try {
demoTwo.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (demoTwo.i % 3 == 1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 这里就是demoTwo.i=1的情况
demoTwo.notifyAll();
System.out.println("当前的线程是:" + Thread.currentThread().getName() + " ,当前的i的值是______" + demoTwo.i);
demoTwo.i++;
demoTwo.j = 2;
}
}
}).start();
new Thread(() -> {
while (true) {
synchronized (demoTwo) {
while (demoTwo.j != 2) {
try {
demoTwo.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (demoTwo.i % 3 == 2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 这里就是demoTwo.i=1的情况
demoTwo.notifyAll();
System.out.println("当前的线程是:" + Thread.currentThread().getName() + " ,当前的i的值是______" + demoTwo.i);
demoTwo.i++;
demoTwo.j = 3;
}
}
}).start();
new Thread(() -> {
while (true) {
synchronized (demoTwo) {
while (demoTwo.j != 3) {
try {
demoTwo.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (demoTwo.i % 3 == 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 这里就是demoTwo.i=1的情况
demoTwo.notifyAll();
System.out.println("当前的线程是:" + Thread.currentThread().getName() + " ,当前的i的值是______" + demoTwo.i);
demoTwo.i++;
demoTwo.j = 1;
}
}
}).start();
}
}
3.1总结
从上面两个案例可以看出来,涉及到syncronized关键字的时候,一般涉及到while来进行操作。