资讯专栏INFORMATION COLUMN

多线程的创建和启动

IT那活儿 / 780人阅读
多线程的创建和启动

点击上方“IT那活儿”公众号,关注后了解更多内容,不管IT什么活儿,干就完了!!!



多线程的创建和启动

1. 多线程的实现原理

  • Java语言的JVM允许程序运行多个线程,多线程可以通过Java中的java.lang.Thread类来体现。
  • Thread类的特性。
每个线程都是通过某个特定的Thread对象的run()方法来完成操作,经常把run()方法的主体作为线程体。
通过Thread方法的start()方法来启动这个线程,而非直接调用run()。

2. 多线程的创建

2.1 继承Thread类

  • 创建一个继承于Thread类的子类;
  • 重写Thread类的run()方法;
  • 创建Thread类的子类的对象;
  • 通过此对象调用start()来启动一个线程。
示例一--多线程执行同一段代码:
示例二--多线程执行多段代码:
示例三--创建Thread匿名子类:

2.2 实现Runnable接口

  • 创建一个实现Runnable接口的类;
  • 实现类去实现Runnable接口中的抽象方法:run();
  • 创建实现类的对象;
  • 将此对象作为参数传到Thread类的构造器中,创建Thread类的对象;
  • 通过Thread类的对象调用start()方法。
示例:

2.3 两种创建方式比较

  • Java中只允许单进程,以卖票程序TiketSales类来说,很有可能这个类本来就有父类,这样一来就不可以继承Thread类来完成多线程了,但是一个类可以实现多个接口,因此实现的方式没有类的单继承性的局限性,用实现Runnable接口的方式来完成多线程更加实用。
  • 实现Runnable接口的方式天然具有共享数据的特性(不用static变量)。
    因为继承Thread的实现方式,需要创建多个子类的对象来进行多线程,如果子类中有变量A,而不使用static约束变量的话,每个子类的对象都会有自己独立的变量A,只有static约束A后,子类的对象才共享变量A。
    而实现Runnable接口的方式,只需要创建一个实现类的对象,要将这个对象传入Thread类并创建多个Thread类的对象来完成多线程,而这多个Thread类对象实际上就是调用一个实现类对象而已。实现的方式更适合来处理多个线程有共享数据的情况。
  • 联系:Thread类中也实现了Runnable接口。
  • 相同点:两种方式都需要重写run()方法,线程的执行逻辑都在run()方法中。

2.4 通过实现Callable接口

与Runnable相比,Callable功能更加强大:
  • 相比run()方法,可以有返回值;

  • 方法可以抛出异常;
  • 支持泛型的返回值;
  • 需要借助FutureTask类,比如获取返回结果。
示例:

2.5 通过线程池创建

1)背景
经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
2)思路
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回线程池中。可以避免频繁的创建销毁,实现重复利用。

3)优点

  • 提高响应速度(减少了创建新线程的时间);
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建);
  • 便于管理。
示例:



Thread类的常用方法

  • start():启动当前线程, 调用当前线程的run()方法;
  • run() : 通常需要重写Thread类中的此方法, 将创建的线程要执行的操作声明在此方法中;
  • currentThread() : 静态方法, 返回当前代码执行的线程;
  • getName() : 获取当前线程的名字;
  • setName() : 设置当前线程的名字;
  • yield() : 释放当前CPU的执行权;
  • join() : 在线程a中调用线程b的join(), 此时线程a进入阻塞状态, 知道线程b完全执行完以后, 线程a才结束阻塞状态;
  • stop() : 已过时. 当执行此方法时,强制结束当前线程;
  • sleep(long militime) : 让线程睡眠指定的毫秒数,在指定时间内,线程是阻塞状态;
  • isAlive() :判断当前线程是否存活。



线程的调度

1. CPU的调度策略

  • 时间片:cpu正常情况下的调度策略。即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的。

    如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。而不会造成CPU资源浪费。

    在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。

    在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。

  • 抢占式:高优先级的线程抢占cpu。

2. Java的调度算法

  • 同优先级线程组成先进先出队列(先到先服务),使用时间片策略。
  • 堆高优先级,使用优先调度的抢占式策略。

1)线程的优先级等级(一共有10档)

  • MAX_PRIORITY:10;

  • MIN_PRIORITY:1;

  • NORM_PRIORITY:5 (默认优先级)。

2)获取和设置当前线程的优先级

  • getPriority()获取;
  • setPriority(int p)设置。
