12 多线程并发拓展

1️⃣死锁
1 概念

所谓的死锁是指两个或者两个以上的线程在执行过程中,因争夺资源而造成的互相等待的情况,这些永远在等待的进程称为死锁进程;由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力的情况下永远无法分配到必须的资源而无法继续前进;

2 死锁发生所具备的条件

① 互斥条件 : 指进程对所分配的资源进行排他性的使用,即在一段时间内某资源只有一个进程占用,如果此时还有其他线程请求资源,请求者只能等待(等占用资源的进程释放才可以);

② 请求和保持条件 : 指进程已经保持了至少一个资源,但又提出了一个新的资源请求而该资源已被其他进程占有,此时请求进程将会阻塞,但又不释放自己已经获得的资源;

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

④ 环路等待条件 : 顾名思义就是说在发生死锁的时候,持有资源的进程之间形成一个环形的链,彼此都处于等待状态;

3 死锁代码演示
/**
 * 一个简单的死锁类
 * 当DeadLock类的对象flag==1时(td1),先锁定o1,睡眠500毫秒
 * 而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒
 * td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定;
 * td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定;
 * td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。
 */

@Slf4j
public class DeadLock implements Runnable {
    public int flag = 1;
    //静态对象是类的所有对象共享的
    private static Object o1 = new Object(), o2 = new Object();

    @Override
    public void run() {
        log.info("flag:{}", flag);
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    log.info("1");
                }
            }
        }
        if (flag == 0) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    log.info("0");
                }
            }
        }
    }

    public static void main(String[] args) {
        DeadLock td1 = new DeadLock();
        DeadLock td2 = new DeadLock();
        td1.flag = 1;
        td2.flag = 0;
        //td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。
        //td2的run()可能在td1的run()之前运行
        new Thread(td1).start();
        new Thread(td2).start();
    }
}
4 如何避免死锁

① 加锁顺序
线程已经要按照一定的顺序进行加锁,从上边的例子我们就已经可以得出这样的结论;

② 加锁时限
我们的系统在尝试获取锁的时候可以加上一定的时限,超过时限的时候就放弃对该锁的请求并释放自己占有的锁;

③ 死锁检测
这种方法实现起来相对比较难,死锁检测是一种比较好的预防死锁的机制,它主要是对那些不可能实现按序加锁并且锁超时也不可行的场景,每当一个线程获得锁会在线程和锁相关的数据结构中进行记录,同时每当有线程请求锁也将存储在数据结构中,当一个线程请求失败的时候这个线程可以遍历锁的关系图,看是否有死锁发生并决定后续采用什么样的操作,但是这个存储的数据结构需要我们根据实际情况来进行设计;
如果检测到死锁的情况我们可以做什么样的操作呢?
1 释放所有锁进行回退,并且等待一段时间以后(时间是随机)进行重试,这种方法和简单的加锁超时有一些类似,不一样的是只有在死锁发生的时候才会回退;虽然释放了所有的锁但是如果有大量的线程同时请求同一种锁仍然会发生死锁,这个时候我们可以赋予线程优先级来解决这个问题,为了避免同样的情况发生我们可以在死锁发生的时候设置随机的线程优先级;


2️⃣ 并发最佳实践
1 使用本地变量

在多线程并发环境下应该尽量使用本地变量,而不是创建一个类或者实例的变量,通常情况我们使用对象实例作为变量可以节省内存并且可以重用,因为每次在方法中创建本地变量会消耗很多内存;

2 使用不可变类

不可变类比如String Integer等一旦创建就不会改变了,不可变可以降低代码中的同步数量;

3 最小化锁的作用于范围 : S = 1 / (1 - a + a / n)

a = 并行计算部分所占的比例;
n = 并行处理的节点个数;
S = 加速比;
当1 - a = 0的时候是没有串行只有并行,最大的加速比=n;当a = 0的时候只有串行没有并行,此时最小的加速比s = 1;当n = 无穷大时极限的s = 1 / 1 - a,同时这也是加速比的上限;例如如果串行代码占总体代码的25%,那么并行处理的总体性能不可能超过4;S = 1 / (1 - a + a / n)这个公式也被称为安达尔定理;

4 使用线程池的Executor,而不是直接new Thread执行

创建一个线程的代价是昂贵的,如果要得到一个可伸缩的Java应用我们需要使用线程池,JDK中提供了各种ThreadPool线程池和Executor;

5 宁可使用同步也不要使用线程的wait和notify

从Java1.5以后增加了许多同步工具,此时我们首先需要考虑的是应该使用同步方法而不是线程的wait和notify;此时使用队列生产消费的设计化比使用线程的等待要好的多,

6 使用BlockingQueue实现生产-消费模式

大部分并发问题都可以使用生产-消费实现,BlockingQueue是其中最好的实现方式,阻塞队列不只是可以实现单个生产单个消费也可以处理多个生产和消费;

7 使用并发集合而不是加锁的同步集合

Java提供了多种并发集合与同步集合,在多线程并发的环境下建议多使用并发集合来解决问题;

