当前位置: 首页 > article >正文

JAVAEE初阶第二节——多线程基础(上)

系列文章目录

JAVAEE初阶第二节——多线程基础(上)


计算机的工作原理

  1. 认识线程(Thread)
  2. Thread 类及常见方法
  3. 线程的状态

文章目录

  • 系列文章目录
    • JAVAEE初阶第二节——多线程基础(上)
  • 计算机的工作原理
  • 一.认识线程(Thread)
    • 1.概念
      • 1.1为啥要有线程
      • 1.2 线程
        • 1.2.1 线程如何解决额外开销问题
      • 1.3进程和线程的区别(重难点)
      • 1.4 Java 的线程 和 操作系统线程 的关系
        • 1.4.1 Thread 类实现一个多线程程序:
        • 1.4.2 多线程具体表现
    • 2.创建线程的几种方法
  • 二. Thread 类及常见方法
    • 1. Thread 的常见构造方法
    • 2.Thread 的几个常见属性
    • 3.启动一个线程-start()
      • 3.1 run 和 start的区别
    • 4.终止一个线程
      • 4.1 使用标志位终止线程
      • 4.2使用Thread.currentThread().isInterrupted() 代替自定义标志位
        • 清除标志位的作用
        • try catch 语句要起到的作用
    • 5.等待一个线程-join()
      • 5.1 案例一(计算1 + 2 +3.....1000)
      • 5.2 案例二(计算1 + 2 +3.....1000000000)(线程安全)
    • 6.获取当前线程引用
    • 7.休眠当前线程
  • 三.线程的状态


一.认识线程(Thread)

1.概念

1.1为啥要有线程

  1. “并发编程” 成为 "刚需

单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU资源
有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程
有些任务场景下,需要频繁的创建和销毁进程的时候,此时如果使用多进程编程,系统开销就会很大。

例如,早期编写一个服务器程序的时候,是基于多进程的编程模式。服务器同一时刻收到很多请求时,针对每个请求,都会创建出一个进程,给这个请求提供一定的服务并返回对应的响应。
一旦这个请求处理完了,此时这个进程就要销毁了。于是在一天的时间里服务器就需要不停的创建新的进程,也不停的销毁旧的进程频繁创建和释放这样的操作,开销是比较大的!

  1. 虽然多进程也能实现 并发编程, 但是线程比进程更轻量

创建线程比创建进程更快.
销毁线程比销毁进程更快.
调度线程比调度进程更快

  1. 最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 “线程池”(ThreadPool) 和 “协程” (Coroutine)(后面介绍)

1.2 线程

一个线程就是一个 “执行流”. 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 “同时” 执行着多份代码.

1.2.1 线程如何解决额外开销问题

在这里插入图片描述
这个图说明了一个进程有一个PCB,但是实际上,一个进程可以有多个PCB.意味着这个进程包含了一个线程组,其中就包含了多个线程.

下面就用这样的方式来表示一个线程
在这里插入图片描述

  • PCB有内存指针这个属性,而多线程的PCB的内存指针就指向的是同一个内存空间,这就意味着,只需要在创建第一个线程的时候需要从系统分配资源后续的线程,就不必分配,直接共用前面的那份资源就可以了。(除了内存之外,文件描述符表(操作硬盘)也是多个线程共用一份的)

1)操作系统,进行"多任务调度"本质上是在调度PCB。(线程在系统中的调度规则,就和之前的进程是一样的)
2)但也不是随便搞两个线程,就能资源共享。把这些能够资源共享的这些线程,分成组,就称为"线程组"。再换句话讲,线程组,也就是进程的一部分

![[Pasted image 20240303092011.png]]

有了线程以后资源分配的基本单位还是进程,但是调度执行的基本单位变成了线程

