0


这是一个不完整的详细Java多线程,但对于初学者足够了,相信我,你会爱上她的

零基础学习之Java多线程

概述

要深入学习多线程,首先我们需要找到什么是线程,根据百度百科的定义:线程(thread)是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。。我们了解到线程是进程的实际运行单位,那问题来了什么是进程呢?根据百度百科定义:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,进程是程序的实体。这个就比较好理解了,进程不就是我们运行的程序吗?那这样来看,线程就是某个程序的一次实际的运行(一个程序可以打开多次,一个进程可以有多个线程组成)。

那这里的Java多线程指的就是Java开发中,程序启动多个线程实现并发执行,提高CPU的利用率。

并发:指两个或多个事件可以在同一个时间段内发生。也就是说在同一个时刻只能执行一条指令,但多个指令可以被快速轮换执行,使得在宏观上具有多个进程同时执行的效果

线程的创建

创建线程是使用多线程的第一步。首先先介绍了Java创建线程的方式,创建了线程才会有多个线程的情况。Java创建多线程有四种方式:

  • 继承Thread类
  • 实现Runnable接口
  • 实现实现Callable(本文未涉及)
  • 使用线程池方式(本文未涉及)

本篇文章主要介绍通过继承Thread类和实现Runnable接口这两种方式来创建线程。 具体如下:

继承Thread类

在Java中,使用Thread类来表示线程,所有的线程都是Thread类的对象或者其子类的实例。(Thread类Java.lang包下的类)

通过继承方式实现的线程,会有Java单继承的缺点

创建线程的步骤

  • 定义一个类,让其继承Thread类
  • 在该类中重写Thread类的run()方法、
  • 创建该类的实例,即创建了线程对象

其中:run()方法的方法体就是线程的执行逻辑,也称为线程执行体。

线程的使用步骤

  • 创建线程
  • 通过线程对象调用start()方法。

注:线程不用手动结束,执行完线程其自动结束

代码示例

/*
    继承Thread创建线程
 */publicclassDemo1{publicstaticvoidmain(String[] args){//创建自定义线程对象
        MyThread myThread =newMyThread();//调用start方法,开启线程
        myThread.start();}}//继承Thread方法,自定义线程classMyThreadextendsThread{/*
     * 重写run方法,完成该线程执行的逻辑
     */@Overridepublicvoidrun(){
        System.out.println("继承Thread方式创建线程");}}

实现Runnable接口

上面说到通过继承Thread类的方式实现线程有Java单继承的缺点,针对单继承这样一个问题,Java类是通过实现接口来解决的。那么创建线程是不是也可以通过接口呢?答案是肯定的。在Java中,可以通过实现Runnable接口的方式来创建线程。

注:在Thread类的源码中可以看出,其也是实现了Runnable接口

创建线程的步骤

  • 定义一个类,让其实现Runnable接口
  • 在该类中重写Runnable接口中的run()方法
  • 创建该类的对象(线程的任务对象)
  • 通过Thread类的有参构造方法创建线程对象(真正的线程对象)

线程的使用步骤

  • 创建线程
  • 通过线程对象调用start()方法。

代码示例

/*
    实现Runnable类创建线程
 */publicclassDemo2{publicstaticvoidmain(String[] args){//创建自定义类对象  线程任务对象
        MyThread1 runnable =newMyThread1();//创建线程对象
        Thread thread =newThread(runnable,"xiaocheng");//调用start方法,开启线程
        thread.start();}}//实现Runable类,创建线程classMyThread1implementsRunnable{/*
     * 重写run方法,完成该线程执行的逻辑
     */@Overridepublicvoidrun(){
        System.out.println("实现Runable类,创建线程");}}

使用匿名内部类创建线程

既然创建多线程是通过继承Thread类,然后创建类的实例的方式实现的。那如果是本身这个线程只用一次,在其他地方没有调用的情况,是不是可以通过匿名内部类来创建线程呢? Why not?
下面用一个例子来说明使用匿名内部类如何来创建线程:

代码示例
/*
    匿名内部类创建线程
 */publicclassDemo3{publicstaticvoidmain(String[] args){//匿名内部类,一个参数newThread(newRunnable(){//依然需要重写run方法,实现线程的执行逻辑@Overridepublicvoidrun(){
                System.out.println("匿名内部类创建线程1");}}){}.start();//匿名内部类,两个参数newThread(newRunnable(){//依然需要重写run方法,实现线程的执行逻辑@Overridepublicvoidrun(){//调用currentThread().getName()方法,获取当前线程的名字
                System.out.println(Thread.currentThread().getName());}},"匿名内部类创建线程2 ").start();}}

