Java并发编程——基础知识(一)

1.进程与线程

1.1 基本概念

  • 进程:对运行时程序的封装,是系统进行资源调度和分配的的基本单位,实现了操作系统的并发
  • 线程:进程的子任务,是CPU调度和分派的基本单位用于保证程序的实时性,实现进程内部的并发;线程是操作系统可识别的最小执行和调度单位。每个线程都独自占用一个虚拟处理器:独自的寄存器组指令计数器和处理器状态。每个线程完成不同的任务,但是共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源

1.2 进程与线程的区别

  1. 一个线程只属于一个进程,而一个进程中可以有多个线程,但至少有一个线程,线程依赖于进程而存在.
  2. 进程执行过程中拥有独立的内存单元,而多个线程共享进程的内存.(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量.)
  3. 进程是资源分配的最小单位,线程是CPU调度的最小单位.
  4. 由于在创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、IO设备等.因此,操作系统所付出的开销将显著地大于创建或撤销线程时的开销.类似地,在进行进程切换时,涉及到整个当前进程CPU环境的保存以及新被调度运行的进程的CPU环境的设置.而线程切换只须保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作.可见,进程切换的开销远大于线程切换的开销.
  5. 由于同一进程中的多个线程具有相同的地址空间,致使它们之间的同步和通信的实现也变得容易.进程间通信IPC,线程间可以直接读写进程数据段(如 全局变量)来进行通信——需要同步或互斥手段的辅助,以保证数据的一致性.在有的系统中,线程的切换、同步和通信都无须操作系统内核的干预.
  6. 进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂.
  7. 进程间不会相互影响,线程中一个线程挂掉可能导致整个进程挂掉.
  8. 进程适应于多核、多机分布,线程适用于多核.

1.3 进程间的通信方式

​ 进程间的通信主要包括管道、系统IPC(包括消息队列、信号量、信号、共享内存等)以及套接字socket.

1.3.1 管道

​ 管道主要包括匿名管道和命名管道.管道可用于具有亲缘关系的父子进程间的通信.命名管道除了具有管道所具有的所有功能外,它还允许无亲缘关系的进程间的通信.

  • 匿名管道PIPE
    1. 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端.
    2. 它只能用于具有亲缘关系的进程间通信(父子进程或兄弟进程).
    3. 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write等函数.但它不是普通的文件,并不属于其他任何的文件系统,并且只存在于内存中.
  • 命名管道FIFO:
    1. FIFO可以在无关的进程中交换数据.
    2. FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中.

1.3.2 系统IPC

  • 消息队列

    消息队列是消息的链接表,存放在内核中.一个消息队列由一个标识符(即队列ID)来标记(消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点)具有写权限的进程可以按照一定的规则向消息队列中添加新的信息.对消息队列有读权限的进程可以从中读取信息.

    特点:

    1. 消息队列是面向记录的.其中的消息具有特定的格式和特定的优先级.
    2. 消息队列独立于发送和接受进程.进程终止时,消息队列中的内容并不会被删除.
    3. 消息队列可以实现消息的随机查询.消息不一定要以先进先出的次序读取,也可以按消息的类型读取.
  • 信号量:

    信号量与已经介绍过的IPC结构不同,它是一个计数器,可以用来控制多个进程对共享资源的访问.信号量用于实现进程间的互斥同步,而不是用于存储进程间的通信数据.

    特点:

    1. 信号量用于进程间同步,若要进程间传递数据需要结合共享内存使用.
    2. 信号量基于操作系统的PV操作,程序对信号量的操作都是原子操作.
    3. 每次对信号量的PV操作不仅限于对信号量值加1或减1,而且可以加减任意正整数.
    4. 支持信号量组
  • 信号

    信号是一种比较复杂的通信方式,用于通知接受进程某个事件已经发生.

  • 共享内存:

    它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新.这种方式需要依靠某种同步操作,如互斥锁和信号量等.

    特点:

    1. 共享内存是最快的一种IPC,因为进程是直接对内存进行读取.
    2. 因为多个进程可以同时操作,所以需要进程同步
    3. 信号量+共享内存通常要结合在一起使用,信号量用来同步对共享内存的访问.

1.3.3 套接字

​ socket也是一种进程间通信机制.与其他通信机制不同的是,它可以用于不通主机之间的通信

1.4 线程间通信方式

  1. 临界区:通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问.
  2. 互斥量Synchronized/Lock:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问.
  3. 信号量Semphare:为控制具有有限数量的用户资源而设计的,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目.
  4. 事件(信号),Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作.

二、并发与并行

  1. 并发:多个进程在一个CPU下采用时间片轮转方式,在一段时间内,让多个进程得以推进,类似“雨露均沾”思想,一个CPU在短时间内会自动调度不同任务.(同一时间间隔内,多个事件交替发生)
  2. 并行:多个任务在多个CPU分别同时运行,这称之为并行.(同一时刻,多个事件发生)

三、线程的风险

  1. 线程安全性问题:多线程环境下,程序的运行结果与预期不符.(伴生现象:不容易复现)
  2. 线程的活跃度问题
    • 死锁:指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去
    • 饥饿:如果线程优先级“不均”,并且CPU繁忙的情况下,优先级低的线程得到执行的机会很小,就可能发生线程“饥饿”;持有锁的线程,如果执行的时间过长,也可能导致“饥饿”问题。饥饿嘛,线程一直得不到CPU时间,一直被饿着.
    • 活锁:线程拿到锁,却又相互释放锁,不执行功能.
  3. 线程的性能问题
  • 消耗时间:线程的创建和销毁都需要时间,当有大量的线程创建和销毁时,那么这些时间的消耗则比较明显,将导致性能上的缺失.
  • 消耗CPU和内存:如果发生大量的线程被创建、执行和销毁,这可是非常耗CPU和内存的,这样将直接影响系统的吞吐量,导致性能急剧下降,如果内存资源占用的比较多,还很可能造成OOM.
  • 容易导致GC频繁的执行:大量的线程的创建和销毁很容易导致GC频繁的执行,从而发生内存抖动现象,而发生了内存抖动,对于移动端来说,最大的影响就是造成界面卡顿.
  • 线程的上下文切换:在线程调度过程中需要访问由操作系统和JVM共享的数据结构。应用程序、操作系统以及JVM都使用一组相同的CPU,在JVM和操作系统的代码中消耗越多的CPU时钟周期,应用程序的可用CPU时钟周期就越来越少。当一个新的线程被切换进来时,它所需要的数据可能不在当前处理器的本地缓存中,因此上下文切换将导致一些缓存缺失,因而线程在首次调度运行时会更加缓慢。

参考文章:

原文地址:http://www.cnblogs.com/cxz0214/p/16879515.html

发表评论

您的电子邮箱地址不会被公开。