本次内容主要线程的安全性、死锁相关知识点。

1、什么是线程安全性

1.1 线程安全定义

   前面使用8个篇幅讲到了Java并发编程的知识,那么我们有没有想过什么是线程的安全性?在《Java并发编程实战》中定义如下:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在调用代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

1.2 无状态类

  没有任何成员变量的类,就叫无状态类,这种类一定是线程安全的。但是有一种情况是,这个类方法的参数中用到了对象,看下面的代码:

public class StatelessClass {
public void test(User user) {
//do business
}
}

  此时这个类还是线程安全的吗?那肯定也是,为什么呢?因为多线程下的使用,固然user这个对象的实例会不正常,但是对于StatelessClass这个类的对象实例来说,它并不持有User的对象实例,它自己并不会有问题,有问题的是User这个类,而非StatelessClass本身。

1.2 volatile

  并不能保证类的线程安全性,只能保证类的可见性,最适合一个线程写,多个线程读的情景。

1.3 锁和CAS

  我们最常使用的保证线程安全的手段,使用synchronized关键字,使用显式锁,使用各种原子变量,修改数据时使用CAS机制等等。

1.4 ThreadLocal

  ThreadLocal是实现线程封闭的最好方法。关于ThreadLocal如何保证线程的安全性,请阅读《java线程间的共享》,里面有详细的介绍。

1.5 安全的发布

  1)类中持有的成员变量,如果是基本类型,发布出去,并没有关系,因为发布出去的其实是这个变量的一个副本。看下面的代码:

public class SafePublish {
private int number; public SafePublish() {
number = 2;
} public int getNumber() {
return number;
} public static void main(String[] args) {
SafePublish safePublish = new SafePublish();
int result = safePublish.getNumber();
System.out.println("before modify, result = " + result);
result = 3;
System.out.println("before modify, result =" + result);
System.out.println("getNumber() = " + safePublish.getNumber());
}
}

从程序输出可以看到,number的值并没被改变,因为result只是一个副本,这样的成员变量发布出去是安全的。

  2)如果类中持有的成员变量是对象的引用,如果这个成员对象不是线程安全的,通过get等方法发布出去,会造成这个成员对象本身持有的数据在多线程下不正确的修改,从而造成整个类线程不安全的问题。看下面代码:

public class UnSafePublish {
private final User user = new User(); public User getUser() {
return user;
} public static void main(String[] args) {
UnSafePublish unSafePublish = new UnSafePublish();
User user = unSafePublish.getUser();
System.out.println("before modify, user = " + unSafePublish.getUser());
user.setAge(88);
System.out.println("after modify, user = " + unSafePublish.getUser());
} static class User {
private int age; public int getAge() {
return age;
} public void setAge(int age) {
this.age = age;
} @Override
public String toString() {
return "UserVo[" +
"age=" + age +
']';
}
}
}

从程序输出可以看到,user对象的内容发生了改变,如果多个线程同时操作,user对象在堆中的数据是不可预知的。

  那么这个问题应该怎么处理呢?我们在发布这对象出去的时候,就应该用线程安全的方式包装这个对象。对于我们自己使用或者声明的类,JDK自然没有提供这种包装类的办法,但是我们可以仿造这种模式或者委托给线程安全的类,当然,对这种通过get等方法发布出去的对象,最根本的解决办法还是应该在实现上就考虑到线程安全问题。对上面的代码进行改造:

public class SafePublicUser {
private final User user; public User getUser() {
return user;
} public SafePublicUser(User user) {
this.user = new SynUser(user);
} /**
* 线程安全的类,将内部成员对象进行线程安全包装
*/
static class SynUser extends User {
private final User user;
private final Object lock = new Object(); public SynUser(User user) {
this.user = user;
} @Override
public int getAge() {
synchronized (lock) {
return user.getAge();
}
} @Override
public void setAge(int age) {
synchronized (lock) {
user.setAge(age);
}
}
} static class User {
private int age; public int getAge() {
return age;
} public void setAge(int age) {
this.age = age;
} @Override
public String toString() {
return "UserVo[" +
"age=" + age +
']';
}
}
}

2、死锁

2.1 死锁定义

  死锁的发生必须具备以下四个必要条件:

1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。

2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

举个例子来说明:

  老王和老宋去大保健,老王抢到了1号技师,擅长头部按摩,老宋抢到了2号技师,擅长洗脚。但是老王和老宋都想同时洗脚和头部按摩,于是互不相让,老王抢到了1号,还想要2号,老宋抢到了2号,还想要1号。在洗脚和头部按摩这个事情上老王和老宋就产生了死锁,怎么样可以解决这个问题呢?

