我个人对可重入函数的理解如下:
- 可重入函数需要指明可重入对象,分为线程可重入函数和信号可重入函数
- 线程可重入函数是可以被多个线程同时调用、且保证安全的函数,也叫做线程安全函数
- 信号可重入函数是在信号处理程序中保证调用安全的函数,也叫做异步信号安全函数
下图是APUE书中所说的POSIX.1中不能保证线程安全的函数,换而言之,未列入图中的函数都是线程可重入函数。
通过和信号可重入函数对比,不难得出以下结论:
- 所有的信号可重入函数,都是线程安全函数
- 线程安全,异步信号未必安全
我个人对可重入函数的理解如下:
- 可重入函数需要指明可重入对象,分为线程可重入函数和信号可重入函数
- 线程可重入函数是可以被多个线程同时调用、且保证安全的函数,也叫做线程安全函数
- 信号可重入函数是在信号处理程序中保证调用安全的函数,也叫做异步信号安全函数
下图是APUE书中所说的POSIX.1中不能保证线程安全的函数,换而言之,未列入图中的函数都是线程可重入函数。
通过和信号可重入函数对比,不难得出以下结论:
- 所有的信号可重入函数,都是线程安全函数
- 线程安全,异步信号未必安全
转载于:https://www.cnblogs.com/songhe364826110/p/11517101.html
一个问题
为了理解可重入函数的概念,我们先来看这样一个例子:
main函数调用insert函数向链表中头插节点node1,插入操作分为两步。
假设它刚做完第一步的时候。由于硬件中断使进程切换到内核。再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表中头插节点node2。
把插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态从main函数调用的insert函数中继续执⾏第⼆步。
结果是:main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点node1插入链表中:
像这样,两个执行流main和sighandler同时访问insert函数,就会导致结点丢失。类似于insert这种情况的函数就叫做不可重入函数
这里的重入是指:同一时刻一个函数被多个执行流调用相对的,有可重入函数:只访问自己私有栈的变量和参数。这个例子同样说明:没有线程,一个进程里也会有多个执行流,比如信号的捕捉。
所以,我们可以总结出可称为不可重入函数的3个条件:
1.调用了malloc或free,因为malloc也是用全局链表来管理堆的。
2.调⽤了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使⽤全局数据结构。
3.必须以线程安全的方式实现的系统函数。从第3条我们可以看出,可重入/不可重入函数与一个概念相关,这就是线程安全。
线程安全与可重入函数
首先,我们需要明确线程安全的概念:一个线程对全局变量这种具有全局性的东西的操作影响到别的线程。简单来说,只要一个程序/进程的线程之间没有涉及到对全局变量的操作,那么这个程序/进程就是线程安全的。
这样看来,线程安全和可重入函数貌似是一样的。后者也是只访问自己私有栈的变量和参数,没有访问全局变量。
其实,可重入函数与线程安全并不相同,一般来说,可重入的函数一定是线程安全的,但反过来就不一定成立了。
前方高能:一大段文字即将来袭。。。
总结:可重入函数与线程安全的区别与联系
1.线程安全是在多个线程情况下引发的,而可重入函数在只有一个线程的情况下也可以有。
2.线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
3.如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
4.如果在某函数中将对临界资源的访问加上锁,则这个函数是线程安全的。
5.线程安全函数能够使不同的线程访问同一块地址空间,而可重入函数要求不同的执行流对数据操作互不影响。
6.可重入函数是线程安全函数的一种,其特点在于它们被多个线程调用时,不会引用任何共享数据。
1、可重入函数
1)举例说明:
main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入。
insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数。
反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant)函数。
2)可重入性(reentrant)针对函数,它有两个方面的内涵:
(1)可并行/并发,同时进入:指可重入函数被某任务调用时,其它任务可同时进行调用而不产生错误的结果;或称在相同的输入情况下可重入函数的执行所产生的效果,并不因其并发的调用而产生不同,也称并发安全。
(2)中断后可重新进入:指可重入函数可被任意的中断,当中断执行完毕,返回断点可以继续正确的运行下去;或称在相同的输入情况下可重入函数的执行所产生的结果,并不因为在函数执行期间有中断的调用而产生不同,也称中断安全。
2、不可重入函数的条件
如果函数满足以下条件,则属于不可重入函数:
1)调用了malloc或free。(因为malloc也是用全局链表来管理堆的)
2)调用了标准I/O库函数。(标准I/O库的很多实现都以不可重入的方式使用全局数据结构)
3)众所周知它们使用了静态数据结构体
3、可重入函数的条件
1)不为连续的调用持有静态数据。
2)不返回指向静态数据的指针;所有数据都由函数的调用者提供。
3)使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
4)绝不调用任何不可重入函数。
4、线程安全
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
5、两者的区别与联系
可重入和线程安全是两个不同的概念:可重入函数一定是线程安全的;线程安全的函数可能是重入的,也可能是不重入的;线程不安全的函数一定是不可重入的。
可重入 => 线程安全
可重入函数要解决的问题是,不在函数内部使用静态或全局数据,不返回静态或全局数据,也不调用不可重入函数。
线程安全函数要解决的问题是,多个线程调用函数时访问资源冲突。
转载于:https://blog.51cto.com/lovemeright/1826032
线程安全与锁优化
文章目录
一、线程安全概念
为了更好的理解线程安全,我们不把线程安全看做是一个二元对立的选项来看,而是按照线程安全的”安全程度“由强至弱来排序。
Java线程各种操作共享的数据分为五类:
不可变
不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要进行任何的线程安全保障措施。被final修饰的对象只要被正确构建出来,就永远是不可变的,也就是线程安全的。
String类型就是不可变类型,我们调用它的substring()、replace()方法都不会影响原来的值,只会返回一个新构造的字符串对象。
绝对线程安全
不管运行环境如何,调用者也不需要任何的同步措施,就可以达到线程安全。这种状态通常不可能达到或要付出非常大的代价。
Vector是一个线程安全的容器,但是仍然会出错。
//线程一执行:
for(int i=0;i<vector.size();i++){
vector.remove(i);
}
//线程二执行
for(int i=0;i<vector.size();i++){
vector.get(i);
}
该情况下,可能在线程二执行了i++之后执行get(i)之前被阻塞调用了线程一,之后i位置的元素被移除了,此时线程二执行get(i)就会出错
相对线程安全
普遍意义上的线程安全,它保证对象单独的操作时线程安全的,但是对于一些特定顺序的连续调用就可能需要在调用端使用额外的同步手段来保证调用正确性。例1-2就是一个很好的例子。
绝大部分线程安全类都属于这种类型。如Vector、HashTable、Collections的synchronizedCollection()方法包装的集合
线程兼容
线程兼容是指对象本身并不是线程安全的,但是可以通过调用端正确地使用同步手段保证对象可以在并发环境中使用。
如ArrayList和HashMap就是线程兼容的。可以自行添加锁来保证安全
线程对立
无论是否采取同步措施,都无法在多线程环境下保证并发。
如采取同步措施后出现的死锁问题
实现线程安全的方法有三种
互斥同步
互斥同步是最常见的一种并发正确性保障手段。
互斥是因,同步是果。互斥是方法,同步是目的。
synchronized关键字经过编译之后会在同步代码块的前后分别形成monitorenter和monitorexit这两个字节码指令。这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象
目前简易执行流程,与实际执行有出入
缺点:
由于Java的线程是映射到操作系统的原生线程之上,如果阻塞和唤醒一条线程就需要从用户态转换到核心态,需要耗费处理器较多时间。如对于简单的同步块进行内核状态转换消耗的时间可能比业务代码执行时间还要长,使系统的性能较低。是一个重量级操作。
后期会优化在阻塞前加入一段自旋等待的过程,避免频繁切换状态。
在基本用法上ReentrantLock与synchronized很相似。都具备可重入性。
区别:
在JDK1.5时ReentrantLock的性能比synchronized性能稳定且高的多。synchronized仍有很大的优化空间
互斥同步的主要问题就是进行线程阻塞和唤醒带来的性能问题。因此这种同步方式被称为阻塞同步,属于悲观的并发策略。无论共享数据是否出现竞争都会进行加锁。**很出现很多不必要的状态切换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。**影响性能
非阻塞同步
随着硬件指令集的发展,我们可以使用**基于冲突检测的乐观并发策略。**通俗的说先进先操作,如果没有其他线程争用共享数据,那么操作就成功了,如果有争用产生了冲突,那就进行其他的补偿措施(一般为循环尝试)。这种操作不需要把线程挂起,因此被称为非阻塞同步。
CAS指令由三个操作数:CAS(V,A,B)
在硬件层面保证原子性
问题:CAS操作的经典问题ABA问题
线程A读取了数据A,线程B将A修改为B后又修改为A,之后线程A再次访问数据A。这样就存在漏洞。大多数情况下ABA不会影响程序并发的争取性。可以为数据添加一个版本号时间戳来标记数据是否被修改过。
无同步方案
要保证线程安全不一定要同步。同步只是保障数据争用时的正确性的手段。如果一个方法不涉及共享数据,则无需任何同步措施保证正确性。
可重入代码:
可以在代码执行的任何时刻中断它,转而执行另一段代码,而控制权返回后,原来的程序不会出现任何错误。
线程本地存储:
如果一段代码中需要的数据必须与其他代码共享,可以试试将这些共享数据的代码放入同一个线程执行。如果可以,则无须同步也能保证线程之间不出现数据争用。
自旋锁与自适应锁
自旋锁
由于互斥同步的阻塞对性能影响较大。并且在很多情况下对于共享数据的锁定状态只会持续很短一段时间,为了这段时间去挂起和恢复线程并不值得。所以当物理机可以让两个线程同时执行时,我们可以让请求锁的线程稍等一会并不将其挂起。为了让线程等待,我们只需要让线程执行一个忙循环(自旋),这就是自旋锁。
自适应锁
自适应锁就是对自旋锁的自旋次数进行适应性调整。
如:一个锁对象曾经自旋成功,那么虚拟机认为这次仍然可能成功,进而允许他自旋等待更长的时间。如果一个锁对象自旋很少成功,那么以后获取这个锁时就减少自旋或不自旋。
锁消除
锁消除是指在虚拟机即时编译器在运行时发现一些代码上要求同步,但是实际上不可能出现对于共享数据的竞争,就会进行把同步操作进行省略。
例:
public String concatstring(String s1,String s2,String s3){
return s1+s2+s3;
}
表面上没有锁的存在,实际上jdk会对String连接进行优化,于是以上代码就会变成new StringBuffer().append(s1).append(s2).append(s3).toString();,然而StringBuffer是线程安全的,在其内部就存在锁,但是此时根本无需锁的存在,这里就使用了JVM的锁消除。
锁粗化
在编写代码时,推荐将同步块的作用范围限制得尽量小。但是也有例外情况,如果一系列连续的操作都对同一个对象反复加锁和解锁比如在循环体内加锁。那么就需要不停的加锁和释放锁,及时没有线程竞争也会导致性能消耗。这时只需要将锁的作用域扩大到整个循环。只需要加一个锁就可以了。这就是锁粗化。
轻量级锁
传统的互斥锁属于“重量级”锁。相对应的就是“轻量级”锁。是对“重量级”锁的补充。要理解轻量级锁必须先了解虚拟机的对象的内存布局。HotSpot虚拟机的对象头分为两部分,一部分存储对象自身的运行时数据,一部分存储指向方法区对象类型数据的指针,如果是数组对象的话,还有一部分用于存储数组长度。
Mark Word区域是实现轻量级锁与偏向锁的关键(随着锁标记位的不同,Mark Word的内容不断变化)。
代码进入同步代码块的时候,如果此同步对象没有被锁定,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间。用于存储当前Mark Word的拷贝,之后尝试CAS操作,成功就修改Mark Word为指向Lock Record的指针并修改锁标记位为00。此时Mark Word内容参考上图。加锁流程图如下:
解锁过程流程图如下:
轻量级锁提升程序性能的依据是“对于绝大部分的锁,在整个同步周期内都不存在竞争”。
偏向锁
如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那么偏向锁就是无竞争的情况下降整个同步都消除掉。
偏向锁的“偏”是偏心的偏。他的意思是这个锁会偏向于第一个获得他的线程,在接下来的情况如果没有其他的线程要获取他,则持有偏向锁的线程永远不需要同步。而如果有其他的线程要获取这个锁时,偏向模式宣告结束,根据对象是否被锁定恢复到未锁定或轻量级锁定的状态。
偏向锁优缺点:
偏向锁可以提高带有同步但是无竞争的程序性能。但是一个共享数据总是被不同的线程访问,那么偏向模式就是多余的。