1,线程概述
1.1,进程和线程
并发是指系统能够同时处理多个任务或操作,通常通过在单个处理器或多个处理器之间快速切换上下文来实现。这些任务可能不是同时进行的,但是它们在时间上重叠。
并行是指系统同时执行多个任务或操作,通常通过在多个处理器上同时执行不同的任务来实现。这些任务是同时进行的,因此可以更快地完成。
进程:进程是系统中运行的一个程序,程序一旦运行就是进程。进程是系统分配资源的实体,每个进程都有独立的地址空间。一个进程无法访问另一个进程的变量和数据机构如果想让一个进程访问另一个进程的资源,需要使用进程间通信,比如管道,文件,套接字。
线程:线程是进程的进一步划分,是CPU调度和分派的最小单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
协程:协程是一种程序组件,是一种比线程更加轻量级的存在。正如一个进程可以有多个线程,一个线程可以有多个协程。协程是用户视角的一种抽象,操作系统并没有协程的概念。协程运行在线程之上,协程的主要思想是在用户态实现调度算法,用少量线程完成大量任务的调度。多进程、多线程已经提高了系统的并发能力,但是在当今互联网高并发场景下,为每个任务都创建一个线程是不现实的,因为会消耗大量的内存 (进程虚拟内存会占用 4GB [32 位操作系统],而线程也要大约 4MB)。
【内存空间共享问题】
- 多线程进程内部的所有内存都由所有线程"共享"。这通常是线程的定义,因为它们都在同一内存空间中运行。
- 当从第一个进程派生出另一个进程时,它们将共享相同的内存空间。如果某个进程尚未从另一个进程派生,则它们通常不共享任何内存(通信除外)。
【协程的优势】
- 内存占用要小,且创建开销要小:用户态的协程,可以设计的很小,可以达到 KB 级别。是线程的千分之一。线程栈空间通常是MB级别, 协程栈空间最小KB级别。
- 减少上下文切换的开销:让可执行的线程尽量少,这样切换次数必然会少;让线程尽可能的处于运行状态,而不是阻塞让出时间片,当一个协程执行完成后,可以选择主动让出,让另一个协程运行在当前线程之上(分时复用)。即使有协程阻塞,该线程的其他协程也可以被 runtime 调度,转移到其他可运行的线程上。
1.2,多线程优势
当操作系统创建一个进程时,必须为该进程分配独立的内存空间,并分配大量的相关资源;但创建一个线程则简单得多,因此使用多线程来实现并发比使用多线程实现并发的性能要高很多。
- 进程之间不能共享内存,但线程之间共享内存非常容易。
- 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高。
- Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调用方式,从而简化了Java的多线程编程。
【应用场景】
并发请求处理:在高并发的情况下,使用多线程可以让应用程序同时处理多个请求,提高系统的吞吐量和响应速度。
图像处理:在图像处理和计算机视觉应用中,多线程可以加速对图像的处理和分析,提高应用程序的实时性和准确性。
游戏开发:在游戏开发中,多线程可以处理不同的游戏逻辑、渲染和音效,提高游戏的流畅性和效果。
Web服务器:在Web服务器中,多线程可以处理多个用户请求,提高Web应用程序的并发能力和响应速度。
1.3,守护线程
守护线程(默认情况下创建的都是非守护线程)是一种在后台运行的线程,它不会阻止程序的结束,即使所有的非守护线程都已经结束,它也会自动退出。与之相反,非守护线程在主线程结束之前必须完成它们的任务,否则程序将一直等待它们结束。
2,线程的状态和生命周期
2.1,线程的生命周期
- suspend():暂时挂起线程。
- resume():恢复挂起的线程。
- stop():停止线程。
2.2,线程的状态
- 创建状态:在程序中用构造方法创建了一个线程对象后,新的线程对象便处于新建状态,此时,它已经有了相应的内存空间和其他资源,但还处于不可运行状态。新建一个线程对象可采用Thread类的构造方法,例如“Thread thread = new Thread();”。
- 就绪状态:新建线程对象后,调用该线程的start()方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待CPU服务,这表明线程已经具备了运行条件。
- 运行状态:当就绪状态的线程被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的run()方法。run()方法定义了该线程的操作和功能。
- 堵塞状态:一个正在执行的线程在某些特殊情况下,如被人为挂起或需要执行耗时的输入/输出操作时,会让除CPU并暂时中止自己的执行,进入堵塞状态。在可执行状态下,如果调用sleep(),suspend(),wait()等方法,线程都将进入堵塞状态。堵塞时,线程不能进入排队队列,只有当引起堵塞的原因被消除后,线程才可以转入就绪状态。
- 死亡状态:线程调用stop()方法或run()方法执行结束后,即处于死亡状态。处于死亡状态的线程不具备继续运行的能力。
3,线程的创建
创建线程有三种方式,分别是继承Thread类、实现Runnable接口、实现Callable接口。
3.1,继承Thread类
通过继承Thread类来创建并启动线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法将作为线程执行体。
- 创建Thread子类的实例,即创建了线程对象。
- 调用线程对象的start()方法来启动该线程。
class 类名 extends Thread{ 属性...; 方法...; public void run(){ 线程主体; } }
在Thread子类中,必须明确地覆盖Thread类中的run()方法,此方法为线程的主体。
class MyThread extends Thread { private String name; public MyThread(String name) {this.name = name;} public void run() { for (int i = 0; i < 5; i++) {System.out.println("i = " + i);} } } public class HelloWord { public static void main(String args[]) { MyThread thread1 = new MyThread("线程 A"); MyThread thread2 = new MyThread("线程 B"); thread1.run();thread2.run(); } } ====================================================================== name = 线程 A,i = 0 name = 线程 A,i = 1 name = 线程 B,i = 0 name = 线程 B,i = 1
【问题】为啥仍然是顺序执行呢?其实此时线程实际上并没有被启动,还是属于顺序式的执行方式,那么如何启动线程呢?如果要正确地启动线程,是不能直接调用run()方法的,而应该是调用从Thread类中继承而来的start()方法。
thread1.start();thread2.start(); ================================ name = 线程 A,i = 0 name = 线程 B,i = 0 name = 线程 A,i = 1 name = 线程 B,i = 1
【问题】为什么启动线程不能直接使用run()方法而必须通过start()方法?问的好!线程的运行需要本机操作系统的支持!下面的源码中可以看出,start()实际上是调用了start0()方法,而start0()方法又被native关键字声明,此关键字表示调用本机的操作系统函数,因为多线程的实现需要靠底层操作系统的支持。而run()仅仅是一个类中的一个方法而已。
public synchronized void start() { /** * This method is not invoked for the main method thread or "system" * group threads created/set up by the VM. Any new functionality added * to this method in the future may have to also be added to the VM. * * A zero status value corresponds to state "NEW". */ if (threadStatus != 0) throw new IllegalThreadStateException(); /* Notify the group that this thread is about to be started * so that it can be added to the group's list of threads * and the group's unstarted count can be decremented. */ 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 */ } } } private native void start0();
另外,一个类通过继承Thread类来实现,那么只能调用一次start()方法,如果调用多次,则会抛出“IllegalThreadStateException”异常,因为synchronized的作用。
3.2,实现Runnable接口
通过实现Runnable接口来创建并启动线程的步骤如下:
- 定义Runnable接口的实现类,并实现该接口的run()方法,该run()方法将作为线程执行体。
- 创建Runnable实现类的实例,并将其作为Thread的target来创建Thread对象,Thread对象为线 程对象。
- 调用线程对象的start()方法来启动该线程。
class 类名称 implements Runnable{ 属性...; 方法...; public void run(){ 线程主图; } }
class MyThread implements Runnable { private String name; public MyThread(String name) { this.name = name; } public void run() { for (int i = 0; i < 2; i++) { System.out.println(name + ":" + i); } } }
从Thread可以知道,要想启动一个多线程必须要使用start()方法完成,如果继承了Thread类,则可以直接使用start()方法,但是Runnable接口并没有start()方法的定义。此时还是要依靠Thread类完成启动,在Thread类中提供了public Thread(Runnable target)和public Thread(Runnable target,String name)两个构造方法。
class MyThread implements Runnable { private String name; public MyThread(String name) {this.name = name;} public void run() { for (int i = 0; i < 2; i++) {System.out.println(name + ":" + i);} } } public class HelloWord { public static void main(String args[]) { MyThread thread1 = new MyThread("线程 A"); Thread t1 = new Thread(thread1); t1.start(); Thread t2 = new Thread(new MyThread("线程 B")); t2.start(); } } ========================================================================= 线程 A:0 线程 B:0 线程 A:1 线程 B:1
3.3,实现Callable接口
通过实现Callable接口来创建并启动线程的步骤如下:
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有 返回值。然后再创建Callable实现类的实例。
- 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返 回值。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
class MyCall implements Callable { private String name; public MyCall(String name) { this.name = name; } public Object call() throws Exception { for (int i = 0; i < 2; i++) { System.out.println(name + ":" + i); } return null; } } public class Main { public static void main(String[] args) { MyCall myCall = new MyCall("燕双嘤"); FutureTask futureTask = new FutureTask(myCall); Thread a = new Thread(futureTask); a.start(); } }
3.4,Thread类&Runnable接口
class Thread implements Runnable { private Runnable target; public Thread(Runnable target,String name){ init(null,target,name,0); } public void init(ThreadGroup g,Runnable target,String name,long stackSize){ ... this.target = target; ... } public void run(){ if(target!=null){ target.run(); } } }
从源码可以看出来,Thread类也是Runnable接口的子类,但在Thread类中并没有完全地实现Runnable接口中的run()方法。在Thread类中的run()方法调用的是Runnable接口中的run()方法,也就是说此方法是由Runnable子类完成的,所以如果要通过继承Thread类实现多线程,则必须覆写run()方法。
实际上Thread类和Runnable接口之间在使用上还是有区别的,如果一个类继承Thread类,则不适合于多个线程共享资源,而实现Runnable接口,就可以方便地实现资源的共享。
卖票5 卖票5 卖票5 卖票4 卖票4 卖票3 卖票5 卖票2 卖票4 卖票1 卖票3 卖票3 卖票2 卖票1 卖票4 卖票2 卖票3 卖票2 卖票1 卖票1
从上面的结果可以看出来,Runnable接口虽然启动了3个线程,但是3个线程一共才卖5张,即ticket属性被所有线程对象共享。可见Runnable接口相对于Thread类来说有如下优势:
- 适合多个相同程序代码的线程去处理同一资源的情况。
- 可以避免由于Java的单继承特性带来的局限。
- 增强了程序的健壮性,代码能被多个线程共享,代码与数据是独立的。
所以,在开发时建议使用Runnable接口实现多线程。
3.5,Runnable接口&Callable接口
实现Runnable接 口与实现Callable接口的方式基本相同,只是Callable接口里定义的方法有返回值,可以声明抛出异常而已。因此可以将实现Runnable接口和实现Callable接口归为一种方式。
采用实现Runnable、Callable接口的方式创建多线程的优缺点:
- 【优势】线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
- 【优势】在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资 源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
- 【劣势】编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。
采用继承Thread类的方式创建多线程的优缺点:
- 【劣势】因为线程类已经继承了Thread类,所以不能再继承其他父类。
- 【优势】编写简单,如果需要访问当前线程,则无须使用Thread.currentThread()方法,直接使用 this即可获得当前线程。
鉴于上面分析,因此一般推荐采用实现Runnable接口、Callable接口的方式来创建多线程。
4,线程的操作
方法名称 | 类型 | 描述 |
public Thread(Runnable target) | 构造 | 接收Runnable接口子类对象,实例化Thread对象 |
public Thread(Runnable target, Strring name) | 构造 | 接收Runnable接口子类对象,实例化Thread对象,并设置线程名称 |
public Thread(String name) | 构造 | 实例化Thread对象,并设置线程名称 |
public static Thread currentThread() | 普通 | 返回目前正在执行的线程 |
public final String getName() | 普通 | 返回线程的名称 |
public final int getProperiority() | 普通 | 返回线程的优先级 |
public boolean isInterrupted() | 普通 | 判断目前线程是否被中断,如果是,返回true,否则返回false |
public final boolean isAlive() | 普通 | 判断线程是否在活动,如果是,返回true,否则返回false |
public final void join() throws Interrupted | 普通 | 等待线程死亡 |
public final synchronized void join(long millis) throws InterruptedException | 普通 | 等待millis毫秒后,线程死亡 |
public void run() | 普通 | 执行线程 |
public final void setName(String name) | 普通 | 设定线程的名称 |
public final setPririty(int newPriority) | 普通 | 设定线程的优先级 |
public static void sleep(long millis) throws InterruptedException | 普通 | 使目前正在执行的线程休眠millis毫秒 |
public void start() | 普通 | 开始执行线程 |
public String toString | 普通 | 返回代表线程的字符串 |
public static void yield() | 普通 | 将目前正在执行的线程暂停,允许其他线程执行 |
public final void setDaemon(boolean on) | 普通 | 将一个线程设置成后台运行 |
4.1,取得和设置线程的名称
可以通过getName()方法取得线程的名称,还可以通过setName()方法设置线程的名称。线程的名称一般在启动线程前设置,但也允许为已经运行的线程设置名称。允许两个线程对象有相同的名称,但应该尽量避免这种情况的发生。
另外,如果没有设置名称,系统会为其自动分配名称。名字按照Thread-Xx 格式。
在Java中所有的线程都是同时启动的,那个线程先抢到了CPU资源,那个线程就先运行。
class MyThread implements Runnable { public void run() { for (int i = 0; i < 2; i++) { System.out.println(Thread.currentThread().getName()+":"+i); } } } public class HelloWord { public static void main(String[] args) { MyThread my = new MyThread(); new Thread(my).start(); new Thread(my).start(); new Thread(my,"燕双嘤").start(); } } ======================================= Thread-0:0 燕双嘤:0 燕双嘤:1 Thread-1:0 Thread-1:1 Thread-0:1
问题:Java程序每次运行至少启动几个线程?
答案:因为Java是多线程编程语言,所以Java程序运行时也是以线程方式运行的,主方法也是一个线程(main),但对于一个Java程序每次运行至少启动两个线程。Java命令执行一个类时,实际上都会启动一个JVM,每一个JVM实际上就是在操作系统中启动了一个进程,Java本身具备了垃圾回收机制。所以在Java运行时至少会启动两个线程,一个是main线程,另一个是垃圾收集线程。
4.2,判断线程是否启动(isAlive)
class MyThread implements Runnable { public void run() { for (int i = 0; i < 2; i++) { System.out.println(Thread.currentThread().getName()+":"+i); } } } public class HelloWord { public static void main(String[] args) { MyThread my = new MyThread(); Thread t= new Thread(my,"燕双嘤"); System.out.println(t.isAlive()); t.start(); System.out.println(t.isAlive()); for (int i = 0; i < 2; i++) { System.out.println(Thread.currentThread().getName()+":"+i); } System.out.println(t.isAlive()); } } ========================================= false true main:0 main:1 true 燕双嘤:0 燕双嘤:1
上面的结果是不确定的,有可能到最后线程已经不存活了,但也有可能继续存活,这要看哪个线程先执行完。主线程有可能比其他线程先执行完,因为线程操作的不确定性,所以主线程有可能最先执行完,那么此时其他线程不会受到任何影响,并不会随着主线程的结束而结束。
4.3,等待(wait)和唤醒(notify)
这两个不能算是线程的独有方法,而是Object的,所有的类都可以使用。
notify()、notifyAll()的区别:
- notify() 用于唤醒一个正在等待相应对象锁的线程(随机),使其进入就绪队列,以便在当前线程释放锁后竞争锁, 进而得到CPU的执行。
- notifyAll() 用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争 锁,进而得到CPU的执行。
【问题】Java中notify一定会唤醒线程吗?不一定。在Java中,使用wait()方法可以使一个线程进入等待状态,而使用notify()方法可以唤醒一个等待该对象锁的线程。但是,notify()方法并不保证一定会唤醒一个线程。当有多个线程等待同一个对象锁时,notify()方法只会唤醒其中一个线程,但是无法保证唤醒哪个线程。
【问题】如何保证notify一定能唤醒?为了保证可靠的线程唤醒,建议使用notifyAll()方法,它可以唤醒所有等待该对象锁的线程,从而避免竞争问题。
【问题】如何知道notify唤醒那些线程?具体哪个线程被唤醒是不确定的,并且取决于操作系统线程调度器的具体实现。
方法 | 类型 | 描述 |
public final void wait() throws Interrupted | 普通 | 线程等待 |
public final void wait(long timeout) throws Interrupter | 普通 | 线程等待,并指定等待的最长时间 |
public final void wait(long timeout,int nanos) throws Interrupter | 普通 | 线程等待,并指定等待的最长时间,指定最长毫秒以及纳秒 |
public final void notify() | 普通 | 唤醒第1个等待线程 |
public final void notifyAll() | 普通 | 唤醒全部等待线程 |
4.4,强制执行(join)和礼让(yield)
public static void main(String[] args) { MyThread my = new MyThread(); Thread t= new Thread(my,"燕双嘤"); t.start(); for (int i = 0; i < 5; i++) { if (i>2){ try{ t.join(); }catch (Exception e){ e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+":"+i); } } =================== main:0 main:1 main:2 燕双嘤:0 燕双嘤:1 main:3 main:4
可以使用join()方法让一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程完成后才可以继续执行。首先主线程运行,占用了CPU,期间子线程运行处于就绪状态,等待CPU调度,当遇到join()后主线程(main)处于就绪状态,子线程运行,等待子线程结束后主线程继续运行。
class MyThread implements Runnable { public void run() { for (int i=0;i<5;i++){ System.out.println(Thread.currentThread().getName()+":"+i); if(i==3){ Thread.currentThread().yield(); } } } } public class HelloWord { public static void main(String[] args) { MyThread my = new MyThread(); Thread t1= new Thread(my,"燕双嘤"); Thread t2= new Thread(my,"杜马"); t1.start();t2.start(); } } ====================================== 燕双嘤:0 杜马:0 杜马:1 杜马:2 杜马:3 燕双嘤:1 燕双嘤:2 燕双嘤:3 杜马:4 燕双嘤:4
如果顺序不对或者,其他结果是正常现象,因为CPU,内存都各种影响线程的运行。但是到3后一定会切换线程。
4.5,休眠(sleep)和中断(interrupt)
sleep和wait的区别:
【对象】这两个方法来自不同的类分别是Thread和Object。
【锁机制】最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法(锁代码块和方法锁)。
【使用范围】wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。
【捕获异常】sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常。
【唤醒机制】一个对象调用了wait方法,必须要采用notify()和notifyAll()方法唤醒该进程;一个对象调用了sleep方法进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,但在sleep的过程中过程中有可能被其他对象调用它的interrupt(),产生InterruptedException异常,如果你的程序不捕获这个异常,线程就会异常终止,进入TERMINATED状态。
调用 Thread.interrupt() 方法会将线程的中断标志设置为 true,如果线程正在阻塞中,则会抛出一个 InterruptedException 异常,如果线程没有阻塞,则需要在代码中检查中断标志来决定如何处理中断。
class MyThread implements Runnable { public void run() { try{ Thread.sleep(10000); }catch (Exception e){ e.printStackTrace(); System.out.println("遇到错误,终止休眠!"); } } } public class HelloWord { public static void main(String[] args) { MyThread my = new MyThread(); Thread t= new Thread(my,"燕双嘤"); t.start(); try { Thread.sleep(2000); }catch (Exception e ){ e.printStackTrace(); } t.interrupt(); } } ==================================================== java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at Package2.MyThread.run(HelloWord.java:8) at java.lang.Thread.run(Thread.java:748) 遇到错误,终止休眠!
一个线程启动之后进入休眠状态,原本是要休眠10s之后再继续执行,但是主方法在线程启动之后的两秒就将其中断,休眠一旦中断之后将执行catch中的代码,并利用里面的return语句返回程序调用处。
4.6,后台线程
Java中,只要前台有一个线程在运行,则整个Java进程都不会消失,所以此时可以设置一个后台线程,这样即使Java进程结束,此后台线程依然会继续执行。要想实现这样的操作,直接使用setDaemon()方法即可。
Thread t= new Thread(my,"燕双嘤"); t.start(); t.setDaemon(true);
4.7,线程的优先级
Java中的线程操作中,所有的线程在运行前都处于就绪状态,那么此时那个线程的优先级高,谁先执行。
定义 描述 表示的常量 public static final int MIN_PRIORITY 最低优先级 1 public static final int NORM_PRIORITY 中等优先级,是线程的默认状态 5 public static final int MAX_PRIORITY 最高优先级 10 class MyThread implements Runnable { public void run() { for (int i=0;i<3;i++){ System.out.println(Thread.currentThread().getName()+":"+i); } } } public class HelloWord { public static void main(String[] args) { MyThread my = new MyThread(); Thread t1= new Thread(my,"燕双嘤"); Thread t2= new Thread(my,"杜马"); Thread t3= new Thread(my,"步鹰"); t1.setPriority(Thread.MAX_PRIORITY);t2.setPriority(Thread.NORM_PRIORITY);t3.setPriority(1); t1.start();t2.start();t3.start(); } } ==================================================================== 燕双嘤:0 燕双嘤:1 燕双嘤:2 杜马:0 杜马:1 杜马:2 步鹰:0 步鹰:1 步鹰:2
线程将根据其优先级的大小来决定哪个线程会先运行,但是并非线程优先级越高就一定会执行,线程的执行是由CPU决定的,受到各种因素影响。主方法(main)的优先级是NORM_PRIORITY,也就是5,可以通过Thread.currentThread().getPriority()验证。