方案1:老板了解到情况,派3号技师过来,3号技师擅长头部按摩,老王只有一个头,所以3号只能给老宋服务,这个时候死锁就被打破。

方案2:大保健会所的老板比较霸道,规定了只能先头部按摩,再洗脚。这种情况下,老王和老宋谁先抢到1号,谁就先享受,另一个没抢到的就等着,这种情况也不会产生死锁。

对死锁做一个通俗易懂的总结:

  死锁是必然发生在多个操作者(M>=2)情况下,争夺多个资源(N>=2,且M>=N)才会发生这种情况。很明显,单线程不会有死锁,只有老王一个去,1号2号都归他,没人跟他抢。单资源呢?只有1号,老王和老宋也只会产生激烈竞争,打得不可开交,谁抢到就是谁的,但不会产生死锁。同时,死锁还有两个重要的条件,争夺资源的顺序不对,如果争夺资源的顺序是一样的,也不会产生死锁,另一个条件就是,争夺者拿到资源后不放手。

2.2 死锁的危害

  一旦程序中出现了死锁,危害是非常致命的,大致有以下几个原因:

1)线程不工作了,但是整个程序还是活着的。

2)没有任何的异常信息可以供我们检查。

3)程序发生了发生了死锁,是没有任何的办法恢复的,只能重启程序,对生产平台的程序来说,这是个很严重的问题。

2.3 死锁的例子

  上面讲了那么多关于死锁的概念,现在直接撸一段死锁代码看看。

public class DeadLockDemo {
private static Object No1 = new Object();
private static Object No2 = new Object(); /***
* 老王抢到了1号,还想要2号
* @throws InterruptedException
*/
private static void laowang() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (No1) {
System.out.println(threadName + " get NO1");
Thread.sleep(100);
synchronized (No2) {
System.out.println(threadName + " get NO2");
}
}
} /***
* 老宋抢到了2号,还想要1号
* @throws InterruptedException
*/
private static void laosong() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (No2) {
System.out.println(threadName + " get NO2");
Thread.sleep(100);
synchronized (No1) {
System.out.println(threadName + " get NO1");
}
}
} private static class Laowang extends Thread {
private String name; public Laowang(String name) {
this.name = name;
} @Override
public void run() {
Thread.currentThread().setName(name);
try {
laowang();
} catch (Exception e) {
e.printStackTrace();
}
}
} private static class Laosong extends Thread {
private String name; public Laosong(String name) {
this.name = name;
} @Override
public void run() {
Thread.currentThread().setName(name);
try {
laosong();
} catch (Exception e) {
e.printStackTrace();
}
}
} public static void main(String[] args) throws InterruptedException {
Laosong laosong = new Laosong("laosong");
laosong.start(); Laowang laowang = new Laowang("laowang");
laowang.start(); Thread.sleep(10);
}
}

程序输出可以看到,老宋抢到了2号,老王抢到了1号,因为产生了死锁,程序没有结束,但是并没有往下执行。

2.4 死锁的定位

  通过JDK的jps查看应用的id,再使用jstack查看应用持有锁的情况。

  可以看到"laowang"这个线程持有了<0x000000076b393b78>锁,还想获得<0x000000076b393b88>锁;"laosong"这个线程持有了<0x000000076b393b88>锁,还想获取<0x000000076b393b78>锁。

2.5 死锁的解决方案

1)保证拿锁的顺序一致,内部通过顺序比较,确定拿锁的顺序。

2)采用尝试拿锁的机制。

我们分别用这2种解决方案来改造上面死锁的代码,先看方案1:

public class NormalLockDemo {
private static Object No1 = new Object();
private static Object No2 = new Object(); /**
* 按照No1、No2顺序加锁
* @throws InterruptedException
*/
private static void laowang() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (No1) {
System.out.println(threadName + " get NO1");
Thread.sleep(100);
synchronized (No2) {
System.out.println(threadName + " get NO2");
}
}
} /**
* 按照No1、No2顺序加锁
* @throws InterruptedException
*/
private static void laosong() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (No1) {
System.out.println(threadName + " get NO1");
Thread.sleep(100);
synchronized (No2) {
System.out.println(threadName + " get NO2");
}
}
} static class Laowang extends Thread {
private String name; public Laowang(String name) {
this.name = name;
} @Override
public void run() { Thread.currentThread().setName(name);
try {
laowang();
} catch (Exception e) {
e.printStackTrace();
}
}
} static class Laosong extends Thread {
private String name; public Laosong(String name) {
this.name = name;
} @Override
public void run() {
Thread.currentThread().setName(name);
try {
laosong();
} catch (Exception e) {
e.printStackTrace();
}
}
} public static void main(String[] args) throws InterruptedException {
Laosong laosong = new Laosong("laosong");
laosong.start(); Laowang laowang = new Laowang("laowang");
laowang.start(); Thread.sleep(1000);
System.out.println("2个人都完成了大保健");
}
}

