java并发编程

java并发编程

  • 基础知识篇
    • 计算机结构简述
    • 进程
    • 线程
    • 多线程环境下存在的问题
  • 使用篇
    • 创建线程
    • 线程池
    • Unsafe
    • Atomic
    • synchronized
    • AQS
    • LOCK

基础知识篇

学并发编程不先了解操作系统总有些雾里看花的感觉。

计算机结构简述

计算机五大核心组成部分:

  1. 控制器(Control):是整个计算机的中枢神经,其功能是对程序规定的控制信息进行解 释,根据其要求进行控制,调度程序、数据、地址,协调计算机各部分工作及内存与外设的访 问等。
  2. 运算器(Datapath):运算器的功能是对数据进行各种算术运算和逻辑运算,即对数据进 行加工处理。
  3. 存储器(Memory):存储器的功能是存储程序、数据和各种信号、命令等信息,并在需 要时提供这些信息。
  4. 输入(Input system):输入设备是计算机的重要组成部分,输入设备与输出设备合你为 外部设备,简称外设,输入设备的作用是将程序、原始数据、文字、字符、控制命令或现场采 集的数据等信息输入到计算机。常见的输入设备有键盘、鼠标器、光电输入机、磁带机、磁盘 机、光盘机等。
  5. 输出(Output system):输出设备与输入设备同样是计算机的重要组成部分,它把外算 机的中间结果或最后结果、机内的各种数据符号及文字或各种控制信号等信息输出出来。微机 常用的输出设备有显示终端CRT、打印机、激光印字机、绘图仪及磁带、光盘机等。

CPU多核:一个现代CPU除了处理器核心之外还包括寄存器、L1L2L3缓存这些存储设备、浮点运算 单元、整数运算单元等一些辅助运算设备以及内部总线等。一个多核的CPU也就是一个CPU上 有多个处理器核心,这样有什么好处呢?比如说现在我们要在一台计算机上跑一个多线程的程 序,因为是一个进程里的线程,所以需要一些共享一些存储变量,如果这台计算机都是单核单 线程CPU的话,就意味着这个程序的不同线程需要经常在CPU之间的外部总线上通信,同时还 要处理不同CPU之间不同缓存导致数据不一致的问题,所以在这种场景下多核单CPU的架构就 能发挥很大的优势,通信都在内部总线,共用同一个L2L3缓存,L1为独有。

进程

进程状态:
新的(NEW):进程正在被创建
运行(RUNNING):指令正在被执行
等待(WAIT):进程等待某个事件的发生(如I/O完成或受到信号)
就绪 (READY):进程等待分配处理器
终止(TERMINAL):进程完成执行
java并发编程_第1张图片
进程状态切换:

  • 空->新建:创建执行一个程序的新进程,可能的事件有:新的批处理作业、交互登录(终端用户登录到系统)、操作系统因为提供一项服务而创建、由现有的进程派生等。
  • 新建->就绪:操作系统准备好再接纳一个进程时,把一个进程从新建态转换为就绪态。(start)
  • 就绪->运行:需要选择一个新进程运行时,操作系统的调度器或分配器根据某种调度算法选择一个处于就绪态的进程。
  • 运行->退出:导致进程终止的原因有:正常完成、超过时限、系统无法满足进程需要的内存空间、进程试图访问不允许访问的内存单元(越界)、算术错误(如除以0或存储大于硬件可以接纳的数字)、父进程终止(操作系统可能会自动终止该进程所有的后代进程)、父进程请求终止后代进程等。
  • 运行->就绪:最常见的原因是,正在运行的进程到达了“允许不中断执行”的最大时间段,该把处理器的资源释放给其他在就绪态的进程使用了;还有一中原因可能是由于具有更改优先级的就绪态进程抢占了该进程的资源,使其被中断转换到就绪态。
  • 运行->阻塞:如果进程请求它必须等待的某些事件,例如一个无法立即得到的资源(如I/O操作),只有在获得等待的资源后才能继续进程的执行,则进入等待态(阻塞态)。
  • 阻塞->就绪:当等待的事件发生时,处于阻塞态的进程转换到就绪态。
  • 就绪->退出:在上图中没有标出这种转换,在某些进程中,父进程可以在任何时刻终止一个子进程,如果一个父进程终止,所有相关的子进程都被终止。
  • 阻塞->退出:跟上一项原因类似。

