【有梦想的IT人】多线程的创建,使用,管理

最近看到很多关于线程,进程,管理和使用的问题,所以觉得有必要总(zhuang)结(bi)一下!

共同学习

一.线程(Thread)

1.线程,进程,以及并发和并行

什么是进程?
① 进程是系统进行资源分配的最小单位,也是系统进行资源调度的基本执行单元。
② 它是程序的一次执行过程。简单点的说“进程是正在运行的程序的实例”。
③ 每个进程运行在受保护的独立的内存空间内,进程和进程之间互不干扰。

什么是线程?

① 线程是CPU调度的最小单位,是进程中的一个实体在(也是进程调度的单位)。
② 每一个应用程序在启动之后,都会默认开启一条主线程,除了主线程,其他的线程都是子线程。
③ 一个线程可以创建和撤销另一个线程。
④同一个进程当中的线程共享该进程的资源

进程、线程区别是什么?

① 进程和线程都是程序运行的基本单元,一个程序至少有一个进程,一个进程至少有一个线程。
② 线程的划分尺度小于进程,使得多线程程序的并发性高。
③ 同一个进程中的多个线程之间可以并发执行,线程共享内存,从而极大地提高了程序的运行效率。

并发
简单的来说就是指一个时间段内,多个任务同时处于运行活跃状态,而不是在同一时刻运行多个任务。那问题来了,为什么我们感觉像是多个任务同时执行的呢?这是因为每个线程都被分了一个时间段,叫做时间片。因为CPU处理速度很快,所以看上去就是多个任务在同时执行,实际上就只有一个任务在执行。

并行
并行相对来说能简单些,指若干个程序段同时在系统中运行,这些程序的执行在时间上是重叠的,一个程序段的执行尚未结束,另一个程序段的执行已经开始,无论从微观还是宏观,程序都是一起执行的。

Paste_Image.png

你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以我认为它们最关键的点就是:是否是『同时』。

2.创建线程的两种方法

①继承Thread类
该子类必须重写 Thread 类的 run 方法,接下来new这个对象的实例。然后调用start()方法创建并启动线程。需要注意的是:程序是在调用start()方法之后,才开辟了一个子线程,并执行run方法。如果你直接调run方法(比如在main方法最下面写上m1.run();),它就是在主线程中执行的,而且是要比开辟三个线程要快的,这就说明新线程创建的过程不会阻塞主线程的后续执行。
注意:同一个Thread不能重复调用start方法,会出现java.lang.IllegalThreadStateException异常。
②实现Runnable接口
重写run()方法,然后调用new Thread(runnable)的方式创建一个线程,然后调用start()方法启动线程。
注意:Runnable接口只能通过Thread的静态方法Thread.currentThread()取得当前的Thread对象,再调用getName()方法,来取得当前线程的名字。
实现Runnable接口比继承Thread类所具有的优势:

  • 适合多个相同的程序代码的线程去处理同一个资源。
  • 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。

3.线程的生命周期

Paste_Image.png
  • 新建状态(New):新创建了一个线程对象。
  • 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运- **行线程池中,变得可运行,等待获取CPU的使用权。
  • 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
  • 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    ①等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
    ②同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池
    ③其他阻塞:运行的线程执行sleep()join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
  • 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

4.线程管理

1.线程调度
对于计算机的CPU(以单核为例),在任意时刻只能执行一条指令,每个线程只有获得CPU的使用权才能执行指令。当有多个处于可运行状态的线程在等待CPU,JVM的一项任务就是负责线程的调度。JVM按照特定机制为多个线程分配CPU的使用权过程就是线程调度。
线程调度的两种模型:分时调度模型和抢占式调度模型。
① 分时调度模型:是指让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的CPU的时间片,这个比较好理解。
② JVM采用抢占式调度模型:就是让线程抢夺CPU资源,运行顺序是不确定的,优先权高的线程,会有一定几率优先占用CPU。处于运行状态的线程会一直运行下去,直至它不得不放弃CPU。比如线程运行完毕、线程阻塞、运行被打断。

