侧边栏壁纸
博主头像
孔子说JAVA博主等级

成功只是一只沦落在鸡窝里的鹰,成功永远属于自信且有毅力的人!

  • 累计撰写 292 篇文章
  • 累计创建 132 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

java多线程基础知识点及实现

孔子说JAVA
2022-05-30 / 0 评论 / 0 点赞 / 96 阅读 / 7,363 字 / 正在检测是否收录...

Java 给多线程编程提供了内置的支持。程序的每一部分都称作一个线程,又称为单线程,它是进程中一个单一顺序的控制流。而一个多线程程序包含两个或多个能并发运行的部分(即包含多个单线程),并且每个线程定义了一个独立的执行路径。

1、基本概念

1.1 进程和线程

线程是一个应用程序进程中不同的执行路径,进程从来不执行任何东西,它只是线程的容器。线程总是在某个进程环境中创建的,而且它的整个生命周期都在该进程中。下面是线程和进程的相关概念。

程序(即静态代码):是为完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,或称静态对象。

  • 一个java应用程序至少有三个线程,分别为:main()主线程,gc()垃圾回收线程,异常处理线程。如果发生了异常,会影响主线程。

进程(即执行中的程序):是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程。进程是资源分配的最小单位,一个进程至少包含一个线程,每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1~n个线程。

线程(即进程中的执行单元):进程可进一步细化为线程,是一个程序内部的一条执行路径。线程本身依靠程序进行运行,线程是程序中的顺序控制流,只能使用分配给程序的资源和环境。

  • 同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。
  • 线程是CPU调度的最小单位。
  • Java中的线程分2类,1.守护线程(如垃圾回收线程,异常处理线程),2.用户线程(如主线程)。若JVM中都是守护线程,当前JVM将退出。

单线程:程序中只存在一个线程,实际上主方法就是一个主线程。

多线程:在一个程序中运行多个任务(同一程序中有多个顺序流在执行),目的是更好地使用CPU资源。

多进程:是指操作系统能同时运行多个任务(程序)。

1.2 并行和并发

并行:多个CPU实例或者多台机器同时执行多个任务,是真正的同时。比如多个人同时做不同的事。在多线程编程实践中,线程的个数往往多于CPU的个数,所以一般都称多线程并发编程而不是多线程并行编程。

并发:一个CPU(采用时间片)同时执行多个任务。通过cpu调度算法,让用户看上去同时执行,实际上从CPU操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。比如秒杀平台。

1.3 线程安全和调度

生命周期:线程和进程的生命周期一样,分为五个阶段:创建、就绪、运行、阻塞、终止。

线程安全:经常用来描绘一段代码。当多个线程访问某个类/某个对象/共享数据时,不管运行时环境采用何种调度方式,或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

线程不安全:多个线程对同一个共享数据进行操作时,线程没来得及更新共享数据,从而导致另外线程没得到最新的数据,从而产生线程安全问题,不安全意味着线程的调度顺序会影响最终结果。如不加事务的转账代码。

同步:Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全的,来保证结果的准确。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能。

线程调度:线程调度是指按照特定机制为多个线程分配CPU的使用权。有两种调度模型:分时调度模型(所有线程平分cpu的时间片,轮流占用CPU)和抢占式调度模型(根据优先级占用CPU)。Java使用的是抢占式调度,也就是每个线程将由操作系统来分配执行时间,线程的切换不由线程本身来决定(协同式调度)。

2、多线程的优缺点

如果是单核CPU的话,只使用单个线程完成多个任务(调用多种方法),肯定要比用多线程来完成的时间更短(这是因为我们使用多线程的话,线程的切换也会需要时间),那为什么我们单核CPU阶段就已经开始使用多线程了?自然是因为多线程有它不可替代的有优点,下面简单介绍一下多线程的优缺点:

2.1 多线程的优点

①提高应用程序的响应,对图形化界面更有意义,可以增强用户的体验。

②可以提高计算机系统的CPU的利用率,最大限度地利用CPU资源。

③多线程还可以改善程序的结构,将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。

④对于进程来说,线程的开销更小,资源共享性好。

2.2 多线程的缺点

