并发:如果逻辑控制流在时间上重叠,那么这种现象就是并发。
现代操作系统提供了三种构建并发程序的方法:
1,进程。每个逻辑控制流都是一个进程。
通过fork,exec函数,构造子进程。
2,I/O多路复用。在一个进程的上下文context中显示的调度他们自己的逻辑控制流。
逻辑流被模型化为状态机。
基本思路是使用select函数,要求内核将进程挂起,只有当一个或多个I/O事件发生后,才将控制返回给应用程序。所以也称为事件驱动型的编程。
3,线程。线程是运行在单一进程上下文中的逻辑流。由内核调度。可以把线程看成是其他两种方式的混合体。像进程一样由内核调度,像I/O多路复用一样共享同一虚拟存储空间。
Posix线程(Pthreads)是在C程序中处理线程的一个标准接口,定义了大约60个函数,允许程序创建,kill和回收线程资源。
某些公共的业务状态,流程状态,id,全局静态属性或变量,都是多线程的共享变量。共享变量在虚拟空间只有一份实例,却被多个线程引用。
共享变量,多线程并发的同步问题如何解决呢?通常的解决方式是:使用信号量(P、V)来实现线程的互斥访问,同时通过信号量的条件真假变化来通知其他线程对共享资源的访问。
关于信号量(P、V)的使用,可以参考其他CSDN文章。
并发线程有阻塞和非阻塞两种。java的synchronized同步方法是一种互斥锁,使用操作系统提供的同步原语,将线程阻塞。线程的阻塞和唤醒,需要线程上下文的切换,性能消耗较大。
线程非阻塞的方式有自旋锁。自旋锁的原理是通过操作系统内核提供的CAS原子操作,来实现共享变量的互斥访问和对等线程之间的通知调度。同时线程处于非阻塞的状态,无需上下文切换。
CAS原理:CAS(Compare And Swap)也叫做比较与交换,是一种无锁原子算法,映射到操作系统就是一条cmpxchg硬件汇编指令。
它包含3个参数CAS(V,E,N),V表示待更新的内存值,E表示预期值,N表示新值,当V值等于E值时,才会将V值更新成N值,如果V值和E值不等,操作失败或者重新再来,这就是一次CAS的操作。
为了保证CAS的原子性,CPU提供了下面两种方式:
- 总线锁定
- 缓存锁定
共享变量在多核CPU下,会存在缓存一致性问题。每个CPU内核都有自己的高速缓存,会将运算需要的数据从主存复制一份到CPU高速缓存中。解决方法有:
1)在总线加LOCK锁的方式;
2)通过缓存一致性协议;
通过在共享变量中加上volatile,保证变量的可见性,使线程强制从内存读取变量。volatile和缓存一致性协议又有何关系呢?volatile的实现依赖缓存一致性,内存屏障。
自旋锁:
自旋锁作为锁的一种,和互斥锁一样也是为了在并发环境下保护共享资源的一种锁机制。在任意时刻,只有一个执行单元能够获得锁。
自旋锁是通过加锁程序中的无限循环,由当前尝试加锁的线程反复轮训当前锁的状态直到最终获取到锁。
互斥锁与自旋锁的优缺点:
互斥锁的优点是当加锁失败时,线程会及时的让出cpu,从而提高cpu的利用率,但缺点是如果短时间内如果涉及到大量线程的加锁/解锁,则频繁的唤醒/阻塞会因为大量的线程上下文切换而降低系统的性能。因此互斥锁适用于线程会在较长时间内持有锁的场景。
与互斥锁相对的,自旋锁由于一直处于持续不断的轮训中,因此可以非常迅速的感知到锁状态的变化,在两个线程间能够瞬间完成锁的释放与获取。但如果需要争用锁的线程长时间都无法获取到锁,则会造成CPU长时间空转,造成CPU资源极大的浪费。因此自旋锁只适用于线程在加锁成功后会在极短的时间内释放锁的场景(需要保护的临界区非常小)。自旋锁和互斥锁起到了一个互补的作用,在不同的需求场景下发挥自己的作用。
自旋锁的实现有多种:原始自旋锁、票锁、CLH锁、MCS锁。
AQS 的全称为(AbstractQueuedSynchronizer),这个类在 java.util.concurrent.locks 包下面。CountDownLatch,CyclicBarrier都是基于AbstractQueuedSynchronizer来实现的。
AbstractQueuedSynchronizer采用CLH锁来实现,保证了线程的公平性,属于公平锁,由于CLH采用双向链表节点,分散了多线程对同一内存地址的竞争。每个线程只监控自己的node节点和节点存储的状态信息。而MCS锁与CLH锁类似,采用了链表,区别在于,线程需要监控next下一个节点,在NUMA的多核CPU架构下,下一个节点可能分布在其他CPU下,所以CLH锁的性能更高。
参考: