java 多线程总结篇4——锁机制

在开发Java多线程应用程序中,各个线程之间因为要共享资源,必须用到锁机制。Java提供了多种多线程锁机制的实现方式,常见的有synchronized、ReentrantLock、Semaphore、AtomicInteger等。每种机制都有优缺点与各自的适用场景,必须熟练掌握他们的特色才能在Java多线程应用开发时驾轻就熟。——《Java锁机制详解》html

线程同步有关的类图关系可用如下的图总结:java

一、Java Concurrency API 中的 Lock 接口是什么?对比同步它有什么优点?c++

Lock接口比同步方法和同步块(这里的同步就是考察Synchronized关键字)提供了更具扩展性的锁操做。Lock不是Java语言内置的,synchronized是Java语言的关键字,所以是内置特性,Lock是一个类,经过这个类能够实现同步访问;他们容许更灵活的结构,能够具备彻底不一样的性质,而且能够支持多个相关类的条件对象。它的优点有:可使锁更公平;可使线程在等待锁的时候响应中断;可让线程尝试获取锁,并在没法获取锁的时候当即返回或者等待一段时间;能够在不一样的范围,以不一样的顺序获取和释放锁。redis

关于API及代码的例子请移步:《java并发编程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()方法是日常使用得最多的一个方法,就是用来获取锁。若是锁已被其余线程获取,则进行等待。因为在前面讲到若是采用Lock,必须主动去释放锁,而且在发生异常时,不会自动释放锁。所以通常来讲,使用Lock必须在try{}catch{}块中进行,而且将释放锁的操做放在finally块中进行,以保证锁必定被被释放,防止死锁的发生。一般使用Lock来进行同步的话,是如下面这种形式去使用的:数据库

Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){
     
}finally{
    lock.unlock();   //释放锁
}

tryLock()方法是有返回值的,它表示用来尝试获取锁,若是获取成功,则返回true,若是获取失败(即锁已被其余线程获取),则返回false,也就说这个方法不管如何都会当即返回。在拿不到锁时不会一直在那等待。tryLock(long time, TimeUnit unit)方法和tryLock()方法是相似的,只不过区别在于这个方法在拿不到锁时会等待必定的时间,在时间期限以内若是还拿不到锁,就返回false。若是若是一开始拿到锁或者在等待期间内拿到了锁,则返回true。编程

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //若是不能获取锁,则直接作其余事情
}

lockInterruptibly()方法比较特殊,当经过这个方法去获取锁时,若是线程正在等待获取锁,则这个线程可以响应中断,即中断线程的等待状态。也就使说,当两个线程同时经过lock.lockInterruptibly()想获取某个锁时,倘若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法可以中断线程B的等待过程。因为lockInterruptibly()的声明中抛出了异常,因此lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。缓存

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }

ReentrantLock,意思是“可重入锁”,ReentrantLock是惟一实现了Lock接口的类,而且ReentrantLock提供了更多的方法。如下给出一个ReentrantLock的运行实例:安全

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意这个地方,声明为类的属性
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         //能够用Java箭头函数特性改写上述冗余代码:
         // new Thread(){()->Thread.currentThread}.start();
        new Thread(){
            public void run() {
                test.insert(Thread.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) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"释放了锁");
            lock.unlock();
        }
    }

上文中提到了Lock接口以及对象,使用它,很优雅的控制了竞争资源的安全访问,可是这种锁不区分读写,称这种锁为普通锁。为了提升性能,Java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,若是没有写锁的状况下,读是无阻塞的,在必定程度上提升了程序的执行效率。Java中读写锁有个接口java.util.concurrent.locks. ReadWriteLock,也有具体的实现ReentrantReadWriteLock,于是会有下面的提问:数据结构

二、ReadWriteLock是什么?

读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm本身控制的,咱们只要上好相应的锁便可。若是你的代码只读数据,能够不少人同时读,但不能同时写,那就上读锁;若是你的代码修改数据,只能有一我的在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!读写锁接口:ReadWriteLock,它的具体实现类为:ReentrantReadWriteLock

