资讯专栏INFORMATION COLUMN

@Java | Thread & synchronized - [ 多线程 基本使用]

zhunjiee / 471人阅读

摘要:线程线程是进程中的一个实体,作为系统调度和分派的基本单位。下的线程看作轻量级进程。因此,使用的目的是让相同优先级的线程之间能适当的轮转执行。需要注意的是,是线程自己从内部抛出的,并不是方法抛出的。

本文及后续相关文章梳理一下关于多线程和同步锁的知识,平时只是应用层面的了解,由于最近面试总是问一些原理性的知识,虽说比较反感这种理论派,但是为了生计也必须掌握一番。(PS:并不是说掌握原理不好,但是封装就是为了更好的应用,个人感觉没必要为了学习而学习,比较倾向于行动派,能将原理应用到实际才算参透,本文也仅仅是背书而已)
知识点

进程:进程就是一段程序的执行过程,指在系统中能独立运行并作为资源分配的基本单位,它是由一组机器指令、数据和堆栈等组成的,是一个能独立运行的活动实体。

线程:线程是进程中的一个实体,作为系统调度和分派的基本单位。Linux下的线程看作轻量级进程。

线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止
多进程是指操作系统能同时运行多个任务(程序)
多线程是指在同一程序中有多个顺序流在执行
多线程使用说明 如何创建线程

实现Runnable接口

继承Thread类

通过Callable和Future创建线程

1. 通过实现Runnable接口创建并运行线程

- 实现Runnable接口

Public class A implements Runnable {
    public void run () {    // 必须实现run方法
        // 线程执行的业务代码
    }
}
上面实现了Runnable接口并通过run方法包装了需要通过线程执行的代码

- 运行线程
通过实现了Runnable接口的类的实例创建线程对象(Thread)来运行线程,常用的Thread构造方法:

Thread(Runnable threadOb,String threadName);
其中threadOb 是一个实现Runnable接口的类的实例threadName指定线程的名字

调用线程类中的start()方法运行线程 new Thread(threadOb,threadName).start();(可创建多个线程对象运行同一个Runnable接口实例的run方法,实现资源共享

PS:start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),等待CPU分配调度执行
2. 继承Thread类创建并运行线程

- 继承Thread类

public class A extends Thread {
    @Override
    public void run() {    // 重写run方法
        // 线程执行的业务代码
    }
}
上面继承了Runnable接口并通过重写run方法包装了需要通过线程执行的代码,通过源码可以看到Thread类也是实现了Runnable接口的,所以本质上和上一种方式无太大区别,不同的是Thread类不适合共享资源线程实现

- 运行线程

同样是调用线程类中的start()方法运行线程,此时线程类为继承Thread的类

3. 通过Callable和Future创建并运行线程

- 实现Callable接口

public class A implements Callable {
    @Override  
    public T call() throws Exception  // 实现call()方法
    {  
        //  线程执行的业务代码
    }  
}
创建Callable接口的实现类(通过泛型制定线程执行结束后的返回值类型),并实现call()方法,该call()方法将作为线程执行体,并且有返回值(返回值类型为Callable接口泛型制定的类型)

- 使用FutureTask类来包装Callable对象

FutureTask ft = new FutureTask<>(callableObj);
其中callableObjCallable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象call()方法的返回值

- 运行线程