1.3进程和线程的区别(重难点)

  1. 进程是包含线程的,一个进程中至少会有一个线程(主线程)
  2. 每一个线程都是独立的执行流,可以执行以下代码,并单独参与到CPU调度中(每个线程都有自己的一份支持线程调度的PCD属性(状态,上下文,记账信息,优先级))
  3. 进程与进程之间是相互独立的,不共享资源(内存空间和文件描述符表),每个进程都有自己的资源。同一个进制中的所有线程都公用这一份资源(内存空间和文件描述符表)
  4. 进程与进程间互不影响。但同一个进程中的某个线程异常是可能到处该进程中的其他线程异常终止的(严重情况下这个进程也会异常终止)
  5. 同一个进程中的线程间会相互干扰从而引起线程安全问题。线程太多也会让调度开销更大。(线程数量要更好合适)
  6. 进程是系统分配资源的基本单位,线程是系统调度的基本单位。

1.4 Java 的线程 和 操作系统线程 的关系

线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使 用(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.

1.4.1 Thread 类实现一个多线程程序:
//1.首先创建一个类,继承自Thread
 class MyThread extends Thread{
	  @Override
	  public void run(){
		  // 这个run方法就是该线程的入口方法
		  System.out.println("Hellow  World!!");
	  }
 }
 public class ThreadTest{
	public static void main(String[] args){
	//2.根据干才创建的类,创建出实例(线程的实例才是真正的线程)
	Thread thread = new MyThread();
	//3.调用 Thread 中的 start方法才能真正的调用到系统的api,在系统内核中创建出一个线程。
	thread.start();
	}
 }
1.4.2 多线程具体表现

每个线程都是一个独立的执行流,每个线程都能够独立的去cpu上调度执行

class MyThread extends Thread{  
@Override  
	public void run(){  
		while (true){  
		System.out.println("This is Thread!");  
		}  
	}  
}  
public class ThreadTest{  
	public static void main(String[] args){  
		Thread t = new MyThread();  
		t.start();  
		while (true){  
		System.out.println("This is Main");  
		}  
	}  
}
  1. 上面这个多线程代码如果按照之前的理解,如果在一个代码中出现两个死循环,那就只会执行一个,另一个循环就进不去了,可是执行程序后两个程序发现都在执行
    在这里插入图片描述

这说明这两个线程就是独立的执行流

  • 过程分析:

此处在调用start创建线程之后,程序两兵分两路,一路沿着main方法继续执行打印This is Main,另一路进入到线程的run方法,打印This is Thread!

当有多个线程的时候,这些线程执行的先后顺序是不确定的,因为在操作系统内核中有一个“调度器”,它能实现一种类似“随机调度”的效果(与多线程线程安全有关)

刚才只是通过打印的方式,看到了两个执行流还可以通过一些第三方的工具,更直观的看到多个线程的情况。jdk中,有一个jconsole工具。

在这里插入图片描述

console只能列出java的进程其他不是java的程序,无法分析的。

![[Pasted image 20240303135246.png]]

第一个main就对应main方法的主线程
Thread-0就是创建的t线程(名字一部分改)

其余的线程都是JVM自带的线程,这些自带的线程,要完成一些垃圾回收(gc:自动释放内存),监控统计各种指标(为错误代码提供参考和线索),把统计指标通过网络的方式,传输给其他程序。

  1. 由于这俩循环,都是死循环,循环体就是单纯打印一旦程序运行起来,这俩循环就会转的飞快!也会导致cpu占用率比较高,进一步就会提高电脑的功耗(电脑非常烫)

所以就可以在循环中加上sleep来降低循环的速度,这里用的是JAVA中封装后的版本,是Thread提供的静态方法

class MyThread extends Thread{  
	@Override  
	public void run() {  
		while(true){  
			System.out.println("This is Thread!");  
			try {  
			Thread.sleep(1000);  
			} catch (InterruptedException e) {  
			throw new RuntimeException(e);  
			}  
		}  
	}  
}
public class ThreadTest {  
	public static void main(String[] args) throws InterruptedException {  
		Thread thread = new MyThread();  
		thread.start();  
		while(true){  
			System.out.println("This is Main!");  
			Thread.sleep(1000);  
			/*try {  
			Thread.sleep(1000);  
			} catch (InterruptedException e) {  
			throw new RuntimeException(e);  
			}  
			*/  
		}  
	}  
}

在这里插入图片描述

线程在sleep(1000)的过程中就可能被提前唤醒

上面MyThread类的sleep只能try catch,不能throws是因为该类继承自的父类Thread中的run方法就没有throws这个异常,如果在子类重写的方法中加上throws就修改了方法签名,就不能构成“重写”了。

2.创建线程的几种方法

  1. 继承 Thread 类,重写run。
//1.第一步:继承 Thread 来创建一个线程类
class MyThread extends Thread{  
@Override  
	public void run(){  
		System.out.println("线程运行的代码");  
	}  
}
public class ThreadTest {  
	public static void main(String[] args)  {  
	//2.第二步:创建 MyThread 类的实例
		Thread thread = new MyThread();  
	//3.第三步:调用 start 方法启动线程	
		thread.start();  
}
  1. 实现Runnable接口, 重写run。
//1.第一步:实现 Runnable 接口
class MyThread1 implements Runnable{  
	@Override  
	public void run() {  
	System.out.println("线程运行的代码!");  
	}  
}  
public class ThreadTest {  
	public static void main(String[] args) {  
	//2.第二步:创建 Thread 类实例,调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.
		Runnable runnable = new MyThread1();  
		Thread thread = new Thread(runnable);  
	//3.第三步:调用 start 方法
		thread.start();  
	}  
}

第一步这里的Runnable可以理解为“可执行的”,通过这个接口就可以抽象表示出一段可以被其他实体执行的代码(不仅仅可以搭配线程来使用)

这种写法还是要搭配Thread类才能真正在系统中创建出进程
这种写法其实就是把线程和要执行的任务解耦合了

  1. 使用匿名内部类继承Thread,重写run
public class ThreadTest {  
	public static void main(String[] args) {  
		Thread thread = new Thread(){  
			@Override  
			public void run() {  
			System.out.println("线程运行的代码!");  
			}  
		};  
		thread.start();  
	}  
}

上面创建线程thread的实例后面{ }的意思是要定义一个类,与此同时,这个新的类继承自Thread。此处 { } 中可以定义子类的属性和方法。

  1. 实现Runnable,重写run,匿名内部类
public class ThreadTest {  
	public static void main(String[] args) {  
		Thread thread = new Thread(new Runnable() {  
			@Override  
			public void run() {  
			System.out.println("线程运行的代码!");  
			  
			}  
			});  
		thread.start();  
	}  
}

Thread构造方法的参数,填写了Runnable的匿名内部类的实例

  1. lambda 表达式创建 Runnable 子类对象(推荐)
public class ThreadTest {  
	public static void main(String[] args) {  
		Thread thread = new Thread(() -> {  
		System.out.println("线程运行的代码!");  
		});  
		thread.start();  
	}  
}

这个写法相当于实现Runnable重写run,lambda表达式代替了Runnable的位置

()里是形参列表,这里能带参数。线程的入口不需要参数。比如使用lambda代替Comparator,可以带上两个参数。
前面应该有一个函数名,此处作为匿名函数,就没有名字了

函数式接口属于lambda背后的实现,相当于java在没破坏原有的规则的基础上给了lambda一个合法性解释.

编译器编译的时候Thread构造方法,有好几个版本,于是就挨个往里匹配,其中匹配到Runnable这个版本的时候,发现正好有个run方法,无参数。正好和lambda能匹配上。

二. Thread 类及常见方法

Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联

1. Thread 的常见构造方法

在这里插入图片描述

Thread t1 = new Thread();  
Thread t2 = new Thread(new MyRunnable());  
Thread t3 = new Thread("这是新的线程");  
Thread t4 = new Thread(new MyRunnable(), "这是新的线程");

自己创建的线程,默认是按照Thread-01234。给不同的线程,起不同的名字。(对于线程的执行,没有啥影响),主要是方便调试。

//演示线程名字如何修改的代码
public class ThreadTest {  
	public static void main(String[] args) {  
		Thread thread = new Thread(() -> {  
		System.out.println("线程运行的代码!");  
		}, "新的线程");  
		thread.start();  
	}  
}

在这里插入图片描述

线程之间的名字是可以重复的,同一个工作,需要多个线程完成,都可以起一样的名字。但是名字也不要乱起。最好还是有一定的描述性.

2.Thread 的几个常见属性

![[Pasted image 20240303155626.png]]

ID是JVM自动分配的身份标识,会保证唯一性
名称:各种调试工具用到
状态:进程中有:就绪状态,阻塞状态。线程也有状态。Java中对线程的状态,又进行了进一步的区分(比系统原生的状态,更丰富一些)
优先级:由于系统随机调度,在java中,设置优先级,效果不是很明显(对内核调度器的调度过程产生一些影响)
关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
是否存活,即简单的理解,为 run 方法是否运行结束了

  • 如何理解“后台线程”与“前台线程”:
public class ThreadTest {  
	public static void main(String[] args) {  
	Thread thread = new Thread(() -> {  
	while (true) {  
		System.out.println("线程运行的代码!");  
		}  
		}, "新的线程");  
	thread.start();  
	}  
}

![[Pasted image 20240303162102.png]]

仔细观察,线程列表中,已经没有main了!按照之前的理解,main执行完毕,进程就应该结束!但是很明显,这个进程仍然在继续执行!(这些其他的都是jvm内置的线程,它们都是后台线程,都不会阻止进程结束)

![[Pasted image 20240303161833.png]]

只有出现这句话才是说明进程结束了

咱们代码创建的线程,默认就是前台线程,会阻止进程结束。只要前台线程没执行完,进程就不会结束。即使main已经执行完毕了

将这个线程设置成“守护线程”(后台线程)就能解决这个问题了。

public class ThreadTest {  
	public static void main(String[] args) {  
		Thread thread = new Thread(() -> {  
		while (true) {  
			System.out.println("线程运行的代码!");  
			}  
		}, "新的线程");  
		//在线程开始前将这个线程设置为“守护线程”(后台线程),这样就不会阻止进程的结束了  
		thread.setDaemon(true);  
		thread.start();  
	}  
}

在这里插入图片描述

再次执行,发现,控制台啥都没打印,就没了.进程就结束了!

“后台线程”说明这个线程是你不能轻易感知到的线程,所以它就不能阻止进程结束,否则进程无法结束了你却感知不到,这就不太合理。
相对的“前台进程”说明这个线程是你能明显看到的线程,所以它就能阻止进程结束。你也方便对它进行调整。

3.启动一个线程-start()

之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了.只有Thread类使用start方法,启动一个线程,线程才开始运行。对于同一个Thread对象来说,start只能调用一次

如果调用一次就会报错

![[Pasted image 20240303170750.png]]

3.1 run 和 start的区别

start和run其实是八竿子打不着,互不相干的内容

class MyThread3 extends Thread{  
	@Override  
	public void run() {  
		System.out.println("This is Thread!");  
	}  
}  
public class ThreadTest {  
	public static void main(String[] args) throws InterruptedException {  
		Thread thread = new MyThread();  
		thread.start();  
		//thread.run();  
	}  
}

这里看起来的执行结果是一样的,但是调用start()是创建一个新的线程,
由新的线程执行打印This is Thread!而调用run()还是在main主线程中,打印的hello。

这里只要改一下代码,调用run()的错误就能体现出来了。

class MyThread3 extends Thread{  
	@Override  
	public void run() {  
		while(true){  
			System.out.println("This is Thread!");  
			try {  
			Thread.sleep(1000);  
			} catch (InterruptedException e) {  
			throw new RuntimeException(e);  
			}  
		}  
	}  
}  
public class ThreadTest {  
	public static void main(String[] args) throws InterruptedException {  
		Thread thread = new MyThread();  
		thread.run();  
		//thread.start();  
		while (true) {  
			System.out.println("This is Main!");  
			Thread.sleep(1000);  
			try {  
			Thread.sleep(1000);  
			} catch (InterruptedException e) {  
			throw new RuntimeException(e);  
			}  
		}  
	}  
}

在这里插入图片描述

调用run的时候执行程序就能发现控制台中只有thread线程的信息,没有main的信息
这是因为主函数调用run()后,代码就只能停留在run的循环中下方main中的循环是无法执行到的。

在这里插入图片描述

调用start()后就能发现thread线程的信息和main的信息都能正常打印了

4.终止一个线程

4.1 使用标志位终止线程

public class ThreadStop {  
    //1.定义一个静态的成员变量 isQuit作为线程结束的标志位
	private static boolean sign = false;  
	public static void main(String[] args) {  
	Thread thread = new Thread(()->{  
	//2.当标注位为true时,线程结束
		while(!sign){  
			System.out.println("线程工作中!");  
			try {  
			Thread.sleep(1000);  
			} catch (InterruptedException e) {  
			throw new RuntimeException(e);  
		}  
	}  
	System.out.println("线程工作完毕!");  
	});  
	thread.start();  
	try {  
	Thread.sleep(4000);  
	} catch (InterruptedException e) {  
	throw new RuntimeException(e);  
	}  
	System.out.println("线程即将结束工作!");  
	//3.改变标注位的值,让线程能够因为它的改变而结束
	sign = true;  
	}  
}

通过上述代码,就可以让线程结束掉,具体线程啥时候结束,取决于在main线程中何时修改sign的值。
当前代码中main线程必须打印完毕之后才会设置sign。thread线程中的线程才能结束,然后才会打印线程工作完毕!
当sign的值被修改后,是主线程先执行的打印操作还是thread线程先执行的打印操作就不确定了(随机调度引起)

这种方法想要在main线程中结束thread线程,前提必须是thread的中存在根据标志位改变结束的代码。而不是thread中的代码可以随便写都能实现。

注意: 如果把isQuit作为main方法中的局部变量是不可行的,会编译报错

在这里插入图片描述

报错信息提示你这个捕获的变量,得是final或者"事实final"

这是因为lambda表达式讲过的一个语法,变量捕获,lambda表达式/匿名内部类是可以访问到外面定义的局部变量的!

lambda表达式,本质上是"函数式接口"=>匿名内部类.而内部类,访问外部类的成员这个事情本身就是可以的!这个事情就不受变量捕获的影响了.

4.2使用Thread.currentThread().isInterrupted() 代替自定义标志位

Thread.currentThread( )这个操作,是获取当前线程实例()哪个线程调用,得到的就是哪个线程的实例(类似于this)

public class ThreadStop {  
	public static void main(String[] args) throws InterruptedException {  
		Thread thread = new Thread(()->{  
			while(!Thread.currentThread().isInterrupted()){  
				System.out.println("线程工作中!");  
				try {  
				Thread.sleep(1000);  
					} catch (InterruptedException e) {  
						e.printStackTrace();
					}  
				}  
				System.out.println("线程工作完毕!");  
			});  
		thread.start();  
		try {  
		Thread.sleep(4000);  
		System.out.println("线程即将结束工作!");  
		thread.interrupt();  
	}  
}

这个代码本质上,就是使用Thread实例内部自带的标志位,来代替刚才手动创建的sign变量了.

在这里插入图片描述

执行一次代码,可以看到,代码中出现了一个异常但是thread线程并没有真的结束!
如果没有sleep,interrupt可以让线程顺利结束,但是有sleep引起了变数!在执行sleep的过程中,调用interrupt但可能sleep休眠时间还没到,被提前唤醒了。

被提前唤醒线程被会做两件事:
1.抛出InterruptedException(紧接着就会被catch获取到)
2.清除Thread对象的islnterrupted标志位(不只sleep,很多方法都会清除异常)
于是明明已经通过interrupt方法,已经把标志位设为true了但是sleep提前唤醒操作,就把标志位又设回false(此时循环还是会继续执行了)
这种情况下要想让线程结束,只需要在catch中加上break就行了

public class ThreadStop {  
	public static void main(String[] args) throws InterruptedException {  
		Thread thread = new Thread(()->{  
			while(!Thread.currentThread().isInterrupted()){  
				System.out.println("线程工作中!");  
				try {  
				Thread.sleep(1000);  
					} catch (InterruptedException e) {  
						e.printStackTrace();
						break;
					}  
				}  
				System.out.println("线程工作完毕!");  
			});  
		thread.start();  
		try {  
		Thread.sleep(4000);  
		System.out.println("线程即将结束工作!");  
		thread.interrupt();  
	}  
}

![[Pasted image 20240303191044.png]]

清除标志位的作用

sleep清空标志位,是为了给程序猿更多的可操作性空间"
前一个代码,写的是sleep(1000),结果现在1000还没到,就要终止线程.这就相当于是两个前后矛盾的操作。此时,要想写更多的代码,来对这样的情况进行具体的处理的.程序猿就可以在catch语句中,加入一些代码,来做一些处理
1)让线程立即结束 (加上break)
2)让线程不结束,继续执行 (不加break)
3)让线程执行一些逻辑之后,再结束 ( 写一些其他代码,再break)

try catch 语句要起到的作用

对于一个服务器程序来说,稳定性,是非常重要的!无法保证服务器就一直不出问题,这些"问题"在java代码中,就会以异常的形式体现出来并可以通过catch语句,对这些异常进行处理.
处理的方式:
1)尝试自动恢复:能自动恢复,就尽量自动回复 (比如出现了一个网络通信相关的异常
就可以在catch尝试重连网络)
2)记录日志:(异常信息记录到文件中)有些情况,并非是很严重的问题只需要把这个问题记录下来即可。(并不需要立即解决,后面程序员有空的时候再解决)
3)发出报警:针对一些比较严重的问题。(给程序员发邮件,发短信,发微信,打电话…)
4)也有少数的正常的业务逻辑,会依赖到catch.比如文件操作中有的方法,就是要通过catch来结束循环之类的。(非常规操作)

