2-多线程

Java
466
0
0
2022-12-13
标签   Java多线程

多线程概述(并发编程)

进程

程序是静止的,而运行中的程序就是进程

特征

  • 动态性:进程是运行中的程序,要动态的占用内存,CPU和网络等资源
  • 独立性:进程和进程之间是相互独立的,彼此有自己独立内存区域
  • 并发性:假如CPU是单核,同一时刻内存中只有一个进程在被执行,CPU会依时为每个进程服务,由于切换速度非常快,给我们的感觉就是这些进程在同时执行,这就是并发性

并行:

同一时刻同时有多个进程在执行(多核CPU)

线程

线程属于进程,一个进程可以包含多个线程,这就是多线程。(线程是进程中的一个独立执行单元)线程的创建开销相对于进程来说比较小,线程也支持并发性

作用

  • 提高程序效率,线程支持并发性,可以有更多机会得到CPU
  • 多线程可以解决很多业务模型
  • 大型高并发技术的核心技术

线程创建方式一:继承Thread类

代码实现

package ThreadTest;

/*
* 创建线程的方法一:
* 1. 直接定义类继承Thread
* 2. 重写run()方法
* 3. 创建线程对象
* 4. 调用线程对象的start()方法启动线程
*/


// 定义一个线程类继承Thread类
class MyThread extends Thread{
    //重写run()方法
    @Override
    public void run() {
        // 线程的执行方法 
        for(int i=0;i<5;i++){
            System.out.println("子线程输出:"+i);
        }
    }

}


public class ThreadDemo {
    // 这里可以把启动后的ThreadDemo当成一个进程 
    // main方法是由主线程执行的,理解成main方法就是一个主线程

    public static void main(String[] args) {
        //创建线程对象
        Thread t=new MyThread();  //多态 
        //调用start()方法启动线程
        t.start();  //此时一共存在两个线程,一个是main函数表示的主线程,另一个就是t表示的子线程

        for(int i=0;i<5;i++){
            System.out.println("主线程输出:"+i);
        }
    }
}

运行结果

主线程输出:0
主线程输出:1
子线程输出:0
主线程输出:2
子线程输出:1
主线程输出:3
子线程输出:2
主线程输出:4
子线程输出:3
子线程输出:4

可以看到多线程是并发的,两个线程之间的执行顺序是完全随机的,双方都不断争抢CPU执行,并且执行过程始终向前推进

继承Thread类的优点

  • 编码相对简单

继承Thread类的缺点

  • 自定义线程类继承了Thread类,导致无法再继承其他类,功能单一,不能继续拓展(单继承的局限性)

线程的注意事项

线程的启动必须调用start()方法

否则操作就会被当作普通类处理,按照代码顺序正常执行。 也就是说,如果直接调用自定义线程类的run方法,实际上就变成了普通类执行,此时只有一个主线程在运行

start()方法底层其实是给CPU注册当前线程,并且触发run()方法的执行

一般建议进程中先创建子线程,主线程的任务放在之后(否则程序按顺序执行,先接触到主线程内容,并没有发现后续的子线程启动,导致主线程必然先进行完,整个流程实际上仍然是顺序执行)

线程创建方式二:实现Runnable接口

  1. 创建一个线程任务类实现Runnable接口
  2. 重写run()方法
  3. 创建一个线程任务对象
  4. 线程任务对象包装成线程对象
  5. 调用线程对象的start()方法启动线程

代码实现

package ThreadTest;

public class ThreadDemo4 {
    public static void main(String[] args) {
        //3. 创建"线程任务"对象(不是线程对象,只是执行线程任务的)
        Runnable target=new MyRunnable();
        //4. 把线程任务对象包装成线程对象 
        //Thread t=new Thread(target);   单纯创建线程对象
        Thread t=new Thread(target,"TheFirstThread");  //利用线程任务创建线程对象的同时为线程命名 
        //启动线程
        t.start();

        for(int i=0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+"==>"+i);
        }

    }

}


//1. 首先,创建线程任务类实现Runnable接口
class MyRunnable implements Runnable {

    //2. 重写run()方法
    @Override
    public void run() {
        for(int i=0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+"==>"+i);
        }
    }
}

方式二的匿名内部类写法(简化写法)

package ThreadTest;

public class ThreadDemo4 {
    public static void main(String[] args) {
        Runnable target=new Runnable() {
            @Override
            public void run() {
                for(int i=0;i<5;i++){
                    System.out.println(Thread.currentThread().getName()+"==>"+i);
                }
            }
        };
        Thread t=new Thread(target,"TheFirstThread");  
        t.start();

        for(int i=0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+"==>"+i);
        }
    }
}

方式二实现Runnable接口创建的优点

  • 线程任务类只是实现了Runnable接口,所以可以继续继承其他类,而且可以继续实现其他接口,避免了单继承的局限性
  • 同一个线程任务对象可以被包装成多个线程对象
  • 适合多个相同的程序代码的线程去共享同一个资源
  • 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立
  • 线程池可以放入实现Runnable或Callable线程任务对象

方式二实现Runnable接口创建的缺点

  • 代码较为复杂
  • 继承的Runnable接口的run()方法没有返回值,不能直接得到线程执行的结果

注意:Thread类本身也实现了Runnable接口