《ReadWriteLock场景应用》:在多线程的环境下,对同一份数据进行读写,会涉及到线程安全的问题。好比在一个线程读取数据的时候,另一个线程在写数据,而致使先后数据的不一致性;一个线程在写数据的时候,另外一个线程也在写,一样也会致使线程先后看到的数据的不一致性。这时候能够在读写方法中加入互斥锁,任什么时候候只能容许一个线程的一个读或写操做,而不容许其余线程的读或写操做,这样是能够解决这样以上的问题,可是效率却大打折扣了。由于在真实的业务场景中,一份数据,读取数据的操做次数一般高于写入数据的操做,而线程与线程间的读读操做是不涉及到线程安全的问题,没有必要加入互斥锁,只要在读-写,写-写期间上锁就好了。API调用请移步

三、锁机制有什么用

有些业务逻辑在执行过程当中要求对数据进行排他性的访问,因而须要经过一些机制保证在此过程当中数据被锁住不会被外界修改,这就是所谓的锁机制。

四、什么是乐观锁(Optimistic Locking)?如何实现乐观锁?如何避免ABA问题

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,因此每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了不少这种锁机制,好比行锁,表锁等,读锁,写锁等,都是在作操做以前先上锁。乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可使用版本号等机制。乐观锁适用于多读的应用类型,这样能够提升吞吐量,像数据库若是提供相似于write_condition机制的其实都是提供的乐观锁。

五、解释如下名词:重排序,自旋锁,偏向锁,轻量级锁,可重入锁,公平锁,非公平锁,乐观锁,悲观锁