5.等待一个线程-join()

有时,需要等待一个线程完成它的工作后,才能进行自己的下一步工作。但是多个线程的执行顺序是不确定(随机调度,抢占式执行)虽然线程底层的调度是无序的,但是可以在应用程序中,通过一些api,来影响到线程执行的顺序,join就是一种方式。

比如,thread2线程等待thread1线程,此时,一定是thread1先结束,thread2后结束.使用join是可能会使t2线程阻塞的。

  • main线程在thread线程后结束
public static void main(String[] args) throws InterruptedException {  
	Thread thread = new Thread(()->{  
	while(!Thread.currentThread().isInterrupted()){  
		System.out.println("thread线程工作中!");  
		try {  
			Thread.sleep(1000);  
		} catch (InterruptedException e) {  
			break;  
		}  
	}  
	System.out.println("thread线程工作完毕!");  
	});  
	thread.start();  
	Thread.sleep(4000);  
	thread.interrupt();  
	System.out.println("这里是主线程!");  
}

上面代码从看上去应该是主线程的日志在thread线程结束并打印结束的日志后才会打印,可是真的是这样吗?

在这里插入图片描述

运行起来后发现结果和想象的不太一样,这是因为4000ms的时间结束了后,此时是先打印thread线程结束的日志还是主线程的日志就不好说了.
并且,使用Thread.sleep(4000)来让主线程"等待"thread线程完全是因为在当前的代码逻辑下,已经知道了thread线程会在4000ms后结束,如果thread线程的结束时间是未知的,这种方法就无能为力了.
这时候就可以使用join来实现 main线程在thread线程后结束