进程控制块(PCB):作为信息仓库,进程与进程间不同

  • 进程状态(Process state): 状态可包括新的,就绪,运行,等待,终止等。
  • 程序计数器(Program counter) : 计数器表示进程要执行的下个指令的地址。
  • CPU寄存器(CPU registers):与程序计数器一起,这些寄存器的状态信息在出现中断时也需要保存,以便进程以后能正确的执行。
  • CPU调度信息(CPU scheduling information):这类信息包括进程优先级、调度队列指针和其他调度参数。
  • 内存管理信息(Memory-management information):根据内存系统,这类信息包括基址和界限寄存器的值,页表或段表。
  • 记账信息(Accounting information):这类信息包括CPU时间、实际使用时间、时间界限、记账数据、作业或进程数量等。
  • I/O状态信息(I/O status information):这类信息包含分配给进程的I/O设备列表、打开的文件列表等。

调度队列:
进程进入系统时,会被加入到作业队列中,该队列包含系统中所有进程。驻留在内存中就绪的、等待运行的程序保存在就绪队列中。该队列常用链表来实现,其头节点指向链表的第一个和最后一个PCB块的指针。每个PCB包括一个指向就绪队列的下一个PCB的指针域。
Linux 进程控制块是通过C结构task_struct表示,即PCB。
调度程序(scheduler):
进程会在各种调度队列之间迁移,为了调度,操作系统必须按某种方式从这些队列中选择进程
上下文切换:
将CPU切换到另一进程需要保存当前状态并恢复另一进程状态
进程创建:
通常进程需要一定的资源(如CPU时间,内存,文件,I/O设备)来完成其任务。子进程被创建时,子进程可能直接从操作系统,也可能只从父进程那里获取资源。父进程可能必须在其子进程之间分配资源或共享资源(如内存或文件),限制子进程只能使用父进程的资源能防止创建过多的进程带来的系统超载。
进程终止:
UNIX:可以通过系统调用exit()来终止进程,父进程可以通过系统调用wait()以等待子进程的终止。系统调用wait()返回了中止子进程的进程标识符,以使父进程能够知道哪个子进程终止了。如果父进程终止,那么其所有子进程会以init进程作为父进程,因此,子进程仍然有一个父进程来收集状态和执行统计。
进程间通信:

  • 进程共享内存:共享内存系统需要建立共享内存区域。通常一块共享内存区域驻留在生成共享内存段进程的地址空间。其他希望使用这个共享内存段进行通信的进程必须将此放到它们自己的地址空间上。数据的形式或位置取决于这些进程而不是操作系统,进程还负责保证他们不向同一区域同时写数据
  • 消息传递系统:消息传递提供一种机制以允许进程不必通过共享地址空间来实现通信和同步,这在分布式系统中很有用。例如用于WWW的chat程序就是通过信息交换来实现通信。

RMI和RPC之间区别:在RMI中,远程接口使每个远程方法都具有方法签名。如果一个方法在服务器上执行, 但是没有相匹配的签名被添加到这个远程接口上,那么这个新方法就不能被RMI客户方所调用。在RPC中,当一个请求到达RPC服务器时,这个请求就包含了 一个参数集和一个文本值,通常形成“classname.methodname”的形式。这就向RPC服务器表明,被请求的方法在为 “classname”的类中,名叫“methodname”。然后RPC服务器就去搜索与之相匹配的类和方法,并把它作为那种方法参数类型的输入。这里 的参数类型是与RPC请求中的类型是匹配的。一旦匹配成功,这个方法就被调用了,其结果被编码后返回客户方

线程

一种CPU利用的基本单元,它是形成多线程计算机的基础。线程是CPU使用的基本单元,它由线程ID、程序计数器、寄存器集合和栈组成。它与属于统一进程的其他线程共享代码段、_数据段和其他操作系统资源
动机:

  • 响应度高(Responsiveness):即使其部分阻塞或执行较冗长操作,该程序仍能继续执行,从而增加了对用户的相应程度。
  • 资源共享(Resource Sharing):线程默认共享它们所属进程的内存和资源。代码和数据共享的优点是它允许一个应用程序在同一地址空间有多个不同的活动线程。
  • 经济(Economy):进程创建所需要的内存和资源的分配比较昂贵。由于线程能共享它们所属进程的资源,所以创建和切换线程会更为经济。进程创建比线程慢30呗,切换比线程慢5倍。
  • 多处理器体系结构的利用(Utilization of MP Architectures):多线程的优点之一是能充分使用多处理器体系结构。以便每个进程能并行运行在不同的处理器上。不管有多少CPU,单线程进程只能运行在一个CPU上,在多CPU上使用多线程加强了并发功能。

