(一)Java多线程基础篇

本文深入探讨了多线程编程的基础概念,包括进程与线程的区别、同步与异步、并发与并行的概念,详细解释了线程的五种基本状态及线程的创建与启动方式。此外,文章还介绍了多线程中的重要API及其背后的含义,如yield()、join()、sleep()等方法的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、多线程中的一些名词概念

1.1 进程与线程的区别

1)进程:

  • 进程就是程序在并发环境中的执行过程,也就是一个正在运行的程序。所谓正在运行的程序,就少不了三个东西,一个是cpu,一个是程序本身(代码什么的),还有一个运行环境。比如说:一个人正在做饭(进程),首先要有一个人(CPU),还要有一个菜谱(代码),最后要有一个厨房(运行环境)。
  • 进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。

2)线程:

  • 所谓线程,就是程序的一个个子模块。比如说:一个人正在做饭(进程),他要洗菜(线程),他也要切菜(线程),他还要和面(也是线程),把做饭这个事情,分成一个个小的模块,再有这个人(CPU)去一一处理。
  • 线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。

3)进程与线程的区别

  • 进程是资源分配的最小单位,线程是程序执行的最小单位。一个程序要运行,给你cpu让你运行,但是要具体怎么运行,就看程序是如何分配的了。
  • 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。这也就是后面为什么要用锁的原因了。
  • 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
  • 但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。
1.2 同步与异步的区别

多线程并发时,多个线程同时请求同一个资源,必然导致此资源的数据不安全,A线程修改了B线程的处理的数据,而B线程又修改了A线程处理的数理。显然这是由于全局源造成的,有时为了解决此问题,优先考虑使用局部变量,退而求其次使用同步代码块,出于这样的安全考虑就必须牺牲系统处理性能,加在多线程并发时资源挣夺最激烈的地方,这就实现了线程的同步机制

  • 同步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为同步机制存在,A线程请求不到,怎么办,A线程只能等待下去
  • 异步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为没有同步机制存在,A线程仍然请求的到,A线程无需等待

显然,同步最最安全,最保险的。而异步不安全,容易导致死锁,这样一个线程死掉就会导致整个进程崩溃,但没有同步机制的存在,性能会有所提升

1.3 并发与并行的区别
  • 并发:就是让一个处理器处理多个任务,但这些任务不一定要同时进行。比如说:一个人吃三个馒头,他可以一个一个的吃,并不一定要一口吃完。
  • 并行:同时发生的两个并发事件,也就是多个处理器同时处理多个任务。比如说:三个人同时吃三个馒头。
1.4 什么是锁、什么是死锁
  • 锁:可以理解为普通意义上的一把锁,不过他是用来锁资源的。比如给一段资源(方法、代码块)加上一把锁,则这段资源同一时间,只能有一个线程对他访问,只有等这个线程访问完了,其他的线程才能访问。这不就是同步机制吗???没错,锁存在的意义,就是让他产生同步机制的。
  • 死锁:是指同一个进程集合中的每个进程都在等待仅有该集合中的另一个进程才能引发的事件而无限期地僵持下去的局面,也就是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。比如说:在一条单车道的路上,双向各来一辆车,这两个车都在等对方给自己让路,就会形成一种僵局,也就是死锁。

2、线程中的五种基本状态

在这里插入图片描述

2.1 新建状态(New)

当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

2.2 就绪状态(Runnable)

当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

2.3 运行状态(Running)

当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

2.4 阻塞状态(Blocked)

处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

  • 1)等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

  • 2)同步阻塞: 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

  • 3)其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

2.5 死亡状态(Dead)

线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

3、线程的创建于启动

3.1 继承Thread类

1)重写该类的run()方法。

class MyThread extends Thread {

    private int i = 0;

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

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Thread myThread1 = new MyThread();     // 创建一个新的线程  myThread1  此线程进入新建状态
                Thread myThread2 = new MyThread();     // 创建一个新的线程 myThread2 此线程进入新建状态
                myThread1.start();                     // 调用start()方法使得线程进入就绪状态
                myThread2.start();                     // 调用start()方法使得线程进入就绪状态
            }
        }
    }
}
  • 继承Thread类,通过重写run()方法定义了一个新的线程类MyThread,其中run()方法的方法体代表了线程需要完成的任务,称之为线程执行体。当创建此线程类对象时一个新的线程得以创建,并进入到线程新建状态。通过调用线程对象引用的start()方法,使得该线程进入到就绪状态,此时此线程并不一定会马上得以执行,这取决于CPU调度时机。
3.2 实现Runnable接口

-1)实现Runnable接口,并重写该接口的run()方法,该run()方法同样是线程执行体,创建Runnable实现类的实例,并以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象。

class MyRunnable implements Runnable {
    private int i = 0;

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

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Runnable myRunnable = new MyRunnable(); // 创建一个Runnable实现类的对象
                Thread thread1 = new Thread(myRunnable); // 将myRunnable作为Thread target创建新的线程
                Thread thread2 = new Thread(myRunnable);
                thread1.start(); // 调用start()方法使得线程进入就绪状态
                thread2.start();
            }
        }
    }
}

