多线程2

目录

1      背景... 1

1.1.1       多线程vs多进程... 1

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

2.1      继承Thread.. 2

2.2      实现Runnable. 2

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

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

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

1      线程状态转换/方法... 4

1.1      状态转换: 4

1.2      常用函数... 5

1.3      多线程练习... 6

1.3.1        三个线程顺序打印ABC.. 6

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

 

 

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.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、largestPoolSize三个变量。

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

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

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

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

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

 

 

2.3.2Executor创建的三种类型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

3.3 多线程练习

1.3.1 三个线程顺序打印ABC

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

4.安全性问题

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

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

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

 

线程stack,堆。

 

4.1竞态条件(并发编程中,由于不恰当执行时序出现不正确的结果的情况)

 

 延迟初始化

 读取-修改-写入操作

 

如何避免静态条件

   在某个线程修改变量时,通过某种方式防止其他线程使用此变量,确保其他线程只能在修改操作完成前或者完成后读取和修改状态,而不是在修改状态过程中。

 

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

 AtomicLong….

 

好处:简单方便  弊端:涉及多个变量间的操作,变量状态间有关系时依然有问题

 

 

加锁机制

 

Synchronized

内置锁(内置):可以对方法加,对代码块加。进入同步代码块自动获得,退出同步代码块自动释放,同一时刻只有一个线程可以访问锁保护的变量。

1.是互斥锁,线程A获得锁,线程B无法获取,进入阻塞

2.是可以重入的,子类改写父类synchronized方法,调用super.XXX()。

锁使用不当很容易引起死锁等活跃性问题,同时也导致严重的性能问题。(减小锁的粒度,对于耗时长的计算尽量不要使用锁)

 

锁不仅仅是互斥性(防止某个线程正在使用对象状态其他线程修改该状态),更是可见性(线程修改了对象状态后,其他线程能看到,get方法也要加)

重排序问题: http://blog.csdn.net/u012312373/article/details/44983523

 

posted on 2017-12-27 17:55  小付瓜  阅读(81)  评论(0)    收藏  举报