匿名内部类不熟的话,可以点击上面的链接看下我写过的博客

Thread类常用的方法

从上面的例子我们可以看出来,线程对象实际都是Thread实例。那这样来说,如果我们使用线程的话,除了我们自定以类中的方法外,Thread类中的方法也是必须要知道的。那下面就介绍下Thread类中常用的方法:

  • public void run():实现多线程的执行逻辑(必不可少的方法)
  • public void start() :线程开始执行的方法 (线程启动方法)
  • public static void sleep(long millis) :暂停当前线程的执行(参数为毫秒数)
  • public String getName() :获取当前线程名字
  • public static Thread currentThread() :返回对当前正在执行的线程对象的引用(可以和其他的一起用)
  • public final boolean isAlive():判断线程的状态,是否为活动状态(非终止)
  • public final int getPriority() :获取线程的优先级
  • public final void setPriority(int newPriority) :改变线程的优先级
    • public final void stop():使线程停止执行
  • public final void setDaemon(boolean on):指定线程设置为守护线程。(必须在线程启动之前设置,否则会报IllegalThreadStateException异常)
  • public final boolean isDaemon():判断当前线程是否为守护线程

线程生命周期

前面说到创建线程和启动线程,还有常用方法里说到的线程的停止,这些都是线程在整个执行过程中可能出现的状态。总的来说,线程在其生命周期内(执行过程中)一共有五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。
线程生命周期中的状态转换图如下:

在这里插入图片描述

其中,各个状态说明如下:

新建(New)

通过上述方法创建线程,不调用任何方法即为新建一个线程,此时该线程只有JVM为其分配的内存,并初始化了实例变量的值,没有执行线程任何操作。

就绪(Runnable)

线程的就绪状态是在其调用start()方法之后,此时线程启动,即变成了就绪状态,JVM此时会为其创建方法调用栈和程序计数器。(注意这个时候只是启动线程,没有线程的执行行为)。

注意:程序只能对新建状态的线程调用start(),并且只能调用一次,如果对非新建状态的线程,如已启动的线程或已死亡的线程调用start()都会报错IllegalThreadStateException异常。

运行(Running)

处于就绪状态的线程获得CPU资源,那该线程就会调用run()方法,开启线程的执行逻辑,此时线程处于运行状态。(所谓多线程,指的是系统中有多个CPU资源,多个线程获得CPU资源后处于运行状态)。

阻塞(Blocked)

系统中的线程由于实际情况并不总是能够顺利执行完毕,但如果稍微有情况不能执行就杀死线程会造成很大的浪费,这时候就会出现线程阻塞的概念,阻塞的线程还可以通过一定的条件转化为就绪状态,继续等待下次的运行。

线程遇到如下情况,会进入阻塞状态:

  • 线程调用了sleep()方法
  • 线程试图获取一个同步监视器,但该同步监视器正被其他线程持有;
  • 线程执行过程中,同步监视器调用了wait(),让它等待某个通知(notify / notifyAll);
  • 线程执行过程中,同步监视器调用了wait(time)
  • 线程执行过程中,遇到了其他线程对象的加塞(join);
  • 线程被调用suspend方法被挂起(已过时,因为容易发生死锁);

阻塞状态的线程,可以通过下面的形式重新转换到就绪状态,等待下次的运行:

  • 线程的sleep()时间到;
  • 线程成功获得了同步监视器;
  • 线程等到了通知(notify / notifyAll);
  • 线程wait的时间到了
  • 加塞的线程结束了;
  • 被挂起的线程又被调用了resume恢复方法(已过时,因为容易发生死锁);

死亡(Dead)

线程总归要结束的,不管是被动的还是主动的(执行完了),

  • run()方法执行完成;
  • 线程执行过程中抛出了一个未捕获的异常(Exception)或错误(Error)
  • 直接调用该线程的stop()来结束该线程(已过时,因为容易发生死锁)

线程安全

这里说的线程安全指的是多线程才会有的安全性问题。这是因为当系统同时运行多个线程的时候,会出现多个线程同时访问同一个资源的情况,如果仅仅是涉及到读操作的话,不会出现问题。但是涉及到写(即修改)就会出安全问题。

举个例子解释一下安全问题:

比如在我们在线上购票的过程中,是很多人可以同时购买的(多线程),我们首先要看看有没有票(涉及到读操作),然后如果有票的话,就可以买票了(涉及到写操作)。理论上,每次有人购票的时候,系统就会让票的数量减少一个,但是如果多人同时购票的话(多个线程访问同一个资源),会出现多人同时购票,而系统只让票的数量减一,这些人就会是重票。