public static void main(String[] args) throws InterruptedException {  
	Thread thread = new Thread(()->{  
	Random random = new Random();  
	int n = random.nextInt(10);  
	for(int i = 0;i < n;i++){  
		System.out.println("thread线程工作中!");  
		try {  
			Thread.sleep(1000);  
		} catch (InterruptedException e) {  
			e.printStackTrace();  
		}  
	}  
	System.out.println("线程工作完毕!");  
	});  
	thread.start();  
	//线程等待,在哪个线程中调用就是哪个线程等待  
	thread.join();//main线程等待thread线程  
	thread.interrupt();  
	System.out.println("这里是主线程");  
}

在这里插入图片描述

运行上面代码,发现在不知道thread线程的结束时间的情况下,结果就和设想的一样.
上述代码实现控制线程结束实现的先后,是通过api来控制的.通过让main线程不直接去调度器中参加调度,而是先等待thread线程执行结束,再参与调度.

5.1 案例一(计算1 + 2 +3…1000)

private static int result = 0;  
public static void main(String[] args) {  
	Thread thread = new Thread(()->{  
		for (int i = 1; i <= 1000 ; i++) {  
			result += i;  
		}  
	});  
	thread.start();  
	System.out.println("result = " + result);  
}

在这里插入图片描述

如果上面这种写法就不能得出结果,因为如果主线程在thread线程还没有计算完result的值就直接打印result,就会导致result的结果仍然是0.
所以要想让main线程得到正确的result,就需要让main等待thread线程执行结束.(join)

