synchronized保证线程安全的原理-内置锁
在看synchronized之前,我们首先来看内置锁,什么是内置锁呢?就是说,Java中每一个对象都可以用作同步的锁,那么,这些锁就被称之为内置锁.
其实synchronized的原理其实就是加了锁,内置锁和互斥锁所决定的。每个对象都有锁,而这些锁都是互斥的,一个进来之后,另外的就不能进来了,因此就可以保证线程的安全性。
synchronized 的介绍
synchronized 是 Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码,而这段代码也被称为临界区。
synchronized 有多个叫法,而每个叫法都表明synchronized 的特性:
1、内置锁(又叫 隐式锁):synchronized 是内置于JDK中的,底层实现是native;同时,加锁、解锁都是JDK自动完成,不需要用户显式地控制,非常方便。
- synchronized 放在普通方法上,内置锁就是当前类的实例。
- synchronized 放在静态方法上,内置锁就是当前类的class。
- synchronized 放在代码块上,内置锁就是当前传的对象。
2、同步锁:synchronized 用于同步线程,使线程互斥地访问某段代码块、方法。这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,知道线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。
3、对象锁:准确来说,是分为对象锁、类锁。synchronized 以当前的某个对象为锁,线程必须通过互斥竞争拿到这把对象锁,从而才能访问 临界区的代码,访问结束后,就会释放锁,下一个线程才有可能获取锁来访问临界区(被锁住的代码区域)。synchronized锁 根据锁的范围分为 对象锁 和 类锁。对象锁,是以对象实例为锁,当多个线程共享访问这个对象实例下的临界区,只需要竞争这个对象实例便可,不同对象实例下的临界区是不用互斥访问;而类锁,则是以类的class对象为锁,这个锁下的临界区,所有线程都必须互斥访问,尽管是使用了不同的对象实例;
总的来说,对象锁的粒度要比类锁的粒度要细,引起线程竞争锁的情况比类锁要少的多,所以尽量别用类锁,锁的粒度越少越好。
看下面的例子:
FruitCount fruitCount = new FruitCount();
FruitCount fruitCount_3 = new FruitCount();
//线程1、2 使用了同一个FruitCount对象(fruitCount )
Thread thread_1 = new Thread(new MyRunable(fruitCount));
Thread thread_2 = new Thread(new MyRunable(fruitCount));
//线程3使用了不同的FruitCount对象(fruitCount_3 )
Thread thread_3 = new Thread(new MyRunable(fruitCount_3));
线程1、2将会互斥访问getAmount( )方法,线程3则独享getAmount( )方法;线程1、2的getAmount( )方法中的对象锁是fruitCount ,线程3的则是 fruitCount_3;这便是对象锁的粒度范围,不同的对象,锁是相互隔离的。而对于setData( )方法,三个线程都要互斥访问访问它,因为是同一个锁 -- FruitCount.class类锁。
class FruitCount{
static int price = 5;
static int num = 10;
public void setData(int price,int num){
//类锁,以FruitCount.class为锁
synchronized(FruitCount.class){
this.price = price;
this.num = num;
}
}
public int getAmount(){
//对象锁,以当前对象为锁
synchronized (this) {
int amount = price*num;
return amount;
}
}
}
class MyRunable implements Runnable{
FruitCount fruitCount;
public MyRunable(FruitCount fruitCount){
this.fruitCount = fruitCount;
}
@Override
public void run() {
//setData方法 有类锁
fruitCount.setData(5, 10);
//getAmount方法 里面有对象锁,就是fruitCount对象
fruitCount.getAmount();
}
}
synchronized保证线程安全的原理-JVM层面
synchronized在Java虚拟机中的实现原理,就是说Java虚拟机在执行同步代码块的时候,其实它是基于进入和退出monitor对象来实现方法同步的,也就是说,它有两个字节码指令分别就是monitorenter和monitorexit,当进入代码块的时候monitorente,当代码块执行完毕之后就会执行monitorexit
那么,我们来看一下是不是这样的,我们如何来看字节码指令呢?
其实主要关注的,第17行就是monitorexit,这是退出,第4行就是monitorenter,这是进入,从第四行开始进入执行,执行到第17行退出就可以了,那么,为什么后面还会有monitorexit呢?其实,我们在执行的过程中,它并非是一行一行的去执行的,比如说有异常
比如说我们来看一下这里的异常表,它有可能就直接跳转到第23行,我们发现第23行是aload_1,然后接着执行第24行的monitorexit,就执行了退出,后面还有athrow,就是说
字节码并非是从第一行开始往下挨个的执行,比如说遇到goto了,它可能就会跳,比如说遇到ifle这样的它也会跳,本例中遇到ifle后就跳转到第19行,所以,我们发现ireturn上面有一个monitorexit退出,也就是说,你是不需要担心同步代码块线程拿到锁之后没释放就走了,这在理论层面上是不会发生的,因为,不管是异常退出或者是正常执行或者是扔出异常或者是等等的各种原因,锁都是会释放的。
下面我们看一下同步方法
发现,同步方法中并没有我们想要的monitorenter和monitorexit,方法的同步是使用另外一种方式实现的,但是这种方式在Java虚拟机中并没有给出详细的说明,所以,我们这里也就不去深究它了。好了,这是我们从Java字节码的角度来看synchronized的执行,其实,就是通过monitorenter和monitorexit。