Java并发原理

进程、线程和并发实体

《操作系统原理》里面很重要的一个概念是进程。进程是程序动态的概念,它用来表示程序在执行的一组数据结构。这组数据结构中记录了指令加载到内存中的地址,打开的文件,线程信息,共享内存等。

每个进程可以有多个线程。它也是一组数据结构包括:下一条要执行的指令,寄存器,堆栈,状态等。一幅图来表示

 

上图画出了4个线程(线程2、3、4和1是一样的,没有全画),如果程序没有启动任何线程,其实也会用到线程——主线程(图中的线程1)。所以最后消耗CPU的线程而不是进程

其实在Linux中(我实在不知道Windows,我猜应该差不多)进程和线程是同一个数据结构——task_struct,对于内核(kernel)来说并没有进程和线程的区别,只有进程——kernel称之为task。所以在Linux中进程和线程并没有父子关系而是平行的结构,表示进程的数据结构填充的数据多一些,包括了打开文件,共享内存之类的,这个被称为主线程;其他线程的数据结构这些项目则为空,并且有一个“父进程”的指针,指向了“主线程”。

明白了这一点我们就清楚了,操作系统调度的最小对象其实是——线程,但是名字叫task,教科书上叫进程。。。。有点混乱了吗?所以我们引入一个新的术语——并发实体。所有CPU调度的最小单位我们统称为并发实体,无论是进程还是线程或者是其他的什么“怪胎”。(没错,我会在下一次介绍这些怪胎。)

为什么要线程

勇敢提出这个问题的人要受到表扬,他冒着被无情嘲讽的危险提出了一个很白痴的问题。(我觉得回答不出来这个问题的人,才是真正的白痴)回答这个问题要回答另一个基本的问题——为什么要并发

我们想象一下,一个Web服务器,可能是下面的代码

while (true){
     request = next_http_request()
     request_work(request)
}

程序循环获取新请求(next_http_request),执行请求(request_work),然后继续下一次循环。request_work会从硬盘读取文件,然后发送给客户端作为HTTP的响应,而硬盘I/O是一个阻塞操作,也就是说request_work会一直等待读取完数据之后才能释放CPU的控制权,然后下一个请求才有机会被执行。

这就是并发要解决的问题,当request_work发起I/O之后CPU是完全空闲下来的,而可怜的新请求(next_http_request)必须等待I/O完成之后才可以获取CPU的控制权。所以CPU的利用率非常低,并发要解决的问题就是提高CPU的利用率。明白这一点我们也就清楚了,并发只对非CPU密集型程序管用,如果CPU利用率非常高,更多的并发只会让情况更加糟糕

那么并发为什么一定是多线程而不是多进程呢?其实在Linux下进程和线程的创建成本没有什么区别(都是task_struct),但是进程之间可以共享数据的方式只能通过非常复杂的IPC来实现,线程之间代码都是共享的,地址空间也是共享的,所以共享数据的方式更加高效。(进程要考虑隔离,一个进程没有办法直接访问另一个进程;线程不用隔离,线程之间共享内存)

我们修改成多线程版本的Web服务器

while (true){
     request = next_http_request()
     request_work_in_thread(request)
}

request_work_in_thread方法会启动一个线程(work线程),然后CPU开始执行next_http_request获得下一个请求。

request对象是在主线程创建的,可以直接传递给request_work_in_thread中的work线程使用。

我们提高CPU利用率所以需要并行,我们要提高并发实体之间共享数据的效率所以选择了线程作为并发实体的实现

Java的线程

好了,回到了Java。在Java中启动一个线程非常简单——只要new Thread就搞定了。JVM会把它变成操作系统的API,如果是Linux则会生成一个task_struct的结构。至于Runable之类的东西其实最后还是Thread,即便是Java并发包最后也还是用Thread。

所以至此,我们成功把《操作系统原理》中进程、线程和Java的线程“融汇贯通了”。下面开始另一个东西——PV操作。


竞争条件,临界区、PV信号量

恩,你这一部分估计也已经还给老师了。没关系,我们一起回忆一下。

举个例子:

public void plus(int value){
  count = count + value;
}

当多线程同时调用plus的时候程序的逻辑是错误的。count+value并不是一个原子操作,它会被变成三个CPU指令

  • 获取count的数据到寄存器(还记得吗?CPU只访问寄存器)
  • 寄存器+value,并且写回寄存器
  • 寄存器写回到内存
    如果T1,T2两个线程同时执行(count=0)
  • T1 获取count的数据到寄存器
  • T1 将寄存器的值加10
  • T2 获取count的数据到寄存器
  • T2 将寄存器的值加30
  • T2 寄存器写回到内存(count=30)
  • T1 寄存器写回到内存(count=10)
    我们期望的可能是40,但是实际情况是10,因为T2访问数据的时候T1还没有来得及写回到内存中。当两个线程访问同一个数据,最后的结果依赖于线程的顺序这个就叫竞争条件。避免竞争条件的方法就是——通过临界区把一组动作“原子化”。例子中就是把:count = count + value,原子化。(原子化是指一次做完;其他人排队等候。)

就像你去买咖啡,收银员是所有人共享的(竞争条件),如果他经历:问杯型、种类、口味;收费;给你发票,这三个过程不能被打断否则会乱掉的。所以大家需要依次排队。(临界区)

