一 开篇
客观的讲,sychronized 和 lock都属于悲观锁(共享的资源每次只能给一个线程使用,其他线程处于阻塞状态,用完之后才释放资源给其他线程使用)。都能够实现数据的同步访问,sychronized是java中的一个关键字,属于java内置的语言特性,在Java1.5之后,在java.util.concurrent.locks包下提供了另外一种方式来实现同步访问,那就是Lock。
那么疑问来了,既然sychronized可以实现数据的同步访问,那么为什么还要增加lock呢?接下来我们来讲解下增加lock的原因。
二 sychronized 和 Lock的区别
1.来源:sychronized是java的关键字,是java的内置特性。Lock不是java关键字,是java.util.concurrent.locks包下的一个类,通过这个类可以实现数据的同步访问。
2.释放锁:sychronized方法(代码块)执行完毕之后或者执行期间出现异常,系统会自动释放锁,不会导致死锁现象。Lock必须要用户去手动释放锁,如果没有主动释放,那么很可能出现死锁现象。
3.响应中断:Lock可以让等待的线程响应中断,而sychronized不行,会一直等待下去
4.获取锁状态:Lock可以知道线程是否成功获取锁,sychronized不行
5.高并发效率:Lock可以提高多线程作业效率
在性能上来说,如果资源竞争不是很激烈的情况下,两者性能相差不多,当有大量的线程竞争资源时,而Lock的性能远远高于sychronized,所以根据情况选择合适的锁是关键
三 为什么引入Lock类?
1.为了能够灵活设置线程等待时间和响应中断,提高程序执行效率
从上面了解到,sychronized释放锁的情况有两种:1.sychronized方法(代码块)执行完毕。2.线程执行过程中发生异常,JVM自动释放锁。那么如果中途遇到等待IO或者sleep等原因被阻塞了,其他线程只能干等,这种情况下,程序的执行效率将会非常低下。但Lock可以设置线程等待的时间和相应中断!!!,其他线程可放弃等待,去做其他事情。
2.有效识别线程读操作,提高资源可利用范围
sychronized加锁时,多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。
但Lock可以使得多个线程都是读操作时,线程之间不会发生冲突。
3.sychronized无法知道线程有没有获取到锁,但Lock可以知道
tryLock()和tryLock(long time,Unit unit)方法是有返回值的,获取到锁返回true,否则返回false
四 java.util.concurrent.locks包下的类和接口介绍
java.util.concurrent.locks包下,我们需要关注的也就:两个接口(Lock接口,ReadWriteLock接口),两个实现类(ReentrantLock类,ReentrantReadWriteLock类),其中,ReentrantLock类是Lock接口的唯一实现,ReentrantReadWriteLock类是ReadWriteLock接口的实现。
1.Lock接口
通过查看源码可知,Lock是一个接口,方法如下:
public interface Lock {
//获取锁,无返回值
void lock();
//获取锁,获取不到中断(发音:因特rub特-波类)
void lockInterruptibly() throws InterruptedException;
//获取锁,成功返回true,否则false
boolean tryLock();
//尝试一段时间内获取锁,成功返回true,否则false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//手动释放锁
void unlock();
Condition newCondition();
}
Lock接口中的方法:lock(),lockInterruptibly(),tryLock(),tryLock(long time,TimeUnit unit)都是用来获取锁的。unLock()是用来释放锁的,newCondition()方法暂不做赘述。
1. lock()方法
lock()方法是比较常用的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
前面讲到,如果使用Lock,必须要手动释放锁,即便发生异常,也不会释放锁。因此,使用Lock时,建议必须要用try{}catch(){} 捕获一下,然后将释放锁的操作放在finally()代码块中,防止死锁发生,使用方式如下。
Lock lock=...;
lock.lock();
try{
//事务处理
}catch(Exception e){
}finally{
//释放锁
lock.unlock();
}
2. tryLock()方法
tryLock()方法是有返回值的,尝试获取锁,如果成功返回true,否则返回false,如果拿不到锁可以尝试去做其他事情,不用一直等待。
tryLock(long time, TimeUnit unit)方法可tryLock()类似,区别在于获取不到锁时会等待一段时间,在时间期限内拿到锁返回ture,否则返回false,使用方式如下。
Lock lock=...;
if(lock.tryLock()){
try{
//处理事务
}catch(Exception e){
}finally{
//释放锁
lock.unLock();
}
}else{
//获取不到锁,做其他操作
}
3.lockInterruptibly()方法
lockInterruptibly()方法比较特殊,能够响应中断,即中断等待状态,也就是或如果两个线程A,B分别获取锁,A获取到锁是,B一直处于等待状态,可以通过B.interrupt()方法能够中断线程B的等待过程。
由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try{}catch{}中或者抛出InterruptedException 异常,使用代码如下。
public class LockTest{
private Lock lock=new ReentrantLock();
public void method(){
try{
lock.lockInterruptibly();
//事务处理
System.out.println(Thread.currentThread().getName()+"获取到锁!!!");
System.out.println(Thread.currentThread().getName()+"先睡它个30s!!!");
Thread.sleep(30000);
}catch(Exception e){
}finally{
//释放锁
lock.unlock();
}
}
@Test
public void testInterruptibly() throws InterruptedException{
Runnable t1=new Runnable(){
@Override
public void run(){
method();
}
};
Runnable t2=new Runnable(){
@Override
public void run(){
method();
}
};
String tName=Thread.currentThread().getName();
System.out.println(tName+"-启动t1!");
Thread t11=new Thread(t1);
t11.start();
System.out.println(tName+"-我等个5秒,再启动t2");
Thread.sleep(5000);
System.out.println(tName+"-启动t2");
Thread t22=new Thread(t2);
t22.start();
System.out.println(tName+"-t2获取不到锁,t1睡觉了,没释放,我等个5秒!");
Thread.sleep(5000);
System.out.println(tName+"-等了5秒了,不等了,把t1中断了!");
t22.interrupt();
Thread.sleep(Long.MAX_VALUE);
}
}
注意:已经获取到锁的线程是无法用interrupt()方法中断的,interrupt()方法只能中断被阻塞的线程。因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。
而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
2. ReentrantLock实现类
ReentrantLock,(发音:瑞恩垂特Lock)意思为“可重入锁”,ReentrantLock具有公平和非公平两种模式,也各有优缺点:
公平锁是严格的以FIFO的方式进行锁的竞争,但是非公平锁是无序的锁竞争,刚释放锁的线程很大程度上能比较快的获取到锁,队列中的线程只能等待,所以非公平锁可能会有“饥饿”的问题。但是重复的锁获取能减小线程之间的切换,而公平锁则是严格的线程切换,这样对操作系统的影响是比较大的,所以非公平锁的吞吐量是大于公平锁的,这也是为什么JDK将非公平锁作为默认的实现。
ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法.
lock(),tryLock(),tryLock(long time,TimeUnit unit),lockInterruptibly()方法使用方式即我们上面介绍的那样,唯一注意的地方是声明Lock的时候
private
Lock lock =
new
ReentrantLock();
//注意这个地方
提示:如果希望线程能够正确的获取锁,要注意:同一个实例对象,多个线程才有竞争关系,加锁才有意义,不同的实例对象要有竞争关系,必须在对象类级别加锁。
3. ReadWriteLock接口
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();
}
readLock()用来获取读锁,wirteLock()用来获取写锁。也就是说将资源的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的ReentrantReadWriteLock实现了ReadWriteLock接口。
4.ReentrantReadWriteLock实现类
ReentrantReadWriteLock实现了接口ReadWriteLock,ReentrantReadWriteLock里面提供了丰富的方法,最主要的两个方法为:readLock()和wirteLock()用来获取读锁和写锁。
我们知道,sychronized加锁是无法识别线程是读操作还是写操作的,一律是一个线程获取到锁后,其他线程等待状态,但ReentrantReadWriteLock的读锁和写锁实现了读写分离,读操作可以允许多个线程同时进行,使用方式如下。
private ReadWriteLock rwl=new ReentrantReadWriteLock();
public void m1(Thread thread){
rwl.readLock().lock();//读锁
try {
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName()+"正在进行读操作");
}
System.out.println(thread.getName()+"读操作完毕");
} finally {
rwl.readLock().unlock();
}
}
不同的线程可同时申请读锁,大大提高程序执行效率。
但要注意的是,如果有一个线程已经占用了读锁,其他线程想要申请写锁,那么申请写锁的线程必须要等待读锁释放。
如果一个线程已经占用了写锁,其他线程想申请读锁或者写锁,则申请线程必须要等待写锁释放。
五 锁概念介绍
1.可重入锁
定义:如果锁具备可重入性,那么我们称之为可重入锁。像sychronized和ReentrantLock都是可重入锁。可重入性在我看来实际是表明了锁的分配机制:锁是基于线程分配的,而不是基于方法调用的分配。
举个简单的例子,有两个sychronized修饰的方法,method1和method2,其中method1调用了method2,那么如果一个线程获取了method1的锁,那么该不需要再去申请method2的锁,可直接执行方法method2,代码如下:
public class MyClass{
public sychronized void method1(){
method2();
}
public sychronized void method2(){
}
}
上述method1和method2都被sychronized修饰了,如果sychronized不具备可重入性,假如某一时刻,线程A获取到了method1的锁,那么还需申请该对象锁,问题是A已经占用了该对象锁,这样A就会一直等待永远获取不到的锁。
然而,sychronized和ReentrantLock都具有可重入性,所以不会发生上述这种情况。
2.可中断锁
可中断锁:顾名思义,可以响应中断的锁。sychronized是不可中断锁,Lock是可以中断的。
如果某个线程执行锁代码期间,其他线程不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。就是前面讲述的Lock中的lockInterruptiblly()方法。通过threadX.interrupt()可实现线程的中断。
3.公平锁和非公平锁
遵循FIFO的原理,即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
sychronized是非公平锁,无法保证获取锁的顺序和请求锁的顺序一致,所以就可能出现某个线程永远获取不到锁的情况。但重复的获取锁,可以减少线程间的切换消耗的资源,所以非公平锁的吞吐量会比公平锁的要大。
ReentrantLock和ReentrantReadWriteLock默认都是非公平锁,但是可以设置成公平锁,看下这两个类的源代码:
ReentrantLock内部设置了2个静态内部类,一个NonfairSync,一个FairSync,分别用来实现公平锁和非公平锁。
看下ReentrantLock的构造函数,如下图,根据构造函数可以看到,默认无参情况下是非公平锁。设置锁的公平性可以
ReentrantLock lock=new ReentrantLock(true);
传true参数代表公平锁,不传或者传false,代表非公平锁。
此外,ReentrantLock类中还定义了很多其他的方法,比如:
- isFair() //判断锁是否是公平锁
- isLocked() //判断锁是否被任何线程获取了
- isHeldByCurrentThread() //判断锁是否被当前线程获取了
- hasQueuedThreads() //判断是否有线程在等待该锁
在ReentrantReadWriteLock中也有类似的方法,同样也可以设置为公平锁和非公平锁。不过要记住,ReentrantReadWriteLock并未实现Lock接口,它实现的是ReadWriteLock接口。
4.读写锁
读写锁其实就是将对资源的访问分成了2个锁,一个读锁和一个写锁。
正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。
ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。
可以通过readLock().lock()获取读锁,通过writeLock().lock()获取写锁。
六 拓展 volatile
在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉。
Java语言是支持多线程的,为了解决线程并发的问题,在语言内部引入了 同步块 和 volatile 关键字机制。
用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。volatile很容易被误用,用来进行原子性操作。
举个例子,实际操作,我们实现一个计数器,每次线程启动的时候,会调用计数器inc方法,对计数器进行加一,同时启动1000个线程,去进行i++计算,看看实际结果,运行结果:Counter.count=995
实际运算结果每次可能都不一样,本机的结果为:运行结果:Counter.count=995,可以看出,在多线程的环境下,Counter.count并没有期望结果是1000
很多人以为,这个是多线程并发问题,只需要在变量count之前加上volatile就可以避免这个问题,那我们在修改代码看看,看看结果是不是符合我们的期望
运行结果:Counter.count=992
运行结果还是没有我们期望的1000,下面我们分析一下原因
- 在 java 垃圾回收整理一文中,描述了jvm运行时刻内存的分配。其中有一个内存区域是jvm虚拟机栈,每一个线程运行时都有一个线程栈,
- 线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存
- 变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,
- 在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。
- read and load 从主存复制变量到当前工作内存use and assign 执行代码,改变共享变量值 store and write 用工作内存数据刷新主存相关内容
- 其中use and assign 可以多次出现
- 但是这一些操作并不是原子性,也就是 在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样
对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的
例如假如线程1,线程2 在进行read,load 操作中,发现主内存中count的值都是5,那么都会加载这个最新的值
在线程1堆count进行修改之后,会write到主内存中,主内存中的count变量就会变为6
线程2由于已经进行read,load操作,在进行运算之后,也会更新主内存count的变量值为6
导致两个线程及时用volatile关键字修改之后,还是会存在并发的情况。