①线程也是程序,需要占用内存,线程越多占用的内存就越多。

②多线程需要协调和管理,需要CPU时间跟踪线程。

③线程之间对共享资源的访问会互相影响,必须解决竟用共享资源的问题。

④对于进程来说,线程共享资源需要耗费一定的锁资源,同步相对复杂。而且一个线程崩溃可能导致整个进程崩溃,这个当然是自己的应用程序有问题(但是相对编程更复杂)。

3、线程的实现

在java中要想实现多线程,有三种手段,

  • 1.继承Thread类
  • 2.实现Runable接口
  • 3.实现Callable接口,并与Future、线程池结合使用。

3.1 继承Thread类

Thread类在java.lang包中定义, 继承Thread类必须重写run()方法。线程对象通过调用start()方法去启动线程。

  • 注意,不是调用run()方法启动线程,run方法中只是定义需要执行的任务,如果调用run方法,即相当于在主线程中执行run方法,跟普通的方法调用没有任何区别,此时并不会创建一个新的线程来执行定义的任务。
  • 查看Thread类的实现源代码会发现Thread类是实现了Runnable接口的。

下面我们来看一个具体的例子:

public class Test {
    public static void main(String[] args)  {
        MyThread thread = new MyThread();
        thread.start();
    }
}
class MyThread extends Thread{
    private static int num = 0;
    public MyThread(){
        num++;
    }
    @Override
    public void run() {
        System.out.println("主动创建的第"+num+"个线程");
    }
}

在上面代码中,通过调用start()方法,就会创建一个新的线程了。为了分清start()方法调用和run()方法调用的区别,请看下面一个例子:

public class Test {
    public static void main(String[] args)  {
        System.out.println("主线程ID:"+Thread.currentThread().getId());
        MyThread thread1 = new MyThread("thread1");
        thread1.start();
        MyThread thread2 = new MyThread("thread2");
        thread2.run();
    }
}
 
class MyThread extends Thread{
    private String name;
 
    public MyThread(String name){
        this.name = name;
    }
 
    @Override
    public void run() {
        System.out.println("name:"+name+" 子线程ID:"+Thread.currentThread().getId());
    }
}

image-1653877940438

从输出结果可以得出以下结论:

  • 1)thread1和thread2的线程ID不同,thread2和主线程ID相同,说明通过run方法调用并不会创建新的线程,而是在主线程中直接运行run方法,跟普通的方法调用没有任何区别;
  • 2)虽然thread1的start方法调用在thread2的run方法前面调用,但是先输出的是thread2的run方法调用的相关信息,说明新线程创建的过程不会阻塞主线程的后续执行。

3.2 实现Runnable接口

在Java中创建线程除了继承Thread类之外,还可以通过实现Runnable接口来实现类似的功能。实现Runnable接口必须重写其run方法。

public class Test {
    public static void main(String[] args)  {
        System.out.println("主线程ID:"+Thread.currentThread().getId());
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
} 
class MyRunnable implements Runnable{
    public MyRunnable() {
    }
 
    @Override
    public void run() {
        System.out.println("子线程ID:"+Thread.currentThread().getId());
    }
}

下面我们来看一个具体的例子:

public class Test {
    public static void main(String[] args)  {
        System.out.println("主线程ID:"+Thread.currentThread().getId());
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
} 
class MyRunnable implements Runnable{
    public MyRunnable() {
    }
 
    @Override
    public void run() {
        System.out.println("子线程ID:"+Thread.currentThread().getId());
    }
}

Runnable的中文意思是“任务”,顾名思义,通过实现Runnable接口,我们定义了一个子任务,然后将子任务交由Thread去执行。