8 使用Semaphore创建有界的访问

为了建立可靠的稳定的系统,对于数据库文件系统等必须要做有界的访问,Semaphore是一个可以限制这些资源开销的选择;我们可以使用Semaphore来控制多个线程同时访问某个资源的线程数;

9 宁可使用同步代码块也不使用同步方法

使用同步代码块只会锁定一定对象而不会将整个方法锁定,如果更改共同的变量和类的字段,首先应该选择的是原子性变量;

10 避免使用静态变量

静态变量在并发执行环境下会制造很多问题,如果你优先使用静态变量需要将其先制成final变量,如果是用来保存集合的话我们可以考虑使用只读集合,否则就需要做特别多的同步处理以及并发处理;


3️⃣Spring与线程安全
1 Spring bean

Spring作为一个IOC容器帮我们管理了许许多多的bean,但是Spring并没有保证这些线程的安全,Spring对每一个bean提供了一个scope属性作为该bean的作用域,他是这个bean的生命周期,比如scope为singleton就是单例在第一次被注入时会创建一个单例对象;它是每个bean的默认scope,该对象的声明周期与IOC容器是一致的但是只会在第一次被注入时创建;

2 无状态对象

我们交由Spring管理的对象大多数都是无状态对象,这种不会因为多线程而导致状态被破坏的对象很适合Spring的默认scope,每个单例的无状态对象都是线程安全的;


4 HashMap与ConcurrentHashMap解析
1 HashMap

① 概述

从上图中我们可以看出HashMap的底层就是一个数组结构,数组中的每一项又是一个链表;当我们新建一个HashMap的时候就会初始化一个数组出来,HashMap有两个参数会影响他们的性能分别是初始容量(默认16)和加载因子(0.75);容量是哈希表中桶的数量,初始容量是哈希表在创建时的容量,加载因子是哈希表在他的容量自动增加之前可以达到多满的一个尺度,当哈希表中的条目数量超过了加载因子与当前容量的乘积将会调用resize的方法进行扩容,然后将容量进行翻倍;
初始容量与加载因子在初始化的时候是可以指定的,

② HashMap的寻址方式
对于一个新插入的数据或者需要读取的数据,HashMap需要加上key按照一定的计算规则计算出哈希值并对数组长度进行取模结果作为数组的index,在计算机中取模的代价要高于位操作的代价,所以HashMap要求数组的长度必须是2的n次方,此时它将对key的哈希值对n的2-1次方进行与运算它的结果与取模的操作结果是相同的;HashMap并不要求我们在创建的时候传入一个2的n次方的整数,而是在初始化时计算出一个满足条件的容量;众所周知HashMap不是线程安全的,主要体现在扩容操作时的reHash会出现死循环;

③ 单线程的reHash

④ 多线程并发下的reHash
2 ConcurrentHashMap

① 概述

ConcurrentHashMap底层仍然是一个数组加链表,与HashMap不同的是ConcurrentHashMap最外层不是一个大的数组而是一个Segment数组,每一个Segment包含一个与HashMap数据结构差不多的链表数组,当我们读取某个key的时候它先取出该key的Hash值并将Hash值的高位对Segment个数取模,从而得到该key属于哪一个Segment,接着就像操作HashMap一样操作Segment,为了保证不同的值均匀的分配到不同的Segment里边它计算Hash值也做了一定的优化,Segment继承自JUC里边的ReentrantLock,所以我们可以很轻松的堆每一个Segment做锁相关的处理;需要注意的是JDK7及以前是基于分段锁来进行处理的;而JDK8对于ConcurrentHashMap进行优化引入了红黑树
3 HashMap与ConcurrentHashMap的不同点

① HashMap不是线程安全的而ConcurrentHashMap是线程安全的;
② HashMap允许key和value而ConcurrentHashMap是不允许的;
③ HashMap不允许通过迭代器遍历的同时通过HashMap来修改而ConcurrentHashMap是允许该行为的并且这个更新对后续的遍历是可见的;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  • 本文是我自己在秋招复习时的读书笔记,整理的知识点,也是为了防止忘记,尊重劳动成果,转载注明出处哦!如果你也喜欢,那...
    波波波先森阅读 11,150评论 4 56
  • Java继承关系初始化顺序 父类的静态变量-->父类的静态代码块-->子类的静态变量-->子类的静态代码快-->父...
    第六象限阅读 2,081评论 0 9
  • 在一个方法内部定义的变量都存储在栈中,当这个函数运行结束后,其对应的栈就会被回收,此时,在其方法体中定义的变量将不...
    Y了个J阅读 4,390评论 1 14
  • 第三章 Java内存模型 3.1 Java内存模型的基础 通信在共享内存的模型里,通过写-读内存中的公共状态进行隐...
    泽毛阅读 4,288评论 2 22
  • 任何事情都要规划,工作也不例外。得明确自己每天需要完成的工作。并有所记录。不拖延不夸张
    登登一君阅读 243评论 0 0