关于线程同步:多线程之经典死锁场景及其解决哲学家就餐问题

编程练习-题目来源于宋劲杉《linuxc》哲学家就餐问题。这是由计算机科学家Dijkstra提出的经典死锁场景。原版的故事里有五个哲学家(不过咱们写的程序能够有N个哲学家),这些哲学家们只做两件事--思考和吃饭,他们思考的时候不须要任何共享资源,然而吃饭的时候就必须应用餐具,而餐桌上的餐具是无限的,原版的故事里,餐具是叉子,吃饭的时候要用两把叉子把面条从碗里捞进去。很显然把叉子换成筷子会更正当,所以:一个哲学家须要两根筷子能力吃饭。 当初引入问题的要害:这些哲学家很穷,只买得起五根筷子。他们坐成一圈,两个人的两头放一根筷子。哲学家吃饭的时候必须同时失去左手边和右手边的筷子。如果他身边的任何一位正在应用筷子,那他只有等着。 假如哲学家的编号是A、B、C、D、E,筷子编号是1、2、3、4、5,哲学家和筷子围成一圈如下图所示:图 35.2. 哲学家问题 每个哲学家都是一个独自的线程,每个线程循环做以下动作:思考rand()%10秒,而后先拿左手边的筷子再拿右手边的筷子(筷子这种资源能够用mutex示意),有任何一边拿不到就始终等着,全拿到就吃饭rand()%10秒,而后放下筷子。 编写程序仿真哲学家就餐的场景: Philosopher A fetches chopstick 5Philosopher B fetches chopstick 1Philosopher B fetches chopstick 2Philosopher D fetches chopstick 3Philosopher B releases chopsticks 1 2Philosopher A fetches chopstick 1Philosopher C fetches chopstick 2Philosopher A releases chopsticks 5 1...解决方案参考自https://blog.csdn.net/theLost... 1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<malloc.h> 4 #include<time.h> 5 #include<unistd.h> 6 #include<pthread.h> 7 #include<semaphore.h> 8 9 #define NUM 5 10 11 sem_t chopsticks[NUM];//sem_t信号量参数,示意每根快筷子最多只能同时被一人拿起 12 sem_t r;//拿起左筷子的信号量参数,只能同时有4人拿起左筷子,否则后产生死锁 13 int philosophers[NUM] = {0,1,2,3,4};//哲学家数组,示意5位哲学家0,1,2,3,4, 14 15 pthread_mutex_t chops[NUM];//互斥量即锁的控制变量 16 17 //int Islocked[NUM] = {0};//本人实现互斥锁须要的控制变量数组 18 //多个线程同时调用通常的swap函数的时候容易产生指令错排从而影响后果 19 //而咱们应用intel X86的指令集中提供了替换两数的指令xchg,寄存器管制不会呈现并发的状况 20 //void xchg(int *x,int *y){//汇编中的替换指令 21 // __asm__("xchgl %0, %1" : "=r" (*x) : "m" (*y)); 22 //} 23 24 //外围函数(利用信号量管制最多4集体拿起左筷子来杜绝死锁的产生) 25 void *philosopher(void *arg){//arg由init函数的第四个参数传递而来 26 int i = *((int *)arg);//i代表第i个哲学家 27 int left = i;//右边的筷子设为i 28 int right = (i+1)%NUM;//循环队列构造,左边筷子设为i+1 29 //int leftkey;//本人实现左互斥锁须要的key变量 30 //int rightkey;//本人实现右互斥锁须要的key变量 31 while(1){ 32 //leftkey = 1; 33 //rightkey = 1; 34 35 printf("哲学家%d正在思考\n",i); 36 sleep(rand()%NUM);//过程挂起一段时间,代表思考了一会 37 38 printf("哲学家%d饿了\n",i); 39 40 sem_wait(&r);//信号量r - 1(若r=0则挂起期待,r>0则能够取得线程资源执行后续操作,r不会<0) 41 sem_wait(&chopsticks[left]);//留神每个chopsticks的信号量只有1个 42 pthread_mutex_lock(&chops[left]); 43 //do{ 44 //xchg(&leftkey,&Islocked[left]);//此时leftkey为非1,会将1替换给Islocked[left] 45 //于是第二个线程进来时就会阻塞在do_while循环实现临界资源的锁定。即linux外面的mutex性能 46 //}while(leftkey); 47 printf("哲学家%d拿起了%d号筷子,当初只有一支筷子,不能进餐\n",i,left); 48 49 //do{ 50 //xchg(&rightkey,&Islocked[right]); 51 //}while(rightkey); 52 sem_wait(&chopsticks[right]); 53 pthread_mutex_lock(&chops[right]); 54 printf("哲学家%d拿起了%d号筷子,当初只有两支筷子,开始进餐\n",i,right); 55 sleep(rand()%NUM);//过程挂起一段时间,代表吃了一段时间完结 56 57 //Islocked[left] = 0;//以后一个拿左筷子信号执行完就能够让第二个线程进入 58 sem_post(&chopsticks[left]); 59 pthread_mutex_unlock(&chops[left]); 60 printf("哲学家%d放下了%d号筷子\n",i,left); 61 62 //Islocked[right] = 0;//以后一个拿左筷子信号执行完就能够让第二个线程进入 63 sem_post(&chopsticks[right]); 64 pthread_mutex_unlock(&chops[right]); 65 printf("哲学家%d放下了%d号筷子\n",i,right); 66 67 sem_post(&r);//开释资源,信号量+1,于是能够同时唤醒挂起期待信号量>0的线程 68 69 } 70 } 71 int main(int argc,char **argv){ 72 srand(time(NULL)); 73 pthread_t PHD[NUM];//要开拓的线程组 74 75 int i= 0; 76 for(i = 0;i < NUM; i++){ 77 sem_init(&chopsticks[i],0,1);//信号量初始化,每只筷子只能同时被拿起一次 78 } 79 sem_init(&r,0,4);//信号量控制变量r,管制同时拿起左筷子的信号不能超过4 80 81 int j = 0; 82 for(j = 0; j < NUM; j++){ 83 pthread_mutex_init(&chops[j],NULL);//互斥锁的初始化 84 } 85 86 int k = 0; 87 for(k = 0; k < NUM; k++){ 88 pthread_create(&PHD[k],NULL,philosopher,&philosophers[k]);//创立5个哲学家的行为线程 89 } 90 91 int l = 0; 92 for(l = 0; l < NUM; l++){ 93 pthread_join(PHD[l],NULL);//期待每个线程终止,将这些线程由终止态变为detach态并发出线程所占用的资源 94 } 95 96 int m = 0; 97 for(m = 0; m < NUM; m++){ 98 sem_destroy(&chopsticks[m]);//开释变量所占用的信号量相干资源 99 }100 sem_destroy(&r);//同上,开释左筷子监控占用的信号量资源101 102 int n = 0;103 for(n = 0; n < NUM; n++){104 pthread_mutex_destroy(&chops[n]);//销毁互斥锁,开释资源105 }106 107 return 0;108 }我这里是在linux环境下应用mutex实现,在windows环境下能够用正文掉的while-do-while循环来模仿实现互斥锁mutex,也能胜利运行失去后果!gcc philosopher_mutex.c -o philosopher_mutex -lpthread ...

