线程安全之 synchronized 和 ReentrantLock + 面试题

前面我们介绍了很多关于多线程的内容,在多线程中有一个很重要的课题需要我们攻克,那就是线程安全问题。线程安全问题指的是在多线程中,各线程之间因为同时操作所产生的数据污染或其他非预期的程序运行结果。

线程安全

1)非线程安全事例

比如 A 和 B 同时给 C 转账的问题,假设 C 原本余额有 100 元,A 给 C 转账 100 元,正在转的途中,此时 B 也给 C 转了 100 元,这个时候 A 先给 C 转账成功,余额变成了 200 元,但 B 事先查询 C 的余额是 100 元,转账成功之后也是 200 元。当 A 和 B 都给 C 转账完成之后,余额还是 200 元,而非预期的 300 元,这就是典型的线程安全的问题。

2)非线程安全代码示例

上面的内容没看明白没关系,下面来看非线程安全的具体代码:

class ThreadSafeTest {
static int number = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> addNumber());
Thread thread2 = new Thread(() -> addNumber());
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("number:" + number);
}
public static void addNumber() {
for (int i = 0; i < 10000; i++) {
++number;
}
}
}

以上程序执行结果如下:

number:12085

每次执行的结果可能略有差异,不过几乎不会等于(正确的)累计之和 20000。

3)线程安全的解决方案

线程安全的解决方案有以下几个维度:

  • 数据不共享,单线程可见,比如 ThreadLocal 就是单线程可见的;
  • 使用线程安全类,比如 StringBuffer 和 JUC(java.util.concurrent)下的安全类(后面文章会专门介绍);
  • 使用同步代码或者锁。

线程同步和锁

1)synchronized

① synchronized 介绍

synchronized 是 Java 提供的同步机制,当一个线程正在操作同步代码块(synchronized 修饰的代码)时,其他线程只能阻塞等待原有线程执行完再执行。

② synchronized 使用

synchronized 可以修饰代码块或者方法,示例代码如下:

// 修饰代码块
synchronized (this) {
// do something
}
// 修饰方法
synchronized void method() {
// do something
}

使用 synchronized 完善本文开头的非线程安全的代码。

方法一:使用 synchronized 修饰代码块,代码如下:

class ThreadSafeTest {
static int number = 0;
public static void main(String[] args) throws InterruptedException {
Thread sThread = new Thread(() -> {
// 同步代码
synchronized (ThreadSafeTest.class) {
addNumber();
}
});
Thread sThread2 = new Thread(() -> {
// 同步代码
synchronized (ThreadSafeTest.class) {
addNumber();
}
});
sThread.start();
sThread2.start();
sThread.join();
sThread2.join();
System.out.println("number:" + number);
}
public static void addNumber() {
for (int i = 0; i < 10000; i++) {
++number;
}
}
}

以上程序执行结果如下:

number:20000

方法二:使用 synchronized 修饰方法,代码如下:

class ThreadSafeTest {
static int number = 0;
public static void main(String[] args) throws InterruptedException {
Thread sThread = new Thread(() -> addNumber());
Thread sThread2 = new Thread(() -> addNumber());
sThread.start();
sThread2.start();
sThread.join();
sThread2.join();
System.out.println("number:" + number);
}
public synchronized static void addNumber() {
for (int i = 0; i < 10000; i++) {
++number;
}
}
}

以上程序执行结果如下:

number:20000

③ synchronized 实现原理

synchronized 本质是通过进入和退出的 Monitor 对象来实现线程安全的。

以下面代码为例:

public class SynchronizedTest {
public static void main(String[] args) {
synchronized (SynchronizedTest.class) {
System.out.println("Java");
}
}
}

当我们使用 javap 编译之后,生成的字节码如下:

Compiled from "SynchronizedTest.java"
public class com.interview.other.SynchronizedTest {
public com.interview.other.SynchronizedTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."\<init\>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // class com/interview/other/SynchronizedTest
2: dup
3: astore_1
4: monitorenter
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String Java
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
Exception table:
from to target type
5 15 18 any
18 21 18 any
}