说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有高优先级的线程执行完成以后,低优先级的线程才执行。



线程的生命周期

1. JDK中用Thread State类定义了线程的几种状态

  • 新建:当一个Thread类或其子类的对象被声明并创建时,新的线程对象处于新建状态。
  • 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源。
  • 运行当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能。
  • 阻塞:在某种特殊情况下,被认为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态。
  • 死亡:线程完成了它的全部工作或线程被提前强制性的中止或出现异常倒置导致结束。
2. 线程的生命周期流程图



线程的同步

1. 多线程的安全性问题解析

1.1 线程的安全问题

  • 多个线程执行的不确定性引起执行结果的不稳定性;
  • 多个线程对账本的共享, 会造成操作的不完整性, 会破坏数据;
  • 多个线程访问共享的数据时可能存在安全性问题。
示例:
1.2 输出结果
1.3 错误分析
当票数为1的时候,三个线程中有线程被阻塞没有执行票数-1的操作,这是其它线程就会通过if语句的判断,这样一来就会造成多卖了一张票,出现错票的情况。
极端情况为,当票数为1时,三个线程同时判断通过,进入阻塞,然后多执行两侧卖票操作。
1.4 重票分析
如果t1在输出票号22和票数-1的操作之间被阻塞,这就导致这时候t1卖出了22号票,但是总票数没有减少。在t1被阻塞期间,如果t2运行到输出票号时,那么t2也会输出和t1相同的票号22。
通过以上两种情况可以看出,线程的安全性问题时因为多个线程正在执行代码的过程中,并且尚未完成的时候,其他线程参与进来执行代码所导致的。

2. 多线程安全性问题解决

2.1 原理
当一个线程在操作共享数据的时候,其他线程不能参与进来。知道这个线程操作完共享数据的时候,其他线程才可以操作。即使当这个线程操作共享数据的时候发生了阻塞,依旧无法改变这种情况。
在Java中,我们通过同步机制,来解决线程的安全问题。
2.2 解决方式
1)同步代码块

synchronized(同步监视器){需要被同步的代码块}

  • 优点:同步的方式,解决了线程安全的问题。
  • 缺点:操作同步代码时,只能有一个线程参与,与其他线程等待。相当于是一个单线程的过程,效率低。
2)同步方法
将所要同步的代码放到一个方法中,将方法声明为synchronized同步方法。之后可以在run()方法中调用同步方法。

要点:

  • 同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。
  • 非静态的同步方法,同步监视器是:this。
  • 静态的同步方法,同步监视器是:当前类本身。
3)Lock锁-JDK 5.0的新特性
JDK5.0之后,可以通过实例化ReentrantLock对象,在所需要同步的语句前,调用ReentrantLock对象的lock()方法,实现同步锁,在同步语句结束时,调用unlock()方法结束同步锁。
建议使用顺序:Lock->同步代码块(已经进入了方法体,分配了相应的资源)->同步方法(在方法体之外)。

2.3 线程同步的死锁问题

1)原理

  • 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了死锁。
  • 出现死锁后,并不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
  • 使用同步时应避免出现死锁。
2)Java中思索最简单的情况
一个线程T1持有锁L1并且申请获得锁L2,而另一个线程T2持有锁L2并且申请获得锁L1,因为默认的锁申请操作都是阻塞的,所以线程T1和T2永远被阻塞了。导致了死锁。这是最容易理解也是最简单的死锁的形式。但是实际环境中的死锁往往比这个复杂的多。可能会有多个线程形成了一个死锁的环路,比如:线程T1持有锁L1并且申请获得锁L2,而线程T2持有锁L2并且申请获得锁L3,而线程T3持有锁L3并且申请获得锁L1,这样导致了一个锁依赖的环路:T1依赖T2的锁L2,T2依赖T3的锁L3,而T3依赖T1的锁L1。从而导致了死锁。
从上面的例子分析出原因:
线程在获得一个锁L1的情况下再去申请另外一个锁L2,也就是锁L1想要包含了锁L2,也就是说在获得了锁L1,并且没有释放锁L1的情况下,又去申请获得锁L2,这个是产生死锁的最根本原因。另一个原因是默认的锁申请操作是阻塞的。

3)死锁的解决办法

  • 专门的算法、原则。
  • 尽量减少同步资源的定义。
  • 尽量避免嵌套同步。
示例:



线程通信

