J2SE复习内容 - 多线程进阶

2019-04-24  本文已影响0人  做梦枯岛醒

在之前的一篇文章中,简单梳理了多线程的一些基础。这篇文章要说一些相关的更深入的知识。

系列目录:
J2SE复习内容 - 多线程基础
J2SE复习内容 - 多线程进阶

0. 原子性,可见性和指令重排

在了解相关的知识之前,需要首先掌握这几个概念。

原子性

顾名思义,一个操作如果是不可再分的,那么就称它是原子性的,在多线程环境下,如果一个操作是原子性的,那么就可以保证它在执行时刻不会被中途打断,从而保证数据的安全。

我们常见的Atomic相关类就是原子类,但是对于long和double类型的赋值不是原子性

可见性

可见性是指当前一个线程修改了共享变量的值,其他线程能够立即得知这个修改,我们一般使用volatile关键字来保证多线程操作的变量可见性,而普通的变量却不能保证这一点。
具体的实现可以看后面的文章。

指令重排 (或者有序性)

指令重排是指JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序。指令重排的目的是为了在不改变程序执行结果的前提下,优化程序的运行效率。但是在某些情况下,会影响到多线程的执行结果。使用volatile关键字也可以禁止指令重排,具体的实现可以看后面的文章。

梳理一下。
sychronized只能实现原子性(有序性)
volatile只能实现可见性和有序性

1. 可重入原理

我们所见的显式锁reentrantlock和隐式锁sychronized都是可重入的锁,可重入意味着在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是可以拿到锁的。同样有继承关系的情况下,也可以拿到子类的锁执行父类的方法。
那我们怎么能得知一个锁是不是可重入的?按照定义来看,我们写了如下的一段简单代码。

public class Lock {
    public synchronized void m1(){
        System.out.println("m1");
        m2();
    }
    public synchronized void m2(){
        System.out.println("m2");
    }
}

当一个线程执行了m1方法,获得了锁,又想要在m1中调用m2方法的时候,还可以继续获得这把锁(同样的锁),

Q1:那具体是什么原理呢?

每一个锁关联一个线程持有者计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。

2. 乐观锁与悲观锁

乐观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁,如我们的reentrantlock和sychronized

乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。使用乐观锁可以在一定程度上提高吞吐量

对于CAS不太了解的童鞋可以先科普什么是 CAS 机制,

Q2:那么选择悲观锁还是乐观锁呢?

那么很显然CAS要不断的做写时判断,所以在多写的情境下,很容易出现ABA问题或者CPU高压力,那么此时用悲观锁就比较好。
但是当少写的情况下,CAS的轻量既保证了效率,又提高了吞吐量(其实是一个东西吧),所以比较适合。

3. sychronized和ReentrantLock的对比

回顾一下使用sychronized的情况

synchronized (lockObject) { 
  // update object state
}

在回顾一下ReentrantLock的使用情况

Lock lock = new ReentrantLock();
lock.lock();
try { 
  // update object state
}
finally {
  lock.unlock(); 
}

然后我们根据积累的经验来分析这个问题。

首先java先提供了sychronized这个关键字来实现同步,那这个关键字有什么缺陷呢?

在sychronized没有优化之前,一直是以重量级锁的身份存在的,并且我们都知道,sychronized是系统控制的锁,如果一个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能等待,非常影响程序执行效率。

同时考虑下面一种情况,对于一个文件,写操作和写操作会发生冲突,写操作和读操作会发生冲突,读操作和读操作不会冲突。但我们使用sychronized的时候,所有操作必须排队,而使用Lock就可以做到同时读。

那ReentrantLock又有什么优势和缺陷呢?
在总结之前,我们要看一下它的实现,从而才能得出结论。



实现了Lock接口,那么我们关注一下Lock接口内部的方法。

public interface Lock {
   /*锁定*/
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    /*解锁*/
    void unlock();
    /*线程协作工具*/
    Condition newCondition();
}

最上面4个方法都是锁定,接下来是一个解锁方法,最后是一个线程协作工具,我们在这里重点分析一下前4个方法是有什么区别。

首先是最普通的lock()方法,用来获取一个锁,不多解释。
然后是lockInterruptibly()方法,字面意思可以看出来是允许被打断,再看看两者的注释。