Java线程状态:
创建状态(New)、就绪状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)、死亡状态(Dead)
java并发编程_第2张图片

  • sleep()方法:
    1.线程休眠会交出CPU,让CPU去执行其他的任务。
    2.调用sleep()方法让线程进入休眠状态后,sleep()方法并不会释放锁,即当前线程持有某个对象锁时,即使调用sleep()方法其他线程也无法访问这个对象。
    3.调用sleep()方法让线程从运行状态转换为阻塞状态;sleep()方法调用结束后,线程从阻塞状态转换为可执行状态。
  • yield()方法:
    1.调用yield()方法让当前线程交出CPU权限,让CPU去执行其他线程。
    2.yield()方法和sleep()方法类似,不会释放锁,但yield()方法不能控制具体交出CPU的时间。
    3.yield()方法只能让拥有相同优先级的线程获取CPU执行的机会。
    4.使用yield()方法不会让线程进入阻塞状态,而是让线程从运行状态转换为就绪状态,只需要等待重新获取CPU执行的机会。
  • join()方法:指的是如果在主线程中调用该方法时就会让主线程休眠,让调用join()方法的线程先执行完毕后再开始执行主线程。
  • wait()方法:使用前必须先获得对象锁(通过synchronized)。释放锁并阻塞。
  • notify()方法:随机通知一个阻塞的线程,竞争获取锁。
  • notifyAll()方法:通知等所有阻塞的线程,竞争获取锁。
  • LockSupport.park()方法:不会释放资源如锁。

系统调用fork()和exec():
UNIX系统有两种形式的fork(),一种复制所有线程,另一种只复制调用了系统调用fork()的线程。
Exec()工作方式:如果一个线程调用系统调用exec(),那么exec()参数所指定的程序会替换整个进程,包括所有线程。
线程取消:
一是异步取消(asynchronous cancellation):一个线程立即终止目标线程。
二是延迟取消(deferred cancellation):目标线程不断地检查它是否应终止,这允许目标线程有机会以有序方式来终止自己。
如果资源已经分配給要取消的线程,或者要取消的线程正在更新与其他线程所共享的数据,那么取消会有困难,对于异步取消尤为麻烦。操作系统回收取消线程的系统资源,但是通常不回收所有资源。因此,异步取消线程并不会使所需的系统资源空闲。相反采用延迟取消时,允许一个线程检查它是否是在安全的点被取消,pthread称这些点为取消点(cancellation point)

多线程环境下存在的问题

缓存一致性问题:
多个处理器的运算任务都涉及同一 块主内存区域时,由于每个CUP都有自己L1缓存,会导致缓存数据不一致,自然会产生错误结果回写到内存。

使用篇

创建线程

创建线程方法四种方法:

  • 继承Thread类创建线程
  • 实现Runnable接口创建线程
  • 使用Callable和Future创建线程
  • 使用线程池例如用Executor框架

线程池

Executor框架
主要用来创建线程池,代理了线程池的创建,使得你的创建入口参数变得简单 重要方法
newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool: 创建一个定长线程池,可控制线程最大并发线程会在队列中等待。 newScheduledThreadPool: 创建一个定长线程池,支持定时及周期行。
newSingleThreadExecutor: 创建一个单线程化的线程池
ThreadPoolExecutor

Name type explain
corePoolSize int 核心线程池大小
maximumPoolSize int 最大线程池大小
keepAliveTime long 线程最大空闲时间
unit TimeUnit 时间单位
workQueue BlockingQueue 线程等待队列
threadFactory ThreadFactory 线程创建工厂
handler RejectedExecutionHandler 拒绝策略

Fork/Join 框架
用于实现“分而治之”的算法,特别是分治之后递归调用的函数。不但可以加快速度也可以防止由于递归导致堆栈溢出。

  • ForkJoinPool:它实现ExecutorService接口和work-stealing算法。它管理工作线程和提供关于任务的状态和它们执行的信息。
  • ForkJoinTask: 它是将在ForkJoinPool中执行的任务的基类。它提供在任务中执行fork()和join()操作的机制,并且这两个方法控制任务的状态。通常, 为了实现你的Fork/Join任务,你将实现两个子类的子类的类:RecursiveAction对于没有返回结果的任务和RecursiveTask 对于返回结果的任务。

Unsafe

Unsafe提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类。