public static void main(String[] args) throws InterruptedException {  
	Thread thread = new Thread(()->{  
		for (int i = 1; i <= 1000 ; i++) {  
			result += i;  
		}  
	});  
	thread.start();  
	thread.join();  
	System.out.println("主线程的result = " + result);  
}

在这里插入图片描述

5.2 案例二(计算1 + 2 +3…1000000000)(线程安全)

  1. 用一个线程来计算
public static void main(String[] args) throws InterruptedException {  
	Thread thread = new Thread(()-> {//线程1  
		for (long i = 1; i <= 100_0000_000l ; i++) {  
			result += i;  
		}  
	});  
	thread.start();  
	long begin = System.currentTimeMillis();//thread线程开始的时间(时间戳)  
	thread.join();  
	long end = System.currentTimeMillis();//thread线程结束的时间(时间戳)  
	System.out.println("result = " + result );  
	System.out.println("time = " + (end - begin) + "ms" );  
}

在这里插入图片描述

因为计算的数目较大,为了节省时间,可以再加上一个线程进行计算

  1. 用两个线程计算(线程安全问题)
public static void main(String[] args) throws InterruptedException {  
	Thread thread1 = new Thread(()-> {//线程1  
		for (long i = 1; i <= 50_0000_000l ; i++) {  
			result += i;  
		}  
	});  
	Thread thread2 = new Thread(()-> {//线程2  
		for (long i = 1; i <= 50_0000_000l ; i++) {  
			result += i;  
		}  
	});  
	thread1.start();  
	thread2.start();  
	long begin = System.currentTimeMillis();//thread线程开始的时间(时间戳)  
	  
	thread1.join();  
	thread2.join();  
	long end = System.currentTimeMillis();//thread线程开始的时间(时间戳)  
	System.out.println("result = " + result );  
	System.out.println("time = " + (end - begin) + "ms" );  
}

