多线程基础知识

本文详细介绍了Java中的进程与线程概念,包括它们的区别。线程的实现方式通过Thread类、Runnable接口和Callable接口进行了演示,并分析了相关源码。此外,还探讨了线程的状态、通用操作如命名、休眠和中断,以及sleep()与wait()的区别。最后,解释了守护线程的定义、设置和生命周期。内容深入浅出,适合Java多线程学习者参考。

1 进程与线程

1.1 进程

进程是具有一定独立功能的程序,是系统进行资源分配和调度的一个独立单位。

1.2 线程

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源,但是它可与同属一个进程的其他线程共享进程所拥有的全部资源。
一个线程可以创建和撤销另一个线程,同一个进程中的多个线程可以并发执行。

1.3 进程与线程的区别

进程与线程的主要差别在于他们是不同的造作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其他进程产生影响,而线程知识一个进程中的不同执行路径。

  • 一个程序至少有一个进程,一个进程至少有一个线程;
  • 线程的划分尺度小于进程,使得多线程程序的并发性高;
  • 进程在执行过程中拥有独立的内存单元,多个线程共享内存,极大地提高了程序的运行效率;
  • 线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

2 线程实现

主线程负责 处理整体流程,子线程负责处理耗时操作。

2.1 Thread类实现

2.1.1 代码实现

继承Thread类

public class MyThread extends Thread{

    private String title;

    public MyThread(String title){
        this.title = title;
    }
    @Override
    public void run() {
        System.out.println(title + "start run...");
    }
}

 public static void main(String[] args) {
     MyThread myThread1 = new MyThread("MyThread-1");
     MyThread myThread2 = new MyThread("MyThread-2");
     MyThread myThread3 = new MyThread("MyThread-3");
     myThread1.start();
     myThread2.start();
     myThread3.start();
 }
2.2.2 Thread类中的start()方法源码分析
public synchronized void start() {
   if (threadStatus != 0)
       throw new IllegalThreadStateException();
   group.add(this);

   boolean started = false;
   try {
       start0();
       started = true;
   } finally {
       try {
           if (!started) {
               group.threadStartFailed(this);
           }
       } catch (Throwable ignore) {
           /* do nothing. If start0 threw a Throwable then
                 it will be passed up the call stack */
       }
   }
}
// 本地方法-->jvm-->操作系统的底层函数
private native void start0();

start()方法中会抛出一个“IllegalThreadStateException”异常,但是整个程序中没有进行try…catch处理,因为这个异常一定是一个运行时异常,每一个线程类的对象只允许启动一次,如果重复启动则会抛出此异常。

2.2 Runnable接口实现

2.2.1 实现Runnable接口

java中有单继承的局限,提供根据接口实现。

public class MyRunnabe implements Runnable{
    private String title;

    public MyRunnabe(String title){
        this.title = title;
    }
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(title + " is run " + i +" ...");
        }
    }
}

public static void main(String[] args) {
    Thread thread1 = new Thread(new MyRunnabe("MyRunnable-1"));
    Thread thread2 = new Thread(new MyRunnabe("MyRunnable-2"));
    Thread thread3 = new Thread(new MyRunnabe("MyRunnable-3"));
    thread1.start();
    thread2.start();
    thread3.start();
}
2.2.2 源码分析

Runable接口:支持lambda表达式

@FunctionalInterface
public interface Runnable {
   public abstract void run();
}

Thread类:由Thread源码可以看出Thread类也实现了Runnable接口

public class Thread implements Runnable {
    // 支持传入实现了Runnable接口的类
    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }
    
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }
    
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
		// 其他代码
        
        this.target = target;
        
        // 其他方法
    }
    
    // 其他代码
    
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
}

多线程的设计中,使用了代理模式的结构,用户自定义的线程主体只负责项目核心功能的实现,其他的操作全部交由Thread类来处理。在多线程开发中:多个线程对象访问同一资源。
在进行Thread启动多线程的时候调用的是start()方法,然后执行的是run()方法。run()方法说明:由第22行代码与run()方法可知,所执行的run()方法是调用Runnable接口子类覆写的run()方法。
Runnable有一个缺点:没有返回值

2.3 Callable接口实现

2.3.1 实现Callable接口
public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName()+" call()方法执行中。。。。");
        return 1;
    }
}
public static void main(String[] args) {
    FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
    Thread thread = new Thread(futureTask);
    thread.start();

    try {
        System.out.println("返回结果" + futureTask.get());
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + " main()方法执行完成");

}
2.3.2 源码分析
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

Callable定义时可以设置一个返回类型,此类型就是返回的数据类型。

public class FutureTask<V> implements RunnableFuture<V> {
    // 其他代码
	public FutureTask(Callable<V> callable) {
        if (callable == null) {
            throw new NullPointerException();
        } else {
            this.callable = callable;
            this.state = 0;
        }
    }
    // 其他代码
}
2.3.3 Runnable与Callable的区别
  • Runnable是JDK 1.0 提出的多线程的实现接口,而Callable是在JDK1.5 之后提出的;
  • Runnable接口中只提供有一个run()方法,并且没有返回值;
  • Callable接口提供有call()方法,可以有返回值。
    线程虽然调用的是start方法,但是最终实现的是run方法,并且所有线程是交替执行的。只要定义了多线程,多线程的启动只有一种方案:Thread类中的start0()。

