一、Synchronized
1.1 概念
关键字synchronized是一种内置的锁机制,用来支持原子性(指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉)。
被synchronized包括的代码叫做同步代码块,synchronized可以保证在同一时刻,只有一个线程可以执行这个同步代码块,同时synchronized可以保证可见性(volatile也可以保证可见性)。
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁(而且无论是通过正常的控制路径退出,还是通过从代码块中抛出异常退出都能自动释放锁)。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
Java的内置锁相当于一种互斥体(或互斥锁),这意味着最多只有一个线程能持有这种锁。 当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或者阻塞,直到线程B释放这 个锁。如果B永远不释放锁,那么A也将永远地等下去。
由于每次只能有一个线程执行内置锁保护的代码块,因此,由这个锁保护的同步代码块会以原子方式执行,多个线程在执行该代码块时也不会相互干扰。任何一个执行同步代码块的线程,都不可能看到有其他线程正在执行由同一个锁保护的同步代码块。
1.2 示例
public class Test {
Object lock = new Object();
public void hello () {
synchronized(lock) {
System.out.println(Thread.currentThread().getName());
for (int i= 0; i < 10; i++) {
System.out.println("i: " + i);
}
}
}
public static void main(String[] args) {
Test test = new Test();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
test.hello();
}
});
}
}
}
1.3 结果
pool-1-thread-2
i: 0
i: 1
i: 2
i: 3
i: 4
i: 5
i: 6
i: 7
i: 8
i: 9
pool-1-thread-6
i: 0
i: 1
i: 2
i: 3
i: 4
i: 5
i: 6
i: 7
i: 8
i: 9
。。。。。。
从结果可以看出同一时间只有一个线程可以获取锁,在使用完成释放后,下一个线程才能获取。
二、重入
当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“重人”意味着获取锁的操作的粒度是“线程”,而不是“调用”。
重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没 有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取 计数值置为1。如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时, 计数器会相应地递减。当计数值为0时,这个锁将被释放。
public class Widget {
public synchronized void doSomething() {
}
}
public class LoggingWidget extends Widget {
@Override
public synchronized void doSomething() {
super.doSomething(); // 掉用父类的doSomething()方法,如果没有重入机制线程将永远阻塞。
}
}
三、用锁来保护状态
由于锁能使其保护的代码路径以串行形式来访问,因此可以通过锁来构造一些协议以实现对共享状态的独占访问。只要始终遵循这些协议,就能确保状态的一致性。
对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。
每个共享的和可变的变量都应该只由一个锁来保栌,从而使維护人员知道是哪一个锁。
一种常见的加锁约定是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所 有访问可变状态的代码路径进行同步,使得在该对象上不会发生并发访问。在许多线程安全类 中都使用了这种模式,例如Vector和其他的同步集合类。
并非所有数据都需要锁的保护,只有被多个线程同时访问的可变数据才需要通过锁来保护。
当某个变量由锁来保护时,意味着在每次访问这个变量时都需要首先获得锁,这样就确保 在同一时刻只有一个线程可以访问这个变量。当类的不变性条件涉及多个状态变量时,那么还 有另外一个需求:在不变性条件中的每个变量都必须由同一个锁来保护。因此可以在单个原子 操作中访问或更新这些变量,从而确保不变性条件不被破坏。
对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。
如果同步可以避免竞态条件问题,那么为什么不在每个方法声明时都使用关键字 synchronized呢?事实上,如果不加区别地滥用synchronized,可能导致程序中出现过多的同步。此外,将每个方法都作为同步方法还可能导致活跃性问题 (Liveness)或性能问题(Performance)。此外,如果只是将每个方法都作为同步方法,例如Vector,那么并不足以确保Vector上复合操 作都是原子的:
// 线程不安全
if (!vector.contains(element)){
vector.add(element);
}