那出现上述的肯定是不行的,下面介绍三种方法来解决线程安全问题

  • 同步代码块
  • 同步方法
  • 锁机制

下面就对这三种方法进行分别介绍

同步代码块

Java中同步代码块是通过添加 synchronized 关键字实现的,将synchronized 关键字用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问(同时只能由一个线程访问该该区块中的资源)。

语法格式

synchronized(同步锁){//需要互斥的代码}

这里的同步锁只是一个概念,指的是为你想实现互斥访问资源的对象上标记了一个锁。

想对谁互斥访问,同步锁里面就写谁,锁对象可以是任意类型

代码示例

/*
    解决线程安全:同步代码块
 */publicclassDemo7{publicstaticvoidmain(String[] args){

        Tacket1 tacket1 =newTacket1();//通过匿名内部类创建线程newThread(newRunnable(){//重写run方法实现线程执行逻辑@Overridepublicvoidrun(){//售票员1卖票(线程1)for(int i =0; i <40; i++){
                    tacket1.sellTacket();}}},"售票员1").start();newThread(newRunnable(){//重写run方法实现线程执行逻辑@Overridepublicvoidrun(){for(int i =0; i <40; i++){//售票员2卖票(线程2)
                    tacket1.sellTacket();}}},"售票员2").start();}}classTacket1{privateint number =30;//票有30张publicvoidsellTacket(){//票数大于0才能卖synchronized(this){//this作为锁,是因为对于这几个线程,Ticket的this是同一个if(number >0){try{
                    Thread.sleep(10);//等10毫秒,效果好点}catch(InterruptedException e){
                    e.printStackTrace();}//票数-1
                System.out.println(Thread.currentThread().getName()+"卖票,剩余票:"+number--);}}}}

同步方法

在Java中,同步方法使用的也是synchronized 关键字,不同于同步代码块的是,同步方法直接将synchronized 关键字修饰方法。这使得当线程执行该方法时候,其他线程就不能执行该方法,也实现了互斥访问资源(方法里有访问资源的操作)。

语法格式

publicsynchronizedvoidmethodname(){//可能会产生线程安全问题的代码}

同步方法的锁对象:

  • 静态方法:当前类的Class对象
  • 非静态方法:this

代码示例

/*
    解决线程安全:同步方法
 */publicclassDemo8{publicstaticvoidmain(String[] args){

        Tacket2 tacket2 =newTacket2();//通过匿名内部类创建线程newThread(newRunnable(){//重写run方法实现线程执行逻辑@Overridepublicvoidrun(){for(int i =0; i <40; i++){//售票员1卖票(线程2)
                    tacket2.sellTacket();}}},"售票员1").start();newThread(newRunnable(){@Overridepublicvoidrun(){for(int i =0; i <40; i++){//售票员2卖票(线程2)
                    tacket2.sellTacket();}}},"售票员2").start();}}classTacket2{privateint number =30;//票有30张publicsynchronizedvoidsellTacket(){//同步方法if(number >0){//票数大于0才能卖try{
                    Thread.sleep(10);//等10毫秒,效果好点}catch(InterruptedException e){
                    e.printStackTrace();}//票数-1
                System.out.println(Thread.currentThread().getName()+"卖票,剩余票:"+number--);}}}

锁机制

任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,这种锁定保证了线程安全。

释放锁的操作

可以锁定,但又不能一直锁定,所以什么时候释放锁也是一个关键点。下面介绍释放锁的操作:

  • 当前线程的同步方法、同步代码块执行结束。
  • 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致当前线程异常结束。
  • 当前线程在同步代码块、同步方法中执行了锁对象的wait()方法,当前线程被挂起,并释放锁。

不会释放锁的操作

有的方法执行了,当前线程会暂停,但此时该线程并不会释放锁,仍然保持锁定状态。具体方法如下:

  • 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该该线程挂起,

死锁

就像不是所有的线程都能完成一样,有些锁可能就不能释放了,变成了死锁。当不同的线程分别锁住对方需要的同步监视器对象不释放,都在等待对方先放弃时就形成了线程的死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。


本文转载自: https://blog.csdn.net/weixin_44480968/article/details/122398584
版权归原作者 Faith_xzc 所有, 如有侵权,请联系我们删除。

“这是一个不完整的详细Java多线程,但对于初学者足够了,相信我,你会爱上她的”的评论:

还没有评论