很多情况下,尽管我们创建了多个线程,也会出现几乎一个线程执行完所有操作的时候,这时候我们就需要让线程间相互交流。

1. 原理
当一个线程执行完成其所应该执行的代码后,手动让这个线程进入阻塞状态,这样一来,接下来的操作只能由其他线程来操作。
当其他线程执行的开始阶段,再手动让已经阻塞的线程停止阻塞,进入就绪状态,虽说这时候阻塞的线程停止了阻塞,但是由于现在正在运行的线程拿着同步锁,所以停止阻塞的线程也无法立马执行。
如此操作就可以完成线程间的通信。

2. 所用到的方法

  • wait():一旦执行此方法,当前线程就会进入阻塞,一旦执行wait()会释放同步监视器。
  • notify():一旦执行此方法,将会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先度最高的。
  • notifyAll() :一旦执行此方法,就会唤醒所有被wait的线程。
3. 说明
这三个方法必须在同步代码块或同步方法中使用。
三个方法的调用者必须是同步代码块或同步方法中的同步监视器。 
这三个方法并不时定义在Thread类中的,而是定义在Object类当中的。因为所有的对象都可以作为同步监视器,而这三个方法需要由同步监视器调用,所以任何一个类都要满足,那么只能写在Object类中。
4. sleep()和wait()的异同
相同点:两个方法一旦执行,都可以让线程进入阻塞状态。

不同点:

  • 两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()。
  • 调用要求不同:sleep()可以在任何需要的场景下调用。wait()必须在同步代码块中调用。
  • 关于是否释放同步监视器:如果两个方法都使用在同步代码块呵呵同步方法中,sleep不会释放锁,wait会释放锁。
示例:


END




本文作者:赵毕皓(上海新炬王翦团队)

本文来源:“IT那活儿”公众号

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/129430.html

相关文章

  • 什么是Java线程

    摘要:是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。通过查看,我们知道了有种方式实现多线程程序。使用的是抢占式调度模型演示如何设置和获取线程优先级返回线程对象的优先级更改线程的优先级线程默认优先级是。线程优先级的范围是。 第五阶段 多线程 前言: 一个场景:周末,带着并不存在的女票去看电影,无论是现场买票也好,又或是手机买票也好,上一秒还有位置,迟钝了一下以后,就显示该座位...

    高璐 评论0 收藏0
  • Java 线程编程基础——Thread 类

    摘要:程序执行时,至少会有一个线程在运行,这个运行的线程被称为主线程。程序的终止是指除守护线程以外的线程全部终止。多线程程序由多个线程组成的程序称为多线程程序。线程休眠期间可以被中断,中断将会抛出异常。 线程 我们在阅读程序时,表面看来是在跟踪程序的处理流程,实际上跟踪的是线程的执行。 单线程程序 在单线程程序中,在某个时间点执行的处理只有一个。 Java 程序执行时,至少会有一个线程在运行...

    zhoutk 评论0 收藏0
  • Java 线程

    摘要:当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程所有运行中的任务通常对应一个进程。线程也被称作轻量级进程,线程是进程的执行单元。在线程的生命周期中,它要经过新 线程概述 线程和进程 几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程 所有运...

    zorro 评论0 收藏0
  • 大话javascript 4期:事件循环(1)

    摘要:脚本执行,事件处理等。引擎线程,也称为内核,负责处理脚本程序,例如引擎。事件触发线程,用来控制事件循环可以理解为,引擎线程自己都忙不过来,需要浏览器另开线程协助。异步请求线程,也就是发出请求后,接收响应检测状态变更等都是这个线程管理的。 一、进程与线程 现代操作系统比如Mac OS X,UNIX,Linux,Windows等,都是支持多任务的操作系统。 什么叫多任务呢?简单地说,就是操...

    codergarden 评论0 收藏0
  • java 线程

    摘要:总结创建线程,方法运行线程。创建线程使用继承类实现创建线程文档该类必须重写方法。为新线程的入口点。中断线程它表示一个线程被中断,会抛出错误。 java多线程 关于内存 每个线程会有自己的线程栈,即,变量不能共享,只能传值拷贝每个线程new出的对象全都保存在堆中,全部共享 线程的生命周期 线程具有5种状态,即新建,就绪,运行,阻塞,死亡。新建,当new出来一个线程以后,jvm为其分配内存...

    IamDLY 评论0 收藏0

发表评论

0条评论

IT那活儿

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<