在并发环境下,解决共享资源冲突问题时,可以考虑使用锁机制。
1.
对象的锁 所有对象都自动含有单一的锁。 JVM负责跟踪对象被加锁的次数。如果一个对象被解锁,其计数变为0。在任务(线程)第一次给对象加锁的时候,计数变为1。每当这个相同的任务(线程)在此对象上获得锁时,计数会递增。 只有首先获得锁的任务(线程)才能继续获取该对象上的多个锁。 每当任务离开一个synchronized方法,计数递减,当计数为0的时候,锁被完全释放,此时别的任务就可以使用此资源。
2.synchronized
同步块 synchronized有两种格式: 格式1: synchronized(任何对象){ //访问共享变量的临界区(程序段),又称同步代码块 } 格式2:同步化方法。在方法的前面加上synchronized,如: public synchronized void add() { //临界区 } 共享变量所关联的对象锁是如何选择的?即 synchronized (任何对象) { //临界区 } 1.synchronized锁定的是一个具体对象,通常是共享变量的对象。用synchronized括起来的程序段是访问该共享变量的临界区,即synchronized代码块。由于所有锁定同一个对象的线程之间,在synchronized代码块上是互斥的,也就是说,这些线程的synchronized代码块之间是串行执行的,不再是互相交替穿插并发执行,因而保证了synchronized 代码块操作的原子性。但synchronized代码块与所有线程的非synchronized 代码块之间以及非synchronized代码块与非synchronized代码块之间都是互相交替穿插并发执行的,故synchronized代码块操作的原子性是逻辑上的,而不是物理上的不可打断。 2.每一个Java对象都有且只有一个对象锁。任何时刻,一个对象锁只能被一个线程所拥有。若两个或多个线程锁定的不是同一个对象,则它们的synchronized代码块可以互相交替穿插并发执行。 3.所有的非synchronized代码块或方法,都可自由调用。如线程A获得了对象锁,调用需要该对象锁的synchronized代码块,其他线程仍然可以自由调用所有非synchronized方法和代码 4.若线程A获得了对象O的对象锁,调用对象O的synchronized代码块或方法,则线程A仍然可以调用其他任何需要对象O的锁的synchronized代码块或方法,这是因为线程A已经获得了对象O的对象锁了。线程A同时可以调用需要另一个对象K的锁的synchronized代码块或方法,这意味着线程A同时拥有对象O和对象K的对象锁。 5.只有当一个线程执行完它所调用的synchronized代码块或方法时,无论是正常执行完,还是异常抛出,该线程才会释放所获取的对象锁。synchronized并不必然地保护数据。程序员应该仔细分析,识别出程序中所有的临界区,并对这些临界区施加synchronized机制。若有一处或多处遗漏,则共享变量中数据就会产生错误 6.临界区中的共享变量应定义为private型。否则,其他类的方法可能直接访问和操作该共享变量,这样synchronized的保护就失去了意义。所以只能通过临界区访问共享变量。故锁定的对象通常是this,即通常格式都是:synchronized(this){…} 7.一定要保证,所有对共享变量的访问与操作均在synchronized代码块中进行。 8.通常共享变量都是实例变量。若临界区中的共享变量是一个类变量,则问题复杂化了,因为类方法与实例方法均可访问类变量。而synchronized锁定的必须是对象,不能是类。建议是若临界区中的共享变量是一个类变量,则应该用类方法来访问操作该类变量。这个类方法成为一个临界区,必须将该类方法定义为synchronized方法。所有要访问该共享类变量的实例方法,都应该调用定义为synchronized的类方法进行。若实例方法一定要想在自己的代码内部,不通过synchronized的类方法访问共享类变量,则可通过synchronized(类名.class){…来访问类锁。Java中,每一个类都有一个类对象,这个类对象实际上是java.lang.Class的一个实例对象,所谓类锁就是这个类对象的一把锁。注意类锁与这个类的实例对象的对象锁虽然都是对象锁,却是不同的两把锁。所有像synchronized(类名.class()){=同步代码块} 这样锁定类对象(注意:不是锁定类的某一个实例对象),其中的synchronized代码块,都是串行执行的,访问或使用类锁要仔细考虑和权衡 9.当一个线程进入死亡状态,线程拥有的所有的对象锁都被释放。
3.Lock
对象锁
Java SE5引入了java.util.concurrent.lock类库,这是解决互斥问题的第二种机制。用ReentrantLock类创建一个Lock对象,来保护临界区。用ReentrantLock保护代码块的基本结构如下。 private Lock locker =new ReentrantLock(); locker.lock(); // 加锁 try{ … }finally{ locker.unlock(); // 解锁 } lock()与unlock()必须配套使用。必须确保lock()对应的unlock()一定会得到执行。因此,必须把unlock()放到finally块中,确保无论是正常执行,还是异常抛出,unlock()一定会得到执行。 synchronized和lock的区别: Lock 的锁定是通过代码实现的,而 synchronized 是在 JVM 层面上实现的 synchronized 在锁定时如果方法块抛出异常,JVM 会自动将锁释放掉,不会因为出了异常没有释放锁造成线程死锁。但是 Lock 的话就享受不到 JVM 带来自动的功能,出现异常时必须在 finally 将锁释放掉,否则将会引起死锁。 在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好,不管用没用过5.0多线程包的程序员都能理解。
ReentrantLock:
ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。
Atomic:
和上面的类似,不激烈情况下,性能比synchronized略逊,而激烈的时候,也能维持常态。激烈的时候,Atomic的性能会优于ReentrantLock一倍左右。但是其有一个缺点,就是只能同步一个值,一段代码中只能出现一个Atomic的变量,多于一个同步无效。因为他不能在多个Atomic之间同步。