互斥锁(互斥量)、自旋锁和Java的关系
互斥锁(互斥量)、自旋锁和Java的关系
注:本文所有指令的环境均指linux
引子:
1.linux有那些锁?
系统OS级别(linux)锁: 互斥量(mutex),自旋锁(spin),信号量(sem)。
2.synchronized是不是自旋锁(jdk1.6)?
不是,原因:1.首先synchronized使用的不是os操作系统的自旋锁,使用的是pthread_mutex_t(互斥锁,发生竞争的时候如果拿不到锁则睡眠),当然synchronized的底层也做了优化,不是说一定回调用mutex,底层的逻辑是先判断是不是偏量锁,如果不是,走轻量锁的逻辑,如果轻量锁解决不了,就会走锁膨胀逻辑,在轻量锁膨胀的过程中,会发生自旋,但是这个自旋不是去拿锁,而是在等待其他线程进行锁膨胀,当锁膨胀到一定级别,会调用pthread_mutex_t,所以也不能算是自旋锁。2.synchronized在jvm层面也没有发生自旋。所以它不是自旋锁。
关于synchronized不是自旋锁的源码图解如下:
1.synchronized的Jvm指令是monitorenter,monitorenter被interpreterRuntime.cpp解析,如图:
2.首先会判断是不是偏向锁:
3.不是会走轻量锁流程slow_enter:
4.slow_enter逻辑没有发生自旋:
5.如果轻量锁解决不了,走inflate方法去膨胀:
6.在膨胀的过程当中发生了自旋,但是并不是自旋等待锁释放,而是在等待其他线程进行锁膨胀,
7.当膨胀为重量锁时,重量锁也没有发生自旋,注释已经解释(并不是自旋等待锁释放,而是在等待其他线程进行锁膨胀:Only that thread can complete inflation -- other threads must wait)。
Java的线程模型
在Java中,基本我们说的线程(Thread)实际上应该叫作“用户线程”,而对应到操作系统,还有另外一种线程叫作“内核线程”,用户线程和内核线程之间必然存在某种关系,多对一模型、一对一模型和多对多模型。
多对一模型
多个用户线程对应到同一个内核线程上(协程),线程的创建、调度、同步的所有细节全部由进程的用户空间线程库来处理。
优点:
- 用户线程的很多操作对内核来说都是透明的,不需要用户态和内核态的频繁切换,使线程的创建、调度、同步等非常快;
缺点:
- 由于多个用户线程对应到同一个内核线程,如果其中一个用户线程阻塞,那么该其他用户线程也无法执行;
- 内核并不知道用户态有哪些线程,无法像内核线程一样实现较完整的调度、优先级等;
一对一模型
即一个用户线程对应一个内核线程(HotSpot),内核负责每个线程的调度。
优点:
- (比如JVM几乎把所有对线程的操作都交给了内核)实现线程模型的容器(jvm)简单,所以我们经常听到在java中使用线程一定要慎重就是这个原因;
缺点:
- 对用户线程的大部分操作都会映射到内核线程上,引起用户态和内核态的频繁切换;
- 内核为每个线程都映射调度实体,如果系统出现大量线程,会对系统性能有影响;
用户态和内核态
其实这是个伪命题因为一个软件级别的线程用户态和内核态是不确定的,什么是内核态什么用户态呢?这就要说到mmu和mmap了
从图上我们可以看出来通过系统调用将Linux整个体系分为用户态和内核态(或者说内核空间和用户空间)。那内核态到底是什么呢?其实从本质上说就是我们所说的内核,它是一种特殊的软件程序,特殊在哪儿呢?控制计算机的硬件资源,例如协调CPU资源,分配内存资源,并且提供稳定的环境供应用程序运行。
用户态就是提供应用程序运行的空间,为了使应用程序访问到内核管理的资源例如CPU,内存,I/O。内核必须提供一组通用的访问接口,这些接口就叫系统调用。
虚拟内存:
-
理解虚拟地址空间还得从物理地址空间开始说起。我们知道内存就像一个数组,每个存储单元被分配了一个地址,这个地址就是物理地址,所有物理地址构成的集合就是物理地址空间。物理地址也就是真实的地址,对应真实的那个内存条。
-
虚拟内存是Windows 为作为内存使用的一部分硬盘空间。虚拟内存在硬盘上其实就是为一个硕大无比的文件,文件名是PageFile.Sys。
-
它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
-
虚拟内存为每个进程提供了一个非常大的、一致的、私有的地址空间。
-
假设每个进程创建加载的时候,会被分配一个大小为4G的连续的虚拟地址空间,虚拟的意思就是,其实这个地址空间是不存在的,仅仅是每个进程“认为”自己拥有4G的内存,而实际上,它用了多少空间,操作系统就在磁盘上划出多少空间给它,等到进程真正运行的时候,需要某些数据并且数据不在物理内存中,才会触发缺页异常,进行数据拷贝。
-
比如dota进程分配虚拟内存地址1-1000,lol进程分配虚拟内存地址1-1000。虚拟内存地址可以重复分配,而且不同的进程映射的实际物理地址是不同的。如果没有虚拟地址,只有物理地址,那么dota分配1-1000,lol就不能再使用1-1000,只能分配1001-2000,但是如果物理内存只有1000个地址,这些程序就无法运行,因为程序需要的内存超过了物理内存大小,但是虚拟内存可以重复分配,就可以同时运行了。(纯属个人理解)
mmu(内存管理单元 Memory Management Unit) :
内存管理单元,是一种负责处理中央处理器(CPU)的内存访问请求的计算机硬件,负责虚拟地址与物理地址的转换,提供硬件机制的内存访问授权。
mmap(内存映射文件的方法):
- mmap是一种内存映射文件的方法,即将硬盘文件映射到进程虚拟地址空间中的一块区域,但没有真正分配物理内存。
- mmap()会返回一个指针ptr,它指向进程虚拟地址空间中的一个地址,这样进程无需再调用read或write对文件进行读写,而只需要通过ptr就能够操作文件。但是ptr所指向的是一个虚拟地址,要操作其中的数据,必须通过MMU将虚拟地址转换成物理地址;
- 虽然建立了内存映射,但文件内容并没有加载到物理内存中,所以当进程在访问这段地址时,会发生缺页异常(page fault),由内核的缺页异常处理程序处理,将这部分文件内容从磁盘拷贝到物理内存;
- 最终直接通过内存操作访问到硬盘上的文件,而不必再调用read,write等系统调用函数。
系统调用:
系统调用是操作系统的最小功能单位。根据不同的应用场景,不同的Linux发行版本提供的系统调用数量也不尽相同,大致在240-350之间。这些系统调用组成了用户态跟内核态交互的基本接口,例如:用户态想要申请一块20K大小的动态内存,就需要brk系统调用,将数据段指针向下偏移,如果用户态多处申请20K动态内存,同时又释放呢?这个内存的管理就变得非常的复杂。
库函数:
库函数就是屏蔽这些复杂的底层实现细节,减轻程序员的负担,从而更加关注上层的逻辑实现。它对系统调用进行封装,提供简单的基本接口给用户,这样增强了程序的灵活性,当然对于简单的接口,也可以直接使用系统调用访问资源,例如:open(),write(),read()等等。库函数根据不同的标准也有不同的版本,例如:glibc库,posix库等。
shell:
shell顾名思义,就是外壳的意思。就好像把内核包裹起来的外壳。它是一种特殊的应用程序,俗称命令行。为了方便用户和系统交互,一般一个shell对应一个终端,呈现给用户交互窗口。当然shell也是编程的,它有标准的shell语法,符合其语法的文本叫shell脚本。很多人都会用shell脚本实现一些常用的功能,可以提高工作效率。
用户态到内核态怎样切换?
往往我们的系统的资源是固定的,例如内存2G,CPU固定,磁盘2TB,网络接口固定。所以就需要操作系统对资源进行有效的利用。假设某个应用程序过分的访问这些资源,就会导致整个系统的资源被占用,如果不对这种行为进行限制和区分,就会导致资源访问的冲突。所以,Linux的设计的初衷:给不同的操作给与不同的“权限”。Linux操作系统就将权限等级分为了2个等级,分别就是内核态和用户态。
intel x86 CPU有四种不同的执行级别0-3,linux只使用了其中的0级和3级分别来表示内核态和用户态,所谓的内核态和用户态其实仅仅是CPU的一个权限而已。
各位有没有发现,前面讲了这么多内核态和用户态什么不同,其实用一句话就能概括:它们权限不同。用户态的进程能够访问的资源受到了极大的控制,而运行在内核态的进程可以“为所欲为”。一个进程可以运行在用户态也可以运行在内核态,那它们之间肯定存在用户态和内核态切换的过程。打一个比方:C库接口malloc申请动态内存,malloc的实现内部最终还是会调用brk()或者mmap()系统调用来分配内存。
从用户态到内核态到底怎么进入?只能通过系统调用吗?还有其他方式吗
从用户态到内核态切换可以通过三种方式:
- 系统调用,这个上面已经讲解过了,在我公众号之前的文章也有讲解过。其实系统调用本身就是中断,但是软件中断,跟硬中断不同。
- 异常:如果当前进程运行在用户态,如果这个时候发生了异常事件,就会触发切换。例如:缺页异常。
- 外设中断:当外设完成用户的请求时,会向CPU发送中断信号。
什么是切换?有哪些切换
而在每个任务运行前,CPU 都需要知道任务从哪里加载、又从哪里开始运行,也就是说,需要系统事先帮它设置好CPU 寄存器和程序计数器
什么是 CPU上下文
其实和spring上下文差不多,CPU 寄存器和程序计数器就是 CPU 上下文,因为它们都是 CPU 在运行任何任务前,必须的依赖环境。
- CPU 寄存器是 CPU 内置的容量小、但速度极快的内存。
- 程序计数器则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。
什么是 CPU 上下文切换
就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
CPU 上下文切换的类型
根据任务的不同,和java并发编程相关的我们只关心以下两种类型 - 进程上下文切换 - 线程上下文切换。
进程上下文切换
1.进程上下文切换之系统调用
进程既可以在用户空间运行,又可以在内核空间中运行。进程在用户空间运行时,被称为进程的用户态,而陷入内核空间的时候,被称为进程的内核态。
从用户态到内核态的转变,需要通过系统调用来完成。比如,当我们查看文件内容时,就需要多次系统调用来完成:首先调用 open() 打开文件,然后调用 read() 读取文件内容,并调用 write() 将内容写到标准输出,最后再调用 close() 关闭文件。
在这个过程中就发生了 CPU 上下文切换,整个过程是这样的:
1、保存 CPU 寄存器里原来用户态的指令位
2、为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。
3、跳转到内核态运行内核任务。
4、当系统调用结束后,CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。
所以,一次系统调用的过程,其实是发生了两次 CPU 上下文切换
不过,需要注意的是,系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程。这跟我们通常所说的进程上下文切换是不一样的:进程上下文切换,是指从一个进程切换到另一个进程运行;而系统调用过程中一直是同一个进程在运行。所以,系统调用过程通常称为特权模式切换,而不是上下文切换。系统调用属于同进程内的 CPU 上下文切换
2、真正的进程上下文切换和系统调用有什么区别呢?
进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。
因此,进程的上下文切换就比系统调用时多了一步:在保存内核态资源(当前进程的内核状态和 CPU寄存器)之前,需要先把该进程的用户态资源(虚拟内存、栈等)保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。
发生进程上下文切换的场景:
-
为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,就会被系统挂起,切换到其它正在等待CPU 的进程运行。
-
进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行。
-
当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度。
-
当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行
线程上下文切换
特点以及场景:
-
前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。
-
前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据
cas不会升级内核态,他仅仅是处理器提供的一个指令,速度非常快。
互斥量(互斥锁pthread_mutex_t):
特点:发生竞争的时候拿不到锁则睡眠,由mutex创建的锁叫做互斥锁,Java中synchronized关键字如果是(10)重量锁状态,synchronized有很多种状态(轻量锁,重量锁,偏向锁),10==重量锁(jvm会使用mutex),底层的同步或锁机制用的是mutex。
mutex为什么叫做重量锁?
因为mutex互斥特点是拿不到锁就会进入sleep(),调用内核提供的sleep()方法(发生系统调用)会从用户态进入内核态,线程从sleep()唤醒之后,接着执行用户的代码,还需要从内核态切回用户态,所以它是一把重量锁,比如ReentrantLock在平台层面是可重入的自旋锁,大部分锁的实现由Java实现,一直在用户态,在OS级别就是重量锁,调用LockSupport.park(),底层调用mutex,由用户态进入内核态,synchronized在jdk1.6的时候使用就会去调用mutex,根据mutex的特点,会由用户态进入内核态。
OS级别:pthread_mutex_t
讲到mutex,就不得不提同步原语,同步原语是由平台(例如操作系统)为了支持线程或进程同步而向其用户提供的简单软件机制。它们通常使用较低级别的机制(例如,原子操作,内存屏障,自旋锁,上下文切换等)来构建。
互斥量,事件,条件变量和信号都是同步原语。所以共享和排他锁。监视器通常被认为是高级别的同步工具。它是一个保证使用其他同步原语(通常是具有条件变量的独占锁来支持等待和信令)的方法互斥的对象。在某些情况下,当监视器用作构建块时,它也被视为同步原语。
查看man手册指令:
man pthread_mutex_init
PTHREAD_MUTEX_DESTROY(3P) POSIX Programmer's Manual PTHREAD_MUTEX_DESTROY(3P)
PROLOG
This manual page is part of the POSIX Programmer's Manual. The Linux implementation of this interface may differ (consult the corre‐
sponding Linux manual page for details of Linux behavior), or the interface may not be implemented on Linux.
NAME
pthread_mutex_destroy, pthread_mutex_init - destroy and initialize a mutex
SYNOPSIS
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
DESCRIPTION
The pthread_mutex_destroy() function shall destroy the mutex object referenced by mutex; the mutex object becomes, in effect, uninitial‐
ized. An implementation may cause pthread_mutex_destroy() to set the object referenced by mutex to an invalid value. A destroyed mutex
object can be reinitialized using pthread_mutex_init(); the results of otherwise referencing the object after it has been destroyed are
undefined.
It shall be safe to destroy an initialized mutex that is unlocked. Attempting to destroy a locked mutex results in undefined behavior.
The pthread_mutex_init() function shall initialize the mutex referenced by mutex with attributes specified by attr. If attr is NULL, the
default mutex attributes are used; the effect shall be the same as passing the address of a default mutex attributes object. Upon suc‐
cessful initialization, the state of the mutex becomes initialized and unlocked.
Only mutex itself may be used for performing synchronization. The result of referring to copies of mutex in calls to
pthread_mutex_lock(), pthread_mutex_trylock(), pthread_mutex_unlock(), and pthread_mutex_destroy() is undefined.
Attempting to initialize an already initialized mutex results in undefined behavior.
可以看到pthread_mutex_destroy指令销毁mutex,pthread_mutex_init创建mutex。使用的时候需要引入pthread.h
下边创建一个创建mutex 的例子:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h> //引入pthread.h
int sharei = 0;
void increase_num(void);
// add mutex 创建一个变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//main 方法
int main()
{
//定义方法的返回
int ret;
//创建三个线程
pthread_t thread1,thread2,thread3;
//线程的创建原语:pthread_create 第一个参数:传出参数,调用之后会传出被创建线程的id 定义 pthread_t pid; 继而 取地址&pid
//第二个参数 线程属性,关于线程属性是linux 的知识,一般传NULL,保持默认属性
//第三个参数 线程的启动后的主体函数 需要你定义一个函数,然后传函数名即可 相当于 run()方法回调
//第四个参数 主体函数的参数 没有可以传null
// thread1 thread2 thread3启动完成后会执行increase_num方法。
ret = pthread_create(&thread1,NULL,(void *)&increase_num,NULL);
ret = pthread_create(&thread2,NULL,(void *)&increase_num,NULL);
ret = pthread_create(&thread3,NULL,(void *)&increase_num,NULL);
//防止主线程直接结束 main 方法结束 等待thread1,thread2,thread3执行完成之后才会继续往下执行
pthread_join(thread1,NULL);
pthread_join(thread2,NULL);
pthread_join(thread3,NULL);
//打印sharei
printf("sharei = %d\n",sharei);
return 0;
}
// 相当于run方法
void increase_num(void)
{
long i,tmp;
for(i =0;i<=9999;++i)
{
//上锁
pthread_mutex_lock(&mutex);
//有同学问为什么不加锁会小于30000
//比如当t1 执行到tmp=sharei的时候假设 sharei这个时候=0,那么tmp也等于0
//然后tmp=tmp+1;结果tmp=1(t1);这个时候如果t2进入了
//t2获取的sharei=0;然后重复t1的动作 tmp=1(t2)
//t2 sharei = tmp; sharei = 1;
//然后CPU切回t1 执行 sharei = tmp; sharei = 1;
//结果两个线程执行了两遍但是结果还是sharei = 1;(本来要等于2的)
tmp=sharei;
tmp=tmp+1;
sharei = tmp;
//解锁
pthread_mutex_unlock(&mutex);
}
}
上面是一个三个线程并发执行的例子,如果我们注释掉上锁pthread_mutex_lock,pthread_mutex_unlock之后编译执行会是什么样子,接下来编译运行下,编译:
gcc mutextest.c -o mu -pthread && ./mu
多运行几次之后的运行结果如下,可以看出来是线程不安全的cpu调度线程交替执行结果小于等于三万:
[root@VM-24-17-centos class2]# ./mu
sharei = 30000
[root@VM-24-17-centos class2]# ./mu
sharei = 25428
[root@VM-24-17-centos class2]# ./mu
sharei = 30000
[root@VM-24-17-centos class2]# ./mu
sharei = 30000
[root@VM-24-17-centos class2]# ./mu
sharei = 30000
[root@VM-24-17-centos class2]# ./mu
sharei = 30000
[root@VM-24-17-centos class2]# ./mu
sharei = 30000
[root@VM-24-17-centos class2]# ./mu
sharei = 30000
[root@VM-24-17-centos class2]# ./mu
sharei = 30000
[root@VM-24-17-centos class2]# ./mu
sharei = 26804
取消注释之后,进行上锁,多运行几次的结果如下,可以看出来是线程安全的:
[root@VM-24-17-centos class2]# ./mu
sharei = 30000
[root@VM-24-17-centos class2]# ./mu
sharei = 30000
[root@VM-24-17-centos class2]# ./mu
sharei = 30000
[root@VM-24-17-centos class2]# ./mu
sharei = 30000
[root@VM-24-17-centos class2]# ./mu
sharei = 30000
[root@VM-24-17-centos class2]# ./mu
sharei = 30000
[root@VM-24-17-centos class2]# ./mu
sharei = 30000
[root@VM-24-17-centos class2]# ./mu
sharei = 30000
[root@VM-24-17-centos class2]# ./mu
sharei = 30000
[root@VM-24-17-centos class2]# ./mu
sharei = 30000
[root@VM-24-17-centos class2]# ./mu
sharei = 30000
[root@VM-24-17-centos class2]# ./mu
sharei = 30000
上边的示例就是基于操作系统级别的加锁(linux)
自旋锁(pthread_spin_t):
特点:当一个线程拿不到锁的时候不会休眠,会一直循环。
OS级别:pthread_spin_t,线程在操作系统中空转。
平台级别:
大部分自旋的实现是平台(JVM、Android)内部对线程的控制,不是调用的操作系统(os)级别的自旋。例如:
现在有线程t1和t2,假设现在有一把锁,它的持有者是t1,那么锁的状态就是1,t2过来加锁,发现已经被别的线程持有了,就会持续的循环等到锁释放,直到锁被t1释放,也就是锁的状态是0为止。(ReentrantLock就是平台层面的可重入自旋锁,在OS级别的话也是进行park(),就是重量级锁)可重入自旋锁即当一个线程第一次已经获取到了该锁,在锁释放之前又一次重新获取该锁,第二次就不能成功获取到。由于不满足CAS,所以第二次获取会进入while循环等待,而如果是可重入锁,第二次也是应该能够成功获取到的。
man手册指令:
man pthread_spin_init
PTHREAD_SPIN_DESTROY(3P) POSIX Programmer's Manual PTHREAD_SPIN_DESTROY(3P)
PROLOG
This manual page is part of the POSIX Programmer's Manual. The Linux implementation of this interface may differ (consult the corre‐
sponding Linux manual page for details of Linux behavior), or the interface may not be implemented on Linux.
NAME
pthread_spin_destroy, pthread_spin_init - destroy or initialize a spin lock object (ADVANCED REALTIME THREADS)
SYNOPSIS
#include <pthread.h>
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
DESCRIPTION
The pthread_spin_destroy() function shall destroy the spin lock referenced by lock and release any resources used by the lock. The effect
of subsequent use of the lock is undefined until the lock is reinitialized by another call to pthread_spin_init(). The results are unde‐
fined if pthread_spin_destroy() is called when a thread holds the lock, or if this function is called with an uninitialized thread spin
lock.
The pthread_spin_init() function shall allocate any resources required to use the spin lock referenced by lock and initialize the lock to
an unlocked state.
If the Thread Process-Shared Synchronization option is supported and the value of pshared is PTHREAD_PROCESS_SHARED, the implementation
shall permit the spin lock to be operated upon by any thread that has access to the memory where the spin lock is allocated, even if it
is allocated in memory that is shared by multiple processes.
If the Thread Process-Shared Synchronization option is supported and the value of pshared is PTHREAD_PROCESS_PRIVATE, or if the option is
not supported, the spin lock shall only be operated upon by threads created within the same process as the thread that initialized the
spin lock. If threads of differing processes attempt to operate on such a spin lock, the behavior is undefined.
The results are undefined if pthread_spin_init() is called specifying an already initialized spin lock. The results are undefined if a
spin lock is used without first being initialized.
创建自旋锁的代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
int sharei = 0;
void increase_num(void);
//定义一把自旋锁
pthread_spinlock_t a_lock;
int main()
{
//初始化自旋锁
pthread_spin_init(&a_lock, 0);
int ret;
pthread_t thread1,thread2,thread3;
ret = pthread_create(&thread1,NULL,(void *)&increase_num,NULL);
ret = pthread_create(&thread2,NULL,(void *)&increase_num,NULL);
ret = pthread_create(&thread3,NULL,(void *)&increase_num,NULL);
pthread_join(thread1,NULL);
pthread_join(thread2,NULL);
pthread_join(thread3,NULL);
printf("sharei = %d\n",sharei);
return 0;
}
void increase_num(void)
{
long i,tmp;
for(i =0;i<=9999;++i)
{
// lock spin 自旋
pthread_spin_lock(&a_lock);
tmp=sharei;
tmp=tmp+1;
sharei = tmp;
pthread_spin_unlock(&a_lock);
}
}
编译运行和上面互斥锁的一样,不加锁的情况下运行结果如下:
[root@VM-24-17-centos class2]# ./zx
sharei = 30000
[root@VM-24-17-centos class2]# ./zx
sharei = 30000
[root@VM-24-17-centos class2]# ./zx
sharei = 30000
[root@VM-24-17-centos class2]# ./zx
sharei = 30000
[root@VM-24-17-centos class2]# ./zx
sharei = 25423
[root@VM-24-17-centos class2]# ./zx
sharei = 3000
结果和互斥锁一样,是线程不安全的,小于等于30000.加上锁之后运行结果就正确了,跟互斥锁的区别就是互斥锁竞争不到就会睡眠,而自旋锁会一直获取锁,获取锁的一个状态。
优点:自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,用户态、内核态切换,执行速度快。非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)。
缺点:在持有锁的线程占用锁时间过长或锁的竞争过于激烈时,线程在自旋过程中会长时间获取不到锁资源,将引起CPU的浪费。所以在系统中有复杂锁依赖的情况下不适合采用自旋锁。
高并发与多线程
synchronized大部分情况下都是偏向锁,在高并发的情况下会膨胀为互斥锁,“高并发和多线程”总是被一起提起,给人感觉两者好像相等,实则 高并发 ≠ 多线程,多线程是完成任务的一种方法,高并发是系统运行的一种状态,通过多线程有助于系统承受高并发状态的实现。 高并发是一种系统运行过程中遇到的一种“短时间内遇到大量操作请求”的情况,主要发生在web系统集中大量访问或者socket端口集中性收到大量请求(例如:12306的抢票情况;天猫双十一活动)。该情况的发生会导致系统在这段时间内执行大量操作,例如对资源的请求,数据库的操作等。如果高并发处理不好,不仅仅降低了用户的体验度(请求响应时间过长),同时可能导致系统宕机,严重的甚至导致OOM异常,系统停止工作等。如果要想系统能够适应高并发状态,则需要从各个方面进行系统优化,包括,硬件、网络、系统架构、开发语言的选取、数据结构的运用、算法优化、数据库优化……而多线程只是其中解决方法之一。
posted on 2022-03-04 10:16 这就是程序猿的快乐吧 阅读(486) 评论(0) 编辑 收藏 举报