一.synchronized作用:
保持多线程下操作的互斥性
保证共享变量的修改及时可见性
有效解决重排序问题
二. 常用用法:
1.修饰普通方法,锁是当前类的实例对象
2.修饰静态方法,锁是当前类的Class对象
3.修饰代码块,锁是synchronized括号里配置的对象
•修饰普通方法
package com.life.abstractFactory.test;
public class MyThread implements Runnable{
//共享资源(临界资源)
static int i=0;
/**
* synchronized 修饰实例方法
*/
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
MyThread instance=new MyThread();
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
/** 输出结果:2000000
1.当前线程的锁便是实例对象instance,注意Java中的线程同步锁可以是任意对象。
2.一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,
所以无法访问该对象的其他synchronized实例方法,但是其他线程还是可以访问该实例对象的其他非synchronized方法。
3.当两个线程分别访问两个不同的实例对象的synchronized方法时(分别持有不同的对象锁),是允许的,
如果此时两个线程操作数据是非共享的,线程安全是有保障的;
如果两个线程操作的是共享数据,那么线程安全就有可能无法保证了。
*/
}
•修饰静态方法
package com.life.abstractFactory.test;
public class MyThread implements Runnable{
//共享资源(临界资源)
static int i=0;
/**
* 锁是当前类 MyThread的class对象
*/
public static synchronized void statcIncrease(){
i++;
}
/**
* 锁是当前类MyThread的实例对象
*/
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
//statcIncrease();
increase();
}
}
public static void main(String[] args) throws InterruptedException {
MyThread instance1=new MyThread();
MyThread instance2=new MyThread();
//当synchronized修饰的是静态方法时,两个线程实际上持有的是同一个锁对象,即当前类的class对象,所以是线程安全的。结果:20000
//当synchronized修饰的是实例方法时,两个线程实际上持有的是不同的锁对象,即当前类的实例对象,所以是线程不安全的。结果:1409582
Thread t1=new Thread(instance1);
Thread t2=new Thread(instance2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
•修饰代码块
package com.life.abstractFactory.test;
public class MyThread implements Runnable{
static MyThread instance=new MyThread();
//共享资源(临界资源)
static int i=0;
@Override
public void run() {
//.......方法 比较大,操作比较耗时时,考虑同步代码块(同步操作共享变量i的代码块)
//锁对象为instance
synchronized(instance){
for(int j=0;j<1000000;j++){
i++;
}
}
//当前实例对象锁
/* synchronized(this){
for(int j=0;j<1000000;j++){
i++;
}
}*/
//当前class对象锁
/* synchronized(MyThread.class){
for(int j=0;j<1000000;j++){
i++;
}
}*/
}
public static void main(String[] args) throws InterruptedException {
MyThread instance1=new MyThread();
MyThread instance2=new MyThread();
Thread t1=new Thread(instance1);
Thread t2=new Thread(instance2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
三.实现原理:
pass:以下部分内容摘自https://blog.csdn.net/javazejian/article/details/72828483
1) synchroniezd同步实际上是通过管理对象的monitor(监视器)实现的;monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。
每一个对象都有一个唯一的monitor与之相关联。
Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示
monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,这是Java中任意对象可以作锁为的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因。
2)Synchronized
修饰代码块底层原理:
代码对应的字节码:
从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令。其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置。
monitorenter:当执行monitorenter执行时,当前线程尝试获取monitor
- 如果当前monitor的进入数为0,则说明monitor未被占用,则当前线程占有该monitor,同时monitor的进入数变为1
- 如果当前monitor进入数不为0,且monitor的占用者是当前线程,则可以重复占用该monitor,同时进入数加1
- 如果其他线程占有了该monitor,则线程进入阻塞状态,直到monitor的进入数重新变为0,再重新尝试获取所有权
monitorexit:
执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。
注:wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
3)Synchronized
修饰方法底层原理:
修饰方法代码对应的字节码:
方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现)。JVM就是根据ACC_SYNCHRONIZED标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
Synchronized通过monitor保持
互斥性的,也跟通过monitor保持可见性和有序性:
- 可见性:执行到monitorenter时,线程会重新从主内存中将数据同步到本地工作内存,从而保证其可以看到其他线程的修改。同时执行monitorexit时,线程也会将本地工作内存的数据同步到主内存中
- 有序性:monitorenter、monitorexit修饰的代码将禁止进行重排序
四.等待唤醒机制与synchronized
所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法,notify/notifyAll和wait方法依赖于monitor对象,所以notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用。
synchronized (obj) {
obj.wait();
obj.notify();
obj.notifyAll();
}
与sleep方法不同的是wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行,而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。
五.synchronized的可重入性
互斥锁指当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态;
重入锁是指但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于,请求将会成功。
在java中synchronized是基于原子性的内部锁机制,是可重入的,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。
public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
static int j=0;
@Override
public void run() {
for(int j=0;j<1000000;j++){
//this,当前实例对象锁
synchronized(this){
i++;
increase();//synchronized的可重入性
}
}
}
public synchronized void increase(){
j++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
正如代码所演示的,在获取当前实例对象锁后进入synchronized代码块执行同步代码,并在代码块中调用了当前实例对象的另外一个synchronized方法,再次请求当前实例锁时,将被允许,进而执行方法体代码,这就是重入锁最直接的体现,需要特别注意另外一种情况,当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。注意由于synchronized是基于monitor实现的,因此每次重入,monitor中的计数器仍会加1。