2.线程管理之设置线程优先级
Java线程优先级共有10个级别,优先级较高的线程会获得较多的运行机会,取值范围是从1到10。如果小于1或大于10,则JDK抛出异常IllegalArgumentException()。Thread类有以下三个静态常量:
① static final int MAX_PRIORITY :值为10,代表最高优先级。
② static final int MIN_PRIORITY :值为1,代表最低优先级。
③ static final int NORM_PRIORITY:值为5,代表默认优先级。

注意:
① 并不是说优先级较高的线程一定会在优先级较低的线程之前运行,优先级高这里是指获得较多的运行机会。
② 优先级高的线程会大部分先执行完,并不一定会全部执行完毕。
③ 子线程的优先级是跟父类优先级是一样的。

设置和获取优先级方法:
thread.setPriority(Thread.MIN_PRIORITY)和 thread.getPriority()

5.线程管理之守护线程

我们在程序中创建的线程默认都是用户线程(User Thread)。与用户线程对应则是守护线程(Daemon Thread),也可称之为后台线程,它的作用就是为其它线程提供服务的。守护线程使用的情况较少,举例来说:JVM的垃圾(GC)回收线程就是守护线程。

注意:
① 当所有的用户线程都结束退出的时候,守护线程也就没啥可服务的了,随着线程的结束而结束。如果JVM只剩下守护线程,虚拟机就会退出。
② 守护线程会随时中断,因此不要在如输入输出流,数据库连接等场合使用守护线程。
③ 守护线程并非是JVM内部可提供,我们自己可以根据需要来设定守护线程。可以通过isDaemon和setDaemon方法来判断和设置一个线程为守护线程。
④ 守护线程必须在start方法前设置,否则会抛出IllegalThreadStateException异常。
⑤ 一个守护线程创建的子线程依然是守护线程。

6.线程管理之常用方法

1.简单方法
public static int activeCount():返回当前线程的线程组中活动线程的数目。
public static Thread currentThread():返回对当前正在执行的线程对象的引用。
public long getId():返回该线程的标识符,线程 ID 是唯一的。
public final void setName(String name):改变线程名称。
public final String getName():返回该线程的名称。
public String toString():返回该线程的字符串表示形式,包括线程名称、优先级和线程组。
public void start():使该线程开始执行,Java 虚拟机调用该线程的 run 方法。

2.比较重要的方法:

  • sleep()方法 线程睡眠
    sleep方法作用就是让当前线程休眠,交出CPU,让CPU去执行其他的任务。当前线程是指this.currentThread()返回的线程。sleep方法使线程转到阻塞状态,当睡眠结束后,就转为可运行状态。

  • join()线程加入
    在当前线程中调用另一个线程的join方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为可运行状态。如果调用此方法时,另一个线程已经运行完毕,那就接着运行当前线程。

  • wait()线程等待
    ① wait:当达到某种状态的时候,wait方法让线程进入等待状态,让本线程休眠。线程自动释放其占有的对象锁,并等待notify。直到有其它线程调用对象的notify方法唤醒该线程,才能继续获取对象锁,继续运行。
    ② notify:唤醒一个正在等待当前对象锁的线程,并让它拿到对象锁。
    ③ notifyAll:唤醒所有正在等待前对象锁的线程。

  • 注意:
    ① 在调用这3个方法的时候,当前线程必须获得这个对象的锁,也就是说这三个方法必须在synchronized(Obj){...}内部。
    ② 在调用notify方法后,并不会马上释放对象锁,而是在synchronized(){}执行结束的时候,自动释放锁,JVM会随机唤醒一个正在等待当前对象锁的线程,让他获得对象锁,唤醒线程。

3.wait()与sleep()方法的区别
相同点: 二者都可以让线程处于冻结状态。

不同点:首先应该明确sleep方法是Thread类中定义的方法,而wait方法是Object类中定义的方法。

