大聪明教你学Java | 带你学习Java多线程

4,519 阅读11分钟

前言

这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战

相信每一位程序猿对“多线程”这个概念应该都不陌生,无论是在开发还是面试的时候,都会遇到多线程的问题。不过,一定有很多小伙伴才刚刚接触到多线程,那么在此就由小弟为各位小伙伴细细说说什么是多线程。

在开始之前,先简单介绍一下什么是线程~

Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

多线程是实现并发机制的一种有效手段。进程和线程一样,都是实现并发的一个基本单位。线程是比进程更小的执行单位,线程是进程的基础之上进行进一步的划分。==所谓多线程是指一个进程在执行过程中可以产生多个更小的程序单元,这些更小的单元称为线程,这些线程可以同时存在,同时运行,一个进程可能包含多个同时执行的线程==。

多线程实现方式

在 Java 中实现多线程有两种手段,一种是继承 Thread 类,另一种就是实现 Runnable 接口。下面我们就分别来介绍这两种方式的使用。

继承Thread类

通过继承Thread类实现多线程一共分四步(比把大象装冰箱多了一步~)

  1. 创建一个继承Thread类的子类
  2. 重写Thread类的run方法
  3. 创建Thread子类的对象
  4. 通过此对象调用start方法

可能会有小伙伴不太清楚,run方法和start方法有什么取别呢?听小弟娓娓道来。 start方法的作用是启动当前线程或者是调用当前线程的重写的run方法。(开启新线程) 调用start方法以后,一条路径代表一个线程,同时执行两线程时,因为时间片的轮换,所以执行过程随机分配,且一个线程对象只能调用一次start方法。

run方法的作用是在主线程中调用以后,直接在主线程下的某一条线程中执行了对应run的方法。(并不新开线程)

简而言之就是我们不能通过run方法来新开一个线程,只能调用线程中重写的run方法(可以在线程中不断的调用run方法,但是不能开启子线程,只能处理一件事),start是开启线程,再调用方法(默认开启一次线程,调用一次run方法,可以同时执行几件事)

以售票窗口为例:

