先来简单理解一下对象锁和类锁:
Java对象锁
对象锁是用于对象实例方法,或者一个对象实例上的
有一个类A,A里面有一些方法或者代码块使用了对象锁。如:
class A{
//同步方法
public void test1() {
synchronized (this) {
}
}
//跟上面的test1方法一样都是对象锁,只是锁的尺寸不同
public synchronized void test2() {
}
//方法二
public void test3() {
}
}
现在有线程1new了一个A的对象,对象为a,并调用了使用对象锁方法test1(),即:
A a = new A();
a.test1();
---到此是线程1执行的代码---
此时当线程1在执行加了对象锁的同步方法test1的那一刻,这时又有一个线程2过来,想要执行下面这段代码会怎么样呢?
a.test2();
---到此是线程2执行的代码---
如果是正常的多线程场景,同一时刻,线程1执行a.test1()和线程2执行a.test2()是互不影响的。
但是这里的线程2必须等线程1释放对象锁之后才可以执行test2(),因为:
test1()方法加了synchronized对象锁,那在线程1执行对象a的test1()方法时,就会锁住对象实例a的内存,在线程1还没释放对象锁之前,任何线程都是没办法进入对象a里面的。而线程1只会在执行完同步方法或者同步代码块只会才会释放对象锁。
但,如果线程2做的是这么一个操作:
a.test3();
这个时候线程3执行test2方法就不需要等待线程1,进行了同步的方法(加锁方法)和没有进行同步的方法(普通方法)是互不影响的,一个线程进入了同步方法,得到了对象锁,其他线程还是可以访问那些没有同步的方法(普通方法)。这里涉及到内置锁的一个概念(此概念出自java并发编程实战第二章):
对象的内置锁和对象的状态之间是没有内在的关联的,虽然大多数类都将内置锁用做一种有效的加锁机制,但对象的域并不一定通过内置锁来保护。当获取到与对象关联的内置锁时,并不能阻止其他线程访问该对象,当某个线程获得对象的锁之后,只能阻止其他线程获得同一个锁。之所以每个对象都有一个内置锁,是为了免去显式地创建锁对象。
在微服务的后端中,每个不同的请求都会新建一个对象来handle,理论上对象锁是不会造成我们想要的约束的
Java类锁
类锁是用于类的静态方法或者一个类的class对象上的
同样的例子,有一个类A,只是A里面使用synchronize的方法不一样了,这种使用方法会锁住类A。如:
class A{
//同步方法
public void test1() {
synchronized (A.class) {
}
}
//跟上面的同步方法一样,都是类锁,只是表现形式不同
public static synchronized void test2() {
}
public synchronized void test3() {
}
}
现在有线程1new了一个A的对象,对象为a,并调用了使用对象锁方法test1(),即:
A a = new A();
a.test1();
---到此是线程1执行的代码---
此时当线程1在执行加了对象锁的同步方法test1的那一刻,这时又有一个线程2过来,想要执行下面这段代码会怎么样呢?
A.test2();
---到此是线程2执行的代码---
这里的线程2必须等线程1释放类锁之后才可以执行A.test2(),因为:
线程1执行的test1()方法里面锁住了A.class,也就是锁住了类A的内存。那同一时刻所有实例想使用test1方法或者是其他加了类锁的形如A.test2()方法,都得等线程1释放类锁后才能执行。线程2要用加了类锁的静态方法test2,那自然是用不了了。
想一下,如果是在高并发的情况下,所有要执行类A的test1请求都得阻塞等待最先进入类A的类锁的线程执行完,释放类锁后,才能执行。如果这个锁住的方法执行的时候过长,或者直接死循环,那整个系统将会被拖垮甚至瘫痪。
但,如果线程2做的是这么一个操作:
a.test3();
结果是线程1执行了类锁的同步方法,线程2执行了对象锁的同步方法,两者互不影响。这证明了类锁和对象锁是两个不一样的锁,控制着不同的区域,它们是互不干扰的。同样,线程获得对象锁的同时,也可以获得该类锁,即同时获得两个锁,这是允许的。
在微服务的后端中,我们常用类锁来进行约束
其实总结起来很简单:
- 一个锁的是类对象,一个锁的是实例对象。
- 若类对象被lock,则类对象的所有同步方法全被lock;
- 若实例对象被lock,则该实例对象的所有同步方法全被lock
到这里大家应该对对象锁和类锁应该有个更加清晰的理解了
注意:
上面我们有讲到synchronize的一个缺陷,就是如果同步的方法死循环了或者执行时间过长了,会影响系统的正常运行。那么为了将这种风险降到最低,一般我们使用类锁的时候,不会直接synchronize(xx.class)或者static synchronized去锁住这个类的所有同步方法,而是用先在这个类里面声明了一个对象实例:Object object=new Object(),然后再synchronize(object)。那么这个方法加锁的对象是object这个对象,当一个线程执行这个方法时,这对其他线程要执行这个类的其他同步方法是没有影响的,只会影响到要执行用synchronize(object)锁住的方法,因为他们持有的锁都完全不一样。
总结五种用法:
一、this
synchronized(this){
//互斥代码
}
这里的this指的是执行这段代码的对象,synchronized得到的锁就是this这个对象的锁
public synchronized void func(){
//互斥代码
}
二、A.class
synchronized(A.class){
//互斥代码
}
这里A.class得到的是A这类,所以synchronized关键字得到的锁是类的锁,这种方法同下面的方法功能是相同的:
public static synchronized void fun(){
//互斥代码
}
所有需要类的锁的方法都不能同时执行,但是它和需要某个对象的锁的方法或者是不需要任何锁的方法可以同时执行。
三、object.getClass()
synchronized(object.getClass){
//互斥代码
}
这种方法一般情况下同第二种是相同,但是出现继承和多态时,得到的结果却是不相同的。所以一般情况下推荐使用A.class的方式。
四、object
private Object lock = new Object();
public void test1(){
synchronized(lock){
//互斥代码
}
}
这里synchronized关键字拿到的锁是对象object的锁,所有需要这个对象的锁的方法都不能同时执行。这是最常用的高并发场景下要锁住某个方法所用的操作。
五、static object
上边的代码稍作修改就可以起到互斥作用,将类中Object对象的声明改为下面这样:
private static Object lock = new Object();
这样不同的类使用的就是同一个object对象,需要的锁也是同一个锁,就可以达到互斥的效果了。