Java并发编程中的锁机制与同步策略
立即解锁
发布时间: 2025-08-17 02:17:19 阅读量: 1 订阅数: 3 

### Java并发编程中的锁机制与同步策略
在Java并发编程中,锁机制是实现线程安全的重要手段。通过合理使用锁,可以避免多个线程同时访问共享资源时产生的数据不一致问题。本文将详细介绍Java中锁的使用、同步方法和块、完全同步对象、遍历策略、单例模式中的锁以及死锁和资源排序等内容。
#### 1. 锁的基本概念
数组对象持有标量元素时,数组本身拥有锁,但单个标量元素没有锁,且无法将数组元素声明为`volatile`。锁定一个对象数组不会自动锁定其所有元素,也没有在单个原子操作中同时锁定多个对象的构造。类实例是对象,与类对象关联的锁用于静态同步方法。
#### 2. 同步方法和块
基于`synchronized`关键字有两种语法形式:块和方法。块同步需要指定要锁定的对象,这使得任何方法都可以锁定任何对象,最常见的参数是`this`。
块同步比方法同步更基础,例如:
```java
synchronized void f() { /* body */ }
```
等价于:
```java
void f() { synchronized(this) { /* body */ } }
```
`synchronized`关键字不被视为方法签名的一部分,因此子类重写超类方法时,`synchronized`修饰符不会自动继承,接口中的方法不能声明为`synchronized`,构造函数也不能使用`synchronized`修饰,但可以在构造函数中使用块同步。
子类中的同步实例方法与超类使用相同的锁,而内部类方法的同步与外部类独立。不过,非静态内部类方法可以通过代码块锁定其包含类,例如:
```java
synchronized(OuterClass.this) { /* body */ }
```
#### 3. 获取和释放锁
锁定遵循内置的获取 - 释放协议,仅由`synchronized`关键字控制。所有锁定都是块结构的,进入同步方法或块时获取锁,退出时释放锁,即使退出是由于异常导致的,也不会忘记释放锁。
锁是按线程而不是按调用操作的。如果锁是空闲的或者线程已经持有该锁,线程可以通过同步点,否则会阻塞。这种可重入或递归锁定与POSIX线程等使用的默认策略不同,它允许一个同步方法在同一对象上自调用另一个同步方法而不会冻结。
同步方法或块仅相对于同一目标对象上的其他同步方法和块遵循获取 - 释放协议,未同步的方法仍然可以随时执行,即使同步方法正在执行。`synchronized`并不等同于原子操作,但可以用于实现原子性。
当一个线程释放锁时,另一个线程可能获取它,但无法保证哪个阻塞线程将接下来获取锁以及何时获取。也没有机制可以发现给定的锁是否被某个线程持有。此外,`synchronized`除了控制锁定外,还会同步底层内存系统。
#### 4. 静态同步
锁定一个对象不会自动保护对该对象类或其任何超类的静态字段的访问,而是通过同步静态方法和块来保护对静态字段的访问。静态同步使用与声明静态方法的类关联的类对象所拥有的锁。类`C`的静态锁也可以在实例方法中通过以下方式访问:
```java
synchronized(C.class) { /* body */ }
```
每个类关联的静态锁与其他类(包括其超类)的静态锁无关。在子类中添加新的静态同步方法来保护超类中声明的静态字段是无效的,建议使用显式的块版本。另外,不建议使用以下形式:
```java
synchronized(getClass()) { /* body */ }
```
这会锁定实际的类,可能与需要保护静态字段的类不同。
JVM在类加载和初始化期间会内部获取和释放类对象的锁。除非编写特殊的`ClassLoader`或在静态初始化序列中持有多个锁,否则这些内部机制不会干扰对类对象进行同步的普通方法和块的使用。但如果子类化`java.*`类,应该了解这些类中使用的锁定策略。
#### 5. 完全同步对象
锁是最基本的消息接受控制机制,可以用于阻止客户端在另一个方法或代码块(在不同线程中运行)正在执行时尝试调用对象的方法。基于锁定的最安全(但并不总是最佳)的并发面向对象设计策略是关注完全同步对象(也称为原子对象),满足以下条件:
- 所有方法都是同步的。
- 没有公共字段或其他封装违规。
- 所有方法都是有限的(没有无限循环或无界递归),最终会释放锁。
- 所有字段在构造函数中初始化为一致状态。
- 对象的状态在每个方法的开始和结束时都是一致的(遵循不变量),即使存在异常。
例如,以下是一个简化的`ExpandableArray`类:
```java
class ExpandableArray {
protected Object[] data; // the elements
protected int size = 0; // the number of array slots used
// INV: 0 <= size <= data.length
public ExpandableArray(int cap) {
data = new Object[cap];
}
public synchronized int size() {
return size;
}
public synchronized Object get(int i) // subscripted access
throws NoSuchElementException {
if (i < 0 || i >= size )
throw new NoSuchElementException();
return data[i];
}
public synchronized void add(Object x) { // add at end
if (size == data.length) { // need a bigger array
Object[] olddata = data;
data = new Object[3 * (size + 1) / 2];
System.arraycopy(olddata, 0, data, 0, olddata.length);
}
data[size++] = x;
}
public synchronized void removeLast()
throws NoSuchElementException {
if (size == 0)
throw new NoSuchElementException();
data[--size] = null;
}
}
```
如果没有同步,这个类的实例在并发环境中无法可靠使用,可能会遇到读写冲突或写写冲突。
#### 6. 遍历策略
在完全同步的类中,可以通过将操作封装在同步方法中来添加另一个原子操作。但对于集合的遍历操作,由于客户端可能想要对集合元素应用的操作数量是无限的,将所有操作编码为同步方法是没有意义的。常见的遍历解决方案有以下三种:
##### 6.1 同步聚合操作
将应用于每个元素的操作抽象出来,作为参数传递给单个同步的`applyToAll`方法。例如:
```java
interface Procedure {
void apply(Object obj);
}
class ExpandableArrayWithApply extends ExpandableArray {
public ExpandableArrayWithApply(int cap) { super(cap); }
synchronized void applyToAll(Procedure p) {
for (int i = 0; i < size; ++i)
p.apply(data[i]);
}
}
```
可以使用以下方式打印集合中的所有元素:
```java
v.applyToAll(new Procedure() {
public void apply(Object obj) {
System.out.println(obj);
}
});
```
这种方法可以消除遍历期间其他线程添加或删除元素可能导致的干扰,但可能会长时间持有集合的锁,导致活性和性能问题。
##### 6.2 索引遍历和客户端侧锁定
要求客户端使用索引访问方法进行遍历,例如:
```java
for (int i = 0; i < v.size(); ++i)
System.out.println(v.get(i));
```
这种方法在执行每个元素操作时避免持有锁,但每个元素需要进行两次同步操作(`size`和`get`)。更重要的是,由于锁定粒度更细,可能会出现干扰问题,需要使用客户端侧锁定来保持大小检查和访问的原子性:
```java
for (int i = 0; true; ++i) {
Object obj = null;
synchronized(v) {
if (i < v.size())
obj = v.get(i);
else
break;
}
System.out.println(obj);
}
```
但这种方法也可能有问题,例如如果类支持元素重排方法,可能会多次打印同一个元素。作为更极端的措施,客户端可以用`synchronized(v)`包围整个遍历,但这可能会导致长时间锁定问题。如果元素操作耗时,可以先复制数组进行遍历。
客户端侧锁定在非面向对象的多线程编程中更常用,这种风格有时更灵活,但会破坏封装性。正确性依赖于对类内部工作原理的特殊知识,如果类后来被修改,这些知识可能不再适用。不过,在封闭子系统中,这种方法可能是可以接受的。
##### 6.3 版本化迭代器
集合类支持快速失败迭代器,如果在遍历过程中集合被修改,迭代器会抛出异常。最简单的方法是维护一个版本号,每次更新集合时递增该版本号。迭代器在请求下一个元素时检查该值,如果发生变化则抛出异常。版本号字段应该足够宽,以确保在遍历过程中不会溢出,通常`int`类型就足够了。
以下是一个应用版本化迭代器的`ExpandableArrayWithIterator`类:
```java
class ExpandableArrayWithIterator extends ExpandableArray {
protected int version = 0;
public ExpandableArrayWithIterator(int cap) { super(cap); }
public synchronized void removeLast()
throws NoSuchElementException {
super.removeLast();
++version; // advertise update
}
public synchronized void add(Object x) {
super.add(x);
++version;
}
public synchronized Iterator iterator() {
return new EAIterator();
}
protected class EAIterator implements Iterator {
protected final int currentVersion;
protected int currentIndex = 0;
EAIterator() { currentVersion = version; }
public Object next() {
synchronized(ExpandableArrayWithIterator.this) {
if (currentVersion != version)
```
0
0
复制全文
相关推荐