可以看出 JVM(Java 虚拟机)是采用 monitorenter 和 monitorexit 两个指令来实现同步的,monitorenter 指令相当于加锁,monitorexit 相当于释放锁。而 monitorenter 和 monitorexit 就是基于 Monitor 实现的。

2)ReentrantLock

① ReentrantLock 介绍

ReentrantLock(再入锁)是 Java 5 提供的锁实现,它的功能和 synchronized 基本相同。再入锁通过调用 lock() 方法来获取锁,通过调用 unlock() 来释放锁。

② ReentrantLock 使用

ReentrantLock 基础使用,代码如下:

Lock lock = new ReentrantLock();
lock.lock(); // 加锁
// 业务代码...
lock.unlock(); // 解锁

使用 ReentrantLock 完善本文开头的非线程安全代码,请参考以下代码:

public class LockTest {
static int number = 0;
public static void main(String[] args) throws InterruptedException {
// ReentrantLock 使用
Lock lock = new ReentrantLock();
Thread thread1 = new Thread(() -> {
try {
lock.lock();
addNumber();
} finally {
lock.unlock();
}
});
Thread thread2 = new Thread(() -> {
try {
lock.lock();
addNumber();
} finally {
lock.unlock();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("number:" + number);
}
public static void addNumber() {
for (int i = 0; i < 10000; i++) {
++number;
}
}
}

尝试获取锁

ReentrantLock 可以无阻塞尝试访问锁,使用 tryLock() 方法,具体使用如下:

Lock reentrantLock = new ReentrantLock();
// 线程一
new Thread(() -> {
try {
reentrantLock.lock();
Thread.sleep(2 * 1000); } catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}).start();
// 线程二
new Thread(() -> {
try {
Thread.sleep(1 * 1000);
System.out.println(reentrantLock.tryLock());
Thread.sleep(2 * 1000);
System.out.println(reentrantLock.tryLock());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();

以上代码执行结果如下:

false

true

尝试一段时间内获取锁

tryLock() 有一个扩展方法 tryLock(long timeout, TimeUnit unit) 用于尝试一段时间内获取锁,具体实现代码如下:

Lock reentrantLock = new ReentrantLock();
// 线程一
new Thread(() -> {
try {
reentrantLock.lock();
System.out.println(LocalDateTime.now());
Thread.sleep(2 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}).start();
// 线程二
new Thread(() -> {
try {
Thread.sleep(1 * 1000);
System.out.println(reentrantLock.tryLock(3, TimeUnit.SECONDS));
System.out.println(LocalDateTime.now());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();

以上代码执行结果如下:

2019-07-05 19:53:51

true

2019-07-05 19:53:53

可以看出锁在休眠了 2 秒之后,就被线程二直接获取到了,所以说 tryLock(long timeout, TimeUnit unit) 方法内的 timeout 参数指的是获取锁的最大等待时间。

③ ReentrantLock 注意事项

使用 ReentrantLock 一定要记得释放锁,否则该锁会被永久占用。

相关面试题

1.ReentrantLock 常用的方法有哪些?

答:ReentrantLock 常见方法如下:

  • lock():用于获取锁
  • unlock():用于释放锁
  • tryLock():尝试获取锁
  • getHoldCount():查询当前线程执行 lock() 方法的次数
  • getQueueLength():返回正在排队等待获取此锁的线程数
  • isFair():该锁是否为公平锁

2.ReentrantLock 有哪些优势?

答:ReentrantLock 具备非阻塞方式获取锁的特性,使用 tryLock() 方法。ReentrantLock 可以中断获得的锁,使用 lockInterruptibly() 方法当获取锁之后,如果所在的线程被中断,则会抛出异常并释放当前获得的锁。ReentrantLock 可以在指定时间范围内获取锁,使用 tryLock(long timeout,TimeUnit unit) 方法。

3.ReentrantLock 怎么创建公平锁?

答:new ReentrantLock() 默认创建的为非公平锁,如果要创建公平锁可以使用 new ReentrantLock(true)。

4.公平锁和非公平锁有哪些区别?

答:公平锁指的是线程获取锁的顺序是按照加锁顺序来的,而非公平锁指的是抢锁机制,先 lock() 的线程不一定先获得锁。

5.ReentrantLock 中 lock() 和 lockInterruptibly() 有什么区别?

答:lock() 和 lockInterruptibly() 的区别在于获取线程的途中如果所在的线程中断,lock() 会忽略异常继续等待获取线程,而 lockInterruptibly() 则会抛出 InterruptedException 异常。

题目解析:执行以下代码,在线程中分别使用 lock() 和 lockInterruptibly() 查看运行结果,代码如下:

 Lock interruptLock = new ReentrantLock();
interruptLock.lock();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
interruptLock.lock();
//interruptLock.lockInterruptibly(); // java.lang.InterruptedException
} catch (Exception e) {
e.printStackTrace();
}
}
});
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();
TimeUnit.SECONDS.sleep(3);
System.out.println("Over");
System.exit(0);

执行以下代码会发现使用 lock() 时程序不会报错,运行完成直接退出;而使用 lockInterruptibly() 则会抛出异常 java.lang.InterruptedException,这就说明:在获取线程的途中如果所在的线程中断,lock() 会忽略异常继续等待获取线程,而 lockInterruptibly() 则会抛出 InterruptedException 异常。

6.synchronized 和 ReentrantLock 有什么区别?

答:synchronized 和 ReentrantLock 都是保证线程安全的,它们的区别如下:

  • ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
  • ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
  • ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等;
  • ReentrantLock 性能略高于 synchronized。

7.ReentrantLock 的 tryLock(3, TimeUnit.SECONDS) 表示等待 3 秒后再去获取锁,这种说法对吗?为什么?

答:不对,tryLock(3, TimeUnit.SECONDS) 表示获取锁的最大等待时间为 3 秒,期间会一直尝试获取,而不是等待 3 秒之后再去获取锁。

8.synchronized 是如何实现锁升级的?

答:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,JVM(Java 虚拟机)让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否尤其线程 id 一致,如果一致则可以直接使用,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,不会阻塞,执行一定次数之后就会升级为重量级锁,进入阻塞,整个过程就是锁升级的过程。

总结

本文介绍了线程同步的两种方式 synchronized 和 ReentrantLock,其中 ReentrantLock 使用更加灵活,效率也率高,不过 ReentrantLock 只能修饰代码块,使用 ReentrantLock 需要开发者手动释放锁,如果忘记释放则该锁会一直被占用。synchronized 使用场景更广,可以修饰普通方法、静态方法和代码块等。


欢迎关注我的公众号,回复关键字“Java” ,将会有大礼相送!!! 祝各位面试成功!!!



%97%E5%8F%B7%E4%BA%8C%E7%BB%B4%E7%A0%81.png)

线程安全之 synchronized 和 ReentrantLock的更多相关文章

  1. 线程同步synchronized和ReentrantLock

    一.线程同步问题的产生及解决方案 问题的产生: Java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突. 如下例:假设有一个卖票 ...

  2. 高级java必会系列二:多线程经常使用的3个关键字:synchronized、ReentrantLock、volatile

    系列一讲解了多线程,本章讲解多线程开发中经常使用到的3个关键字synchronized.ReentrantLock.volatile. 一.synchronized 互斥锁,即操作互斥,并发线程过来, ...

  3. synchronized和ReentrantLock

    一.什么是sychronized sychronized是java中最基本同步互斥的手段,可以修饰代码块,方法,类. 在修饰代码块的时候需要一个reference对象作为锁的对象. 在修饰方法的时候默 ...

  4. Synchronized与ReentrantLock区别总结(简单粗暴,一目了然)

    这篇文章是关于这两个同步锁的简单总结比较,关于底层源码实现原理没有过多涉及,后面会有关于这两个同步锁的底层原理篇幅去介绍. 相似点:这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的 ...

  5. java多线程系列6 synchronized 加强版 ReentrantLock

    ReentrantLock类是可重入.互斥.实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力.ReenreantLock类的常用方法有: Re ...

  6. java的两种同步方式, Synchronized与ReentrantLock的区别

    java在编写多线程程序时,为了保证线程安全,需要对数据同步,经常用到两种同步方式就是Synchronized和重入锁ReentrantLock. 相似点: 这两种同步方式有很多相似之处,它们都是加锁 ...

  7. synchronized 和 ReentrantLock 区别

    synchronized 使用: 1:当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁.结果,其它线程对该object对象所有同步代 ...

  8. 【多线程】:Synchronized和ReentrantLock的对比

    相同点: 两者都是可重入锁,同一个线程每进入一次,锁的计数器都自增1,等到锁的计数器下降为0时才能释放锁. 底层实现对比: Synchronized是依赖于JVM实现的,而ReentrantLock是 ...

  9. java多线程 21 : ReentrantReadWriteLock ,synchronized和ReentrantLock的对比

    读写锁ReentrantReadWriteLock概述 大型网站中很重要的一块内容就是数据的读写,ReentrantLock虽然具有完全互斥排他的效果(即同一时间只有一个线程正在执行lock后面的任务 ...

随机推荐

  1. SpringCloud + Consul服务注册中心 + gateway网关

    1  启动Consul 2  创建springcloud-consul项目及三个子模块 2.1 数据模块consul-producer 2.2 数据消费模块consul-consumer 2.3 ga ...

  2. linux 没有音频输出的解决方式

    用户级别的-/.asoundrc 文件. 如果文件不存在,可以手动创建. 其中的各个 ID,请根据实际情况调整: defaults.pcm.card 1 defaults.pcm.device 0 d ...

  3. Win10系统中搭建服务器的方法

    1.我的电脑->找到并点击“控制面板”->点击“程序”->点击“启用或关闭Windows功能”2.按如下图方式选中复选框   服务器批量管理软件 3.点击确定,打开iis管理器(右键 ...

  4. leetcode.310最小高度树

    对于一个具有树特征的无向图,我们可选择任何一个节点作为根.图因此可以成为树,在所有可能的树中,具有最小高度的树被称为最小高度树.给出这样的一个图,写出一个函数找到所有的最小高度树并返回他们的根节点. ...

  5. VS/Xamarin Android入门一

    一.安装和配置(以Visual Studio Pro 2015为例) Visual Studio2015直接提供了这个插件的选择项,稍微提示一下,如果要安装的话,最好准备好十个小时的打算,而且是网速不 ...

  6. react-antd-input-onPressEnter

    2020-01-11 ant-Design ,Input , onPressEnter 和 onChange 的区别 需求:看下图,右边栏配置开关组件的内容.输入内容,不想要左边实时更改 原来的代码: ...

  7. P2871 [USACO07DEC]手链Charm Bracelet(01背包模板)

    题目传送门:P2871 [USACO07DEC]手链Charm Bracelet 题目描述 Bessie has gone to the mall's jewelry store and spies ...

  8. Spring AOP 基于AspectJ

    简介 AspectJ是一个基于Java语言的AOP框架,Spring2.0以后新增了对AspectJ切点表达式支持.因为Spring1.0的时候Aspectj还未出现; AspectJ1.5中新增了对 ...

  9. python对象的初始化

    效果图: 代码: # 对象的初始化 class Person: # 在类中可以定义一些特殊方法(魔术方法) # 特殊方法都是以__开头,__结尾的方法 前后都是两个下划线 # 特殊方法会在特殊的时刻自 ...

  10. 【Magicodes.IE 2.0.0-beta1版本发布】已支持数据表格、列筛选器和Sheet拆分

    为了更好的完善Magicodes.IE,春节期间我们会进行一次大的重构.由于精力有限,急缺文档和翻译(将文档翻译为英文文档)支持,诚邀各位加入.同时在功能方便也做了相关规划,有兴趣的朋友可以参与提交P ...