①sleep方法必须人为地为其指定时间。wait方法既可以指定时间,也可以不指定时间。
②sleep方法时间到,线程处于临时阻塞状态或者运行状态。wait方法如果没有被设置时间,就必须要通过
notify或者notifyAll来唤醒。
③sleep方法不一定非要定义在同步中。wait方法必须定义在同步中。
④当二者都定义在同步中时,线程执行到sleep,不会释放锁。线程执行到wait,会释放锁。

7.synchronized关键字

  • 当多个线程访问同一对象的时候,只能有一个线程取得对象的锁,多个对象需要多个对象的锁。
  • 哪个线程执行了带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁,其他的线程要访问这个对象锁内的内容,都只能等待这个锁被释放后,再去抢占资源获得对象的锁。
  • synchronized修饰非static的方法时,锁的就是对象本身,也就是this。
  • synchronized修饰static的方法时,方法中无法使用this,所以它锁的不是this,而是这个类。所以,static synchronized方法也相当于全局锁。
  • 使用synchronized关键字,应尽量缩小代码块的范围,最好能在代码块上加同步,而不是在整个方法上加同步。因为你锁的范围大的话,时间又长,别的线程就不会获得相应的资源。
  • A线程持有对象的锁,B线程可以以异步方式调用对象中的非synchronized同步的方法。
    参考:http://www.jianshu.com/p/2080d824c7a8

二线程池(ThreadPoolExecutor)

线程池:Java中开辟出了一种管理线程的概念,这个概念叫做线程池.

线程池的类结构

1.线程池的优点:

  • 重用线程池中的线程,避免因为线程的创建和销毁带来的性能消耗
  • 能有效的控制线程的最大并发数,避免大量的线程之间因抢占系统资源而导致的阻塞现象
  • 能够对线程进行简单的管理,并提供定时执行以及指定间隔循环执行等功能

2.ThreadPoolExecutor的构造方法有四个,其实现如下:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                          TimeUnit unit, BlockingQueue<Runnable> workQueue, 
                          ThreadFactory threadFactory) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
            threadFactory, defaultHandler);
}

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                          TimeUnit unit, BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
            Executors.defaultThreadFactory(), handler);
}

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, 
                          TimeUnit unit,BlockingQueue<Runnable> workQueue, 
                          ThreadFactory threadFactory, RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

①构造方法的参数

  • corePoolSize
    程池中的核心线程数,也就是是线程池中的最小线程数;
    核心线程在allowCoreThreadTimeout被设置为true时会超时退出,默认情况下不会退出;
  • maximumPoolSize
    最大线程池大小,当活动线程数达到这个值,后续任务会被阻塞
  • keepAliveTime
    线程池中超过corePoolSize数目的非核心线程最大存活时间;闲置时的超时时长,超过这个值后,闲置线程就会被回收
  • unit
    keepAliveTime 参数的时间单位。这是一个枚举,详情请参考TimeUnit
  • workQueue
    执行前用于保持任务的队列,也就是线程池的缓存队列。此队列仅保持由 execute 方法提交的 Runnable 任务
    关于三种提交策略这篇文章不错
  • threadFactory
    线程工厂,为线程池提供创建新线程的功能,它是一个接口,只有一个方法:Thread newThread(Runnable r)
  • RejectedExecutionHandler
    线程池对拒绝任务的处理策略。一般是队列已满或者无法成功执行任务,这时ThreadPoolExecutor会调用handler的rejectedExecution方法来通知调用者
    ThreadPoolExecutor默认有四个拒绝策略:
      1、ThreadPoolExecutor.AbortPolicy()   直接抛出异常RejectedExecutionException
      2、ThreadPoolExecutor.CallerRunsPolicy()    直接调用run方法并且阻塞执行
      3、ThreadPoolExecutor.DiscardPolicy()   直接丢弃后来的任务
      4、ThreadPoolExecutor.DiscardOldestPolicy()  丢弃在队列中队首的任务
    

