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

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. Sublime Text3 旧版本下载以及破解激活方式

    前言 当前Sublime Text3 出到了32**版本,以前直接输入激活码的方法已经不能使用. 而官网又不提供旧版本的下载链接,因此在此分享旧版本下载方式以及激活方式. 下载方法 通过下面这个链接下 ...

  2. ORACLE数据库实现主键自增

    ORACLE数据库是甲骨文公司的一款关系数据库管理系统. 实现主键自动增长需要四个步骤: 去看 创建表格 去看 创建自增序列 去看 创建触发器 去看 插入测试 1.创建表格(必须有主键) -- 创建学 ...

  3. JavaScript sort() 对json进行排序(数组)

    function up(x,y){//升序 return x[val.prop] - y[val.prop] } function down(x,y){//降序 return y[val.prop] ...

  4. Java基础--插入排序

    直接插入排序算法 (从后往前找到合适位置插入) 基本思想:每步将一个待排序的记录,按其顺序码大小插入到前面已经排序的子序列的合适位置(从后向前找到合适位置后),直到全部插入排序完为止. 例: 34,4 ...

  5. 2019年最新老男孩高性能Web架构与自动化运维架构视频教程

    课程目录L001-老男孩架构15期-Web架构之单机时代L002-老男孩架构15期-Web架构之集群时代L003-老男孩架构15期-Web架构之dnsL004-老男孩架构15期-Web架构之缓存体系L ...

  6. SpringBoot1.5.10.RELEASE项目如何在tomcat容器中运行

    一. SpringBoot内嵌容器的部署方式 SpringBoot内部默认提供内嵌的tomcat容器,所以可以直接打成jar包,丢到服务器上的任何一个目录,然后在当前目录下执行java -jar de ...

  7. SpringBoot入门系列(五)Thymeleaf的常用标签和用法

    前面介绍了Spring Boot 中的整合Thymeleaf .不清楚的朋友可以看看之前的文章:https://www.cnblogs.com/zhangweizhong/category/16577 ...

  8. 基于GIS空间分析的多边形提取技术

    现有基于矢量图形的骨架线提取方法主要包括数据预处理.基于约束 Delauny 三角剖分的骨架线结点生成和骨架线的连接 3 个过程,上述过程都可利用现有 GIS 系统的数据处理.空间分析和建模功能实现. ...

  9. 2020ubuntu1804server编译安装redis5笔记(二)配置redis

    前一篇笔记记录了ubuntu1804server编译安装redis5,接下来要配置redis5了 网址:https://www.cnblogs.com/qumogu/p/12435694.html 第 ...

  10. WEB渗透 - HTTP协议基础

    年初八 星灯花 https只能提高传输层安全 每一次客户端和服务端的通信都是独立的过程 cookie包括了sessionID和其他信息 重要的header S - C Set-Cookie:服务器发给 ...