前者是获取一个lock,而后者是在当前线程没有被interrupted的情况下就获取一个锁。

再通俗的联系一下,我们在线程中经常使用Sleep() 方法,这个方法要求处理一个InterruptedException 异常,当外界要打断此线程的时候只需要调用 目标线程的interrupt方法即可打断。

lockInterruptibly()和上面的情况是一样的, 线程在请求lock并被阻塞时,如果被interrupt,则“此线程会被唤醒并被要求处理InterruptedException”。

Q3:则两者的区别再总结一下吧:
当一个线程获取锁的时候使用的是lockInterruptibly()方法,那么他在等待过程中被别人调用了自己的interrupt() 方法就会被打断。

【调用者:等什么呢在这?今晚女神是我的,我已经锁门了,你赶紧回家睡觉去吧!】
【被调用者:哼!回就回!】

当一个线程获取锁的时候是使用lock()方法,那么他在等待的过程中不会被打断,傻乎乎的一直等下去。

【线程A:讲道理今晚临界区女神应该跟我共度良宵的吧,我再等等吧,说不定一会他的门就开了呢!】
【临界区女神:线程B,啊…… (自行脑补)】
【线程B:我今天不开门,你们就等着吧!】

下面还有两个tryLock的方法,这样说起来就很简单了,

引用 http://www.dewen.net.cn/q/9077 评论中的一句话。

lock(), 拿不到lock就不罢休,不然线程就一直block。 比较无赖的做法。
tryLock(),马上返回,拿到lock就返回true,不然返回false。 比较潇洒的做法。
带时间限制的tryLock(),拿不到lock,就等一段时间,超时返回false。比较聪明的做法。

这样解释后,我们会发现Lock会比sychronized多一些很自由的方法,一会总结。

再来看看ReentrantLock,中文翻译为可重入锁,所以证实了这个锁也是可重入的,关于可重入上面有提到过。
刚才讲,它实现了Lock接口,我们来看一下它的相关构造方法。


可以看到我们可以通过fair参数来确定初始化一个公平锁还是非公平锁。关于公平与否很好理解。
公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁。

可以想象得到,实现非公平锁,只需要随机选一个线程即可,而实现公平锁就需要复杂的计算方法,相对投入也就变大了。

sychronized就是非公平锁。

那么现在我们来回答下面这个问题。

Q4: sychronized和ReentrantLock的区别

  1. sychronized 是一个关键字,ReentrantLock是一个类。
  2. 很显然,sychronized不需要我们手动锁定和手动解锁,而ReentrantLock需要我们手动锁定和解锁,如果不在finally里面手动解锁,将会发生意想不到的致命问题。
  3. sychronized是非公平锁,而ReentrantLock是可以做到公平锁的。
  4. sychronized是不可中断锁(在锁定的时候),而ReentrantLock是可以中断的锁(lockInterruptibly)
  5. ReentrantLock有相应的tryLock尝试锁定方法,可以返回锁定状态,而sychronized没有相应机制。
  6. 在jdk1.7之前,sychronized比较重,所以Lock效率更高,在之后sychronized有所优化,所以效率相差不大

补充一点:Lock可以提高多个线程进行读操作的效率。(使用ReentrantReadWriteLock,具体可以去查看相应资料)

4.Volatile和sychronized对比

上面我们简单的提到了Volatile和sychronized的对比,在这里我们再简单的回顾一下。

不了解Volatile的可以先补习:什么是 volatile 关键字?

Q5: Volatile和sychronized区别

  1. volatile 保证了可见性和禁止了指令重排,而sychronized保证了原子性和可见性,这就意味着Volatile在多线程条件下还是不能保证数据的修改安全性,(要理解两者的使用场景。)
  2. volatile只能使用在变量级别;synchronized则可以使用在方法、代码段等。
  3. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

5. 总结

上面总结了一些面试常见问题,也详细解释了一些隐藏很深的知识点,对于深层原理,后面还会有文章详细介绍。

这些内容都应该属于基础部分,是对于并发学习必须所掌握的内容。

上一篇下一篇

猜你喜欢

热点阅读