也可以自己继承RejectedExecutionHandler来写拒绝策略.
③ThreadPoolExecutor的执行过程:
一个任务通过 execute(Runnable)方法被添加到线程池,任务就是一个 Runnable类型的对象,任务的执行方法就是Runnable类型对象的run()方法。

  • a当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程
  • b当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行
  • c当提交任务数超过【maximumPoolSize+阻塞队列大小】时,新提交任务由RejectedExecutionHandler处理 (关于这里,网上90%以上的人说当任务数>=maximumPoolSize时就会被拒绝,我不知道依据在哪里,也不知道代码验证过没,经过我的验证这种说法是不成立的,具体的看下边日志分析)
  • d当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
  • e当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭

3.官方定义的四种线程池
其实,本应该先说官方定义的这四种线程池,然后再说自定义线程池,但是考虑到里边的一些配置参数,所以本帖先利用自定义线程池把各个配置参数理一下,然后再讲官方定义的四种线程池,这样也便于理解官方定义的这四种线程池
这四种线程池都是通过Executors的工厂方法来实现

①.FixedThreadPool

他是一种数量固定的线程池,且任务队列也没有大小限制;
它只有核心线程,且这里的核心线程也没有超时限制,所以即使线程处于空闲状态也不会被回收,除非线程池关闭;
当所有的任务处于活动状态,新任务都处于等待状态,知道所有线程空闲出来;
因为它不会被回收,所以它能更快的响应;
源码:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

实现:

ExecutorService service = Executors.newFixedThreadPool(3);
service.execute(new WorkerThread("thread-" + num));
②.CachedThreadPool

无界线程池,可以进行自动线程回收
他是一种线程数量不固定的线程池;
它只有非核心线程,且最大线程数为Integer.MAX_VALUE,也就是说线程数可以任意大;
当池中的线程都处于活动状态时,会创建新的线程来处理任务,否则会利用空闲线程来处理任务;所以,任何添加进来的任务都会被立即执行;
池中的空闲线程都有超时限制,为60s,超过这个限制就会被回收,当池中的所有线程都处于闲置状态时,都会因超时而被回收,这个时候,她几乎不占用任何系统资源;
适合做大量的耗时较少的任务;
源码:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

实现:

ExecutorService service = Executors.newCachedThreadPool();
service.execute(new WorkerThread("thread-"));
③.SingleThreadExecutor

只有一个核心线程,所有任务都在同一线程中按序执行,这样也就不需要处理线程同步的问题;
源码:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

实现:

ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new WorkerThread("thread-"));
④.ScheduledThreadPool

它的核心线程数量是固定的,而非核心线程是没有限制的,且非核心线程空闲时会被回收;
适合执行定时任务和具有固定周期的任务
源码:

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
    return new DelegatedScheduledExecutorService
        (new ScheduledThreadPoolExecutor(1));
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}

实现:

ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
或
ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor();

threadPool.schedule(runnable, 20, TimeUnit.SECONDS);// 20秒后执行任务
或
threadPool.scheduleAtFixedRate(runnable,10,20,TimeUnit.SECONDS);//延迟10s,每20s执行一次任务

参考:https://www.jianshu.com/p/3da543063b8c

Our youth never dies,just fades away.

嗨~我是夏尼采,一个有梦想的IT男
每周输出1篇有用的文章。
如果文章对您有帮助,希望能点个赞或者关注我。
您的关注和点赞是对我最大的鼓励,感谢您的阅读

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容

  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,377评论 1 15
  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 2,921评论 1 18
  • 该文章转自:http://blog.csdn.net/evankaka/article/details/44153...
    加来依蓝阅读 7,257评论 3 87
  • 写在前面的话: 这篇博客是我从这里“转载”的,为什么转载两个字加“”呢?因为这绝不是简单的复制粘贴,我花了五六个小...
    SmartSean阅读 4,650评论 12 45
  • 又来到了一个老生常谈的问题,应用层软件开发的程序员要不要了解和深入学习操作系统呢? 今天就这个问题开始,来谈谈操...
    tangsl阅读 4,004评论 0 23