February 19, 2022 · 2 min · jiezi

关于线程同步:线程间同步之消费者与生产者案例Condition-Variable

该程序通过一个生产者-消费者的例子来展现线程间的同步状况,具体形式为生产者生产一个构造体串在链表的表头上,消费者从表头取走构造体,生产者未生产或生产的曾经被拿完,则消费者须要挂起期待. 本文通过两种链表形式来进行实现,留神是在linux环境下编译链接:1、生产者和消费者拜访链表的程序是LIFO的。 1 #include <stdlib.h> 2 #include <pthread.h> 3 #include <stdio.h> 4 5 struct msg { 6 struct msg *next; 7 int num; 8 }; 9 10 struct msg *head; 11 pthread_cond_t has_product = PTHREAD_COND_INITIALIZER; 12 pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; 13 14 void *consumer(void *p) 15 { 16 struct msg *mp; 17 18 for (;;) { 19 pthread_mutex_lock(&lock); 20 while (head == NULL) 21 pthread_cond_wait(&has_product, &lock); 22 mp = head; 23 head = mp->next; 24 pthread_mutex_unlock(&lock); 25 printf("Consume %d\n", mp->num); 26 free(mp); 27 sleep(rand() % 5); 28 } 29 } 30 31 void *producer(void *p) 32 { 33 struct msg *mp; 34 for (;;) { 35 mp = malloc(sizeof(struct msg)); 36 mp->num = rand() % 1000 + 1; 37 printf("Produce %d\n", mp->num); 38 pthread_mutex_lock(&lock); 39 mp->next = head; 40 head = mp; 41 pthread_mutex_unlock(&lock); 42 pthread_cond_signal(&has_product); 43 sleep(rand() % 5); 44 } 45 } 46 2、生产者和消费者拜访链表的程序是FIFO的。 ...

February 19, 2022 · 2 min · jiezi

如何线程安全地遍历List

