简介
上一篇文章《Java中synchronized实现对象锁的两种方式及原理解析》中,介绍了方法锁的两种实现方式及Synchronized
的底层原理,本文将讲解synchronized
的类锁的两种实现方式。
一.类锁的定义
什么是类锁
类锁指synchronize修饰的静态方法或指定锁为class对象。
类锁来自何处?
不同的线程,访问使用类锁的方法的时候,他们获取到的“锁”,其实是Class对象。因为同一个类中有且只有一个Class对象,但同一个类中可以有很多个其他对象。此时,就出现了同一个类中多个对象对Class对象使用的竞争,类锁则保证了在同一时间,只允许一个线程访问被类锁锁住的方法。
类锁有两种实现方式:
synchronized
加在static
方法上(静态方法锁)。synchronized(*.class
)代码块。
接下来我们分别来看一下类锁的两种实现形式,以及它们之间的区别。
二.静态方法锁的方式实现类锁
我们使用类锁,应该保证在同一个类中,不同对象的线程,执行同一方法时,保证多个线程串行执行此方法。则说明我们的类锁生效了,带着这个预期我们来看下,通过静态方法锁实现多个线程串行执行目标方法。
完整的代码实例:
public class ClassLock1 implements Runnable {
// 这两个实例,声明为static,是因为要在main方法中测试,与方法锁无关
static ClassLock1 instance1 = new ClassLock1();
static ClassLock1 instance2 = new ClassLock1();
// 关键: static synchronized两个关键字同时使用
private static synchronized void method() {
System.out.println("线程名:" + Thread.currentThread().getName() + ",运行开始");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程:" + Thread.currentThread().getName() + ",运行结束");
}
@Override
public void run() {
method();
}
public static void main(String[] args) {
Thread thread1 = new Thread(instance1);
Thread thread2 = new Thread(instance2);
thread1.start();
thread2.start();
try {
//两个线程必须都执行完毕后
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("两个线程都已经执行完毕");
}
}
运行结果:
线程名:Thread-1,运行开始
线程:Thread-1,运行结束
线程名:Thread-0,运行开始
线程:Thread-0,运行结束
两个线程都已经执行完毕
二.静态方法锁的实现原理:
当在静态方法上,使用synchronized
关键字修饰时,就可以是想方法的同步了。但其实使用类锁的第二个关键点在,不同实例的线程,调用类中的方法时,也就是上述代码中,main方法中的两个线程thread1
、thread2
分别对应的instance1
和instance2
。这个代码的精髓在于,同一个类中不同的两个对象(instance1
和instance2
)的两个线程,要控制这两个线程串行执行,则需要使用类锁。那大家可能有下面的疑问:为什么要用类锁,用对象锁会怎样?
为什么要用类锁,用对象锁会怎样?
先解释如果不加static
,类锁会变成什么?如果看过前一篇文章《Java中synchronized实现对象锁的两种方式及原理解析》,你会知道:
synchronized能够保证在同一时刻最多只有一个线程执行该段代码,以保证并发安全。
但只用的synchronized
修饰的方法其实是方法锁,也就是对象锁,对象锁的作用范围仅限于同一个对象内
。而上述代码中,我们有两个不同的对象(instance1
和instance2
),所以如果你把static
关键字去掉,则此同步方法就成了不同对象中的方法锁,又因为方法锁是对象锁中的一种实现形式,即此时instance1
和instance2
分别持有的是一把对象锁。对象锁的特性是,每个对象的对象锁只能锁住自己对象内的线程,而无法锁住其他的对象中的线程,这就是为何对象锁不能锁住不同对象中的线程的原因,这也是对象锁和类锁的区别。
方法锁的作用范围是,同一个对象内的不同线程,而类锁则可以控制不同对象中的不同线程。
为什么同步方法上一定要加上static
才能实现类锁?
同步方法加上static
关键字后,那此方法在class创建的时候,就已经初始化好了。类中所有的实例,同步使用这个方法,锁的作用范围是最大的。
如果不加static
关键字,则该方法会被instance1
和instance2
同时访问到,因为锁的范围仅仅局限在instance1
和instance2
对象中。此时是不能实现instance1
和instance2
两个对象所在的不同线程之间的同步。
三.synchronized(*.class)代码块方式实现类锁
类锁的第二种实现方式,它的语义是:
只要同步代码块中是*.class,不管有多少个对象调用这段代码,都表示大家共用一个对象,这就实现了不同的实例,串行的执行此代码块。
完整代码示例:
public class ClassLock2 implements Runnable {
static ClassLock2 instance1 = new ClassLock2();
static ClassLock2 instance2 = new ClassLock2();
@Override
public void run() {
method();
}
private void method() {
//关键:用*.class作为锁对象,即使在不同的实例中,也能保证线程安全
//对比:此形式类锁,与对象锁中的同步代码块锁,只有锁对象不同,同步代码块锁中的锁对象是this,而类锁是*.class)
synchronized (ClassLock2.class) {
System.out.println("线程名:" + Thread.currentThread().getName() + ",运行开始");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程:" + Thread.currentThread().getName() + ",运行结束");
}
}
public static void main(String[] args) {
Thread thread1 = new Thread(instance1);
Thread thread2 = new Thread(instance2);
thread1.start();
thread2.start();
try {
//两个线程必须都执行完毕后
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("两个线程都已经执行完毕");
}
}
运行结果
线程名:Thread-0,运行开始
线程:Thread-0,运行结束
线程名:Thread-1,运行开始
线程:Thread-1,运行结束
两个线程都已经执行完毕
可以看出,两个线程依然是串行执行,此种方式和上一种方式的结果一致。
为什么Java的synchronized
实现类锁或对象锁提供了两种方式?
这两种方式是:使用synchronized
修饰方法或使用synchronized
代码块。
提供两种方式的原因是因为synchronized
关键字的缺陷之一就是效率低。
synchronized只有当同步方法执行完或执行时抛出异常或调用wait系列方法时,才会释放锁。若不释放锁,其他线程只能等待,所以相对相率比较低。
做任何事,提高效率的第一性原理是:
在满足要求的情况下,尽可能的少做事
所以synchronized
关键字除了可以加在方法上之外,还提供了同步代码块,可以让其在方法内部使用。我们在程序编写时,应该在满足要求的情况下,尽可能的减少synchronized
代码块内的代码量,因为synchronized
代码块内的代码,都是串行执行的,效率自然不如多个线程并行处理效率高,所以应该将那些不需要同步的代码移动到synchronized
代码块外执行,这样的效率会更高。这就是synchronized
为什么提供两种方式做同步。
四.总结
本文承接上文《Java中synchronized实现对象锁的两种方式及原理解析》讲解完对象锁后,又讲解了类锁。Java中的synchronized
实现的对象锁和类锁的各种实现方式已经交代清楚,以及其实现原理也做了初步的解析。
但这些只是基础部分,接下来的文章中,将重点分析7中不同情况下的synchronized
是否能够保证线程安全,彻底理解synchronized
关键字的作用域,这样才能不再使用同步的过程中,给自己挖坑,并且后续文章中,也是面试官常常问到场景,绝对不容错过。喜欢本文,请点赞和收藏。