public class Test extends Thread{
    public static void main(String[] args){
        window t1 = new window();
        window t2 = new window();
        window t3 = new window();

        t1.setName("售票口1");
        t2.setName("售票口2");
        t3.setName("售票口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

class window extends Thread{
    private static int ticketNum = 100; //将其加载在类的静态区,所有线程共享该静态变量

    @Override
    public void run() {
        while(true){
            if(ticketNum>0){
                System.out.println(getName()+" 售出第 "+ticketNum+" 张票");
                ticketNum--;
            }else{
                break;
            }
        }
    }
}

在这里插入图片描述

实现Runnable接口

通过实现Runnable接口来实现多线程一共分为五个步骤

  1. 创建一个实现了Runable接口的类
  2. 实现类去实现Runnable中的抽象方法:run方法
  3. 创建实现类的对象
  4. 将此对象作为参数传递到Thread类中的构造器中,创建Thread类的对象
  5. 通过Thread类的对象调用start方法
public class Test1 {

    public static  void main(String[] args){
        window1 w = new window1();

        Thread t1=new Thread(w);
        Thread t2=new Thread(w);
        Thread t3=new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

class window1 implements Runnable{
    private int ticketNum = 100;
    @Override
    public void run() {
        while(true){
            if(ticketNum>0){
                System.out.println(Thread.currentThread().getName()+"当前售出第"+ticketNum+"张票");
                ticketNum--;
            }else{
                break;
            }
        }
    }
}

在这里插入图片描述

从上面两个程序可以看出,现在的两个线程对象是交错运行的,哪个线程对象抢到了 CPU 资源,哪个线程就可以运行,所以程序每次的运行结果肯定是不一样的,在线程启动虽然调用的是 start() 方法,但实际上调用的却是 run() 方法定义的主体。

这时候肯定又有小伙伴会感到疑惑了,通过 Thread 类和 Runable 接口都可以实现多线程,那么两者有哪些联系和区别呢?

这时候我们就要看一下Thread 类的定义了:

public class Thread implements Runnable

在这里插入图片描述 从 Thread 类的定义可以清楚的发现,Thread 类也是 Runnable 接口的子类,但在Thread类中并没有完全实现 Runnable 接口中的 run() 方法(感兴趣的小伙伴可以研究一下完整源码~)。

在 Thread 类中的 run() 方法调用的是 Runnable 接口中的 run() 方法,也就是说此方法是由 Runnable 子类完成的,所以如果要通过继承 Thread 类实现多线程,则必须覆写 run()。

实际上 Thread 类和 Runnable 接口之间在使用上也是有区别的,如果一个类继承 Thread类,则不适合于多个线程共享资源,而实现了 Runnable 接口,就可以方便的实现资源的共享。

线程的状态

提到了多线程,我们就得再说说多线程的状态了~线程一般具有五种状态,分别是创建,就绪,运行,阻塞,终止。

  • 创建:创建状态就很好理解了,就是在程序中用构造方法创建了一个线程对象后,新的线程对象就处于此状态,这时候它已经有了相应的内存空间和其他资源,但还处于不可运行状态。
  • 就绪:创建线程对象后,调用其start方法就可以启动线程。当线程启动时,线程进入就绪状态,同时线程将进入线程队列排队,等待CPU服务,这表明它已经具备了运行条件。
  • 运行:当就绪状态被调用并获得CPU资源时,线程就进入了运行状态。此时,自动调用该线程对象的run方法进入了运行状态。
  • 阻塞:一个正在执行的线程在某些特殊情况下,如被人为挂起或需要执行耗时的输入/输出操作,会让 CPU 暂时中止自己的执行,进入阻塞状态。在可执行状态下,如果调用sleep、suspend、wait等方法,线程都将进入阻塞状态,发生阻塞时线程不能进入排队队列,只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。
  • 终止:线程调用stop方法时或在run方法执行结束后,即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。

再举个例子帮助大家来理解这五个状态(皇帝选妃的例子):皇上(皇上代表了CPU)选中了一个女子并且要让她来做自己的妃子(妃子代表了线程),太监们知道以后就开始给这个女子准备宫殿和衣食住行的物品(相当于线程进入了创建状态,被分配了资源),万事俱备以后这个女子就等待着皇上翻她的牌子(线程进入了队列里排队),终于有一天皇上翻了她的牌子,她就伴驾伺候皇上(线程进入了运行状态),但是在伴驾的时候,由于她犯错惹怒了皇上,皇上很生气就罚他不许伴驾(线程遇到了特殊情况,进入了阻塞状态),后面皇上越来越生气,索性直接把这个女子打入冷宫,并且永不相见,这个妃子也就再也不能伴驾伺候皇上了(线程处于了死亡状态,并且不再具备运行能力了)。

P.S. 为什么用皇上这个例子呢~ 因为皇上肯定不只是有一个妃子嘛,后宫佳丽三千人(●'◡'●),就像CPU肯定不只有一个线程一样~

线程的操作方法

强制运行

在线程操作中,可以使用 join方法让一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续执行。老规矩,直接上代码~

public class ThreadJoinDemo{
    public static void main(String args[]){
        MyThread mt = new MyThread();
        Thread t = new Thread(mt,"窗口A");
        t.start() ;
        for(int i = 0;i < 50;i++){
            if(i > 10){
                try{
                    t.join() ;  // 线程强制运行
                }catch(InterruptedException e){

                }
            }
            System.out.println("Main窗口售票,第 " + i + " 张") ;
        }
    }
};

class MyThread implements Runnable{
    public void run(){ 
        for(int i=0;i<50;i++){
            System.out.println(Thread.currentThread().getName()
                    + "售票,第" + i +" 张") ;
        }
    }
};

大家知道main方法运行的时候就是一个线程,就代表了一个售票窗口(即窗口M),当循环的过程中我们用了join方法强制窗口A售票,那么此时窗口M会等待窗口A售卖完50张票后再继续执行。 在这里插入图片描述

休眠

在程序中允许一个线程进行暂时的休眠,直接使用 Thread.sleep() 即可实现。

public class ThreadSleepDemo{
    public static void main(String args[]){
        MyThread mt = new MyThread();
        Thread t = new Thread(mt,"窗口A");
        t.start();
    }
};

class MyThread implements Runnable{
    public void run(){
        for(int i = 0;i < 50;i++){
            try{
                if(i > 30){
                    System.out.println("累了,休息一下~");
                    Thread.sleep(500) ; // 线程休眠500ms 再执行
                }
            }catch(InterruptedException e){

            }
            System.out.println(Thread.currentThread().getName()
                    + "售票,第" + i +" 张") ;  // 取得当前线程的名字
        }
    }
};

在这里插入图片描述 从运行结果中可以看出,当i大于30以后,线程每次执行都会先休眠500ms再执行结果打印。

中断线程

如果需要中断某线程,我们可以使用interrupt()方法中断其运行状态。我们把上面的代码修改一下~

public class ThreadSleepDemo{
    public static void main(String args[]){
        MyThread mt = new MyThread();
        Thread t = new Thread(mt,"窗口A");
        t.start();
        try{
            Thread.sleep(2000) ;    // 线程休眠2秒
        }catch(InterruptedException e){
            System.out.println("休眠被终止") ;
        }
        t.interrupt() ; // 中断线程执行
    }
};

class MyThread implements Runnable{
    public void run(){
        for(int i = 0;i < 50;i++){
            try{
                if(i > 30){
                    Thread.sleep(5000) ; // 线程休眠5s 再执行
                }
            }catch(InterruptedException e){
                System.out.println("休眠被终止,已中断此线程") ;
                return ;
            }
            System.out.println(Thread.currentThread().getName()
                    + "售票,第" + i +" 张") ;  // 取得当前线程的名字
        }
    }
};

在这里插入图片描述

后台线程

后台线程的指得就是即使Java线程结束了,此线程依然会继续执行,要想实现后台线程这样的操作,直接使用setDaemon()方法即可。也就是我们只需增加一行t.setDaemon(true);即可实现后台进程。(这里就不放实例代码了(●'◡'●))

优先级

在多线程操作中,所有的线程在运行前都会保持在就绪状态,那么此时,哪个线程的优先级高,哪个线程就有可能会先被执行。

class MyThread3 implements Runnable{ // 实现Runnable接口
    public void run(){  // 覆写run()方法
        for(int i=0;i<5;i++){
            try{
                Thread.sleep(500) ; // 线程休眠
            }catch(InterruptedException e){
            }
            System.out.println(Thread.currentThread().getName()
                    + "运行,i = " + i) ;  // 取得当前线程的名字
        }
    }
};
public class ThreadPriorityDemo{
    public static void main(String args[]){
        Thread t1 = new Thread(new MyThread3(),"线程A") ;  // 实例化线程对象
        Thread t2 = new Thread(new MyThread3(),"线程B") ;  // 实例化线程对象
        Thread t3 = new Thread(new MyThread3(),"线程C") ;  // 实例化线程对象
        t1.setPriority(Thread.MIN_PRIORITY) ;   // 优先级最低
        t2.setPriority(Thread.MAX_PRIORITY) ;   // 优先级最高
        t3.setPriority(Thread.NORM_PRIORITY) ;  // 优先级最中等
        t1.start() ;    // 启动线程
        t2.start() ;    // 启动线程
        t3.start() ;    // 启动线程
    }
};

==需要注意的是并非优先级越高就一定会先执行,哪个线程先执行将由 CPU 的调度决定。==

礼让

在线程操作中,也可以使用yield方法将一个线程的操作暂时让给其他线程执行。

public class ThreadYieldDemo{
    public static void main(String args[]){
        MyThread4 my = new MyThread4() ;  // 实例化MyThread对象
        Thread t1 = new Thread(my,"小车A") ;
        Thread t2 = new Thread(my,"小车B") ;
        t1.start() ;
        t2.start() ;
    }
};

class MyThread4 implements Runnable{
    public void run(){
        for(int i = 0;i < 5;i++){
            try{
                Thread.sleep(500) ;
            }catch(Exception e){
            }
            System.out.println(Thread.currentThread().getName()
                    + "运行,i = " + i) ;  // 取得当前线程的名字
            if(i==2){
                System.out.print("堵车了,让你先走:") ;
                Thread.currentThread().yield() ;    // 线程礼让
            }
        }
    }
};

在这里插入图片描述

线程同步

在说线程同步之前,我们得先说一下线程的安全问题。 举个例子,一个人在公共厕所上厕所,他进去以后没锁门,这时候又来了一个人发现没锁门,推门就进来了,二者四目相对是不是特别尴尬~这就像是线程安全问题一样,多个线程对同一个共享数据进行操作时,线程没来得及更新共享数据,从而导致另一个线程没得到最新的数据,最终就产生了线程安全问题。

那么我们应该怎么解决这个线程安全的问题呢?

其实刚刚上面的例子中就已经暗示了一个办法,你不想让别人打扰你上厕所,那锁上门就可以了😄

方法一:lock锁

我们把售票的代码稍微改造一下~

public class LockTest {

    public static void main(String[] args){
        Window w= new Window();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口A");
        t2.setName("窗口B");
        t3.setName("窗口C");

        t1.start();
        t2.start();
        t3.start();
    }
}

class Window implements Runnable{
    private int ticket = 100;   //定义一百张票
    //实例化锁
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            //调用锁定方法lock
            lock.lock();
            if (ticket > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "售出第" + ticket + "张票");
                ticket--;
                lock.unlock();//手动解锁
            } else {
                break;
            }
        }
    }
}

方法二:同步代码块

继续改造代码~

public class LockTest {

    public static void main(String[] args){
        Window w= new Window();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口A");
        t2.setName("窗口B");
        t3.setName("窗口C");

        t1.start();
        t2.start();
        t3.start();
    }
}

class Window implements Runnable{
    private int ticket = 100;   //定义一百张票
    //实例化锁
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            //调用锁定方法lock
            synchronized(this) { // 要对当前对象进行同步
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售出第" + ticket + "张票");
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

方法三:同步方法

再一次改造代码~

public class LockTest {

    public static void main(String[] args){
        Window w= new Window();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口A");
        t2.setName("窗口B");
        t3.setName("窗口C");

        t1.start();
        t2.start();
        t3.start();
    }
}

class Window implements Runnable{
    private int ticket = 100;   //定义一百张票
    //实例化锁
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            this.sale();
        }
    }

    public synchronized void sale() { // 要对当前对象进行同步
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "售出第" + ticket + "张票");
            ticket--;
        }
    }
}