遍历List的多种方式在讲如何线程安全地遍历 List 之前,先看看遍历一个 List 通常会采用哪些方式。 方式一:for(int i = 0; i < list.size(); i++) { System.out.println(list.get(i));}方式二:Iterator iterator = list.iterator();while(iterator.hasNext()) { System.out.println(iterator.next());}方式三:for(Object item : list) { System.out.println(item);}方式四(Java 8):list.forEach(new Consumer<Object>() { @Override public void accept(Object item) { System.out.println(item); }});方式五(Java 8 Lambda):list.forEach(item -> { System.out.println(item);});方式一的遍历方法对于 RandomAccess 接口的实现类(例如 ArrayList)来说是一种性能很好的遍历方式。但是对于 LinkedList 这样的基于链表实现的 List,通过 list.get(i) 获取元素的性能差。 方式二和方式三两种方式的本质是一样的,都是通过 Iterator 迭代器来实现的遍历,方式三是增强版的 for 循环,可以看作是方式二的简化形式。 方式四和方式五本质也是一样的,都是使用Java 8新增的 forEach 方法来遍历。方式五是方式四的一种简化形式,使用了Lambda表达式。 遍历List的同时操作List会发生什么?先用非线程安全的 ArrayList 做个试验,用一个线程通过增强的 for 循环遍历 List,遍历的同时另一个线程删除 List 中的一个元素,代码如下: ...

November 3, 2019 · 3 min · jiezi

Java并发23并发设计模式-两阶段终止模式优雅地终止线程

前面我们都是在讲如何创建线程,接下来我们说下如何终止线程。 java的线程小节中,我曾讲过:线程执行完或者出现异常就会进入终止状态。这样看,终止一个线程看上去很简单啊!一个线程执行完自己的任务,自己进入终止状态,这的确很简单。不过我们今天谈到的“优雅地终止线程”,不是自己终止自己,而是在一个线程 T1 中,终止线程 T2;这里所谓的“优雅”,指的是给 T2 一个机会料理后事,而不是被直接终止。 Java 语言的 Thread 类中曾经提供了一个 stop() 方法,用来终止线程,可是早已不建议使用了,原因是这个方法用是直接终止的线程,线程并没有机会料理后事。 如何理解两阶段终止模式前辈们经过认真对比分析,已经总结出了一套成熟的方案,叫做两阶段终止模式。顾名思义,就是将终止过程分成两个阶段,其中第一个阶段主要是线程 T1 向线程 T2发送终止指令,而第二阶段则是线程 T2响应终止指令 两阶段终止模式示意图### 那在 Java 语言里,终止指令是什么呢?这个要从 Java 线程的状态转换过程说起。我们在 java的线程小节中曾经提到过 Java 线程的状态转换图。 从这个图里你会发现,Java 线程进入终止状态的前提是线程进入 RUNNABLE 状态,而实际上线程也可能处在休眠状态,也就是说,我们要想终止一个线程,首先要把线程的状态从休眠状态转换到 RUNNABLE 状态。如何做到呢?这个要靠 Java Thread 类提供的interrupt() 方法,它可以将休眠状态的线程转换到 RUNNABLE 状态。 线程转换到 RUNNABLE 状态之后,我们如何再将其终止呢?RUNNABLE 状态转换到终止状态,优雅的方式是让 Java 线程自己执行完 run() 方法,所以一般我们采用的方法是设置一个标志位,然后线程会在合适的时机检查这个标志位,如果发现符合终止条件,则自动退出 run() 方法。这个过程其实就是我们前面提到的第二阶段:响应终止指令 综合上面这两点,我们能总结出终止指令,其实包括两方面内容:interrupt() 方法和线程终止的标志位。 用两阶段终止模式终止监控操作实际工作中,有些监控系统需要动态地采集一些数据,一般都是监控系统发送采集指令给被监控系统的监控代理,监控代理接收到指令之后,从监控目标收集数据,然后回传给监控系统,详细过程如下图所示。出于对性能的考虑(有些监控项对系统性能影响很大,所以不能一直持续监控),动态采集功能一般都会有终止操作。 动态采集功能示意图### 下面的示例代码是监控代理简化之后的实现,start() 方法会启动一个新的线程 rptThread 来执行监控数据采集和回传的功能,stop() 方法需要优雅地终止线程 rptThread,那 stop() 相关功能该如何实现呢? class Proxy { boolean started = false; // 采集线程 Thread rptThread; // 启动采集功能 synchronized void start(){ // 不允许同时启动多个采集线程 if (started) { return; } started = true; rptThread = new Thread(()->{ while (true) { // 省略采集、回传实现 report(); // 每隔两秒钟采集、回传一次数据 try { Thread.sleep(2000); } catch (InterruptedException e) { } } // 执行到此处说明线程马上终止 started = false; }); rptThread.start(); } // 终止采集功能 synchronized void stop(){ // 如何实现? }} 按照两阶段终止模式,我们首先需要做的就是将线程 rptThread 状态转换到 RUNNABLE,做法很简单,只需要在调用 rptThread.interrupt() 就可以了。线程 rptThread 的状态转换到 RUNNABLE 之后,如何优雅地终止呢?下面的示例代码中,我们选择的标志位是线程的中断状态:Thread.currentThread().isInterrupted() ,需要注意的是,我们在捕获 Thread.sleep() 的中断异常之后,通过 Thread.currentThread().interrupt() 重新设置了线程的中断状态,因为 JVM 的异常处理会清除线程的中断状态。 ...

