Jdk1.5 以后,在 java.util.concurrent.locks 包下,有一组实现线程同步的接口和类,说到线程的同步,可能大家都会想到 synchronized 关键字,
这是 java 内置的关键字,用来处理线程同步的,但这个关键字有很多的缺陷,使用起来也不是很方便和直观,所以就出现了 Lock,下面,我们
就来对比着讲解 Lock。
通常我们在使用 synchronized 关键字的时候会遇到下面这些问题:
(1)不可控性,无法做到随心的加锁和释放锁。
(2)效率比较低下,比如我们现在并发的读两个文件,读与读之间是互不影响的,但如果给这个读的对象使用 synchronized 来实现同步的话,
那么只要有一个线程进入了,那么其他的线程都要等待。
(3)无法知道线程是否获取到了锁。
而上面 synchronized 的这些问题,Lock 都可以很好的解决,并且 jdk1.5 以后,还提供了各种锁,例如读写锁,但有一点需要注意,使用 synchronized
关键时,无须手动释放锁,但使用 Lock 必须手动释放锁。下面我们就来学习一下 Lock 锁。
Lock 是一个上层的接口,其原型如下,总共提供了 6 个方法:
public interface Lock { // 用来获取锁,如果锁已经被其他线程获取,则一直等待,直到获取到锁 void lock(); // 该方法获取锁时,可以响应中断,比如现在有两个线程,一个已经获取到了锁,另一个线程调用这个方法正在等待锁,但是此刻又不想让这个线程一直在这死等,可以通过 调用线程的Thread.interrupted()方法,来中断线程的等待过程 void lockInterruptibly() throws InterruptedException; // tryLock方法会返回bool值,该方法会尝试着获取锁,如果获取到锁,就返回true,如果没有获取到锁,就返回false,但是该方法会立刻返回,而不会一直等待 boolean tryLock(); // 这个方法和上面的tryLock差不多是一样的,只是会尝试指定的时间,如果在指定的时间内拿到了锁,则会返回true,如果在指定的时间内没有拿到锁,则会返回false boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 释放锁 void unlock(); // 实现线程通信,相当于wait和notify,后面会单独讲解 Condition newCondition(); }
那么这几个方法该如何使用了?前面我们说到,使用 Lock 是需要手动释放锁的,但是如果程序中抛出了异常,那么就无法做到释放锁,有可能引起死锁,
所以我们在使用 Lock 的时候,有一种固定的格式,如下:
Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally {// 必须使用try,最后在finally里面释放锁 l.unlock(); }
下面我们来看一个简单的例子,代码如下:
/** * 描述:Lock使用 */ public class LockDemo { // new一个锁对象,注意此处必须声明成类对象,保持只有一把锁,ReentrantLock是Lock的唯一实现类 Lock lock = new ReentrantLock(); public void readFile(String fileMessage){ lock.lock();// 上锁 try{ System.out.println(Thread.currentThread().getName()+"得到了锁,正在读取文件……"); for(int i=0; i<fileMessage.length(); i++){ System.out.print(fileMessage.charAt(i)); } System.out.println(); System.out.println("文件读取完毕!"); }finally{ System.out.println(Thread.currentThread().getName()+"释放了锁!"); lock.unlock(); } } public void demo(final String fileMessage){ // 创建若干个线程 ExecutorService service = Executors.newCachedThreadPool(); // 提交20个任务 for(int i=0; i<20; i++){ service.execute(new Runnable() { @Override public void run() { readFile(fileMessage); try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } } }); } // 释放线程池中的线程 service.shutdown(); } }
Lock与synchronized的对比
1、作用
lock 和 synchronized 都是 Java 中去用来解决线程安全问题的一个工具。
2、来源
sychronized 是 Java 中的一个关键字。
lock 是 JUC 包里面提供的一个接口,这个接口有很多实现类,其中就包括我们最常用的 ReentrantLock(可重入锁)。
3、锁的力度
sychronized 可以通过两种方式去控制锁的力度:
把 sychronized 关键字修饰在方法层面。
修饰在代码块上。
锁对象的不同:
锁对象为静态对象或者是class对象,那这个锁属于全局锁。
锁对象为普通实例对象,那这个锁的范围取决于这个实例的生命周期。
lock锁的力度是通过 lock()与unlock()两个方法决定的。在两个方法之间的代码能保证其线程安全。lock的作用域取决于lock实例的生命周期。
4、灵活性
lock锁比sychronized的灵活性更高。
lock可以自主的去决定什么时候加锁与释放锁。只需要调用lock 的lock()和unlock()这两个方法就可以。
sychronized 由于是一个关键字,所以他无法实现非阻塞竞争锁的方法,一个线程获取锁之后,其他锁只能等待那个线程释放之后才能有获取锁的机会。
5、公平锁与非公平锁
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
优点:所有的线程都能得到资源,不会饿死。
缺点:吞吐量低,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销大。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
缺点:可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,最终饿死。
lock提供了公平锁和非公平锁两种机制(默认非公平锁)。
sychronized是非公平锁。
6、异常是否释放锁
synchronized锁的释放是被动的,当sychronized同步代码块执行结束或者出现异常的时候才会被释放。
lock锁发生异常的时候,不会主动释放占有的锁,必须手动unlock()来释放,所以我们一般都是将同步代码块放进try-catch里面,finally中写入unlock()方法,避免死锁发生。
7、判断是否能获取锁
synchronized不能。
lock提供了非阻塞竞争锁的方法trylock(),返回值是Boolean类型。它表示的是用来尝试获取锁:成功获取则返回true;获取失败则返回false,这个方法无论如何都会立即返回。
8、调度方式
synchronized使用的是object对象本身的wait、notify、notifyAll方法,而lock使用的是Condition进行线程之间的调度。
9、是否能中断
synchronized只能等待锁的释放,不能响应中断。
lock等待锁过程中可以用interrupt()来中断。
10、性能
如果竞争不激烈,性能差不多;竞争激烈时,lock的性能会更好。
lock锁还能使用readwritelock实现读写分离,提高多线程的读操作效率。
11、sychronized锁升级
synchronized 代码块是由一对 monitorenter/monitorexit 指令实现的。Monitor的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。
所以现在JVM提供了三种不同的锁:偏向锁、轻量级锁、重量级锁。
偏向锁:
当没有竞争出现时,默认使用偏向锁。线程会利用 CAS 操作在对象头上设置线程 ID ,以表示对象偏向当前线程。
目的:在很多应用场景中,大部分对象生命周期最多会被一个线程锁定,使用偏向锁可以降低无竞争时的开销。
轻量级锁:
JVM比较当前线程的 threadID 和 Java 对象头中的threadID是否一致,如果不一致(比如线程2要竞争锁对象),那么需要查看 Java 对象头中记录的线程1是否存活(偏向锁不会主动释放因此还是存储的线程1的 threadID),如果没有存活,那么锁对象还是为偏向锁(对象头中的threadID为线程2的);如果存活,那么撤销偏向锁,升级为轻量级锁。
当有其他线程想访问加了轻量级锁的资源时,会使用自旋锁优化,来进行资源访问。
目的:竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,开销大,如果刚刚阻塞不久这个锁就被释放了,就得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。
重量级锁:
自旋失败,很大概率 再一次自选也是失败,因此直接升级成重量级锁,进行线程阻塞,减少cpu消耗。
当锁升级为重量级锁后,未抢到锁的线程都会被阻塞,进入阻塞队列。