重入锁(ReentrantLock是一种递归无阻塞的同步机制。重入锁,也叫作递归锁,指的是同一线程 外层函数得到锁以后 ,内层递归函数仍然有获取该锁的代码,但不受影响。在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁。

自旋锁,因为自旋锁使用者通常保持锁时间很是短,所以选择自旋而不是睡眠是很是必要的,自旋锁的效率远高于互斥锁。如何旋转呢?何为自旋锁,就是若是发现锁定了,不是睡眠等待,而是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其余线程改变时 才能进入临界区。

偏向锁(Biased Locking)是Java6引入的一项多线程优化,它会偏向于第一个访问锁的线程,若是在运行过程当中,同步锁只有一个线程访问,不存在多线程争用的状况,则线程是不须要触发同步的,这种状况下,就会给线程加一个偏向锁。 若是在运行过程当中,遇到了其余线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。

轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的状况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。

重入锁(ReentrantLock)是一种递归无阻塞的同步机制,也叫作递归锁,指的是同一线程 外层函数得到锁以后 ,内层递归函数仍然有获取该锁的代码,但不受影响。 在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁。

公平锁,就是很公平,在并发环境中,每一个线程在获取锁时会先查看此锁维护的等待队列,若是为空,或者当前线程线程是等待队列的第一个,就占有锁,不然就会加入到等待队列中,之后会按照FIFO的规则从队列中取到本身

非公平锁比较粗鲁,上来就直接尝试占有锁,若是尝试失败,就再采用相似公平锁那种方式。

六、何时应该使用可重入锁?

场景1:若是已加锁,则再也不重复加锁。a、忽略重复加锁。b、用在界面交互时点击执行较长时间请求操做时,防止屡次点击致使后台重复执行(忽略重复触发)。以上两种状况多用于进行非重要任务防止重复执行,(如:清除无用临时文件,检查某些资源的可用性,数据备份操做等)

场景2:若是发现该操做已经在执行,则尝试等待一段时间,等待超时则不执行(尝试等待执行)这种其实属于场景2的改进,等待得到锁的操做有一个时间的限制,若是超时则放弃执行。用来防止因为资源处理不当长时间占用致使死锁状况(你们都在等待资源,致使线程队列溢出)。

场景3:若是发现该操做已经加锁,则等待一个一个加锁(同步执行,相似synchronized)这种比较常见你们也都在用,主要是防止资源使用冲突,保证同一时间内只有一个操做可使用该资源。但与synchronized的明显区别是性能优点(伴随jvm的优化这个差距在减少)。同时Lock有更灵活的锁定方式,公平锁与不公平锁,而synchronized永远是公平的。这种状况主要用于对资源的争抢(如:文件操做,同步消息发送,有状态的操做等)

场景4:可中断锁。synchronized与Lock在默认状况下是不会响应中断(interrupt)操做,会继续执行完。lockInterruptibly()提供了可中断锁来解决此问题。(场景3的另外一种改进,没有超时,只能等待中断或执行完毕)这种状况主要用于取消某些操做对资源的占用。如:(取消正在同步运行的操做,来防止不正常操做长时间占用形成的阻塞)

七、简述锁的等级方法锁、对象锁、类锁

方法锁(synchronized修饰方法时)经过在方法声明中加入 synchronized关键字来声明 synchronized 方法。synchronized 方法控制对类成员变量的访问: 每一个类实例对应一把锁,每一个 synchronized 方法都必须得到调用该方法的类实例的锁方能执行,不然所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能得到该锁,从新进入可执行状态。这种机制确保了同一时刻对于每个类实例,其全部声明为 synchronized 的成员函数中至多只有一个处于可执行状态,从而有效避免了类成员变量的访问冲突。

对象锁(synchronized修饰方法或代码块)当一个对象中有synchronized method或synchronized block的时候调用此对象的同步方法或进入其同步区域时,就必须先得到对象锁。若是此对象的对象锁已被其余调用者占用,则须要等待此锁被释放。(方法锁也是对象锁)。java的全部对象都含有1个互斥锁,这个锁由JVM自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,固然若是已经有线程获取了这个对象的锁,那么当前线程会等待;synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的1个好处,方法抛异常的时候,锁仍然能够由JVM来自动释放。 

类锁(synchronized修饰静态的方法或代码块),因为一个class不论被实例化多少次,其中的静态方法和静态变量在内存中都只有一份。因此,一旦一个静态的方法被申明为synchronized。此类全部的实例化对象在调用此方法,共用同一把锁,咱们称之为类锁。对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步。类锁只是一个概念上的东西,并非真实存在的,它只是用来帮助咱们理解锁定实例方法和静态方法的区别的。java类可能会有不少个对象,可是只有1个Class对象,也就是说类的不一样实例之间共享该类的Class对象。Class对象其实也仅仅是1个java对象,只不过有点特殊而已。因为每一个java对象都有1个互斥锁,而类的静态方法是须要Class对象。因此所谓的类锁,不过是Class对象的锁而已。获取类的Class对象有好几种,最简单的就是[类名.class]的方式。

八、Java中活锁和死锁有什么区别?

死锁:是指两个或两个以上的进程(或线程)在执行过程当中,因争夺资源而形成的一种互相等待的现象,若无外力做用,它们都将没法推动下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。死锁发生的四个条件

一、互斥条件:线程对资源的访问是排他性的,若是一个线程对占用了某资源,那么其余线程必须处于等待状态,直到资源被释放。

二、请求和保持条件:线程T1至少已经保持了一个资源R1占用,但又提出对另外一个资源R2请求,而此时,资源R2被其余线程T2占用,因而该线程T1也必须等待,但又对本身保持的资源R1不释放。

三、不剥夺条件:线程已得到的资源,在未使用完以前,不能被其余线程剥夺,只能在使用完之后由本身释放。

四、环路等待条件:在死锁发生时,必然存在一个“进程-资源环形链”,即:{p0,p1,p2,...pn},进程p0(或线程)等待p1占用的资源,p1等待p2占用的资源,pn等待p0占用的资源。(最直观的理解是,p0等待p1占用的资源,而p1而在等待p0占用的资源,因而两个进程就相互等待)

活锁:是指线程1可使用资源,但它很礼貌,让其余线程先使用资源,线程2也可使用资源,但它很绅士,也让其余线程先使用资源。这样你让我,我让你,最后两个线程都没法使用资源。

九、如何确保 N 个线程能够访问 N 个资源同时又不致使死锁?

预防死锁,预先破坏产生死锁的四个条件。互斥不可能破坏,因此有以下3种方法:

1.破坏,请求和保持条件1.1)进程等全部要请求的资源都空闲时才能申请资源,这种方法会使资源严重浪费(有些资源可能仅在运行初期或结束时才使用,甚至根本不使用)1.2)容许进程获取初期所需资源后,便开始运行,运行过程当中再逐步释放本身占有的资源。好比有一个进程的任务是把数据复制到磁盘中再打印,前期只须要得到磁盘资源而不须要得到打印机资源,待复制完毕后再释放掉磁盘资源。这种方法比上一种好,会使资源利用率上升。