July 15, 2019 · 2 min · jiezi

且听我一个故事讲透一个锁原理之synchronized

微信公众号:IT一刻钟大型现实非严肃主义现场一刻钟与你分享优质技术架构与见闻,做一个有剧情的程序员关注可第一时间了解更多精彩内容,定期有福利相送哟。故事从这里展开蜀国有一个皇帝叫蜀道难,他比较难伺候,别的皇帝早朝都是在大殿上同时接见所有大臣,共商国是。他不一样,他说早朝你们不要有事没事都跑过来叽叽喳喳,有事则来,无事则该干啥干啥去,然后安排太监每天早上在大门口守着,每次只允许一个大臣进来汇报情况。“你敢多放进来一个就砍脑袋的干活。“太监赶紧下跪,说“谪!“。第一天,太监传话钦天监求见,皇帝允了,钦天监上殿报曰:”臣禀报,昨日我司夜观星象,西方忽现王星忽明忽暗,恐戎狄那边有乱。““朕知道了,退下吧”。一日无事。第二天,太监传话钦天监求见,皇帝允了。一日无事。第三天,太监传话钦天监求见……一日无事。第四天,钦天监……一日无事。第五天,皇帝不耐烦了,和贾太监说,钦天监这老家伙整天是不是闲着没事,以后他来了不用给我禀报,直接放他上殿讲,讲完让他走吧。国泰民安的日子依旧过着,每天只有钦天监一个人来报告,贾太监每次看到是钦天监来了,也懒得搭理了,直接放他进去了。(这就是偏向锁,稍后我细细道来)又一日,钦天监如往常进殿报道,贾太监站在门口打着盹,忽然耳边传来一个声音:“贾太监,帮我禀告圣上,工部李尚书求见。”“emmm…进去吧…嗯?等等,尚书大人你先等等,钦天监在里面,你等会再来求见吧。”太监一阵后怕,寻思着钦天监还在里面呢,这要是放进去了,我这脑袋可就没了,果然嗜睡误事。过了一会儿,李尚书回来询问求见,被告知钦天监还没走,只好又离去。又过了一会儿,李尚书又回来询问求见,正巧钦天监走了,太监进殿传话说工部李尚书求见,皇帝宣觐见,李尚书进殿上报了一番东南连连大雨,已派人去监察水利,修缮河堤。(这就是轻量级锁)忽一日,西戎狄和北匈奴同时对帝国西方和北方发难,前线战事消息如片片雪花纷纷涌入京城,瞬间殿外来了一群大臣有要事禀告。一会儿这个来问贾公公我可以进去了吗?一会儿那个来问贾公公我可以进去了吗?把贾太监累的哟,一天下来光说“稍后再来”都把嘴皮子磨破了,没几日,贾太监就跪在皇帝面前哭泣道:“圣上啊,快想想办法呀,奴才这身子骨就要交代在门口了。”皇帝一听,说你傻啊,叫他们一个个在门外排队啊,谁叫你要他们稍后来求见的。贾太监细思大喜,觉得有理,次日在门口竖起一个牌子“禀报要事者,这边排队”,贾太监再也不用一个人对着一群人反复回话,只需要每次出来一个,然后传话放进去一个,就可以了。(这就是重量级锁)上面这个故事,分别讲述了synchronized内部四种级别的状态,分别是:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。重量级锁状态我们首先从重量级锁开始讲,重量级锁是通过互斥量(Mutex)来实现的,即一个线程进入了synchronized同步块,在未完成任务时,会阻塞后面的所有线程。就像上面的故事所讲的,要禀告要事的大臣只能在大殿门口外一个接一个的阻塞排队。之所以称它为重量级锁,是因为Java线程是映射到操作系统的原生线程上的,如果要阻塞或唤醒一个线程,都需要依靠操作系统从当前用户态转换到核心态中,这种状态转换需要耗费处理器很多时间,对于简单同步块,可能状态转换时间比用户代码执行时间还要长,导致实际业务处理所占比偏小,性能损失较大。当然这个在虚拟机层面进行了一些比如自旋等待,锁粗化等等的优化,避免陷入频繁的切换状态。在这里我就不细讲了,有兴趣的可以关注我,我后续再和各位看官讲上一讲。轻量级锁状态轻量级锁是JDK6引入的,它的轻量是相较于通过系统互斥量实现的传统锁,轻量锁并不是用来取代重量级锁的,而是在没有大量线程竞争的情况下,减少系统互斥量的使用,降低性能的损耗。轻量级锁是通过CAS(Compare And Swap)机制实现的,即如果锁被其他线程所占用,当前线程会通过自旋来获取锁,从而避免用户态与核心态的转换。就像上面故事所说的,大殿中钦天监在汇报工作,工部尚书要求见,并不需要贾太监每次都进去问一下皇帝,惹得皇帝龙颜大怒,而是大臣自己隔一段时间便来询问贾太监能不能进去,不能就稍后再来问,直到可以进去为止。偏向锁状态偏向锁也是JDK6引入的,它存在的依据是“大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得”。它是通过记录第一次进入同步块的线程id来实现的,如果下一个要进入同步块的线程与记录的线程id相同,则说明这个锁由此线程占有,可以直接进入到同步块,不用执行CAS。就像故事中的,如果每天只有钦天监一个人来的话,就不用贾太监禀告了,贾太监每次一看到钦天监,寻思着,哟,钦天监呢,您自个儿直接进去吧,说完自个儿出来吧。如果说轻量锁是为了消除系统互斥量带来的性能损耗,那么偏向锁就是为了消除CAS带来的性能损耗,使之在无竞争的情况下消除整个同步,性能无限接近非同步。如何通过这四种状态实现性能大幅度提升的Java对象头要说这个问题,我们需要先讲一下Java对象头,每个对象都会有一个对象头,它分为三个部分:内容说明Mark Word存储对象的hashcode或锁信息Class Metadata Address存储到对象类型数据的指针Array length数组的长度(如果当前对象是数组)从表格可见,synchronized锁的信息是存在对象头里一个叫Mark Word的区域里的,考虑到虚拟机的空间效率,Mark Word被设计成非固定的数据结构,会根据对象的状态复用存储空间来存储不同的内容:锁的升级当JVM启用了偏向锁模式(JDK6以上默认开启),新创建对象的Mark Word是未锁定,未偏向但可偏向状态,此时Mark Word中的Thread id为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。偏向锁状态—>无锁不可偏向状态/轻量级锁状态当第一个线程尝试进入同步块时,发现Mark Word中线程ID为0,则会使用CAS将自己的线程ID设置到Mark Word中,并且,在当前线程栈中由高到低顺序找到可用的Lock Record,将线程ID记录下。完成这些,此线程就获取了锁对象的偏向锁。当该偏向线程再次进入同步块时,发现锁对象偏向的就是当前线程,会往当前线程的栈中添加一条Displaced Mark Word为空的Lock Record中,用来统计重入的次数,然后继续执行同步块代码,因为线程栈是私有的,不需要CAS指令进行操作,所以在偏向锁模式下,同一个线程,只会执行一个CAS,之后获取释放锁只需要对Lock Record做操作,性能损耗基本可以忽略。当另外一个线程试图进入同步块时,发现Mark Word中线程ID与自己不相符,这个时候就会引发偏向锁的撤销,变成无锁不可偏向状态或轻量级锁状态,当然,这只是宏观上的描述,严格意义上讲是不准确的,因为里面还存在重偏向机制,这里就不过于深入,在后续的文章中,我会专门出一篇文章,给各位看官详细介绍偏向锁到底是怎么回事。无锁不可偏向状态—>轻量级锁状态当锁对象变成无锁不可偏向状态时,多个线程运行到同步块以后,会检查锁对象状态值标志是否加锁,如果没有锁,就把锁对象的Mark Word信息拷贝存储到当前线程栈桢中Lock Record里,然后通过CAS尝试把对象的Mark Word的值改变成一个指向自己线程的指针。如果成功,则当前线程获得锁对象的轻量级锁,其他线程的CAS就会失败,因为锁对象的Mark Word已经变成一个新的指针了,必须等待线程释放锁,此时其他线程则通过自旋来竞争锁。当获取锁的线程执行完毕释放锁的时候,会将Lock Record里面之前拷贝的值还原到锁对象的Mark Word中。轻量级锁状态—>重量级锁状态当自旋次数超过JVM预期上限,会影响性能,所以竞争的线程就会把锁对象的Mark Word指向重锁,所谓的重锁,实际上就是一个堆上的monitor对象,即,重量级锁的状态下,对象的Mark Word为指向一个堆中monitor对象的指针。然后所有的竞争线程放弃自旋,逐个插入到monitor对象里的一个队列尾部,进入阻塞状态。当成功获取轻量级锁的线程执行完毕,尝试通过CAS释放锁时,因为Mark Word已经指向重锁,导致轻量级锁释放失败,这时线程就会知道锁已经升级为重量级锁, 它不仅要释放当前锁,还要唤醒其他阻塞的线程来重新竞争锁。大概流程如下图所示:这里有一点需注意的是:锁只能升级,不能降级。锁的对比锁优点缺点适用场景偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距如果线程间存在锁竞争,会带来额外的锁撤销的消耗适用于只有一个线程访问同步块场景轻量级锁竞争的线程不会堵塞,提高了程序的响音速度始终得不到锁的线程,使用自旋会消耗CPU追求响应时间,同步块执行速度非常快重量级锁线程竞争不使用自旋,不会消耗CPU线程阻塞,响应时间缓慢追求吞吐量,同步块执行速度较慢synchronized的底层实现synchronized无非以下两种:1.对象锁:修饰非静态方法,修饰代码块2.类锁:修饰静态方法,修饰代码块其中按照修饰类型来分,又可以分为代码块同步和方法同步代码块同步代码块同步锁的是对象,使用monitorenter和monitorexit指令实现的。虽然我知道多一行代码少一位看官的定理,但是这里还是必须贴一张代码图,来证明我没有瞎说,是有理有据的“理据服”。想要降服妖怪,就得先将其打回原形,所以我们先对一段简单的代码进行反编译,得到它的字节码。 final Object lock = new Object(); public int subtr(int i){ synchronized (lock){ return i-1; } }字节码:可以看出,monitorenter指令是在编译后插入到同步代码块的开始位置,monitorexit插入到同步代码块结束的地方,正常情况下monitorenter和monitorexit是一对一的匹配,而后面又出现了一个monitorexit,是因为那里是异常处,用来保证方法执行异常的时候,可以自动解锁,而不会造成死锁。方法同步方法同步的实现官方没有透露,我们尝试对一个方法同步的代码进行反编译。 public synchronized int add(int i){ return i+1; }字节码:从字节码里也看不到monitorenter和monitorexit,智能发现flags那里,多了一个ACC_SYNCHRONIZED的标示,没什么头绪。不过我猜想,底层应该是锁方法所属的对象或类。这就是synchronized的大致原理,打回原形之后来看,是不是就觉得也不过如此?有什么疑问或更好的解读,可以在下方留言,我们进行愉快友好的磋商交流。如果觉得有用,记得分享~