线程创建方式三:实现Callable接口

  1. 定义一个线程任务类实现Callable接口,申明线程执行的结果类型
  2. 重写线程任务类的call方法,这个方法可以直接返回执行结果(解决了Runnable接口的缺点)
  3. 创建一个Callable的线程任务对象
  4. 把Callable的线程任务对象包装成一个未来任务对象
  5. 把未来任务对象包装成线程对象
  6. 调用现成的start()方法启动线程

代码实现:

package ThreadTest;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadDemo5 {
    public static void main(String[] args) {
        //3. 创建Callable线程任务对象
        Callable<String> call=new MyCallable();
        //4. 把Callable任务对象包装成未来任务对象 
        //未来任务对象:本质就是一个Runnaable对象(FutureTask继承自Runnable) 
        //未来任务对象可以在线程执行完后得到线程执行结果
        FutureTask<String> task=new FutureTask<String>(call);
        //5. 把未来任务对象包装成线程对象
        Thread t=new Thread(task,"TheFirstThread");
        t.start();

        for(int i=0;i<20;i++){
            System.out.println(Thread.currentThread().getName()+"==>"+i);
        }

        //6. 最后获取线程执行结果,如果线程没有结果,会让出CPU等待线程执行完毕再来取值 
        try {
            String rs=task.get();  //获取call方法返回的结果(正常/异常)
            System.out.println(rs);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

//1. 创建一个线程任务类实现Callable接口,申明线程返回的结果类型
class MyCallable implements Callable<String>{
    //2. 重写线程任务类的call方法 
    @Override 
    public String call() throws Exception {
        // 计算1~20的和 
        int sum=0;
        for(int i=0;i<20;i++){
            System.out.println(Thread.currentThread().getName()+"==>"+i);
            sum+=i;
        }
        return Thread.currentThread().getName()+"执行的结果是:"+sum;
    }
}

运行结果

TheFirstThread==>0
main==>0
TheFirstThread==>1
main==>1
TheFirstThread==>2
main==>2
TheFirstThread==>3
main==>3
TheFirstThread==>4
main==>4
TheFirstThread==>5
main==>5
TheFirstThread==>6
main==>6
TheFirstThread==>7
main==>7
TheFirstThread==>8
main==>8
TheFirstThread==>9
main==>9
TheFirstThread==>10
main==>10
TheFirstThread==>11
main==>11
TheFirstThread==>12
main==>12
TheFirstThread==>13
main==>13
TheFirstThread==>14
main==>14
TheFirstThread==>15
main==>15
TheFirstThread==>16
main==>16
TheFirstThread==>17
main==>17
TheFirstThread==>18
main==>18
TheFirstThread==>19
main==>19
TheFirstThread执行的结果是:190

方法三,继承Callable接口优点

拥有方法二的全部优点,并且能够通过返回值了解线程是否正常运行,得到线程执行的结果

方法三,继承Callable接口缺点

编码复杂

线程常用API

package ThreadTest;

// 定义一个线程类继承Thread类
class MyThread2 extends Thread{
    //重写run()方法
    @Override
    public void run() {
        // 线程的执行方法 
        for(int i=0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+" : 子线程输出 :"+i);
        }
    }

}

public class ThreadDemo2 {

    // 进程中一共包括两个子线程t1,t2与一个主线程 
    public static void main(String[] args) {
        //创建线程对象
        Thread t1=new MyThread2();

        t1.setName("TheFirstThread");   //为线程命名

        t1.start();

        System.out.println(t1.getName());  //获取子线程的名字

        Thread t2=new MyThread2();

        t2.setName("TheSecondThread");   //为线程命名

        t2.start();

        System.out.println(t2.getName());  //获取子线程的名字

        //获取主线程名称的方法 
        //利用currentThread()方法,可以获取当前所在的线程的对象 
        //(注意,这里说的是在哪个线程内部,而不是正在执行哪个线程,三个线程随机执行,但此时都在主线程内)
        Thread m =Thread.currentThread();
        m.setName("MainThread");
        System.out.println(m.getName());  //从而就可获取到主线程名称

        for(int i=0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+" :"+i);
        }
    }
}

运行结果

TheFirstThread
TheSecondThread
MainThread
MainThread :0
TheSecondThread : 子线程输出 :0
TheFirstThread : 子线程输出 :0
TheSecondThread : 子线程输出 :1
MainThread :1
TheSecondThread : 子线程输出 :2
TheFirstThread : 子线程输出 :1
TheSecondThread : 子线程输出 :3
MainThread :2
TheSecondThread : 子线程输出 :4
TheFirstThread : 子线程输出 :2
TheFirstThread : 子线程输出 :3
TheFirstThread : 子线程输出 :4
MainThread :3
MainThread :4

线程休眠方法:sleep()

让当前线程休眠指定时间(毫秒)后继续执行

package ThreadTest;

// 定义一个线程类继承Thread类
class MyThread3 extends Thread{
    //重写run()方法
    @Override
    public void run() {
        // 线程的执行方法

        for(int i=0;i<5;i++){
            try {
                Thread.sleep(1000);  //使子进程休眠1000ms再运行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" : 子线程输出 :"+i);
        }

    }

}

public class ThreadDemo3 {

    public static void main(String[] args) {
        //创建线程对象
        Thread t1=new MyThread3();
        t1.start();

        for(int i=0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+" :"+i);
        }
    }
}

通过Thread类的含参构造器为线程命名

Thread t1=new MyThread("TheFirstThread");
// 需要重写自定义线程的方法(直接调用父类有参构造器即可)
public MyThread(String name){
    super(name); //调用父类有参构造器并初始化线程对象的名称
}