并发

目录

1       背景... 2

1.1        多线程vs多进程... 2

2       多线程常用方式... 3

2.1        继承Thread. 3

2.2        实现Runnable. 3

2.3        使用线程池管理... 3

2.3.1         线程池核心处理过程(ThreadPoolExecutor)... 5

2.3.2         Executor创建的三种类型ThreadPoolExecutor线程池... 5

3       线程状态转换/方法... 8

3.1        状态转换: 8

3.2        常用函数... 8

4       安全性问题... 11

4.1        内存模型... 12

4.1.1         原子性... 12

4.1.2         可见性... 12

4.1.3         有序性... 12

4.2        竞态条件... 14

4.2.1         使用线程安全对象代替非线程安全域... 14

4.2.2         加锁机制... 14

4.2.3         Volatile变量... 18

4.3        线程封闭... 21

4.3.1         栈封闭... 21

4.3.2         ThreadLocal类... 21

5       同步工具类... 22

5.1        闭锁 CountDownLatch. 22

5.2        循环屏障 CyclicBarrier. 22

5.3        信号量 Semaphore. 23

6       线程间通信... 23

6.1        轮询,使用volatile。... 23

6.2        回调。可以拿数据... 23

6.3        Synchronized ,wait,notify ,lock ,condition等方法。... 24

6.4        线程同步工具类。... 24

6.5        Callable阻塞获取线程返回数据... 24

7       多线程练习... 24

7.1        三个线程顺序打印ABC. 24

7.2        生产者消费者,生产者负责往篮子放苹果,消费者负责吃苹果,控制篮子最多五个苹果。     24

7.3        龟兔赛跑[线程间回调通信]. 24

7.4        生产者消费者KFC. 24

7.5        线程同步工具类... 24

 

1      背景

早期计算机不包含操作系统,从头到尾执行一个程序,对于昂贵稀有的计算机资源是一种浪费。

要实现并发,首先需要操作系统的支持。现在的操作系统大部分都是多任务操作系统,可以“同时”执行多个任务。多任务可以在进程或线程的层面执行。

进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间。多任务操作系统可以“并发”执行这些进程。
线程是指进程中乱序、多次执行的代码块,多个线程可以“同时”运行,所以认为多个线程是“并发”的。多线程的目的是为了最大限度的利用CPU资源。比如一个JVM进程中,所有的程序代码都以线程的方式运行。
这里面的“同时”、“并发”只是一种宏观上的感受,实际上从微观层面看只是进程/线程的轮换执行,只不过切换的时间非常短,所以产生了“并行”的感觉。

1.1      多线程vs多进程

操作系统会为每个进程分配不同的内存块,而多个线程共享进程的内存块。这带来最直接的不同就是创建线程的开销远小于创建进程的开销。
同时,由于内存块不同,所以进程之间的通信相对困难。需要采用pipe/named pipe,signal, message queue, shared memory,socket等手段;而线程间的通信简单快速,就是共享进程内的全局变量。
但是,进程的调度由操作系统负责,线程的调度就需要我们自己来考虑,避免死锁,饥饿,活锁,资源枯竭等情况的发生,这会增加一定的复杂度。而且,由于线程之间共享内存,我们还需要考虑线程安全性的问题。

 

多线程引入的问题:

  1. 安全性问题(永远不发生糟糕的事情)

Public class UnsageSequence

{

Private int value;

 

Public int getNext()

{

return value++;

}

 

}

------------------------------à>>>--------------------------------

 

A  --àvalue->9----------à9+1=10---------àvalue=10

 

B  -------------àvalue->9-------à9+1=10--------------àvalue=10

 

  1. 活跃性问题(某件正确的事情最终会发生):死锁,饥饿,活锁。
  2. 性能问题:上下文切换,服务时间过长,资源消耗过高。

 

2      多线程常用方式

2.1      继承Thread

2.2      实现Runnable

2.3      使用线程池管理

我们主需要提供任务(Runnable, Callable),线程池可以理解为一个工厂,具体负责工人人数配置以及分工。

线程池 ExecutorService的submit方法入参接受Runnable或者Callable类型的参数,Callable可以支持获取线程返回数据,会阻塞到返回对象Future的get方法,直到线程处理完毕返回数据。

线程池负责管理线程的创建和调度。

   1.当前线程数小于核心池数,每来一个任务就新建一个线程去执行。

   2.当前线程大于等于核心池数,小于线程池最大线程数,来一个任务就放到缓冲池队列,放入成功等待执行即可,若放入队列失败(任务缓存队列已满,任务执行速度低于生成任务速度),立即新建线程执行。

   3.当前线程大于线程池最大线程数,采取任务拒绝策略处理。

   4. 线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。

每个变量的作用都已经标明出来了,这里要重点解释一下corePoolSize、maximumPoolSize两个变量。

  假如有一个工厂,工厂里面有10个工人,每个工人同时只能做一件任务。

  因此只要当10个工人中有工人是空闲的,来了任务就分配给空闲的工人做;当10个工人都有任务在做时,如果还来了任务,就把任务进行排队等待;如果说新任务数目增长的速度远远大于工人做任务的速度,那么此时工厂主管可能会想补救措施,比如重新招4个临时工人进来;然后就将任务也分配给这4个临时工人做;如果说这14个工人做任务的速度还是不够,此时工厂主管可能就要考虑不再接收新的任务或者抛弃前面的一些任务了。

  当这14个工人当中有人空闲时,而新任务增长的速度又比较缓慢,工厂主管可能就考虑辞掉4个临时工了,只保持原来的10个工人,毕竟请额外的工人是要花钱的。