在这里插入图片描述

从运行结果来看,计算时间确实变快了,但是计算结果为什么又不对了呢?因为这里存在线程安全问题(后面介绍)

在这里插入图片描述

第一种join:死等,一直等下去直到等待的线程结束(不科学)
后面两种join都是有超时时间的等待,超过了这个时间就不等了
interrupt方法可以将阻塞等待的join提前唤醒.

6.获取当前线程引用

这个方法在前面终止一个线程的时候使用过(Thread.currentThread().isInterrupted())

在这里插入图片描述

public static void main(String[] args) {  
	Thread thread = Thread.currentThread();  
	System.out.println(thread.getName());  
}

如果是继承Thread,直接使用this拿到线程实例.如果是Runnable或者lambda的方式,this就无能为力了。此时this已经不再指向Thread对象就只能使用Thread currentThread( ):来获取到Thread对象的引用了

7.休眠当前线程

前面使用较多的一组方法,有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的.

在这里插入图片描述

public static void main(String[] args) throws InterruptedException {  
	System.out.println(System.currentTimeMillis());  
	Thread.sleep(3 * 1000);  
	System.out.println(System.currentTimeMillis());  
}

三.线程的状态

线程的状态是一个枚举类型 Thread.State

public class ThreadState {  
	public static void main(String[] args) {  
			for (Thread.State state : Thread.State.values()) {  
				System.out.println(state);  
			}  
		}  
	}

