Java基础整理(十九)

多线程(上)

传送门:多线程(下)

1. 基本概念

  • 什么是进程?什么是线程?

    进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列。进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。

    线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位。一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。

  • 什么是并发?什么是并行?

    并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。

    并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

  • 对java程序来说,会先启动JVM,而JVM就是一个进程。JVM再启动一个主线程调用main方法。同时再启动一个垃圾回收线程负责看护、回收垃圾。最起码,java程序中至少有连个线程并发。

  • 进程与进程之间资源内存独立不共享,线程和线程间堆内存和方法区内存共享,但栈内存独立,即一个线程一个栈,互不干扰,这就是多线程并发,目的是为了提高程序的处理效率

  • 使用了多线程机制之后,main方法结束只是主线程结束了,主栈空了,其他的栈(线程)可能还再压栈弹栈。


2. 实现线程的方式

(1)第一种方式

编写一个类,直接继承java.lang.Thread,重写run方法

public class Demo {
    //main方法的代码属于主线程
    public static void main(String[] args) throws Exception {

        //新建一个分支线程对象
        MyThread myThread = new MyThread();
        //启动线程
        myThread.start();
        //这里的代码运行在主线程中
        for(int i = 0; i < 1000; i++){
            System.out.println("主线程-->" + i);
        }
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        //编写程序,这段程序运行在分支线程中(分支栈)
        for(int i = 0; i < 1000; i++){
            System.out.println("分支线程-->" + i);
        }
    }
}

执行结果(交替)在这里插入图片描述

  • start()方法的作用:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了,start()方法结束,线程就启动成功了
  • 启动成功的线程会自动调用run方法,不需要手动调用,由JVM线程调度机制来运作的,并且run方法在分支栈的栈底部(压栈)
  • run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main平级
  • 如果在main方法中不调用start(),而是直接调用run(),则不会启动新的线程

(2)第二种实现方式

编写一个类实现java.lang.Runnable接口

public class Demo {
    //main方法的代码属于主线程
    public static void main(String[] args) throws Exception {

        //创建一个可运行的对象
        MyRunnable myRunnable = new MyRunnable();
        //将可运行的对象封装成一个线程对象
        Thread myThread = new Thread(myRunnable);
        //Thread myThread  = new Thread(new MyRunnable);
        //启动线程
        myThread.start();
        //这里的代码运行在主线程中
        for(int i = 0; i < 1000; i++){
            System.out.println("主线程-->" + i);
        }
    }
}

//这不是一个线程类,是一个可运行的类。它还不是一个线程
class MyRunnable implements Runnable{
    @Override
    public void run() {
        //编写程序,这段程序运行在分支线程中(分支栈)
        for(int i = 0; i < 1000; i++){
            System.out.println("分支线程-->" + i);
        }
    }
}

注:第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其它的类,更灵活。否则用第一种继承Thread,再想继承其他类就无法继承了。


(3)第二种的匿名内部类实现
public class Demo {
    //main方法的代码属于主线程
    public static void main(String[] args) throws Exception {

        //创建线程对象,采用匿名内部类的方式
        Thread myThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 1000; i++){
                    System.out.println("分支线程-->" + i);
                }
            }
        });
        //启动线程
        myThread.start();
        //这里的代码运行在主线程中
        for(int i = 0; i < 1000; i++){
            System.out.println("主线程-->" + i);
        }
    }
}

(4)第三种方式

实现Callable接口(JDK 8新特性)

这种方式实现的线程可以获取线程的返回值,前两种方式是无法获取线程返回值的,因为run方法返回void

  • 使用场景

    系统委派一个线程区执行一个任务,该线程执行完任务之后,可能会有一个执行结果,我们就会需要用这种方式去拿到这个执行结果

public class Test {
    public static void main(String[] args) {

        //创建一个“未来任务类”对象
        //匿名内部类
        FutureTask task = new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception {
                System.out.println("call method begin");
                Thread.sleep(1000*10);
                System.out.println("call method end");
                int a = 1;
                int b = 2;
                return a+b;
            }
        });

        //需要把未来任务类对象传进去
        Thread t = new Thread(task);
        t.setName("t");
        t.start();
        Object obj = null;
        try {
            //在主线程中获取t线程的返回值
            //get方法需要处理异常
            obj = task.get();
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(obj);
    }
}