2)或者说,直接通过lamda表达式也是可以的实现的,因为Runnable接口只有一个方法:

public class ThreadTest {

	public static void main(String[] args) {
		Runnable mt = () -> {
			for (int i = 0; i < 100; i++) {
				System.out.println(Thread.currentThread().getName() + "    " + i);
			}
		};
		
		new Thread(mt).start();
		new Thread(mt).start();
	}

}

3)Thread和Runnable之间到底是什么关系?

class MyRunnable implements Runnable {
    private int i = 0;

    @Override
    public void run() {
        System.out.println("in MyRunnable run");
        for (i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

class MyThread extends Thread {

    private int i = 0;

    public MyThread(Runnable runnable){
        super(runnable);
    }

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

public class ThreadTest {

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Runnable myRunnable = new MyRunnable();
                //这里没有用Thread类,二是继承了Thread的MyThread类
                Thread thread = new MyThread(myRunnable);
                thread.start();
            }
        }
    }
}
  • 首先,可以肯定的是,这种方式是没有问题的。

  • 至于此时的线程执行体到底是MyRunnable接口中的run()方法还是MyThread类中的run()方法呢?通过输出我们知道线程执行体是MyThread类中的run()方法。

  • 为什么呢?

    //Runnable源码是这样写的
     public interface Runnable {
    
         public abstract void run();
    
     }
    
    	//Thread源码中是这样写的
    public class Thread implements Runnable {
    	
    	private Runnable target;
    	
    	private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
    		...
    	}
    	
        @Override
        public void run() {
            if (target != null) {
                target.run();
            }
        }
        
    }
        
    

    如果我们使用Thread thread = new Thread (myRunnable);则由源码可知:当启动这个线程,经过初始化,在运行run()方法的时候,会先判断 target 是否为空,这里是不为空的,因为Thread是继承Runnable的,这里也对其初始化过。

    如果使用Thread thread = new MyThread(myRunnable);,因为没有target 这个变量,重写的run()也没有对他判null,又由多态可知,这里没有机会执行Thread中的run()方法,所以输出的是:MyThread类中的run()方法。

3.3 实现Callable接口

1)具体是创建Callable接口的实现类,并实现clall()方法。并使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。

public class ThreadTest {

    public static void main(String[] args) {

        Callable<Integer> myCallable = new MyCallable();    // 创建MyCallable对象
        FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask来包装MyCallable对象

        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Thread thread = new Thread(ft);   //FutureTask对象作为Thread对象的target创建新的线程
                thread.start();                      //线程进入到就绪状态
            }
        }

        System.out.println("主线程for循环执行完毕..");

        try {
            int sum = ft.get();            //取得新创建的新线程中的call()方法返回的结果
            System.out.println("sum = " + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}


class MyCallable implements Callable<Integer> {
    private int i = 0;

    // 与run()方法不同的是,call()方法具有返回值
    @Override
    public Integer call() {
        int sum = 0;
        for (; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            sum += i;
        }
        return sum;
    }

}
  • 在实现Callable接口中,此时不再是run()方法了,而是call()方法,此call()方法作为线程执行体,同时还具有返回值!在创建新的线程时,是通过FutureTask来包装MyCallable对象,同时作为了Thread对象的target。那么看下FutureTask类的定义:

    public class FutureTask<V> implements RunnableFuture<V> {
    
         //....
    
     }
     
    public interface RunnableFuture<V> extends Runnable, Future<V> {
    
         void run();
    
     }
    

我们发现FutureTask类实际上是同时实现了Runnable和Future接口,由此才使得其具有Future和Runnable双重特性。通过Runnable特性,可以作为Thread对象的target,而Future特性,使得其可以取得新创建线程中的call()方法的返回值。

执行下此程序,我们发现sum = 4950永远都是最后输出的。而“主线程for循环执行完毕…”则很可能是在子线程循环中间输出。由CPU的线程调度机制,我们知道,“主线程for循环执行完毕…”的输出时机是没有任何问题的,那么为什么sum =4950会永远最后输出呢?

原因在于通过ft.get()方法获取子线程call()方法的返回值时,当子线程此方法还未执行完毕,ft.get()方法会一直阻塞,直到call()方法执行完毕才能取到返回值。

4、多线程重要 API 以及其背后的意义

4.1 yield()——线程让步

1)当调用线程的yield()方法时,线程从运行状态转换为就绪状态,但接下来CPU调度就绪状态中的哪个线程还是与线程的优先级紧密相关,CPU从就绪状态线程队列中只会选择与该线程优先级相同或优先级更高的线程去执行。

2)这个方法会释放CPU资源,但是不会释放锁资源,只有他访问的带锁资源执行完之后,锁资源才会释放。(如果刚看不理解没关系,记住这是一个超级重要的知识点就行)

4.2 join()

1)让一个线程等待另一个线程完成才继续执行。如A线程线程执行体中调用B线程的join()方法,则A线程被阻塞,直到B线程执行完为止,A才能得以继续执行。

public class ThreadTest {