  • 注意,这种方式必须将Runnable作为Thread类的参数,然后通过Thread的start方法来创建一个新线程来执行该子任务。如果调用Runnable的run方法的话,是不会创建新线程的,这和普通的方法调用没有任何区别。
  • 事实上,查看Thread类的实现源代码会发现Thread类是实现了Runnable接口的。

在Java中,这2种方式都可以用来创建线程去执行子任务,具体选择哪一种方式要看自己的需求。直接继承Thread类的话,可能比实现Runnable接口看起来更加简洁,但是由于Java只允许单继承,所以如果自定义类需要继承其他类,则只能选择实现Runnable接口。

3.3 使用ExecutorService、Callable、Future实现有返回结果的多线程

ExecutorService、Callable、Future这个对象实际上都是属于Executor框架中的功能类。返回结果的线程是在JDK1.5中引入的新特征,确实很实用,有了这种特征大家就不需要再为了得到返回值而大费周折了,而且即便实现了也可能漏洞百出。

可返回值的任务必须实现Callable接口,类似的,无返回值的任务必须实现Runnable接口。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,再结合线程池接口ExecutorService就可以实现有返回结果的多线程了。下面提供了一个完整的有返回结果的多线程测试例子,在JDK1.5~1.8版本验证过没问题,可以直接使用。代码如下:

/**
* 有返回值的线程 
*/ 
@SuppressWarnings("unchecked")  
public class Test {  
public static void main(String[] args) throws ExecutionException,  
    InterruptedException {  
   System.out.println("----程序开始运行----");  
   Date date1 = new Date();  
 
   int taskSize = 5;  
   // 创建一个线程池  
   ExecutorService pool = Executors.newFixedThreadPool(taskSize);  
   // 创建多个有返回值的任务  
   List<Future> list = new ArrayList<Future>();  
   for (int i = 0; i < taskSize; i++) {  
    Callable c = new MyCallable(i + " ");  
    // 执行任务并获取Future对象  
    Future f = pool.submit(c);  
    // System.out.println(">>>" + f.get().toString());  
    list.add(f);  
   }  
   // 关闭线程池  
   pool.shutdown();  
 
   // 获取所有并发任务的运行结果  
   for (Future f : list) {  
    // 从Future对象上获取任务的返回值,并输出到控制台  
    System.out.println(">>>" + f.get().toString());  
   }  
 
   Date date2 = new Date();  
   System.out.println("----程序结束运行----,程序运行时间【" 
     + (date2.getTime() - date1.getTime()) + "毫秒】");  
}  
}  
 
class MyCallable implements Callable<Object> {  
private String taskNum;  
 
MyCallable(String taskNum) {  
   this.taskNum = taskNum;  
}  
 
public Object call() throws Exception {  
   System.out.println(">>>" + taskNum + "任务启动");  
   Date dateTmp1 = new Date();  
   Thread.sleep(1000);  
   Date dateTmp2 = new Date();  
   long time = dateTmp2.getTime() - dateTmp1.getTime();  
   System.out.println(">>>" + taskNum + "任务终止");  
   return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】";  
}
}

代码说明:上述代码中Executors类,提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。

创建线程池方法:

//创建固定数目线程的线程池。
public static ExecutorService newFixedThreadPool(int nThreads);

//创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
public static ExecutorService newCachedThreadPool();
//创建一个单线程化的Executor。
public static ExecutorService newSingleThreadExecutor();
//创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize);

ExecutoreService提供了submit()方法,传递一个Callable,或Runnable,返回Future。如果Executor后台线程池还没有完成Callable的计算,这调用返回Future对象的get()方法,会阻塞直到计算完成。

future模式:并发模式的一种,可以有两种形式,即无阻塞和阻塞,分别是isDone和get。其中Future对象用来存放该线程的返回值以及状态。

//创建固定数目线程的线程池。
ExecutorService e = Executors.newFixedThreadPool(3);
//submit方法有多重参数版本,及支持callable也能够支持runnable接口类型.
Future future = e.submit(new myCallable());
future.isDone(); //return true,false 无阻塞
future.get(); // return 返回值,阻塞直到该线程运行结束

3.4 创建线程的三种方式对比

  1. 采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。

  2. 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。

有效利用多线程的关键是理解程序是并发执行而不是串行执行的。例如:程序中有两个子系统需要并发执行,这时候就需要利用多线程编程。

通过对多线程的使用,可以编写出非常高效的程序。不过请注意,如果你创建太多的线程,程序执行的效率实际上是降低了,而不是提升了。另外,上下文的切换开销也很重要,如果你创建了太多的线程,CPU 花费在上下文的切换的时间将多于执行程序的时间!

0

评论区