main方法想要执行必须等待get()方法的结果,而get()方法可能需要等待很久,因为get()方法是为了拿到另一个线程的结果,另一个线程是需要时间的。

  • 总结
    • 优点:可以获取到线程的执行结果
    • 缺点:效率比较低,在获取t线程执行结果的时候,当前线程收到阻塞,效率较低

3. 线程的生命周期

在这里插入图片描述


4. 获取当前线程对象

  • 获取线程对象的名字

    String name = 线程对象.getName();
    
  • 修改线程对象的名字

    线程对象.setName("线程名字");
    
  • 若当前线程没有设置名字的时候,默认的名字:Thread-0、Thread-1、Thread-2、Thread-3

public class Demo {
    //main方法的代码属于主线程
    public static void main(String[] args) throws Exception {

        //创建线程对象
        MyThread myThread = new MyThread();
        //设置线程的名字
        myThread.setName("ttt");
        //获取线程的名字
        String threadName=myThread.getName();//ttt
        System.out.println(threadName);
    }
}

class MyThread extends Thread{
    @Override
    public void run() {

    }
}
  • 获取当前对象

    static Thread currentThread(); //静态方法! 返回一个Thread对象
    
public class Demo {
    //main方法的代码属于主线程
    public static void main(String[] args) throws Exception {

        Thread currentThread = Thread.currentThread();
        //这个代码出现在main方法当中,所以当前线程就是主线程
        System.out.println(currentThread.getName());//main
    }
}

示例:

public class Demo {
    //main方法的代码属于主线程
    public static void main(String[] args) throws Exception {

        MyThread t1 = new MyThread();
        t1.setName("t1");
        t1.start();
        MyThread t2 = new MyThread();
        t2.setName("t2");
        t2.start();
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        for(int i=0;i<100;i++){
            //currentThread就是当前线程对象,现在对象是谁?
            //当t1线程执行run()方法时,当前线程就是t1
            //当t2线程执行run()方法时,当前线程就是t2
            Thread currentThread = Thread.currentThread();
            System.out.println(currentThread.getName()+"--->"+i);
        }
    }
}

输出结果:在这里插入图片描述


5. 线程睡眠sleep

static void sleep(long millsis)
  • 静态方法:Thread.sleep(1000);

  • 参数是毫秒

  • 要解决异常

  • 作用:让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让其他线程使用

    这行代码出现在A线程中,A线程会休眠。出现在B线程,B线程休眠

public class Demo {
    //main方法的代码属于主线程
    public static void main(String[] args){

        try {
            Thread.sleep(1000*5); //休眠5秒, 过5秒钟输出hello world
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("hello world");

    }
}

使用Thread.sleep()方法,可以做到效果:间隔特定的时间,去执行一段特定的代码,每隔多久执行一次

常见问题:

public class Demo {
    public static void main(String[] args){

        Thread t = new MyThread();
        t.start();
        try {
            t.sleep(1000*5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("hello world");
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+"--->"+i);
        }
    }
}

由于sleep为静态方法,在执行时会被转成Thread.sleep(1000*5),让当前线程进入休眠,也就是main线程进入休眠。这行代码出现在main,main线程休眠

所以该程序的运行结果为:循环马上直接完毕,过5s后输出hello world


6. 终止线程睡眠

线程对象.interrupt()

public class Demo {
    public static void main(String[] args)  {

        Thread t = new Thread(new MyThread());
        t.setName("t");
        t.start();

        //希望5秒后,t线程醒来
        try {
            Thread.sleep(1000*5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //中断t线程的睡眠
        t.interrupt();

    }
}

class MyThread implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+ "---> begin");
        try {
            //这里的异常只能try..catch,不能抛出
            //因为run方法在父类中没有抛出任何异常,子类不能父类抛出更多的异常
            Thread.sleep(1000*60*60*24*365);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+ "---> end");
    }
}

输出结果:在这里插入图片描述

显然,这种终止线程睡眠的方式是依靠了java的异常处理机制,因为它打印出来了异常信息。如果不想异常信息打印出来的话,可以将run方法中的异常信息注释掉。


7. 线程的终止