有些小伙伴可能会想,Synchronized与lock都可以解决线程安全问题,那二者有什么不同呢?

Synchronized机制在执行完相应的代码以后,会自动释放同步监视器;但是 lock则需要手动启动同步,即增加lock()方法,同时结束同步也需要手动增加unlock()方法,这也就意味着lock方法更为灵活。

优先使用顺序:Lock>同步代码块>同步方法

线程死锁

这里再简单说一下线程的死锁问题。 还是举个形象的例子来描述一下什么是死锁:张三和李四打架,张三对李四说:你先松手,我就松手!;这时候李四也对张三说:你先松手,我就松手!后来这两个人谁也不松手,一直处于了一个僵持的状态。这就跟线程的死锁一样,两个线程都在等待对方先完成,从而造成程序的停滞。

解决死锁的问题也很简单:

  • 减少同步共享变量。
  • 多个线程之间规定先后执行的顺序,从而规避死锁。
  • 减少锁的嵌套。

小结

本人经验有限,有些地方可能讲的没有特别到位,如果您在阅读的时候想到了什么问题,欢迎在评论区留言,我们后续再一一探讨🙇‍

希望各位小伙伴动动自己可爱的小手,来一波点赞+关注 (✿◡‿◡) 让更多小伙伴看到这篇文章~ 蟹蟹呦(●'◡'●)

如果文章中有错误,欢迎大家留言指正;若您有更好、更独到的理解,欢迎您在留言区留下您的宝贵想法。

爱你所爱 行你所行 听从你心 无问东西