如何实现临界区?答案是PV信号量(也叫PV操作,PV原语)。它是著名“河南籍”计算机科学家——E.W.Dijkstra设计的一套算法,老爷子这套严密的理论是现代并发的基础。简单来说他定义了两个操作
设一个计数器s

  • P s-1,如果s小于0则休眠否则继续执行
  • V s+1,如果s<=0则唤醒等待进程否则继续执行

P操作相当于使用资源,执行这个操作相当于:这个桌子我承包了,你们等我用过之后再用。(如果桌子有人那就只能乖乖等着了)
V操作相当于释放资源,执行这个操作相当于:这个桌子我不用了,下一个是谁?来用吧。(如果没有下一个人,那就直接走人了)

没错,PV就是“锁”。《操作系统原理》中竞争条件、临界区都是现实中不存在的概念,只有PV操作被具体实现了,就是我们称之为锁的东西。

Java中的锁

所有的“锁”都是一种PV操作,锁的区别在于你选择的“计数器”是什么?比如你选择的计数器是当前对象那么对应的关键是“synchronized”(还有个名字叫管程,天真的科学家们觉得面向对象的锁好牛B,就赐予它一个专门的名词),如果你的计数器是一个原子类型的值那么可能就是AtomicInteger的inc或者dec操作。这个就是锁的粒度,锁越小竞争条件就越小。就像你买咖啡,把整个咖啡馆当做“竞争条件”或者把收银员当做“竞争条件”,很像然后者对咖啡馆的利用率更高。(还有一些买过咖啡的人至少可以逗留一会)

总结

我不想在文章中介绍Java的API,并发包的类。(这些介绍资料一抓一大把实在太多了)我的目的是通过回忆基础课程,尝试把我们认为最没用的东西联系到实际中。让大家知道——原来教科书上不是骗人的,真的是无处不在。希望你看完这篇文章,能够默默的去翻开尘封已久的《操作系统原理》。郭德纲老师有句话说:演员和演员最后比拼的是文化底蕴;同样的道理,程序员和程序员最后比拼的是基础,当你羡慕别人“游刃有余”的轻松解决一个很难缠的BUG时;动动手指搞定解决了性能问题时;短时间内迅速领悟到一个新技术时,请不要忘记他在这个背后可能花了几年甚至几十年的功夫在练习被你遗弃的“教科书”。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
线程的状态以及各状态之间的转换详解.mp4 线程的初始化,中断以及其源码讲解.mp4 多种创建线程的方式案例演示(一)带返回值的方式.mp4 多种创建线程的方式案例演示(二)使用线程池.mp4 Spring对并发的支持:Spring的异步任务.mp4 使用jdk8提供的lambda进行并行计算.mp4 了解多线程所带来的安全风险.mp4 从线程的优先级看饥饿问题.mp4 从Java字节码的角度看线程安全性问题.mp4 synchronized保证线程安全的原理(理论层面).mp4 synchronized保证线程安全的原理(jvm层面).mp4 单例问题与线程安全性深入解析.mp4 理解自旋锁,死锁与重入锁.mp4 深入理解volatile原理与使用.mp4 JDK5提供的原子类的操作以及实现原理.mp4 Lock接口认识与使用.mp4 手动实现一个可重入锁.mp4 AbstractQueuedSynchronizer(AQS)详解.mp4 使用AQS重写自己的锁.mp4 重入锁原理与演示.mp4 读写锁认识与原理.mp4 细读ReentrantReadWriteLock源码.mp4 ReentrantReadWriteLock锁降级详解.mp4 线程安全性问题简单总结.mp4 线程之间的通信之wait notify.mp4 通过生产者消费者模型理解等待唤醒机制.mp4 Condition的使用及原理解析.mp4 使用Condition重写waitnotify案例并实现一个有界队列.mp4 深入解析Condition源码.mp4 实战:简易数据连接池.mp4 线程之间通信之join应用与实现原理剖析.mp4 ThreadLocal 使用及实现原理.mp4 并发工具类CountDownLatch详解.mp4 并发工具类CyclicBarrier 详解.mp4 并发工具类Semaphore详解.mp4 并发工具类Exchanger详解.mp4 CountDownLatch,CyclicBarrier,Semaphore源码解析.mp4 提前完成任务之FutureTask使用.mp4 Future设计模式实现(实现类似于JDK提供的Future).mp4 Future源码解读.mp4 ForkJoin框架详解.mp4 同步容器与并发容器.mp4 并发容器CopyOnWriteArrayList原理与使用.mp4 并发容器ConcurrentLinkedQueue原理与使用.mp4 Java中的阻塞队列原理与使用.mp4 实战:简单实现消息队列.mp4 并发容器ConcurrentHashMap原理与使用.mp4 线程池的原理与使用.mp4 Executor框架详解.mp4 实战:简易web服务器(一).mp4 实战:简易web服务器(二).mp4 JDK8的新增原子操作类LongAddr原理与使用.mp4 JDK8新增锁StampedLock详解.mp4 重排序问题.mp4 happens-before简单概述.mp4 锁的内存语义.mp4 volatile内存语义.mp4 final域的内存语义.mp4 实战:问题定位.mp4
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值