Atomic

在Atomic包里一共有12个类,四种原子更新方式。Atomic包里的类基本都是使用Unsafe实现的包装类,Unsafe只提供了三种CAS方法,compareAndSwapObject, compareAndSwapInt和compareAndSwapLong,其他操作都是通过数据类型转换实现。
基本类:AtomicInteger、AtomicLong、AtomicBoolean;
引用类型:AtomicReference、 AtomicStampedRerence(ABA实例)、AtomicMarkableReference;
数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
属性原子修改器(Updater):AtomicIntegerFieldUpdater、 AtomicLongFieldUpdater、AtomicReferenceFieldUpdater 注:原子更新类的字段的必须使用public volatile修饰符

synchronized

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

  • 普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
  • 静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
  • 同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

锁升级:无锁- > 偏向锁 -> 轻量级锁 -> 重量级锁
锁消除:是发生在编译器级别的一种锁优化方式,如多次调用一个加synchronized的方法,会被优化为一个,如StringBuffer
锁粗化:锁粗化是合并使用相同锁对象的相邻同步块的过程。如果编译器不能使用锁省略(Lock Elision)消除锁,那么可以使用锁粗化来减少开销。

AQS

特性:阻塞等待队列、共享(emaphore/CountDownLatch)/独占(ReentrantLock)、公平/非公平、可重入、允许中断。state高16位表示读锁状态(读锁个数),低16位表示写锁状态(写锁个数)。
同步队列
基于双向链表数据结构的队列,是FIFO先入先出线程等待队列。
条件队列
Condition是一个多线程间协调通信的工具类,使得某个,或者某些线程一起等待某个条件(Condition),只有当该条件具备时 ,这些等待线程才会被唤醒,从而重新争夺锁
条件与同步队列区别
1.同步队列是有头结点的,而条件队列没有(node.prev == null)
2.节点状态
3.next字段只有同步队列才会使用,条件队列中使用的是nextWaiter字段node.next != null
4.tail同步队列尾结点
waitStatus5个状态值
CANCELLED(1) : 该节点的线程可能由于超时或被中断而处于被取消(作废)状态,一旦处于这个状态,节点状态将一直 处于CANCELLED,因此应该从队列中移除
SIGNAL(-1):表示该节点处于等待唤醒状态,后继节点会被挂起,因此在当前节点释放锁或被取消之后必须唤醒其后继结点
CONDITION(-2):该节点的线程处于等待条件状态,不会被当作是同步队列上的节点,直到被唤醒(signal),设置其值为0,重新进入阻塞状态
PROPAGATE(-3) - 下一个 acquireShared 应无条件传播。表示下一次共享式同步状态获取将会被无条件地传播下去
0:新加入的节点

LOCK

ReentranLock
lock():默认非公平锁,lock(true)为公平锁有个等待队列
unlock():会释放锁,并且唤醒同步队列中线程
BlockingQueue
ArrayBlockingQueue 由数组支持的有界队列
LinkedBlockingQueue 由链接节点支持的可选有界队列
PriorityBlockingQueue 由优先级堆支持的无界优先级队列
DelayQueue 由优先级堆支持的、基于时间的调度队列
Semaphore
控制访问特定资源的线程数目
Semaphore(int permits, boolean fair):

  • permits 表示许可线程的数量
  • fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线 程

CountDownLatch
这个类能够使一个线程等待其他线程完成各自的工作后再执行。例 如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执行。
CountDownLatch(2):创建一个继承AQS的队列Sync,设定sate为2个
countDown():子线程执行完,AQS.releaseShare(1),
countDownLatch.tryRealseShared()释放一个信号量即state减1,为0表明所有子线程的任务完成,要唤醒主线程。doReleaseShared()遍历同步队列,找到一个阻塞状态的线程并唤醒。
await():主线程进入等待队列,当state不为0主线程进入阻塞状态。

CyclicBarrier
栅栏屏障,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程 到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
CyclicBarrier(int parties,Runnable ):设置等待数量,回调方法
await():sate减1,线程加入条件等待队列并挂起。
当计数为0,lock.newCondition().singnalAll()唤醒所有条件队列的节点转移到同步队列当中。同步队列中所有线程都会执行ReentrantLock.unlock(),唤醒同步队列中的一个线程,最后所有任务执行完毕。
当计数大于0,加入条件队列的队尾,释放独占锁(ReentrantLock.exclusiveOwnerThread=null,state=0)

你可能感兴趣的:(java)