April 13, 2019 · 1 min · jiezi

多线程、锁和线程同步方案

多线程多线程技术大家都很了解,而且在项目中也比较常用。比如开启一个子线程来处理一些耗时的计算,然后返回主线程刷新UI等。首先我们先简单的梳理一下常用到的多线程方案。具体的用法这里我就不说了,每一种方案大家可以去查一下,网上教程很多。常见的多线程方案我们比较常用的是GCD和NSOperation,当然还有NSThread,pthread。他们的具体区别我们不详细说,给出下面这一个表格,大家自行对比一下。容易混淆的术语提到多线程,有一个术语是经常能听到的,同步,异步,串行,并发。同步和异步的区别,就是是否有开启新的线程的能力。异步具备开启线程的能力,同步不具备开启线程的能力。注意,异步只是具备开始新线程的能力,具体开启与否还要跟队列的属性有关系。串行和并发,是指的任务的执行方式。并发是任务可以多个同时执行,串行之能是一个执行完成后在执行下一个。在面试的过程中可能被问到什么网情况下会出现死锁的问题,总结一下就是使用sync函数(同步)往当前的串行对列中添加任务时,会出现死锁。锁多线程的安全隐患多线程和安全问题是分不开的,因为在使用多个线程访问同一块数据的时候,如果同时有读写操作,就可能产生数据安全问题。所以这时候我们就用到了锁这个东西。其实使用锁也是为了在使用多线程的过程中保障数据安全,除了锁,然后一些其他的实现线程同步来保证数据安全的方案,我们一起来了解一下。线程同步方案下面这些是我们常用来实现线程同步方案的。OSSpinLockos_unfair_lockpthread_mutexNSLockNSRecursiveLockNSConditionNSConditinLockdispatch_semaphoredispatch_queue(DISPATCH_QUEUE_SERIAL)@synchronized可以看出来,实现线程同步的方案包括各种锁,还有信号量,串行队列。我们只挑其中不常用的来说一下使用方法。下面是我们模拟了存钱取钱的场景,下面是加锁之前的代码,运行之后肯定是有数据问题的。/** 存钱、取钱演示 /- (void)moneyTest { self.money = 100; dispatch_queue_t queue = dispatch_get_global_queue(0, 0); dispatch_async(queue, ^{ for (int i = 0; i < 10; i++) { [self __saveMoney]; } }); dispatch_async(queue, ^{ for (int i = 0; i < 10; i++) { [self __drawMoney]; } });}/* 存钱 /- (void)__saveMoney { int oldMoney = self.money; sleep(.2); oldMoney += 50; self.money = oldMoney; NSLog(@“存50,还剩%d元 - %@”, oldMoney, [NSThread currentThread]); }/* 取钱 /- (void)__drawMoney { int oldMoney = self.money; sleep(.2); oldMoney -= 20; self.money = oldMoney; NSLog(@“取20,还剩%d元 - %@”, oldMoney, [NSThread currentThread]); }加锁的代码,涉及到锁的初始化、加锁、解锁这么三部分。我们从OSSpinLock开始说。OSSpinLock自旋锁OSSpinLock叫做自旋锁。那什么叫自旋锁呢?其实我们可以从大类上面把锁分为两类,一类是自旋锁,一类是互斥锁。我们通过一个例子来区分这两类锁。如果线程A率先到达加锁的部分,并成功加锁,线程B到达的时候会因为已经被A加锁而等待。如果是自旋锁,线程B会通过执行一个循环来实现等待,我们不用管它循环执行了什么,只要知道他在那"转圈圈"等着就行。如果是互斥锁,那线程B在等待的时候会休眠。使用OSSpinLock需要导入头文件#import <libkern/OSAtomic.h>//声明一个锁@property (nonatomic, assign) OSSpinLock lock;// 锁的初始化self.lock = OS_SPINLOCK_INIT;在我们这个例子中,存钱取钱都是访问了money,所以我们要在存和取的操作中使用同一个锁。/* 存钱 /- (void)__saveMoney { OSSpinLockLock(&_lock); //….省去中间的逻辑代码 OSSpinLockUnlock(&_lock);}/* 取钱 */- (void)__drawMoney { OSSpinLockLock(&_lock); //….省去中间的逻辑代码 OSSpinLockUnlock(&_lock);}这就是简单的自旋锁的使用,我们发现在使用的过程中,Xcode一直提醒我们这个OSSpinLock被废弃了,让我们使用os_unfair_lock代替。OSSpinLock之所以会被废弃是因为它可能会产生一个优先级反转的问题。具体来说,如果一个低优先级的线程获得了锁并访问共享资源,那高优先级的线程只能忙等,从而占用大量的CPU。低优先级的线程无法和高优先级的线程竞争(CPU会给高优先级的线程分配更多的时间片),所以会导致低优先级的线程的任务一直完不成,从而无法释放锁。os_unfair_lock的用法跟OSSpinLock很像,就不单独说了。pthread_mutexDefault一看到这个pthread我们应该就能知道这是一种跨平台的方案了。首先还是来看用法。//声明一个锁@property (nonatomic, assign) pthread_mutex_t lock;//初始化pthread_mutex_init(pthread_mutex_t *restrict _Nonnull, const pthread_mutexattr_t *restrict _Nullable)我们可以看到在初始化锁的时候,第一个参数是锁的地址,第二个参数是一个pthread_mutexattr_t类型的地址,如果我们不传pthread_mutexattr_t,直接传一个NULL,相当于创建一个默认的互斥锁。//方式一pthread_mutex_init(mutex, NULL);//方式二// - 创建attrpthread_mutexattr_t attr;// - 初始化attrpthread_mutexattr_init(&attr);// - 设置attr类型pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_DEFAULT);// - 使用attr初始化锁pthread_mutex_init(&_lock, &attr);// - 销毁attrpthread_mutexattr_destroy(&attr);上面两个方式是一个效果,那为什么使用attr,那就说明除了default类型的还有其他类型,我们后面再说。在使用的时候用pthread_mutex_lock(&_lock); 和 pthread_mutex_unlock(&_lock);加锁解锁。NSLock就是对这种普通互斥锁的OC层面的封装。RECURSIVE 递归锁调用pthread_mutexattr_settype的时候如果类型传入PTHREAD_MUTEX_RECURSIVE,会创建一个递归锁。举个例子吧。// 伪代码-(void)test { lock; [self test]; unlock;}如果是普通的锁,当我们在test方法中,递归调用test,应该会出现死锁,因为被lock,在递归调用时无法调用,一直等待。但是如果锁是递归锁,他会允许同一个线程多次加锁和解锁,就可以解决这个问题了。NSRecursiveLock是对递归锁的封装。Condition 条件锁我们直接上这种锁的使用方法,- (void)otherTest{ [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start]; [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];}// 线程1// 删除数组中的元素- (void)__remove { pthread_mutex_lock(&_mutex); NSLog(@"__remove - begin"); if (self.data.count == 0) { // 等待 pthread_cond_wait(&_cond, &_mutex); } [self.data removeLastObject]; NSLog(@“删除了元素”); pthread_mutex_unlock(&_mutex);}// 线程2// 往数组中添加元素- (void)__add { pthread_mutex_lock(&_mutex); sleep(1); [self.data addObject:@“Test”]; NSLog(@“添加了元素”); // 信号 pthread_cond_signal(&_cond); // 广播// pthread_cond_broadcast(&_cond); pthread_mutex_unlock(&_mutex);}我们创建了两个线程,一个往数组中添加数据,一个删除数据,我们通过这个条件锁实现的效果就是在数组中还没有数据的时候等待,数组中添加了一个数据之后在进行删除。条件锁就是互斥锁+条件。我们声明一个条件并初始化。@property (assign, nonatomic) pthread_cond_t cond;//使用完后也要pthread_cond_destroy(&_cond);pthread_cond_init(&_cond, NULL);在__remove方法中if (self.data.count == 0) { // 等待 pthread_cond_wait(&_cond, &_mutex);}如果线程1率先拿到所并加锁,执行到上面代码这里发现数组中还没有数据,就执行pthread_cond_wait,此时线程1会暂时放开_mutex这个锁,并在这休眠等待。线程2在__add方法中最开始因为拿不到锁,所以等待,在线程1休眠放开锁之后拿到锁,加锁,并执行为数组添加数据的代码。添加完了之后会发个信号通知等待条件的线程,并解锁。 pthread_cond_signal(&_cond); pthread_mutex_unlock(&_mutex);线程2执行了pthread_cond_signal之后,线程1就收到了通知,退出休眠状态,继续执行下面的代码。这个地方可能有人会有疑问,是不是线程2应该先unlock再cond_dingnal,其实这个地方顺序没有太大差别,因为线程2执行了pthread_cond_signal之后,会继续执行unlock代码,线程1收到signal通知后会推出休眠状态,同时线程1需要再一次持有这个锁,就算此时线程2还没有unlock,线程1等到线程2 unlock 的时间间隔很短,等到线程2 unlock 后线程1会再去持有这个锁,并加锁。NSCondition就是OC层面的条件锁,内部把mutex互斥锁和条件封装到了一起。NSConditionLock其实也差不多,NSConditionLock可以指定具体的条件,这两个OC层面的类的用法大家可以自行上网搜索。dispatch_semaphore 信号量@property (strong, nonatomic) dispatch_semaphore_t semaphore;//初始化self.semaphore = dispatch_semaphore_create(5);在初始化一个信号的的过程中传入dispatch_semaphore_create的值,其实就代表了允许几个线程同时访问。再回到之前我们存钱取钱这个例子。self.moneySemaphore = dispatch_semaphore_create(1);我们一次只允许一个线程访问,所以在初始化的时候传1。下面就是使用方法。- (void)__drawMoney{ dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER); // … 省略代码 dispatch_semaphore_signal(self.moneySemaphore);}- (void)__saveMoney{ dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER); // … 省略代码 dispatch_semaphore_signal(self.moneySemaphore);}dispatch_semaphore_wait是怎么上锁的呢?如果信号量>0的时候,让信号量-1,并继续往下执行。如果信号量<=0的时候,休眠等待。就这么简单。dispatch_semaphore_signal让信号量+1。小提示在我们平时使用这种方法的时候,可以把信号量的代码提取出来定义一个宏。#define SemaphoreBegin \static dispatch_semaphore_t semaphore; \static dispatch_once_t onceToken; \dispatch_once(&onceToken, ^{ \ semaphore = dispatch_semaphore_create(1); }); \dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);#define SemaphoreEnd \dispatch_semaphore_signal(semaphore);读写安全方案上面我们讲到的线程同步方案都是每次只允许一个线程访问,在实际的情况中,读写的同步方案应该下面这样:每次只能有一个线程写可以有多个线程同时读读和写不能同时进行这就是多读单写,用于文件读写的操作。在我们的iOS中可以用下面这两种解决方案。pthread_rwlock 读写锁这个读写锁的用法很简单,跟之前的普通互斥锁都差不多,大家随便搜一下应该就能搜到,我就不拿出来写了,这里主要是提一下这种锁,大家以后有需要的时候可以用。dispatch_barrier_async 异步栅栏首先在使用这个函数的时候,我们要用自己创建的并发队列。如果传入的是一个串行队列或者全局的并发队列,那dispatch_barrier_async等同于dispatch_async的效果。self.queue = dispatch_queue_create(“rw_queue”, DISPATCH_QUEUE_CONCURRENT);dispatch_async(self.queue, ^{ [self read];}); dispatch_barrier_async(self.queue, ^{ [self write];});在读取数据的时候,使用dispatch_async往对列中添加任务,在写数据时,用dispatch_barrier_async添加任务。dispatch_barrier_async添加的任务会等前面所有的任务都执行完,他再执行,而且他执行的时候,不允许有别的任务同时执行。atomic我们都知道这个atomic是原子性的意思。他保证了属性setter和getter的原子性操作,相当于在set和get方法内部加锁。atomic修饰的属性是读/写安全的,但不是线程安全。假设有一个 atomic 的属性 “name”,如果线程 A 调用 [self setName:@“A”],线程 B 调用 [self setName:@“B”],线程 C 调用 [self name],那么所有这些不同线程上的操作都将依次顺序执行——也就是说,如果一个线程正在执行 getter/setter,其他线程就得等待。因此,属性 name 是读/写安全的。但是,如果有另一个线程 D 同时在调[name release],那可能就会crash,因为 release 不受 getter/setter 操作的限制。也就是说,这个属性只能说是读/写安全的,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作。线程安全需要开发者自己来保证。 ...

April 1, 2019 · 2 min · jiezi