背景
学习并发编程的时候,用到synchronized关键字,这里锁的是对象,对象分两种,一种是类对象,一种是实例对象,然后对于锁住类对象的时候,会不会阻塞实例对象这个问题,自己理解的不好,之前以为会,但其实不会,于是搜索了下,在此做个记录。
结果
在Java中,synchronized关键字可以用于方法或代码块,用来确保同一时间只有一个线程能够执行被锁定的代码段。synchronized可以通过两种方式来锁定对象:锁住实例对象(普通对象)和锁住类对象(静态上下文)。这两者的区别主要体现在以下几个方面:
1. 锁定范围
-
锁住普通对象:当使用
this或者一个特定的对象作为锁时,锁的作用范围是该对象的实例。这意味着在同一时间,对于同一个实例对象,只能有一个线程可以进入被synchronized修饰的方法或代码块。不同的实例对象之间没有互斥关系。 -
锁住类对象:当使用
Class对象或者静态方法上的synchronized时,锁的是整个类,即YourClassName.class。这表示无论创建了多少个该类的实例,所有线程访问这个类中的同步静态方法或同步代码块时,都会受到同一个锁的控制,因此同一时刻只能有一个线程能进入。
2. 对象锁 vs 类锁
-
对象锁:每个对象都有一个与之关联的内置锁。当一个线程进入一个实例对象的
synchronized方法或代码块时,它会获取该对象的锁。其他试图访问同一个对象的线程将被阻塞,直到第一个线程释放锁。 -
类锁:每个类也有一个与之关联的锁,通常通过
Class对象来表示。当一个线程进入一个静态的synchronized方法或代码块时,它会获取该类的锁。由于类锁是针对整个类而不是单个实例,所以它会影响所有该类的实例。
3. 使用场景
-
普通对象锁:当你需要保证对某个特定对象状态的操作是线程安全的时候,应该使用普通对象锁。例如,如果你有一个银行账户类,你可能希望确保每次转账操作都是原子性的,这时你可以用
synchronized修饰账户类的转账方法。 -
类对象锁:当你需要保证某些操作在整个类级别上是线程安全的时候,比如初始化资源、读取配置文件等,这时候你应该使用类对象锁。例如,如果你有一个单例模式的实现,你可能会使用类锁来确保只有一个实例被创建。
4. 示例代码
- 锁住普通对象
public class MyClass {
public synchronized void instanceMethod() {
// 这里的锁是当前实例对象 (this)
}
public void anotherInstanceMethod() {
synchronized (this) {
// 这里的锁也是当前实例对象 (this)
}
}
}
- 锁住类对象
public class MyClass {
public static synchronized void staticMethod() {
// 这里的锁是 MyClass.class
}
public void anyInstanceMethod() {
synchronized (MyClass.class) {
// 这里的锁也是 MyClass.class
}
}
}
选择哪种锁取决于你的具体需求。如果只需要保护特定实例的状态,那么就使用实例锁;如果需要保护整个类级别的状态或资源,则应使用类锁。
类锁会组设实例锁吗?
当一个线程锁住了类对象(即静态上下文的锁),而另一个线程尝试锁住同一个类的不同实例对象时,这两个锁是独立的。因此,如果一个线程正在执行一个需要类锁的方法,而另一个线程尝试执行一个需要实例锁的方法,那么第二个线程不会被阻塞,它会继续执行。
具体来说:
- 类锁(
MyClass.class):这是针对整个类的锁,所有线程在访问该类的同步静态方法或代码块时都会竞争这个锁。 - 实例锁(
this):这是针对特定对象实例的锁,每个对象实例都有自己的锁,线程在访问该实例的同步非静态方法或代码块时会竞争相应的实例锁。
由于类锁和实例锁是不同的锁,它们之间没有互斥关系。因此,如果一个线程持有类锁,其他线程仍然可以获取并持有任何该类的实例锁,反之亦然。这意味着两个线程可以同时执行,一个持有类锁,另一个持有实例锁,而不会相互等待。
示例
假设我们有以下代码:
public class MyClass {
// 静态同步方法,使用类锁
public static synchronized void classMethod() {
try {
System.out.println("Class method is running.");
Thread.sleep(2000); // 模拟长时间操作
System.out.println("Class method finished.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 实例同步方法,使用实例锁
public synchronized void instanceMethod() {
try {
System.out.println("Instance method is running.");
Thread.sleep(2000); // 模拟长时间操作
System.out.println("Instance method finished.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
如果我们创建两个线程,一个调用classMethod(),另一个调用instanceMethod(),并且这两个方法都是同步的但使用了不同类型的锁,那么这两个线程将可以并发执行,不会因为对方持有的锁而被阻塞。
测试代码
你可以通过下面的测试代码来验证这一点:
public class TestSynchronization {
public static void main(String[] args) {
final MyClass myObject = new MyClass();
// 线程1:调用静态同步方法
Thread t1 = new Thread(() -> {
MyClass.classMethod();
});
// 线程2:调用实例同步方法
Thread t2 = new Thread(() -> {
myObject.instanceMethod();
});
// 启动线程
t1.start();
t2.start();
// 等待两个线程完成
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
当你运行这段代码时,你会看到两个线程几乎同时开始,并且classMethod和instanceMethod几乎同时输出“is running.”,然后大约两秒钟后各自输出“finished.”,这表明两个线程确实是在并发执行的,而不是互相等待。
注意事项
尽管类锁和实例锁是独立的,但在设计多线程程序时仍需谨慎,确保不会出现死锁或其他并发问题。此外,如果多个线程需要对共享资源进行协调访问,应该考虑使用更高级别的同步机制,如ReentrantLock、Semaphore、CountDownLatch等,或者使用Java的并发工具包中的类,如ConcurrentHashMap、CopyOnWriteArrayList等。
为什么不会阻塞那?
类锁不会阻塞实例锁的原因在于Java中的锁机制是基于对象的内在锁(intrinsic lock)或监视器(monitor)。每个对象,包括类本身(通过其Class对象),都有一个与之关联的内在锁。当一个线程进入一个synchronized方法或代码块时,它实际上是在获取该方法或代码块所指定的对象的内在锁。
内在锁的工作原理
-
实例锁:当一个线程进入一个同步的实例方法或使用
this作为锁的同步代码块时,它会尝试获取调用该方法的对象的内在锁。如果另一个线程已经持有了这个对象的锁,那么当前线程将被阻塞,直到锁被释放。 -
类锁:当一个线程进入一个同步的静态方法或使用
Class对象作为锁的同步代码块时,它会尝试获取该类的Class对象的内在锁。这意味着所有静态同步方法和使用类锁的同步代码块共享同一个锁,即该类的Class对象的锁。
类锁和实例锁的区别
-
不同的锁对象:类锁和实例锁实际上是两个不同的锁对象。类锁是与类的
Class对象关联的,而实例锁是与具体的对象实例关联的。因此,持有类锁的线程并不会影响到其他线程获取某个实例的锁,反之亦然。 -
互不干扰:由于类锁和实例锁是独立的,它们之间没有直接的关系。一个线程持有的类锁不会阻止其他线程获取同一类的不同实例的锁,也不会阻止其他线程获取其他类的实例锁或类锁。同样地,持有实例锁的线程也不会阻止其他线程获取类锁。
为什么这样设计?
这种设计使得Java的并发控制更加灵活和高效。例如:
-
资源隔离:不同类型的资源可以通过不同的锁来保护,从而避免不必要的等待。比如,静态资源可以用类锁保护,而实例级别的资源可以用实例锁保护。
-
提高并发性:允许更多的线程并行执行,只要它们操作的是不同类型的锁或不同的锁对象。这可以显著提高程序的性能,特别是在多核处理器环境中。
-
简化编程模型:开发者可以根据需要选择合适的锁粒度,而不必担心不同类型的锁之间的相互干扰。
示例解释
假设有一个类MyClass,并且有两个线程T1和T2:
T1调用了MyClass的一个静态同步方法classMethod(),这时T1获得了MyClass.class的类锁。T2调用了MyClass的一个实例同步方法instanceMethod(),这时T2试图获得this(即MyClass的一个实例)的实例锁。
因为T1持有的是MyClass.class的锁,而T2试图获取的是this的锁,这两个锁是完全不同的对象,所以T2不会被T1持有的类锁所阻塞,它可以继续执行。
总结
类锁和实例锁之所以不会互相阻塞,是因为它们是基于不同的锁对象(类的Class对象和具体的实例对象)。这种设计让Java的并发编程更加灵活,允许更高的并发性和更好的资源管理。理解这一点对于正确实现线程安全的程序非常重要。