	public static void main(String[] args) {
		Runnable r = () -> {
			for (int i = 0; i < 100; i++) {
				System.out.println(Thread.currentThread().getName() + " " + i);
			}
		};
		Thread thread1 = new Thread(r);
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName() + " " + i);
			if (i == 30) {
				thread1.start();
				try {
					//在thread1线程启动后才能才能使用这个方法
					thread1.join(); // main线程需要等待thread线程执行完后才能继续执行
				} catch (Exception e) {
					e.printStackTrace();
				}

			}
		}
	}
}
4.3 sleep()

1)让当前的正在执行的线程暂停指定的时间,并进入阻塞状态。在其睡眠的时间段内,该线程由于不是处于就绪状态,因此不会得到执行的机会。即使此时系统中没有任何其他可执行的线程,出于sleep()中的线程也不会执行。因此sleep()方法常用来暂停线程执行。

2)当调用了新建的线程的start()方法后,线程进入到就绪状态,可能会在接下来的某个时间获取CPU时间片得以执行,如果希望这个新线程必然性的立即执行,直接调用原来线程的sleep(1)即可。

3)这是一个静态方法,在哪个线程的执行体里面调用它,就让那个线程暂停指定的时间。

4)在调用sleep()方法的过程中,线程不会释放对象锁。 但是会让出自己所占用的CPU资源,自身进入阻塞状态。

public class ThreadTest {

	public static void main(String[] args) {
		Runnable r = () -> {
			for (int i = 0; i < 100; i++) {
				System.out.println(Thread.currentThread().getName() + " " + i);
			}
		};
		Thread thread1 = new Thread(r);
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName() + " " + i);
			if (i == 30) {
				thread1.start();
				try {
					Thread.sleep(1); // 使得thread必然能够马上得以执行
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
}
4.4 后台线程

1)后台线程主要是为其他线程(相对可以称之为前台线程)提供服务,或“守护线程”。如JVM中的垃圾回收线程。

2)生命周期:后台线程的生命周期与前台线程生命周期有一定关联。主要体现在:当所有的前台线程都进入死亡状态时,后台线程会自动死亡(其实这个也很好理解,因为后台线程存在的目的在于为前台线程服务的,既然所有的前台线程都死亡了,那它自己还留着有什么用…伟大啊 ! !)。

3)设置后台线程:调用Thread对象的setDaemon(true)方法可以将指定的线程设置为后台线程。

public class ThreadTest {

	public static void main(String[] args) {
		Runnable r = () -> {
			for (int i = 0; i < 100; i++) {
				System.out.println(Thread.currentThread().getName() + " " + i);
			}
		};
		Thread thread1 = new Thread(r);
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName() + " " + i);
			if (i == 30) {
				thread1.start();
				thread1.setDaemon(true);
			}
		}
	}
}
4.5 setPriority() / getPriority()——线程优先级

1)每个线程在执行时都具有一定的优先级,优先级高的线程具有较多的执行机会。每个线程默认的优先级都与创建它的线程的优先级相同。main线程默认具有普通优先级。

  • 改变线程的优先级/setPriority():
  • 获取线程优先级:getPriority()。

设置线程优先级:setPriority(int priorityLevel)。参数priorityLevel范围在1-10之间,常用的有如下三个静态常量值:

  • MAX_PRIORITY:10

  • MIN_PRIORITY:1

  • NORM_PRIORITY:5

public class ThreadTest {

	public static void main(String[] args) {
		Runnable r = () -> {
			for (int i = 0; i < 100; i++) {
				System.out.println(Thread.currentThread().getName() + " " + i);
			}
		};
		Thread thread1 = new Thread(r);
		thread1.setPriority(Thread.MAX_PRIORITY);

		Thread thread2 = new Thread(r);
		thread2.setPriority(Thread.NORM_PRIORITY);

		Thread thread3 = new Thread(r);
		thread3.setPriority(Thread.MIN_PRIORITY);

		for (int i = 0; i < 100; i++) {
			if (i == 30) {
				thread1.start();
				thread2.start();
				thread3.start();
			}
		}
	}
}
4.6 interrupt()——中断线程

1)向线程发送中断请求。线程的中断状态将被设置为true。如果目前该线程被一个sleep调用阻塞,那么,InterruptedException异常将会抛出。

2)它基于一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止的思想(我命由我不由天)。是一个比较温柔的做法,它更类似一个标志位。其实作用不是中断线程,而是通知线程应该中断了,具体到底中断还是继续运行,应该由被通知的线程自己处理。

3)并不能真正的中断线程,这点要谨记。需要被调用的线程自己进行配合才行。也就是说,一个线程如果有被中断的需求,那么就需要这样做:

  • 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。
  • 在调用阻塞方法时正确处理InterruptedException异常。(例如:catch异常后就结束线程。)

4)static boolean interrupted():测试当前线程(正在执行这一命令的线程)是否被中断。这是一个静态方法,会产生一个副作用——将当前线程的中断状态重置为false

5)boolean isInterrupted()测试线程是否被终止。这一个调用不会改变线程的中断状态。

4.7 其他方法

1)static Thread currentThread()——当前线程:返回对当前正在执行的线程对象的引用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

yelvens

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

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

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

打赏作者

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

抵扣说明:

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

余额充值