JAVA中,线程有下面几种状态

  1. NEW Thread对象创建好了,但是还没有调用start方法在系统中创建线程
  2. TERMINATED Thread对象仍然存在,但是系统内部的线程已经执行完毕了。
  3. RUNNABLE 就绪状态,表示这个线程正在cpu上执行,或者准备就绪随时可以去Cpu上执行。
  4. IMED WAITING 指定时间的阻塞。就在到达一定时间之后自动解除阻塞使用sleep会进入这个状态。使用带有超时时间的join也会进入这个状态。
  5. WAITING 不带时间的阻塞(死等),必须要满足一定的条件,才会解除阻塞。join或者wait都会进入WAITING.
  6. BLOCKED由于锁竞争,引起的阻塞。
  • 线程状态转移图
    ![[Pasted image 20240304112245.png]]

这些状态,最大的作用就是调试多线程代码的bug的时候,作为重要的参考依据


http://www.kler.cn/a/288393.html

相关文章:

  • 算法每日双题精讲——滑动窗口(长度最小的子数组,无重复字符的最长子串)
  • 前端vue 列表中回显并下拉选择修改标签
  • Sql server查询数据库表的数量
  • 【WRF理论第十二期】输出文件:wrfout 和 wrfrst
  • DAY112代码审计PHP开发框架POP链利用Yii反序列化POP利用链
  • @ComponentScan:Spring Boot中的自动装配大师
  • golang关于slice map函数传参的小问题
  • 娱乐小项目-树莓派履带小车
  • 中兴-ZSRV2路由器-任意文件读取
  • arcgisjs4.0 内网部署字体不显示问题处理
  • 【技术详解】Java泛型:全面解析与实战应用(进阶版)
  • sqli-labs靶场通关攻略(六十一关到六十五关)
  • ARM/Linux嵌入式面经(三十):腾讯 C++开发工程师
  • 【Linux学习】Linux开发工具——vim
  • html+css+js网页设计 博物馆 亚历山大美术馆6个页面
  • Flask中的g的作用
  • Linux学习笔记(4)----Debian压力测试方法
  • 日本IT编程语言对比分析-Python /Ruby /C++ /Java
  • 【加密社】马后炮视角来看以太坊二层战略
  • LLM大模型:不要怪大模型回答质量不行了,那是你不会问~
  • 计算机视觉之 SE 注意力模块
  • 微信小程序接入客服功能
  • 逆向工程核心原理 Chapter23 | DLL注入
  • 【舍入,取整,取小数,取余数丨Excel 函数】
  • 探索四川财谷通信息技术有限公司抖音小店的独特魅力
  • 收银系统源码-收银台UI自定义