从程序输出可以看到,通过顺序拿锁的方式,2个人都完成了大保健,解决了死锁问题。

再看方案2,使用ReentrantLock采用尝试获取锁的方式,如果对ReentrantLock不熟悉,欢迎阅读《java之AQS和显式锁》。

import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; public class TryLock {
private static Lock No1 = new ReentrantLock();
private static Lock No2 = new ReentrantLock(); /***
* 先尝试拿No1锁,再尝试拿No2锁,No2锁没拿到,连同No1锁一起释放掉
* @throws InterruptedException
*/
private static void laowang() throws InterruptedException {
String threadName = Thread.currentThread().getName();
Random r = new Random();
while (true) {
if (No1.tryLock()) {
try {
System.out.println(threadName + " get NO2");
if (No2.tryLock()) {
try {
System.out.println(threadName + " get NO1");
break;
} finally {
No2.unlock();
}
}
} finally {
No1.unlock();
}
}
Thread.sleep(r.nextInt(5));
}
} /**
* 先尝试拿No2锁,再尝试拿No1锁,No1锁没拿到,连同No2锁一起释放掉
*
* @throws InterruptedException
*/
private static void laosong() throws InterruptedException {
String threadName = Thread.currentThread().getName();
Random r = new Random();
while (true) {
if (No2.tryLock()) {
try {
System.out.println(threadName + " get NO2");
if (No1.tryLock()) {
try {
System.out.println(threadName + " get NO1");
break;
} finally {
No1.unlock();
}
}
} finally {
No2.unlock();
}
}
}
Thread.sleep(r.nextInt(5)); } static class Laowang extends Thread {
private String name; public Laowang(String name) {
this.name = name;
} @Override
public void run() {
Thread.currentThread().setName(name);
try {
laowang();
} catch (Exception e) {
e.printStackTrace();
}
}
} static class Laosong extends Thread {
private String name; public Laosong(String name) {
this.name = name;
} @Override
public void run() {
Thread.currentThread().setName(name);
try {
laosong();
} catch (Exception e) {
e.printStackTrace();
}
}
} public static void main(String[] args) throws InterruptedException {
Laosong laosong = new Laosong("laosong");
laosong.start(); Laowang laowang = new Laowang("laowang");
laowang.start(); Thread.sleep(1000);
System.out.println("2个人都完成了大保健");
}
}

  从程序输出可以看到,laowang线程抢到了NO2这把锁,但是在获取NO1的时候失败了,所以把NO2也释放了。这样做就使得2个线程都可以获取到锁,不会有死锁问题产生。

3、结语

  本篇幅就介绍这么多内容,希望大家看了有收获。Java并发编程专题要分享的内容到此就结束了,下一个专题将介绍Java性能优化和JVM相关内容,阅读过程中如发现描述有误,请指出,谢谢。