2.破坏,不可抢占条件。这种方法代价大,实现复杂

3.破坏,循坏等待条件。对各进程请求资源的顺序作一个规定,避免相互等待。这种方法对资源的利用率比前两种都高,可是前期要为设备指定序号,新设备加入会有一个问题,其次对用户编程也有限制

十、死锁与饥饿的区别?

相同点:两者都是因为竞争资源而引发的。

不一样点:

  • 从进程状态考虑,死锁进程都处于等待状态,忙等待(处于运行或就绪状态)的进程并不是处于等待状态,但却可能被饿死;
  • 死锁进程等待永远不会被释放的资源,饿死进程等待会被释放但却不会分配给本身的资源,表现为等待时限没有上界(排队等待或忙式等待);
  • 死锁必定发生了循环等待,而饿死则否则。这也代表经过资源分配图能够检测死锁存在与否,但却不能检测是否有进程饿死;
  • 死锁必定涉及多个进程,而饥饿或被饿死的进程可能只有一个。
  • 在饥饿的情形下,系统中有至少一个进程能正常运行,只是饥饿进程得不到执行机会。而死锁则可能会最终使整个系统陷入死锁并崩溃

十一、怎么检测一个线程是否拥有锁?

java.lang.Thread中有一个方法叫holdsLock(),它返回true若是当且仅当当前线程拥有某个具体对象的锁

Object o = new Object(); 

@Test 
public void test1() throws Exception { 

    new Thread(new Runnable() { 

        @Override 
        public void run() { 
            synchronized(o) { 
                System.out.println("child thread: holdLock: " +  
                    Thread.holdsLock(o)); 
            } 
        } 
    }).start(); 

    System.out.println("main thread: holdLock: " + Thread.holdsLock(o)); 
    Thread.sleep(2000); 

} 
main thread: holdLock: false
child thread: holdLock: true

十二、如何实现分布式锁?

基于数据库实现分布式锁

基于缓存(redis,memcached,tair)实现分布式锁

基于Zookeeper实现分布式锁

能够参考详情《分布式锁的几种实现方式》 、 《分布式锁的3种方式》

1三、有哪些无锁数据结构,他们实现的原理是什么?

java 1.5提供了一种无锁队列(wait-free/lock-free)ConcurrentLinkedQueue,可支持多个生产者多个消费者线程的环境:网上别人本身实现的一种无锁算法队列,原理和jdk官方的ConcurrentLinkedQueue类似:经过volatile关键字来保证数据惟一性(注:java的volatile和c++的volatile关键字是两码事!),可是里面又用到atomic,感受有点boost::lockfree::queue的风格,估计参考了boost的代码来编写这个java无锁队列。

1四、Executors类是什么? Executor和Executors的区别

正如上面所说,这三者均是 Executor 框架中的一部分。Java 开发者颇有必要学习和理解他们,以便更高效的使用 Java 提供的不一样类型的线程池。总结一下这三者间的区别,以便你们更好的理解:

  • Executor 和 ExecutorService 这两个接口主要的区别是:ExecutorService 接口继承了 Executor 接口,是 Executor 的子接口
  • Executor 和 ExecutorService 第二个区别是:Executor 接口定义了 execute()方法用来接收一个Runnable接口的对象,而 ExecutorService 接口中的 submit()方法能够接受Runnable和Callable接口的对象。
  • Executor 和 ExecutorService 接口第三个区别是 Executor 中的 execute() 方法不返回任何结果,而 ExecutorService 中的 submit()方法能够经过一个 Future 对象返回运算结果。
  • Executor 和 ExecutorService 接口第四个区别是除了容许客户端提交一个任务,ExecutorService 还提供用来控制线程池的方法。好比:调用 shutDown() 方法终止线程池。能够经过 《Java Concurrency in Practice》 一书了解更多关于关闭线程池和如何处理 pending 的任务的知识。
  • Executors 类提供工厂方法用来建立不一样类型的线程池。好比: newSingleThreadExecutor() 建立一个只有一个线程的线程池,newFixedThreadPool(int numOfThreads)来建立固定线程数的线程池,newCachedThreadPool()能够根据须要建立新的线程,但若是已有线程是空闲的会重用已有线程。