在java中对线程进行终止,我们常用两种方法,一种是调用线程对象的stop方法另一种是添加布尔标记。更常用的是第二种。

  • 第一种:stop方法

    public class Demo {
        public static void main(String[] args)  {
    
            Thread t = new Thread(new MyThread());
            t.setName("t");
            t.start();
    
            try {
                Thread.sleep(1000*5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            //5秒后线程t停止
            t.stop();
    
        }
    }
    
    class MyThread implements Runnable{
        @Override
        public void run() {
           for(int i=1;i<10;i++){
               System.out.println(Thread.currentThread().getName() + "--->" + i);
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        }
    }
    

    输出结果:在这里插入图片描述

    这种方式存在很大的缺点:容易丢失数据。因为这种方式直接将线程杀死了,线程没有保存的数据将丢失,不建议使用

  • 第二种:布尔标记

    public class Demo {
        public static void main(String[] args)  {
    
            MyThread m = new MyThread();
            Thread t = new Thread(m);
            t.setName("t");
            t.start();
    
            try {
                Thread.sleep(1000*5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            m.setRun(false);
        }
    }
    
    class MyThread implements Runnable{
    
        private boolean run = true;
    
        public boolean isRun() {
            return run;
        }
    
        public void setRun(boolean run) {
            this.run = run;
        }
    
        @Override
        public void run() {
           for(int i=1;i<10;i++){
               if(run){
                   System.out.println(Thread.currentThread().getName() + "--->" + i);
                   try {
                       Thread.sleep(1000);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
           }
        }
    }
    

8. 线程的调度

  • 常见的线程调度模型

    • 抢占式调度模型:哪个线程的优先级比较高,抢到的CPU时间片的概率就多一些。java采用的就是抢占式调度模型
    • 均分式调度模型:平均分配CPU时间片,每个线程占有的CPU时间片时间长度一样。
  • java中提供的与线程调度有关的方法

    实例方法:

    • void setPriority(int newPriority)设置线程的优先级

    • int getPriority()获取线程优先级

      最低优先级是1,默认优先级是5,最高优先级是10

      优先级比较高的获取CPU时间片可能会多一些(大概率)

    • void join() 合并线程

    静态方法:

    • static void yield() 线程让位,暂停当前正在执行的线程对象,并执行其他线程

      该方法不是阻塞方法。让当前线程让位,给其他线程使用。

      该方法执行会让当前线程从“运行状态”回到“就绪状态”,但回到就绪之后,有可能会再次抢到


9. 线程的优先级

public class Demo {
    public static void main(String[] args)  {
        
        System.out.println("最高优先级"+ Thread.MAX_PRIORITY); //10
        System.out.println("最低优先级"+ Thread.MIN_PRIORITY); //1
        System.out.println("默认优先级"+ Thread.NORM_PRIORITY); //5
    }
}
  • 如何获取当前线程的优先级

    public class Demo {
        public static void main(String[] args)  {
    
            Thread currentThread = Thread.currentThread();
            System.out.println(currentThread.getName()+"线程的默认优先级是"+currentThread.getPriority()); //5
        }
    }
    
  • 如何设置当前线程的优先级

    public class Demo {
        public static void main(String[] args)  {
    
            Thread currentThread = Thread.currentThread();
            currentThread.setPriority(2);
            System.out.println(currentThread.getName()+"线程的优先级是"+currentThread.getPriority());//2
        }
    }
    

优先级较高的,会抢到的CPU时间片相对多一些


10. 线程让位和合并

  • 让位

    静态方法:Thread.yield();

    让位:当前线程暂停,回到就绪状态,让给其他线程

  • 合并

    实例方法:线程对象.join() 该方法需要进行异常处理

    将某个线程合并到当前线程中,当前线程受阻

    public class Demo {
        public static void main(String[] args)  {
    
            System.out.println("main begin");
            Thread t = new Thread(new MyThread());
            t.setName("t");
            t.start();
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("main over");
        }
    }
    
    class MyThread implements Runnable{
        
        @Override
        public void run() {
           for(int i=1;i<10;i++){
                   System.out.println(Thread.currentThread().getName() + "--->" + i);
           }
        }
    }
    

    没采用合并之前的输出结果:在这里插入图片描述
    采用合并之后的输出结果:在这里插入图片描述

    合并线程不代表着另一个栈消失!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值