java并发安全的更多相关文章

  1. 多线程的通信和同步(Java并发编程的艺术--笔记)

    1. 线程间的通信机制 线程之间通信机制有两种: 共享内存.消息传递.   2. Java并发 Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式执行,通信的过程对于程序员来说是完全透 ...

  2. 【Java并发编程实战】----- AQS(四):CLH同步队列

    在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形.其主要从两方面进行了改造:节点的结构与节点等待机制.在结构上引入了头 ...

  3. 【Java并发编程实战】----- AQS(三):阻塞、唤醒:LockSupport

    在上篇博客([Java并发编程实战]----- AQS(二):获取锁.释放锁)中提到,当一个线程加入到CLH队列中时,如果不是头节点是需要判断该节点是否需要挂起:在释放锁后,需要唤醒该线程的继任节点 ...

  4. 【Java并发编程实战】----- AQS(二):获取锁、释放锁

    上篇博客稍微介绍了一下AQS,下面我们来关注下AQS的所获取和锁释放. AQS锁获取 AQS包含如下几个方法: acquire(int arg):以独占模式获取对象,忽略中断. acquireInte ...

  5. 【Java并发编程实战】-----“J.U.C”:CLH队列锁

    在前面介绍的几篇博客中总是提到CLH队列,在AQS中CLH队列是维护一组线程的严格按照FIFO的队列.他能够确保无饥饿,严格的先来先服务的公平性.下图是CLH队列节点的示意图: 在CLH队列的节点QN ...

  6. 【Java并发编程实战】-----“J.U.C”:CountDownlatch

    上篇博文([Java并发编程实战]-----"J.U.C":CyclicBarrier)LZ介绍了CyclicBarrier.CyclicBarrier所描述的是"允许一 ...

  7. 【Java并发编程实战】-----“J.U.C”:CyclicBarrier

    在上篇博客([Java并发编程实战]-----"J.U.C":Semaphore)中,LZ介绍了Semaphore,下面LZ介绍CyclicBarrier.在JDK API中是这么 ...

  8. 【Java并发编程实战】-----“J.U.C”:ReentrantReadWriteLock

    ReentrantLock实现了标准的互斥操作,也就是说在某一时刻只有有一个线程持有锁.ReentrantLock采用这种独占的保守锁直接,在一定程度上减低了吞吐量.在这种情况下任何的"读/ ...

  9. Java并发编程:volatile关键字解析

    Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在 ...

  10. JAVA并发编程J.U.C学习总结

    前言 学习了一段时间J.U.C,打算做个小结,个人感觉总结还是非常重要,要不然总感觉知识点零零散散的. 有错误也欢迎指正,大家共同进步: 另外,转载请注明链接,写篇文章不容易啊,http://www. ...

随机推荐

  1. 一起了解 .Net Foundation 项目 No.11

    .Net 基金会中包含有很多优秀的项目,今天就和笔者一起了解一下其中的一些优秀作品吧. 中文介绍 中文介绍内容翻译自英文介绍,主要采用意译.如与原文存在出入,请以原文为准. Microsoft Web ...

  2. LeetCode python实现题解(持续更新)

    目录 LeetCode Python实现算法简介 0001 两数之和 0002 两数相加 0003 无重复字符的最长子串 0004 寻找两个有序数组的中位数 0005 最长回文子串 0006 Z字型变 ...

  3. 线程(Thread)的四种停止方式

    1.正常的程序启动,停止 2.使用退出标记,一般程序在run()方法后,线程会正常结束.但是有一些伺服线程还在运行,他们运行时间较长,只有当外部条件满足时,他们才会停止.实现如下: public cl ...

  4. 必备技能三、render渲染函数

    Vue 推荐使用在绝大多数情况下使用 template 来创建你的 HTML.然而在一些场景中,你真的需要 JavaScript 的完全编程的能力,这就是 render 函数,它比 template ...

  5.  FPGA边沿检测Verilog代码

    FPGA边沿检测Verilog代码(上升沿,下降沿,双边沿) 实现思路:用两个一位寄存器直接异或可以实现 代码实现: ​ module edge_detect( input clk, input rs ...

  6. 如何为SpringBoot服务添加HTTPS证书

    HTTPS是HTTP的安全版本,旨在提供数据传输层安全性(TLS).当你的应用不使用HTTP协议的时候,浏览器地址栏就会出现一个不安全的提示.HTTPS加密每个数据包以安全方式进行传输,并保护敏感数据 ...

  7. 计算机网络原理实验_使用网络协议分析仪Wireshark

    一.实验名称  使用网络协议分析仪Wireshark 二.实验目的: 1. 掌握安装和配置网络协议分析仪Wireshark的方法: 2. 熟悉使用Wireshark工具分析网络协议的基本方法,加深对协 ...

  8. 基于微信小程序的租房小程序

    乐直租全国租房小程序前端 房源分钟上传,可快捷联系房东的小程序. 该小程序操作简单,布局清新,欢迎 start ~ 传送门:Github 扫码体验: pages: 首页 index 选择发布页 bef ...

  9. 利用mnist数据集进行深度神经网络

    初始神经网络 这里要解决的问题是,将手写数字的灰度图像(28 像素 x28 像素)划分到 10 个类别中(0~9).我们将使用 MINST 数据集,它是机器学习领域的一个经典数据集,其历史几乎和这个领 ...

  10. js Number方法总结

    Number构造属性 Number.EPSILON // 两个可表示(representable)数之间的最小间隔. Number.MAX_SAFE_INTEGER // JavaScript 中最大 ...