这个例子中的corePoolSize就是10,而maximumPoolSize就是14(10+4)。也就是说corePoolSize就是线程池大小,maximumPoolSize在我看来是线程池的一种补救措施,即任务量突然过大时的一种补救措施。

2.3.1        线程池核心处理过程(ThreadPoolExecutor)

 

 

2.3.2        Executor创建的三种类型ThreadPoolExecutor线程池

  1. FixedThreadPool

 

特点:线程数固定,队列是无界队列,maxiumPoolSize参数无效。

执行过程如下:

1.如果当前工作中的线程数量少于corePool的数量,就创建新的线程来执行任务。

2.当线程池的工作中的线程数量达到了corePool,则将任务加入LinkedBlockingQueue。

3.线程执行完1中的任务后会从队列中去任务。

注意LinkedBlockingQueue是无界队列,所以可以一直添加新任务到线程池。

 

  1. SingleThreadPool

 

特点:单个线程,其他均与FixedThreadPool相同

 

  1. CachedThreadPool

 

特点:来一个任务,看看队列中是否有空闲线程,无则新建线程。由于队列为无容量阻塞队列,每个插入操作必须等待另一个线程的对应移除操作,插入失败会新建任务。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU资源

 

 

 

链接: https://ww.cnblogs.com/dolphin0520/p/3932921.html(结尾有源码大致分析)

Executor框架:https://www.cnblogs.com/study-everyday/archive/2017/04/20/6737428.html

 

 

3      线程状态转换/方法

3.1      状态转换:

 

1、新建状态(New):新创建了一个线程对象。

2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。

4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)

(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。

(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)

5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

3.2      常用函数

sleep(long millies)

   让线程休眠指定毫秒(暂停执行),但是不释放持有的锁,可以让低优先级线程获得运行机会。

场景及目的:让指定线程休眠一段时间。

join

  当前线程等待某个线程执行结束,不释放持有锁。

  场景及目的:等待定线程结束后再执行。

 

yield() 退让

暂停当前的线程,并执行其他线程。让当前线程到可运行状态,使其他同等优先级的线程获得运行机会,但是本线程同样有可能重新获得运行机会。

场景及目的:是同等优先级的线程可以适当轮转执行。

 

wait()    等待被唤醒

wait(long timeout)  超过时间,会自动唤醒,进入就绪状态。不一定有锁哦!!!

    必须在synchronized(Obj)同步块或者方法里边,释放Obj对象锁,暂停当前线程(休眠),等待其他线程调用Obj对象的notify/nodifyAll唤醒。

notify()    对对象锁唤醒操作

notifyAll()  唤醒所有等待对象锁的线程

   必须在synchronized(Obj)同步块或者方法里边,调动notify后并不是马上释放锁,而是在执行完synchronize语句块,释放对象锁,jvm会在wait()对象锁的线程中任意选取一个线程,赋予其对象锁,继续执行。

  场景及目的: 在线程间同步、唤醒,可以控制线程的执行与等待,通过管理多线程的执行达到统一或想要的结果。

 

 

setPriority(Thread.MAX_PRIORITY)

  设置线程优先级,优先级高并不代表一定先执行,只是先执行可能性更大。

 

a.interrupt

  中断线程a,主要是用于中断任务的执行,阻塞状态会抛异常,其他时候是将中断标识设置为true。

如果a在非阻塞状态,将状态设置为true,等线程调用sleep或者wait,join抛出异常来,状态设置为false

如果a在阻塞状态(sleep,wait,join),会抛异常,wait需要在重新拿到锁后抛异常.

 

总结一下:调用interrupt()方法,立刻改变的是中断状态,但如果不是在阻塞态,就不会抛出异常;如果在进入阻塞态后,中断状态为已中断,就会立刻抛出异常)

方法详解:

http://blog.csdn.net/zhangliangzi/article/details/52485319

 

 

 

4      安全性问题

线程安全性很难给出一个确切定义,核心概念就是正确性,当多个线程访问某个类,这个类始终能表现出正确行为,该类就是线程安全的。编写线程安全的代码,核心要对状态访问操作进行管理(共享的和可变的状态)。无状态对象是线程安全的(不包含任务域、不包含对其他类中域的引用)。

对于多线程访问可变的状态变量未使用合适同步,修复方式:

  1. 不在线程间共享该状态变量
  2. 该状态变量修改为不可变变量
  3. 访问状态变量使用同步

线程stack,堆。

4.1      内存模型

Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

 

4.1.1        原子性

分析下以下哪些为原子操作

x = 10;         //语句1      

y = x;         //语句2

x++;           //语句3

x = x + 1;     //语句4

1为原子,2.3.4不是

 

4.1.2        可见性

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

 

4.1.3        有序性

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

 

在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

 

另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

 

happens-before原则(先行发生原则):

程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。

锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作。

volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。

传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。

线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作。

线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。

线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。

对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。

8条原则摘自《深入理解Java虚拟机》,这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。

解释一下前4条规则:

对于程序次序规则来说,理解就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,因为虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。

第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果处于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。

第三条规则是一条比较重要的规则,也是后文将要重点讲述的内容。直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。

第四条规则实际上就是体现happens-before原则具备传递性。

 

posted on 2018-10-22 20:47  小付瓜  阅读(174)  评论(0)    收藏  举报