2017编程提高第1.5节课——Java并发编程(1)
LanyuanXiaoyao's Blog ヽ(✿゚▽゚)ノ

2017编程提高第1.5节课——Java并发编程(1)


回顾一下


并发编程中最重要的两个概念:进程、线程
这里的栈、堆等概念都是操作系统层面的概念,并不特指某一种语言的设计方式,如Java虚拟机实际上是操作系统的一个进程


操作系统中使用虚拟内存映射的方式扩展每个进程可用的内存,好像每个进程都拥有全部的内存空间,实际上是通过操作系统调度让一个进程空闲的内存释放出来给其他进程使用

线程和进程


多线程中的代码、数据和文件均是各个线程共享的,每个线程拥有一套独立的寄存器和堆栈(逻辑上独立,实际上是同一个寄存器和堆栈空间的不同片),

为什么要用线程?

  • 浏览器
    • 线程1显示图像
    • 线程2从网络接收数据
  • 文字处理器
    • 线程1显示图形
    • 线程2读取用户键盘输入
    • 线程3自动定时的保存文档

进程→资源独立,不方便共享资源
线程→资源可以方便地在各个线程间共享
线程还可以提高cpu的使用率,因为cpu速度远远快于IO速度,通过时间片法让线程切换着运行,让cpu使用率大大提高,响应速度快,体验好,效率高

所以总结起来关于线程优点

  1. 在很多程序中,需要多个线程互相同步或互斥的并行完成工作,而将这些工作分解到不同的线程中去无疑简化了编程模型。
  2. 因为线程相比进程来说,更加的轻量,所以线程的创建和销毁的代价变得更小。
  3. 线程提高了性能,虽然线程宏观上是并行的,但微观上却是串行。从CPU角度线程并无法提升性能,但如果某些线程涉及到等待资源(比如IO,等待输入)时,多线程允许进程中的其它线程继续执行而不是整个进程被阻塞,因此提高了CPU的利用率,从这个角度会提升性能。

线程的实现

完全在用户层实现

当线程在用户空间下实现时,操作系统对线程的存在一无所知,操作系统只能看到进程,而不能看到线程。所有的线程都是在用户空间实现。在操作系统看来,每一个进程只有一个线程。过去的操作系统大部分是这种实现方式,这种方式的好处之一就是即使操作系统不支持线程,也可以通过库函数来支持线程
在这种模式下,每一个进程中都维护着一个线程表来追踪本进程中的线程,这个表中包含每个线程独占的资源,比如栈,寄存器,状态等

优点

  1. 在用户空间下进行进程切换的速度要远快于在操作系统内核中实现
  2. 程序员可以实现自己的线程调度算法。比如进程可以实现垃圾回收器来回收线程
  3. 当线程数量过多时,由于在用户空间维护线程表,不会占用大量的操作系统空间

缺点

但是这种方式的缺点是致命的:由于操作系统不知道线程的存在,因此当一个进程中的某一个线程进行系统调用时,比如缺页中断而导致线程阻塞,此时操作系统会阻塞整个进程,即使这个进程中其它线程还在工作
还有一个问题是假如进程中一个线程长时间不释放CPU,因为用户空间并没有时钟中断机制,会导致此进程中的其它线程得不到CPU而持续等待

JVM本身就是一个进程,如果用这种方式实现,有什么缺点?

在操作系统内核中实现

内核线程建立和销毁都是由操作系统负责、通过系统调用完成的。在内核的支持下运行,无论是用户进程的线程,或者是系统进程的线程,他们的创建、撤销、切换都是依靠内核实现的。
线程管理的所有工作由内核完成,应用程序没有进行线程管理的代码,只有一个到内核级线程的编程接口. 内核为进程及其内部的每个线程维护上下文信息,调度也是在内核基于线程架构的基础上完成


这是一种一对一的实现方式,即一个用户线程对应一个内核线程

优点

  1. 多处理器系统中,内核能够并行执行同一进程内的多个线程
  2. 如果进程中的一个线程被阻塞,能够切换同一进程内的其他线程继续执行(用户级线程的一个缺点)
  3. 所有能够阻塞线程的调用都以系统调用的形式实现,代价可观
  4. 当一个线程阻塞时,内核根据选择可以运行另一个进程的线程,而用户空间实现的线程中,运行时系统始终运行自己进程中的线程
  5. 信号是发给进程而不是线程的,当一个信号到达时,应该由哪一个线程处理它?线程可以“注册”它们感兴趣的信号

缺点

开销大,创建和销毁线程都需要内核的参与

组合实现方式

线程创建完全在用户空间中完成,线程的调度和同步也在应用程序中进行. 一个应用程序中的多个用户级线程被映射到一些(小于或等于用户级线程的数目)内核级线程上。


这是现代操作系统常见的实现方式,是一种多对一的实现,效率高,Java虚拟机即采用这种方式

多线程编程的特点

  • 同一份代码,可以有多个线程执行
    • 既可以在一个CPU核上并发执行
    • 也可以在多个CPU核上并行执行
  • 线程的执行默认是乱序
    • 程序员不能假定执行次序
  • 线程会共享数据(对象的变量)
    • 需要互斥
  • 线程之间也需要合作(同步)

线程的执行并不按照编码的顺序执行,会随机按照哪个线程获得cpu时间就执行哪个

如何实现互斥 ?

  • 只有获得了锁的线程,才能够对共享资源做操作, 换句话说:进入临界区
  • 对共享资源做完操作(即使发生异常),一定要释放锁!


临界区是放置访问修改共享资源的操作(文件,变量),仅能有一个线程

锁到底是个什么东西?

  • “锁”本身如果是软件, 也没法保证原子性!
    • 多个CPU对“锁”操作的时候也会出错
  • 最底层需要硬件指令的支持
    • TestAndSet
    • Swap
    • CAS

锁只能由操作系统硬件实现,而不能由软件来实现
硬件指令都是原语操作(原子操作)

TestAndSet


不能用于多处理器,多处理器时要锁总线

Swap

设计“锁”需要考虑的问题

  • 线程申请锁的时候, 发现已经被别的线程持有, 线程该怎么办?
  • 继续尝试,无限循环
    • 时间片用完了, 变为就绪状态,等待下次调度
    • 自旋锁
  • 把线程放到阻塞队列中

可重入性

  • 自旋锁无法重入
    递归导致的死锁
  • 解决办法
    • 记录这个锁被谁持有
    • 记录重入的次数

线程之间的通信

通过共享变量

wait /notify

join


无条件等待threadA执行完毕才继续后续逻辑

Join的实现


其实就是wait /notify的形式

线程的状态


Thread.sleep(sleeptime)这个方法是自行发起,不需要等待第三方的操作,自己唤醒自己,所以不需要释放锁再重新获得cpu的过程,直接就进入ready状态

JDK中常用的锁

可重入互斥锁


推荐使用

信号量


在同一时刻,只能有3个线程能够获得锁 !!!重点!!!

Reader Writer

CountDownLatch

CyclicBarrier


CountDownLatch和CyclicBarrier的区别在于前者是等待各个线程完成后去做一件事,后者是线程互相等待,然后再去做各自的事情
CyclicBarrier的一个举例:旅游团旅游,首先集合点A集合→各自游览→集合点B集合→各自游览……

死锁


死锁的原因主要在于互相等待

死锁的预防

  • 每个线程申请锁的时候都按照特定的次序
    这种方式等于把线程重新变为串行执行,串行执行是不会产生死锁的
  • 申请锁的时候加上timeout
    高并发的情况难以使用

    例子: 银行转账


相似文章

评论