从Java5之后,在java.util.concurrent.locks包下提供了另外一种方式来实现同步访问,那就是Lock。讨论lock前先了解下synchronized
一:synchronized
synchronized是java中的一个关键字,也就是说是Java语言内置的特性,如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁
释放的两种情况
1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有,在次期间该对象不能被释放或者操作。
2) 线程发生异常,JVM自动会释放锁,无需用户处理。
如果一个对象被加了synchronized,那么不管线程是读或者写操作都不可以操作。我们来看一段代码加深下synchronized的理解
public class SynchronizedTest { public static void main(String[] args) { Thread a = new ThreadRunC(); a.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("我是主线程 不加同步"+C.C); synchronized (C.C) { System.out.println("我是主线程:" + C.C); } } } class ThreadRunC extends Thread { public void run() { synchronized (C.C) { System.out.println("我要开始执行任务C。。。。" + Thread.currentThread().getName()); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("我在执行任务结束了C。。。。" + Thread.currentThread().getName()); } } } class C { static Integer C = new Integer(1); }
结果:
我要开始执行任务C。。。。Thread-0 我是主线程 不加同步1 我在执行任务结束了C。。。。Thread-0 我是主线程:1
有此可见C.C对象在进入子线程后休眠了5s,那么主线程的synchronized (C.C)一直处于等待状态,直到子线程结束,它才执行。
二:Lock
Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock(jdk1.5以后)是一个类(实际是接口),通过这个类可以实现同步访问,它和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
public interface Lock { void lock(); //获取锁。 void lockInterruptibly() throws InterruptedException; //如果当前线程未被中断,则获取锁。 boolean tryLock(); // 仅在调用时锁为空闲状态才获取该锁。 boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁。 void unlock();// 释放锁。 Condition newCondition(); //返回绑定到此 Lock 实例的新 Condition 实例。 }
1:ReentrantLock
ReentrantLock,意思是“可重入锁”,ReentrantLock由最近成功获取锁,还没有释放的线程所拥有,当锁被另一个线程拥有时,调用lock的线程可以成功获取锁。如果锁已经被当前线程拥有,当前线程会立即返回。此类的构造方法提供一个可选的公平参数。默认是非公平锁
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
公平与非公平有何区别,所谓公平就是严格按照FIFO顺序获取锁,非公平安全由程序员自己设计,比如可以按优先级,也可以按运行次数等规则来选择。
公平加锁代码
/** * Sync object for fair locks */ static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
1、首先判断锁有没有被持有,如果被持有,就判断持有锁的线程是不是当前线程,如果不是就啥也不做,返回获取失败,如果是就增加重入数,返回成功获取;
2、如果锁没有被任何线程持有(c==0),首先判断当前结点前面是否还有线程在排除等待锁,如果有,直接返回获取失败,否则将锁持有数设为acquires,一般为1,然后设置锁的拥有者为当前线程,成功获取。
非公平加锁
static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } } final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
代码几乎一模一样,唯一不同的是在知道锁持有数为0时,直接将当前线程设置为锁的持有者,这一点和公平版本的tryAcquire是有区别的,也就是说非公平机制采用的是抢占式模型。
2.ReadWriteLock
public interface ReadWriteLock { /** * Returns the lock used for reading. * * @return the lock used for reading. */ Lock readLock(); /** * Returns the lock used for writing. * * @return the lock used for writing. */ Lock writeLock(); }
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的ReentrantReadWriteLock实现了ReadWriteLock接口。
3 ReentrantReadWriteLock
ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁。实现了ReadWriteLock接口
测试代码
public class LockRWDemo { static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); public static void main(String[] args) { Thread a = new ThreadRunW(); Thread b = new ThreadRunW(); a.start(); b.start(); } } class ThreadRunR extends Thread { public void run() { LockRWDemo.rwl.readLock().lock();; try { long start = System.currentTimeMillis(); while(System.currentTimeMillis() - start <= 1) { System.out.println(Thread.currentThread().getName()+"正在进行读操作"); } System.out.println(Thread.currentThread().getName()+"读操作完毕"); } finally { LockRWDemo.rwl.readLock().unlock(); } } }
读的操作是两个线程交替的进行的,这样就大大提升了读操作的效率。
不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
class ThreadRunW extends Thread { public void run() { LockRWDemo.rwl.writeLock().lock();; try { long start = System.currentTimeMillis(); while(System.currentTimeMillis() - start <= 1) { System.out.println(Thread.currentThread().getName()+"正在进行写操作"); } System.out.println(Thread.currentThread().getName()+"写操作完毕"); } finally { LockRWDemo.rwl.writeLock().unlock(); } } }
此代码的结果是只能等一个线程结束了才能执行另外一个。
三:Lock和synchronized区别和选择
总结来说,Lock和synchronized有以下几点不同:
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。