2.4 start()与run()的区别

  • start()方法用来启动线程,真正实现了多线程运行。这时无需等待run()方法中代码执行完毕,可以直接执行下面的代码;直接调用run()方法相当于调用普通方法,代码会顺序执行,并未实现多线程运行。
  • 通过调用Thread的类的start()方法来启动一个线程,这时线程是处于就绪状态,并没有运行;
  • run()方法为线程体,包含了要执行的这个线程的内容,线程进入运行状态,开始运行run()方法中的代码,run()方法运行结束,此线程终止。

3 线程的生命周期(状态)

当线程被创建并启动后,它并不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建、就绪、运行、阻塞和死亡五种状态。当线程启动之后,它不可能一直霸占着CPU一直运行,CPU需要在多线程之间切换,所以线程状态也会在运行、阻塞之间切换。

线程生命周期

3.1 新建状态

当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值。

3.2 就绪状态

任何一个线程对象都应该使用Thread类进行封装,所有线程启动使用的是start()。当线程对象调用start()方法之后,该线程将进入到就绪状态,但是并没有执行。JVM会为其创建方法调用栈和程序计数器,等待调度运行。

3.3 运行状态

如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程进入到运行状态。

3.4 阻塞状态

线程因为某种原因放弃了CPU的使用权,即让出了CPU时间片,暂时停止运行。直到线程进入到可运行状态,才有机会再次获得CPU时间片转到运行状态。
阻塞的情况有三种:

  • 等待阻塞:运行状态的线程执行Object.wait()方法,此时JVM会将该线程放入等待队列中;
  • 同步阻塞:运行状态的线程在获取对象的同步锁时,如果该同步锁被占用,则JVM会把该线程放入锁池中;
  • 其他阻塞:运行状态的线程执行Thread.sleep(long ms)或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超市、join()等待线程终止或者超时、I/O处理完毕时,线程重新转入可运行状态。

3.5 线程死亡

  • 正常结束:run()或call()方法执行完成,线程正常结束;
  • 异常结束:线程抛出一个未捕获的Exception或Error;
  • 调用stop:直接调用该线程的stop()方法来结束线程(不建议使用)

4 线程通用操作

线程的主要操作方法都是在Thread类中。线程的运行状态是不确定的。

4.1 线程的命名与获得

  • 构造方法:public Thread(String name);
  • set方法:public final synchronized void setName(String name);
  • get方法:public final String getName();
    如果设置了名字则使用设置的名字,如果没有设置,则会自动生成一个不重复的名字,自动属性命名主要是依靠了static属性完成的,在Thread类里面定义有如下操作:
private static int threadInitNumber;
public Thread() {
    this((ThreadGroup)null, (Runnable)null, "Thread-" + nextThreadNum(), 0L);
}
private static synchronized int nextThreadNum() {
    return threadInitNumber++;
}

4.2 线程休眠

使一个线程暂缓执行,可以自动实现线程的唤醒,继续进行后续处理。但是需要注意的是,如果有多个线程,休眠也是有先后顺序的。

  • public static native void sleep(long var0) throws InterruptedException;
  • public static void sleep(long millis, int nanos) throws InterruptedException;

4.3 线程中断

  • public boolean isInterrupted() :线程是否中断
  • public static boolean interrupted():中断线程

所有正在执行的线程都是可以被中断的,中断线程必须进行异常处理

4.4 线程强制运行

当满足某些条件后,某一个线程对象将可以一直独占资源,一直到该线程的程序执行结束。

  • public final void join() throws InterruptedException:强制执行

4.5 线程礼让

  • public static native void yield();

每次调用yield方法只会礼让一次当前的资源。

4.6 线程优先级

从理论上来讲,线程的优先级越高越有可能执行,即越有可能抢占到资源。

  • 设置优先级:public final void setPriority(int newPriority);
  • 获取优先级:public final int getPriority();
  • 低优先级 :public static final int MIN_PRIORITY = 1;
  • 中等优先级public static final int NORM_PRIORITY = 5;
  • 高优先级:public static final int MAX_PRIORITY = 10;
    主线程属于中等优先级,默认创建的线程也是中等优先级。

5 sleep与wait的区别

  • sleep()方法是属于Thread类的,wait()方法是属于Object类中的;
  • sleep()方法导致了程序暂停执行指定时间,让出CPU给其他线程,但是它的监控状态依然保持着,当指定时间到了又会自动恢复到运行状态;
  • sleep()方法调用过程中,线程不会释放对象锁;调用wait方法时,线程会释放对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后此线程才进入对象锁定池准备获取对象锁进入运行状态。

6 守护线程

6.1 定义

守护线程是后台线程,为用户线程提供公共服务,在没有可服务的用户线程时会自动离开。守护线程的优先级比较低,用于为系统中的其他对象和线程服务。
垃圾回收线程就是一个典型的守护线程,它始终在低级别的状态运行,用于实时监控和管理系统中的可回收资源。当我们程序不再有任何运行的Thread,程序就不会再产生任何垃圾,垃圾收集器就无事可做,所以垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。

6.2 设置

通过setDaemon(true)来设置线程为守护线程;将一个用户线程设置为守护线程的方式是通过线程对象创建之前调用线程对象的setDaemon()方法。

6.3 生命周期

守护线程是运行在后台的一种特殊线程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统同生共死。当JVM中所有线程都是守护线程的时候,JVM就可以退出了,如果还有一个或以上的守护线程则JVM不会退出。

7 参考文献:

线程与进程的区别:https://blog.csdn.net/mxsgoden/article/details/8821936

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

flying_fish79

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值