并发编程中,锁是常常须要用到的,今天咱们一块儿来看下Java中的锁机制:synchronized和lock。java
锁的种类挺多,包括:自旋锁、自旋锁的其余种类、阻塞锁、可重入锁、读写锁、互斥锁、悲观锁、乐观锁、公平锁、可重入锁等等,其他就不列出了。咱们这边重点看以下几种:可重入锁、读写锁、可中断锁、公平锁。编程
若是锁具有可重入性,则称做为可重入锁。synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上代表了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举好比说,当一个线程执行到method1 的synchronized方法时,而在method1中会调用另一个synchronized方法method2,此时该线程没必要从新去申请锁,而是能够直接执行方法method2。bash
读写锁将对一个资源的访问分红了2个锁,如文件,一个读锁和一个写锁。正由于有了读写锁,才使得多个线程之间的读操做不会发生冲突。ReadWriteLock
就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。能够经过readLock()获取读锁,经过writeLock()获取写锁。微信
可中断锁,便可以中断的锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁。 若是某一线程A正在执行锁中的代码,另外一线程B正在等待获取该锁,可能因为等待时间过长,线程B不想等待了,想先处理其余事情,咱们可让它中断本身或者在别的线程中中断它,这种就是可中断锁。数据结构
Lock接口中的lockInterruptibly()方法就体现了Lock的可中断性。多线程
公平锁即尽可能以请求锁的顺序来获取锁。同时有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最早请求的线程)会得到该锁,这种就是公平锁。并发
非公平锁即没法保证锁的获取是按照请求锁的顺序进行的,这样就可能致使某个或者一些线程永远获取不到锁。框架
synchronized
是非公平锁,它没法保证等待的线程获取锁的顺序。对于ReentrantLock
和ReentrantReadWriteLock
,默认状况下是非公平锁,可是能够设置为公平锁。性能
synchronized是Java的关键字,当它用来修饰一个方法或者一个代码块的时候,可以保证在同一时刻最多只有一个线程执行该段代码。简单总结以下四种用法。测试
对某一代码块使用,synchronized后跟括号,括号里是变量,一次只有一个线程进入该代码块。
public int synMethod(int m){
synchronized(m) {
//...
}
}
复制代码
方法声明时使用,放在范围操做符以后,返回类型声明以前。即一次只能有一个线程进入该方法,其余线程要想在此时调用该方法,只能排队等候。
public synchronized void synMethod() {
//...
}
复制代码
synchronized后面括号里是一对象,此时线程得到的是对象锁。
public void test() {
synchronized (this) {
//...
}
}
复制代码
synchronized后面括号里是类,若是线程进入,则线程在该类中全部操做不能进行,包括静态变量和静态方法,对于含有静态方法和静态变量的代码块的同步,一般使用这种方式。
Lock接口主要相关的类和接口以下。
ReadWriteLock是读写锁接口,其实现类为ReetrantReadWriteLock。ReetrantLock实现了Lock接口。
Lock中有以下方法:
public interface Lock {
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
复制代码
lock:用来获取锁,若是锁被其余线程获取,处于等待状态。若是采用Lock,必须主动去释放锁,而且在发生异常时,不会自动释放锁。所以通常来讲,使用Lock必须在try{}catch{}块中进行,而且将释放锁的操做放在finally块中进行,以保证锁必定被被释放,防止死锁的发生。
lockInterruptibly:经过这个方法去获取锁时,若是线程正在等待获取锁,则这个线程可以响应中断,即中断线程的等待状态。
tryLock:tryLock方法是有返回值的,它表示用来尝试获取锁,若是获取成功,则返回true,若是获取失败(即锁已被其余线程获取),则返回false,也就说这个方法不管如何都会当即返回。在拿不到锁时不会一直在那等待。
tryLock(long,TimeUnit):与tryLock相似,只不过是有等待时间,在等待时间内获取到锁返回true,超时返回false。
unlock:释放锁,必定要在finally块中释放
实现了Lock接口,可重入锁,内部定义了公平锁与非公平锁。默认为非公平锁:
public ReentrantLock() {
sync = new NonfairSync();
}
复制代码
能够手动设置为公平锁:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
复制代码
public interface ReadWriteLock {
Lock readLock(); //获取读锁
Lock writeLock(); //获取写锁
}
复制代码
一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操做分开,分红2个锁来分配给线程,从而使得多个线程能够同时进行读操做。ReentrantReadWirteLock实现了ReadWirteLock接口,并未实现Lock接口。 不过要注意的是:
若是有一个线程已经占用了读锁,则此时其余线程若是要申请写锁,则申请写锁的线程会一直等待释放读锁。
若是有一个线程已经占用了写锁,则此时其余线程若是申请写锁或者读锁,则申请的线程会一直等待释放写锁。
ReetrantReadWriteLock一样支持公平性选择,支持重进入,锁降级。
public class RWLock {
static Map<String, Object> map = new HashMap<String, Object>();
static ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
static Lock r = rwLock.readLock();
static Lock w = rwLock.writeLock();
//读
public static final Object get(String key){
r.lock();
try {
return map.get(key);
} finally {
r.unlock();
}
}
//写
public static final Object put(String key, Object value){
w.lock();
try {
return map.put(key, value);
} finally {
w.unlock();
}
}
}
复制代码
只需在读操做时获取读锁,写操做时获取写锁。当写锁被获取时,后续的读写操做都会被阻塞,写锁释放后,全部操做继续执行。
下面对synchronized与Lock进行性能测试,分别开启10个线程,每一个线程计数到1000000,统计两种锁同步所花费的时间。网上也能找到这样的例子。
public class TestAtomicIntegerLock {
private static int synValue;
public static void main(String[] args) {
int threadNum = 10;
int maxValue = 1000000;
testSync(threadNum, maxValue);
testLocck(threadNum, maxValue);
}
//test synchronized
public static void testSync(int threadNum, int maxValue) {
Thread[] t = new Thread[threadNum];
Long begin = System.nanoTime();
for (int i = 0; i < threadNum; i++) {
Lock locks = new ReentrantLock();
synValue = 0;
t[i] = new Thread(() -> {
for (int j = 0; j < maxValue; j++) {
locks.lock();
try {
synValue++;
} finally {
locks.unlock();
}
}
});
}
for (int i = 0; i < threadNum; i++) {
t[i].start();
}
//main线程等待前面开启的全部线程结束
for (int i = 0; i < threadNum; i++) {
try {
t[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("使用lock所花费的时间为:" + (System.nanoTime() - begin));
}
// test Lock
public static void testLocck(int threadNum, int maxValue) {
int[] lock = new int[0];
Long begin = System.nanoTime();
Thread[] t = new Thread[threadNum];
for (int i = 0; i < threadNum; i++) {
synValue = 0;
t[i] = new Thread(() -> {
for (int j = 0; j < maxValue; j++) {
synchronized(lock) {
++synValue;
}
}
});
}
for (int i = 0; i < threadNum; i++) {
t[i].start();
}
//main线程等待前面开启的全部线程结束
for (int i = 0; i < threadNum; i++) {
try {
t[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("使用synchronized所花费的时间为:" + (System.nanoTime() - begin));
}
}
复制代码
测试结果的差别仍是比较明显的,Lock的性能明显高于synchronized。本次测试基于JDK1.8。
使用lock所花费的时间为:436667997
使用synchronized所花费的时间为:616882878
复制代码
JDK1.5中,synchronized是性能低效的。由于这是一个重量级操做,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操做都须要转入内核态中完成,这些操做给系统的并发性带来了很大的压力。相比之下使用Java提供的Lock对象,性能更高一些。多线程环境下,synchronized的吞吐量降低的很是严重,而ReentrankLock则能基本保持在同一个比较稳定的水平上。
到了JDK1.6,发生了变化,对synchronize加入了不少优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。致使在JDK1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在将来的版本中还有优化余地,因此仍是提倡在synchronized能实现需求的状况下,优先考虑使用synchronized来进行同步。
本文主要对并发编程中的锁机制synchronized和lock,进行详解。synchronized是基于JVM实现的,内置锁,Java中的每个对象均可以做为锁。对于同步方法,锁是当前实例对象。对于静态同步方法,锁是当前对象的Class对象。对于同步方法块,锁是Synchonized括号里配置的对象。Lock是基于在语言层面实现的锁,Lock锁能够被中断,支持定时锁。Lock能够提升多个线程进行读操做的效率。经过对比得知,Lock的效率是明显高于synchronized关键字的,通常对于数据结构设计或者框架的设计都倾向于使用Lock而非Synchronized。