一、synchronized的缺陷
synchronized是Java的关键字,也就是Java语言内置的特性。那么为什么会出现Lock呢?
线程等待
如果一个代码块被synchronized修饰,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里拿着锁的线程只会有两种情况释放锁:
1.获得锁的线程执行完了该代码块,然后线程释放对锁的占有。
2.线程执行发生了异常,JVM会让线程自动释放锁。
如果这个获取锁的线程由于要等待IO或者其他原因(比如sleep)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,会非常影响效率。而使用lock就可以改变这种局面。
读写问题
在例如,多个线程读写文件,读写操作会发生冲突现象,写写操作会发生冲突现象,但读读不会冲突。但是使用synchronized的话,无论如何都会冲突,因为一个线程拿到锁时,其他的都得等待。而使用lock可以改变这种局面、
总而言之,Lock提供了比synchronized更多的功能。区别如下:
1.Lock不是Java语言内置的,由concurrent提供,Lock是一个接口。 synchronized是Java语言的关键字,是Java内置的。
2。Lock和synchronized有一个很大的区别,synchronized不需要用户手动释放锁,当synchronized方法或代码块执行完后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果未主动释放锁,可能会死锁。
二、java.util.comcurrent.locks包下常用的类
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() tryLock() tryLock(long time,TimeUnit unit) lockInterruptibly()是用来获取锁的。unLock()方法是用来获取锁的。 newCondition先放一下。
上面四个获取锁的方法有何区别?
lock()
首先lock()是用的最多的方法啊,用来获取锁,如果锁已被其他线程获取,则进行等待。
lock必须主动释放锁,并且发生异常时,不会自动释放锁。因此,一般放到try catch块中,用finally来释放锁,以防死锁发生。通常使用下面的写法:
Lock lock = new ReentrantLock();
lock.lock();
try {
//执行任务
} catch (Exception e) {
// TODO: handle exception
}finally {
lock.unlock(); //释放锁
}
tryLock()
tryLock()方法是有返回值的,true表示成功获取锁,false表示获取失败。无论有没有获取到锁,这个方法都会立即返回,不会一直等待。一般写法:
Lock lock = new ReentrantLock();
if(lock.tryLock()){
try {
//执行任务
} catch (Exception e) {
// TODO: handle exception
}finally {
lock.unlock(); //释放锁
}
}else{
//拿不到锁的废物,做点别的吧
}
lockInterruptibly()
lockInteruptibly方法比较特殊,当通过这个方法获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就是说,当两个线程同时执行lock.lockInteruptibly()想获取某个锁时,假若此时线程A获得了锁,而线程B只有在等待,此时如果调用线程B的threadB.interrupt()方法能够中断线程B的等待过程。
由于lockInterruptibly的声明中抛出了异常,所以lock.lockInterruptibly必须放在try块中或者在调用lockinterruptibly()的方法外声明抛出InterrruptedException。lockInteruptibly一般的使用形式如下:
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
当一个线程获取锁之后,是不会被interrupt()方法中断的。因为interrupt方法只能中断阻塞的线程,不能中断正在运行的线程。因此当通过lockInterruptibly()方法获取某个锁时,如果获取不到,只有在等待情况下,是可以响应中断的。
而synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一种等下去。
ReentrantLock 可重入锁
ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。下面看下具体如何使用ReentrantLock。例子1,lock()的正确使用方法
public class Test {
private ArrayList<Integer> arrayList = new ArrayList<>();
public static void main(String[] args) {
final Test test = new Test();
new Thread() {
@Override
public void run() {
test.insert(Thread.currentThread()); //currentThread返回当前正在执行当前代码的线程引用
}
}.start();
new Thread() {
@Override
public void run() {
test.insert(Thread.currentThread()); //currentThread返回当前正在执行当前代码的线程引用
}
}.start();
}
public void insert(Thread thread){
Lock lock = new ReentrantLock();
lock.lock();
try {
System.out.println(thread.getName()+"得到了锁");
for(int i=0;i<5;i++){
arrayList.add(i);
}
} catch (Exception e) {
}finally {
System.out.println(thread.getName()+"释放了锁");
lock.unlock();
}
}
}
执行结果:
为什么会出现这种结果呢,因为Lock lock = new ReentrantLock();写在了方法内部,这样每个线程执行时就会创建一个新的lock对象。 所以应该把这行代码,定义成成员变量。
public class Test {
private ArrayList<Integer> arrayList = new ArrayList<>();
Lock lock = new ReentrantLock();
public static void main(String[] args) {
final Test test = new Test();
new Thread() {
@Override
public void run() {
test.insert(Thread.currentThread()); //currentThread返回当前正在执行当前代码的线程引用
}
}.start();
new Thread() {
@Override
public void run() {
test.insert(Thread.currentThread()); //currentThread返回当前正在执行当前代码的线程引用
}
}.start();
}
public void insert(Thread thread){
lock.lock();
try {
System.out.println(thread.getName()+"得到了锁");
for(int i=0;i<5;i++){
arrayList.add(i);
}
} catch (Exception e) {
}finally {
System.out.println(thread.getName()+"释放了锁");
lock.unlock();
}
}
}
结果:
tryLock的使用功能方法:
public class Test {
private ArrayList<Integer> arrayList = new ArrayList<>();
Lock lock = new ReentrantLock();
public static void main(String[] args) {
final Test test = new Test();
new Thread() {
@Override
public void run() {
test.insert(Thread.currentThread()); //currentThread返回当前正在执行当前代码的线程引用
}
}.start();
new Thread() {
@Override
public void run() {
test.insert(Thread.currentThread()); //currentThread返回当前正在执行当前代码的线程引用
}
}.start();
}
public void insert(Thread thread){
if(lock.tryLock()){
try {
System.out.println(thread.getName()+"得到了锁");
for(int i=0;i<5;i++){
arrayList.add(i);
}
} catch (Exception e) {
}finally {
System.out.println(thread.getName()+"释放了锁");
lock.unlock();
}
}else{
System.out.println(thread.getName()+"没拿到锁");
}
}
}
结果:
lockInterruptibly响应中断的使用方法:
public class Test {
private ArrayList<Integer> arrayList = new ArrayList<>();
Lock lock = new ReentrantLock();
public static void main(String[] args) {
final Test test = new Test();
Thread t1 = new Thread() {
@Override
public void run() {
try {
test.insert(Thread.currentThread());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
System.out.println(Thread.currentThread().getName()+"被中断");
// e.printStackTrace();
}
}
};
Thread t2 = new Thread() {
@Override
public void run() {
try {
test.insert(Thread.currentThread());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
System.out.println(Thread.currentThread().getName()+"被中断");
// e.printStackTrace();
}
}
};
t1.start();
t2.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.interrupt();
}
public void insert(Thread thread) throws InterruptedException{
lock.lockInterruptibly();
try {
System.out.println(thread.getName()+"得到了锁");
long startTime = System.currentTimeMillis();
for( ; ;) {
if(System.currentTimeMillis() - startTime >= 10000)
break;
//插入数据
}
} catch (Exception e) {
}finally {
System.out.println(thread.getName()+"释放了锁");
lock.unlock();
}
}
}
运行结果:等待的线程能够被中断
ReadWriteLock锁
readWrite锁是一个接口,它里面定义了两个方法:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
一个用来获取读锁,一个用来获取写锁。也就是说将文件读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作,下面的ReentrantReadWriteLock实现了ReadWriteLock接口。
ReentrantReadWriteLock
ReentrantReadWriteLock提供了很多丰富的方法,不过最主要的就是readLock和writeLock用来获取读锁和写锁。
下面举例说明ReentrantReadWriteLock的具体用法:
当有多个线程同时读时,先看下synchronized达到的效果:
public class Test {
public static void main(String[] args) {
Test test = new Test();
new Thread(){
@Override
public void run() {
test.method(Thread.currentThread());
}
}.start();
new Thread(){
@Override
public void run() {
test.method(Thread.currentThread());
}
}.start();
}
private synchronized void method(Thread thread){
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1){
System.out.println(thread.getName()+"正在读");
}
System.out.println(thread.getName()+"读完毕");
}
}
结果:图太长了就光截个结果把,只有当0读完时,1才读。
使用reentrantReadWriteLock:
public class Test {
ReadWriteLock lock = new ReentrantReadWriteLock();
public static void main(String[] args) {
Test test = new Test();
new Thread(){
@Override
public void run() {
test.method(Thread.currentThread());
}
}.start();
new Thread(){
@Override
public void run() {
test.method(Thread.currentThread());
}
}.start();
}
private void method(Thread thread){
lock.readLock().lock();
try {
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1){
System.out.println(thread.getName()+"正在读");
}
System.out.println(thread.getName()+"读完毕");
} finally {
lock.readLock().unlock();
}
}
}
截一段结果:两个现在在共同执行
这样就大大提高了读操作的效率。不过当一个线程已经使用了读锁,则此时其他线程如果要申请写锁,申请写锁的线程会一直等待释放读锁。
如果一个线程已经占有了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
如何选择Lock和synchronized?
lock使用一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
synchronized1在发生异常时,会自动释放线程占有的锁,因此不会导致死锁发生,而Lock在发生异常时,如果没有主动通过unLock去释放锁,很可能造成死锁现象,因此使用lOCK时需要在finally中释放锁。
Lock可以让等待的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等下去,不能够响应中断。
通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
Lock可以提高多个线程进行读的效率。
性能上看,竞争不激烈,两者的性能差不多,当竞争非常激烈时,此时Lock的性能远远优于synchronized,具体情况具体选择。
锁的相关介绍:
可重入锁
如果锁具备可重入性,则成为可重入锁。像synchronized热ReentrantLock都是可重入锁。可重入性表明了锁的分配机制:
基于线程的分配,而不是基于方法调用的分配。举个栗子,当一个线程执行到某个synchronized方法时,比如method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新申请锁,而是可以直接执行方法method2
class MyClass {
public synchronized void method1() {
method2();
}
public synchronized void method2() {
}
}
上述代码中两个方法method1和method2都1用synchronized修饰了,某一时刻A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样线程A一直等待永远不会获取到锁。
由于synchronized和Lock都具备可重入性,所以不会发生上述情况。
可中断锁
Java中,synchronized就不是可中断的,而lOCK是可中断的锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,B不想等待了,想先处理其他事,我们可以让它中断自己获取再别的线程中中断它,这就是可中断锁。
前面的lockInterruptibly的用法已经体现了Lock的可中断性。
公平锁
公平锁尽量以请求锁的顺序来获取锁。比如同时有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在Java中,synchronized就是非公平锁,他无法保证等待的线程获取锁的顺序。
而ReentrantLock和ReenrantReadWriteLock,它默认下是非公平锁,但是可以设置为公平锁。看下源码:
有两个静态内部类,一个是NotFairSync,一个是FairSync分别用来实现非公平锁和公平锁。
我们可以在创建ReentrantLock对象时,通过传入参数的方式来设置锁的公平性:
ReentrantLock lock = new ReentrantLock(true);
如果参数为true表示公平锁,不传或者穿false就是非公平锁,默认情况下是非公平锁。另外ReentrantLock类中定义了很多方法如:
isFair() //判断锁是否是公平锁
isLocked() //判断锁是否被任何线程获取了
isHeidByCurrentThread() //判断锁是否被当前线程获取了
在ReentrantReadWriteLock中也有类似方法,同样也可以设置为公平锁和非公平锁。不过要记住,ReentrantReadWriteLock并未实现Lock接口,他实现的是ReadWirteLock接口。
4.读写锁
读写锁将对一个资源的访问分层了两个锁,一个读锁一个写锁。
正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。
ReadWriteLock就是读写锁,他是一个接口,ReentrantReadWriteLock实现了这个接口。
可以通过readLock获取读锁,writeLock获取写锁。