【线程】synchronized的可重入性

前言

从Java多线程:《线程间通信之volatile与sychronized》这篇文章中我们了解了synchronized的基本特性,知道了一旦有一个线程访问某个对象的synchronized修饰的方法或代码区域时,该线程则获取这个对象的锁,其他线程不能再调用该对象被synchronized影响的任何方法。

那么,如果这个线程自己调用该对象的其他synchronized方法,Java是如何判定的?这就涉及到了Java中锁的重要特性:可重入性,也就是今天的主题。

1. 线程安全与可重入性

在回答引言的问题前,我们先讲解一下可重入性。在线程这块知识中,可重入性常常和线程安全进行对比。

1.1. 线程安全

线程安全函数的概念比较直观,众所周知,同一进程的不同线程会共享同一主内存,线程的私有栈中只包括PC,栈,操作数栈,局部变量数组和动态链接。对共享内存进行读写时,若要保证线程安全,则必须通过加锁的方式。

1.2. 可重入

1.2.1. 定义

关于可重入这一概念,我们需要参考维基百科。

若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。

这段解释是不是感觉很奇怪,感觉和操作系统有关系,而我们现在是在讨论编程语言范畴,按道理应该和操作系统不应该有关,原因是重入概念是操作系统引入最早的,后来才在编程中引入。

1.2.1.1 产生背景,操作系统的重入函数概念

可重入概念是在单线程操作系统的时代提出的。一个子程序的重入,可能由于自身原因,如执行了jmp或者call,类似于子程序的递归调用;或者由于操作系统的中断响应。UNIX系统的signal的处理,即子程序被中断处理程序或者signal处理程序调用。所以,可重入也可称作“异步信号安全”。这里的异步是指信号中断可发生在任意时刻。 重入的子程序,按照后进先出线性序依次执行。

编写可重入代码注意的条件

若一个函数是可重入的,则该函数应当满足下述条件:

  • 不能含有静态(全局)非常量数据。
  • 不能返回静态(全局)非常量数据的地址。
  • 只能处理由调用者提供的数据。
  • 不能依赖于单实例模式资源的锁。
  • 调用(call)的函数也必需是可重入的。

上述条件就是要求可重入函数使用的所有变量都保存在呼叫堆叠的当前函数栈(frame)上,因此同一执行线程重入执行该函数时加载了新的函数帧,与前一次执行该函数时使用的函数帧不冲突、不互相覆盖,从而保证了可重入执行安全。

多“用户/对象/进程优先级”以及多进程(Multiple processes),一般会使得对可重入代码的控制变得复杂。同时,IO代码通常不是可重入的,因为他们依赖于像磁盘这样共享的、单独的(类似编程中的静态、全域)资源。

1.2.2. 可重入的条件
  • 不在函数内使用静态或全局数据。
  • 不返回静态或全局数据,所有数据都由函数的调用者提供。
  • 使用本地数据(工作内存),或者通过制作全局数据的本地拷贝来保护全局数据。
  • 不调用不可重入函数。

1.3. 可重入与线程安全的关系

一般而言,可重入的函数一定是线程安全的,反之则不一定成立。

在不加锁的前提下,如果一个函数用到了全局或静态变量,那么它不是线程安全的,也不是可重入的。

如果我们加以改进,对全局变量的访问加锁,此时它是线程安全的但不是可重入的,因为通常的枷锁方式是针对不同线程的访问(如Java的synchronized),当同一个线程多次访问就会出现问题。只有当函数满足可重入的四条条件时,才是可重入的。

上面原文的解释感觉是错误的,二者没有依赖等条件关系, “可重入函数未必是线程安全的;线程安全函数未必是可重入的”

可重入与线程安全两个概念都关系到函数处理资源的方式。但是,他们有重大区别:可重入概念会影响函数的外部接口,而线程安全只关心函数的实现。

大多数情况下,要将不可重入函数改为可重入的,需要修改函数接口,使得所有的数据都通过函数的调用者提供;要将非线程安全的函数改为线程安全的,则只需要修改函数的实现部分。一般通过加入同步机制以保护共享的资源,使之不会被几个线程同时访问。

操作系统背景与CPU调度策略:

  • 可重入是在单线程操作系统背景下,重入的函数或者子程序,按照后进先出的线性序依次执行完毕。

  • 多线程执行的函数或子程序,各个线程的执行时机是由操作系统调度,不可预期的,但是该函数的每个执行线程都会不时的获得CPU的时间片,不断向前推进执行进度。可重入函数未必是线程安全的;线程安全函数未必是可重入的。

例如,一个函数打开某个文件并读入数据。这个函数是可重入的,因为它的多个实例同时执行不会造成冲突;但它不是线程安全的,因为在它读入文件时可能有别的线程正在修改该文件,为了线程安全必须对文件加“同步锁”。

另一个例子,函数在它的函数体内部访问共享资源使用了加锁、解锁操作,所以它是线程安全的,但是却不可重入。因为若该函数一个实例运行到已经执行加锁但未执行解锁时被停下来,系统又启动该函数的另外一个实例,则新的实例在加锁处将转入等待。如果该函数是一个中断处理服务,在中断处理时又发生新的中断将导致资源死锁。fprintf函数就是线程安全但不可重入。

这个例子其实才是本文的核心,该例子引入了不可重入的锁概念,为synchronized锁的可重入性做铺垫对比。

2. synchronized的可重入性

2.1. synchronized是可重入锁

回到引言里的问题,如果一个获取锁的线程调用其它的synchronized修饰的方法,会发生什么?

从设计上讲,当一个线程请求一个由其他线程持有的对象锁时,该线程会阻塞。当线程请求自己持有的对象锁时,如果该线程是重入锁,请求就会成功,否则阻塞。

我们回来看synchronizedsynchronized拥有强制原子性的内部锁机制,是一个可重入锁。因此,在一个线程使用synchronized方法时调用该对象另一个synchronized方法,即一个线程得到一个对象锁后再次请求该对象锁,是永远可以拿到锁的。

在Java内部,同一个线程调用自己类中其他synchronized方法/块时不会阻碍该线程的执行,同一个线程对同一个对象锁是可重入的,同一个线程可以获取同一把锁多次,也就是可以多次重入。原因是Java中线程获得对象锁的操作是以线程为单位的,而不是以调用为单位的。

同一个对象锁也包括父类中的锁

2.2. synchronized可重入锁的实现

之前谈到过,每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。

3.总结

通过本篇文章,我们知道重入的概念是操作系统先引入的,可重入与线程安全之间没有必然关系,可重入函数未必是线程安全的;线程安全函数未必是可重入的。

有的锁是不可重入的,一旦加锁,任何线程都无法再进入,包括自身线程(因此可能导致死锁),由此引申出有的锁是可重入的,synchronized锁对于线程自身,是可重入的,对于其他线程仍是不可重入的,会阻塞。





参考:
《Java多线程:synchronized的可重入性》

《深入理解可重入性》该链接被墙了,我是查看百度快照的

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值