通过FutureTask类的实例创建线程对象(Thread)来运行线程,此时应用的Thread`构造方法:

Thread(FutureTask futureObj,String threadName);
其中futureObj 是一个FutureTask 类的实例threadName指定线程的名字

调用线程类中的start()方法运行线程 new Thread(threadOb,threadName).start();
调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

使用须知

多线程执行的时候是无序的,即谁先获取到CPU资源就可以先执行,随机性比较大

如果start()方法重复调用,会出现java.lang.IllegalThreadStateException异常

直接继承Thread类和实现接口方式创建线程的区别

直接继承Thread类方便快捷,适合相对单一无序的多线程执行

实现接口方式适合多个相同的程序代码的线程去处理同一个资源

实现接口方式可以避免java中的单继承的限制

实现接口方式增加程序的健壮性,代码可以被多个线程共享,代码和数据独立

线程池只能放入实现Runablecallable类线程,不能直接放入继承Thread的类

Java程序运行首先会启动一个JVM进程,然后至少启动两个线程,即main线程垃圾收集线程

Thread常用方法说明

sleep(long millis): 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),可用TimeUnit.MILLISECONDS.sleep方法替换

join(): 等待调用join()的线程终止

使用方式

`Thread t = new AThread(); t.start(); t.join();`
join()的作用是将线程加入到当前线程中,只有执行完join()调用线程才能执行后面的代码

使用场景
正常情况下主线程不依赖子线程执行完成而结束,当主线程需要在子线程完成之后再结束时,可使用此方法

yield(): 暂停当前正在执行的线程对象,并执行其他线程

使用说明
yield()只是将线程从运行状态转到可运行状态(start()方法执行后的状态),不会导致线程转到等待/睡眠/阻塞状态

使用场景
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中

PS:yield()方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得CPU占有权。在一个运行系统中,如果较高优先级的线程没有调用sleep方法,又没有受到IO 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行

setPriority(): 更改线程的优先级,优先级高的线程会获得较多的运行机会
优先级静态常量MIN_PRIORITY=1,NORM_PRIORITY=5,MAX_PRIORITY=10

使用方式

Thread t1 = new Thread("t1");
Thread t2 = new Thread("t2");
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);

使用说明
Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。
线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式

interrupt(): 将线程对象的中断标识设成true

使用说明

中断只是一种协作机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。若要中断一个线程,你需要手动调用该线程的interrupted方法,该方法也仅仅是将线程对象的中断标识设成true;接着你需要自己写代码不断地检测当前线程的标识位;如果为true,表示别的线程要求这条线程中断,此时究竟该做什么需要你自己写代码实现

每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断

通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用

使用方式

在调用阻塞方法时正确处理InterruptedException异常

设置中断监听(另一种方式)

Thread t1 = new Thread( new Runnable(){
       public void run(){
           // 若未发生中断,就正常执行任务
           while(!Thread.currentThread.isInterrupted()){
               // 正常任务代码……
           }
   
           // 中断的处理代码……
           doSomething();
       }
   } ).start();

触发中断

t1.interrupt();

interrupt方法使用参考链接
大闲人柴毛毛

wait(): 主动释放对象锁,同时本线程休眠,直到有其它线程调用对象的notify()唤醒该线程,重新获取对象锁并执行(wait()方法属于Object中的方法,并不属于Thread类)

notify(): 唤醒调用notify()对象的线程,notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行

Thread几个方法比较

1.sleep()yield()的区别

sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行

sleep方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield方法使当前线程让出CPU占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把CPU的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为"退让",它把运行机会让给了同等优先级的其他线程

sleep方法允许较低优先级的线程获得运行机会,但yield()方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得CPU占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep方法,又没有受到IO阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行

2.wait()sleep()区别

共同点:

他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回

wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态,从而使线程立刻抛出InterruptedException

如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。

需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到wait()/sleep()/join()后,就会立刻抛出InterruptedException

不同点:

sleep(),yield()等是Thread类的方法

wait()和notify()等是Object的方法

每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法

wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用

简单来说: 

sleep()睡眠时,保持对象锁,仍然占有该锁;

而wait()睡眠时,释放对象锁。

wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)

sleep()方法

sleep()使当前线程进入停滞状态(阻塞当前线程),让出CPU的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;

sleep()是Thread类的Static的方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并没有被释放,其他线程无法访问这个对象(即使休眠也持有对象锁

在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级

wait()方法

wait()方法是Object类里的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);其他线程可以访问;

wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。

wait()必须放在synchronized block中,否则会在程序runtime时扔出java.lang.IllegalMonitorStateException异常。

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

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

相关文章

  • @Java | Thread &amp; synchronized - [ 线程同步锁 基本使用]

    摘要:线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他非同步方法对于静态同步方法,锁是针对这个类的,锁对象是该类的对象。 对实现了Runnable或者Callable接口类,可以通过多线程执行同一实例的run或call方法,那么对于同一实例中的局部变量(非方法变量)就会有多个线程进行更改或读取...

    Michael_Lin 评论0 收藏0
  • @Java | Thread &amp; synchronized - [ 线程 理论知识]

    摘要:死亡线程方法执行结束,或者因异常退出了方法,则该线程结束生命周期。死亡的线程不可再次复生。直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。枚举程序中的线程。强迫一个线程等待。通知一个线程继续运行。 一. 线程状态转换图 showImg(https://segmentfault.com/img/bV38ef?w=968&h=680); 线程间的状态转换说明: 新建(new)...

    harryhappy 评论0 收藏0
  • Java线程&amp;高并发

    摘要:线程启动规则对象的方法先行发生于此线程的每一个动作。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。通过获取到数据,放入当前线程处理完之后将当前线程中的信息移除。主线程必须在启动其他线程后立即调用方法。 一、线程安全性 定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式,或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行...

    SQC 评论0 收藏0
  • 深入理解 Java 线程系列(1)——一个简单需求的并行改造 &amp; Java线程的通信问题

    摘要:所以接下来,我们需要简单的介绍下多线程中的并发通信模型。比如中,以及各种锁机制,均为了解决线程间公共状态的串行访问问题。 并发的学习门槛较高,相较单纯的罗列并发编程 API 的枯燥被动学习方式,本系列文章试图用一个简单的栗子,一步步结合并发编程的相关知识分析旧有实现的不足,再实现逻辑进行分析改进,试图展示例子背后的并发工具与实现原理。 本文是本系列的第一篇文章,提出了一个简单的业务场景...

    ruicbAndroid 评论0 收藏0
  • Java线程基础(一)——线程与锁

    摘要:一线程的基本概念单线程简单的说,单线程就是进程中只有一个线程。多线程由一个以上线程组成的程序称为多线程程序。当线程调用完方法进入后会自动释放锁,线程获得锁。 一、线程的基本概念 1.1 单线程 简单的说,单线程就是进程中只有一个线程。单线程在程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行。 Java示例: public class SingleThrea...

    WelliJhon 评论0 收藏0

发表评论

0条评论

zhunjiee

|高级讲师

TA的文章

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