Java 5以前的并发编程
Java的线程模型建立在抢占式线程调度的基础上,也就是说:
- 所有线程可以很容易的共享同一进程中的对象。
- 能够引用这些对象的任何线程都可以修改这些对象。
- 为了保护数据,对象可以被锁住。
Java基于线程和锁的并发过于底层,而且使用锁很多时候都是很万恶的,因为它相当于让所有的并发都变成了排队等待。
在Java 5以前,可以用synchronized关键字来实现锁的功能,它可以用在代码块和方法上,表示在执行整个代码块或方法之前线程必须取得合适的锁。对于类的非静态方法(成员方法)而言,这意味这要取得对象实例的锁,对于类的静态方法(类方法)而言,要取得类的Class对象的锁,对于同步代码块,程序员可以指定要取得的是那个对象的锁。
不管是同步代码块还是同步方法,每次只有一个线程可以进入,如果其他线程试图进入(不管是同一同步块还是不同的同步块),JVM会将它们挂起(放入到等锁池中)。这种结构在并发理论中称为临界区(critical section)。这里我们可以对Java中用synchronized实现同步和锁的功能做一个总结:
- 只能锁定对象,不能锁定基本数据类型
- 被锁定的对象数组中的单个对象不会被锁定
- 同步方法可以视为包含整个方法的synchronized(this) { … }代码块
- 静态同步方法会锁定它的Class对象
- 内部类的同步是独立于外部类的
- synchronized修饰符并不是方法签名的组成部分,所以不能出现在接口的方法声明中
- 非同步的方法不关心锁的状态,它们在同步方法运行时仍然可以得以运行
- synchronized实现的锁是可重入的锁
在JVM内部,为了提高效率,同时运行的每个线程都会有它正在处理的数据的缓存副本,当我们使用synchronzied进行同步的时候,真正被同步的是在不同线程中表示被锁定对象的内存块(副本数据会保持和主内存的同步,现在知道为什么要用同步这个词汇了吧),简单的说就是在同步块或同步方法执行完后,对被锁定的对象做的任何修改要在释放锁之前写回到主内存中;在进入同步块得到锁之后,被锁定对象的数据是从主内存中读出来的,持有锁的线程的数据副本一定和主内存中的数据视图是同步的 。
基于synchronized关键字的锁机制有以下问题:
- 锁只有一种类型,而且对所有同步操作都是一样的作用
- 锁只能在代码块或方法开始的地方获得,在结束的地方释放
- 线程要么得到锁,要么阻塞,没有其他的可能性
Java 5的并发编程
Java 5对锁机制进行了重构,提供了显示的锁,这样可以在以下几个方面提升锁机制:
- 可以添加不同类型的锁,例如读取锁和写入锁
- 可以在一个方法中加锁,在另一个方法中解锁
- 可以使用tryLock方式尝试获得锁,如果得不到锁可以等待、回退或者干点别的事情,当然也可以在超时之后放弃操作
显示的锁都实现了java.util.concurrent.Lock接口,主要有两个实现类:
- ReentrantLock – 比synchronized稍微灵活一些的重入锁
- ReentrantReadWriteLock – 在读操作很多写操作很少时性能更好的一种重入锁
可重入锁
- 可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
- 可重入锁(Reentrant Lock),是指允许同一个线程多次对该锁进行acquire动作。对于不可重入的锁,当一个线程多次调用acquire后将造成死锁。
Reentrant 锁意味着什么呢?
简单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。
本文节选自:关于Java并发编程的总结和思考
作者:jiankunking 出处:http://blog.csdn.net/jiankunking