Executor ExecutorService
Executor 是 Java 线程池的核心接口,用来并发执行提交的任务 ExecutorService 是 Executor 接口的扩展,提供了异步执行和关闭线程池的方法
提供execute()方法用来提交任务 提供submit()方法用来提交任务
execute()方法无返回值 submit()方法返回Future对象,可用来获取任务执行结果
不能取消任务 能够经过Future.cancel()取消pending中的任务
没有提供和关闭线程池有关的方法 提供了关闭线程池的方法

1六、什么是Java线程转储(Thread Dump),如何获得它?

线程转储是一个JVM活动线程的列表,它对于分析系统瓶颈和死锁很是有用。

有不少方法能够获取线程转储——使用Profiler,Kill -3命令,jstack工具等等。我更喜欢jstack工具,由于它容易使用而且是JDK自带的。因为它是一个基于终端的工具,因此咱们能够编写一些脚本去定时的产生线程转储以待分析。

1七、如何在Java中获取线程堆栈?

Java虚拟机提供了线程转储(thread dump)的后门,经过这个后门能够把线程堆栈打印出来。一般咱们将堆栈信息重定向到一个文件中,便于咱们分析,因为信息量太大,极可能超出控制台缓冲区的最大行数限制形成信息丢失。这里介绍一个jdk自带的打印线程堆栈的工具,jstack用于打印出给定的Java进程ID或core file或远程调试服务的Java堆栈信息。(Java问题定位之Java线程堆栈分析

示例:$jstack –l 23561 >> xxx.dump
命令 : $jstack [option] pid >> 文件 

>>表示输出到文件尾部,实际运行中,每每一次dump的信息,还不足以确认问题,建议产生三次dump信息,若是每次dump都指向同一个问题,咱们才肯定问题的典型性。

1八、说出 3 条在 Java 中使用线程的最佳实践

  • 给你的线程起个有意义的名字。这样能够方便找bug或追踪。OrderProcessor, QuoteProcessor or TradeProcessor这种名字比Thread-1. Thread-2 and Thread-3好多了,给线程起一个和它要完成的任务相关的名字,全部的主要框架甚至JDK都遵循这个最佳实践。
  • 避免锁定和缩小同步的范围锁花费的代价高昂且上下文切换更耗费时间空间,试试最低限度的使用同步和锁,缩小临界区。所以相对于同步方法我更喜欢同步块,它给我拥有对锁的绝对控制权。
  • 多用同步类少用wait和notify首先,CountDownLatch, Semaphore, CyclicBarrier和Exchanger这些同步类简化了编码操做,而用wait和notify很难实现对复杂控制流的控制。其次,这些类是由最好的企业编写和维护在后续的JDK中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序能够不费吹灰之力得到优化。
  • 多用并发集合少用同步集合,这是另一个容易遵循且受益巨大的最佳实践,并发集合比同步集合的可扩展性更好,因此在并发编程时使用并发集合效果更好。若是下一次你须要用到map,你应该首先想到用ConcurrentHashMap。

1九、【情景开放题】

实际项目中使用多线程举例。你在多线程环境中遇到的常见的问题是什么?你是怎么解决它的?

 

请说出与线程同步以及线程调度相关的方法

 

程序中有3个 socket,须要多少个线程来处理

 

假若有一个第三方接口,有不少个线程去调用获取数据,如今规定每秒钟最多有 10 个线程同时调用它,如何作到

 

如何在 Windows 和 Linux 上查找哪一个线程使用的 CPU 时间最长

 

如何确保 main() 方法所在的线程是 Java 程序最后结束的线程

 

很是多个线程(多是不一样机器),相互之间须要等待协调才能完成某种工做,问怎么设计这种协调方案

 

你须要实现一个高效的缓存,它容许多个用户读,但只容许一个用户写,以此来保持它的完整性,你会怎样去实现它

相关文章
相关标签/搜索