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

Java 多线程(一)

文章目录

  • 多进程
    • Thread
    • 创建线程,其它的写法
    • 面试题
    • Thread类的其它使用方式
    • start和run方法的区别
    • 中断一个线程
    • 线程等待 - join
    • 线程休眠
    • 线程的状态
    • 线程安全问题(最重要,最复杂的部分)

多进程

  1. 多进程并发编程的效率比较低:创建销毁进程都需要申请和释放资源,所以引入了多线程
  2. 同一个进程的线程之间,共用同一份资源(硬盘资源/内存资源)
  3. 进程是资源分配的基本单位,线程是调度执行的基本单位

Thread

  1. 并发执行 = 并发 + 并行
  2. 并发:两个线程在同一个cpu核心上执行,执行速度很快,看不出来是在同一个核心上执行的
  3. 并行:两个线程同时在两个不同的cpu核心上同时执行在这里插入图片描述
class MyThread extends Thread{public void run(){// run方法是线程的入口方法while(true) {System.out.println("hello Thread");}}
}
public class test {public static void main(String[] args) {Thread myThread = new MyThread();// run和start都是Thread的成员,start调用创建线程,线程再调用run方法// myThread.start();myThread.run();// 这句就是主动调用run,先执行完run的代码才会向下继续执行,就是单线程的执行流了while(true){System.out.println("hello main");}// 两者交替执行,并发执行}
}
  1. 多线程程序运行的时候可以使用IDEA或者是jconsole观察到该进程里多线程的运行情况
    在这里插入图片描述
  2. 找到jconsole
    启动jconsole要确保java中的进程已经跑起来了
    如果什么都不显示的话,需要用管理员方式运行
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
6. 可以看到该线程运行的事实运行情况,比如,你的程序卡死了
在这里插入图片描述
7. sleep
在这里插入图片描述

package Demo;import static java.lang.Thread.sleep;class MyThread extends Thread{public void run(){// run方法是线程的入口方法while(true) {System.out.println("hello Thread");try {sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
public class test {public static void main(String[] args) throws InterruptedException {Thread myThread = new MyThread();// run和start都是Thread的成员,start调用创建线程,线程再调用run方法myThread.start();// myThread.run();// 这句就是主动调用run,先执行完run的代码才会向下继续执行,就是单线程的执行流了while(true){System.out.println("hello main");sleep(1000);}// 两者交替执行,并发执行}
}
  1. 调度顺序是随机的
  2. main线程和mythread线程是并发执行的,是独立的执行流
    在这里插入图片描述

创建线程,其它的写法

  1. 继承Thread,重写run
package Demo;import static java.lang.Thread.sleep;class MyThread extends Thread{public void run(){// run方法是线程的入口方法while(true) {System.out.println("hello Thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
public class test {public static void main(String[] args) throws InterruptedException {Thread myThread = new MyThread();// run和start都是Thread的成员,start调用创建线程,线程再调用run方法myThread.start();// myThread.run();// 这句就是主动调用run,先执行完run的代码才会向下继续执行,就是单线程的执行流了while(true){System.out.println("hello main");Thread.sleep(1000);}// 两者交替执行,并发执行}
}
  1. 实现Runnable,重写run
    在这里v插入图片描述
package Demo;class MyRunnable implements Runnable{@Overridepublic void run() {while (true) {System.out.println("hello Thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
public class Demo2 {public static void main(String[] args) throws InterruptedException {Runnable myRunnable = new MyRunnable();// 利用Thread构造方法Thread t = new Thread(myRunnable);t.start();while(true){System.out.println("hello main!");Thread.sleep(1000);}}
}

使用Runnable的写法,和直接继承Thread之间的区别是解耦合
解耦合:让这个任务和这个线程关联程度变低,使得任务更容易被拆解出来
在这里插入图片描述
3. 继承Thread,重写run,使用匿名内部类

package Demo;public class Demo3 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(){public void run(){while(true) {System.out.println("hello Thread!");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}};t.start();while(true){System.out.println("hello main!");Thread.sleep(1000);}}
}
  1. 实现Runnable,重写run,使用匿名内部类
package Demo;public class Demo4 {public static void main(String[] args) throws InterruptedException {/*Runnable runnable = new Runnable() {@Overridepublic void run() {while(true){System.out.println("hello Thread!");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}};*/Thread t = new Thread(new Runnable(){public void run() {while(true){System.out.println("hello Thread!");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}});t.start();while(true){System.out.println("hello main!");Thread.sleep(1000);}}
}
  1. 使用lambda表达式,相当于匿名内部类的简化版本,lambda表达式本质上是一个匿名函数(没有名字的函数,用一次就用完了),主要用来实现’回调函数’的效果

回调函数:不是你主动调用的,也不是现在就立即调用的,把调用的机会交给别人(操作系统,库,框架,别人写的代码)来使用,别人会在合适的时机来调用这个函数

回调函数是通过函数指针调用的函数。你把一个函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,这就是回调函数。

比如qsort就是使用回调函数,在qsort中用函数指针调用比较的函数进行比较

在这里插入图片描述

面试题

  1. Java中有哪些创建线程的方式?
    除了上述的5中方法创建线程,还有其它的方式可以创建线程,后面我们也会学习到

Thread类的其它使用方式

  1. Thread的构造方法
    在这里插入图片描述
    在这里插入图片描述

  2. Thread的属性
    getName();
    在这里插入图片描述

  3. 是否是后台线程,isDaemon();
    后台线程(守护线程):后台线程不结束,并不影响整个线程的结束,整个线程结束了,后台线程也就结束了

前台线程:前台线程没有结束,整个进程是一定不会结束的

默认情况下一个线程是一个前台线程,如果isDaemon()设置为true就是后台线程

package Demo;public class Demo6 {public static void main(String[] args) {Thread t = new Thread(()->{while(true){System.out.println("hello Thread!");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}},"这是一个新线程!");t.setDaemon(true);t.start();}
}

在这里插入图片描述

在你的Java代码中,当将线程设置为守护线程(setDaemon(true))后不打印任何内容,这是因为主线程退出时JVM会立即终止所有守护线程,而不等待它们执行完毕。主线程是前台线程,t线程设置为了后台线程

  1. 使用 t.isAlive() 判定内核线程是不是已经没了,内核线程的生命周期是回到方法执行完毕,线程就没有了

在这里插入图片描述

package Demo;public class Demo7 {public static void main(String[] args) {Thread t = new Thread(()->{System.out.println("线程开始!");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程结束!");});System.out.println(t.isAlive());// 线程还没被创建出来是falset.start();System.out.println(t.isAlive());// 线程创建出来了,但是还没有被销毁是truetry {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(t.isAlive());// t线程被销毁了是false}
}

在这里插入图片描述

  1. lambda本身就是run方法

start和run方法的区别

  1. start方法内部会调用系统的api,在系统内核中创建线程
  2. run方法只是描述了线程中要执行的内容(会在start创建好之后就自动调用)

看起来两者的效果是相似的,但是本质上的区别是是否在系统内部创建出了新的线程

中断一个线程

  1. 中断一个线程就是让一个线程停止运行(销毁一个线程)
  2. 在Java中销毁/终止一个进程比较唯一,就是想办法让run方法尽快执行完毕

方法一:
可以在代码中手动创建出标志位作为run执行结束的条件

public class Demo8 {// 设置标志位来终止run方法的执行static boolean isQuit = false;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while(!isQuit){System.out.println("正在执行任务!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();Thread.sleep(5000);isQuit = true;}
}

在这里插入图片描述
局部变量是lambda的捕获,改成是成员变量就是内部类访问外部类的属性了,不再受final的修饰了

在这里插入图片描述
该方法的缺点:
1.需要手动创建变量
2.当线程内部在sleep的时候,主线程在修改变量,新线程内部不能及时的响应,比如在修改变量的同时,执行完了第一个sleep,需要再回到while的判断处结束新线程

方法二:

public class Demo9 {public static void main(String[] args){Thread t = new Thread(()->{while(!Thread.currentThread().isInterrupted()){System.out.println("线程正在工作!");try {Thread.sleep(1000);} catch (InterruptedException e) {// 1.假装没有听见,循环继续正常执行e.printStackTrace();// 2.加上一个break,让线程立即结束// 3.做一些其他工作,其他工作完成之后结束// 其他工作的代码放这里break;}}});// 报错是因为虽然 t.interrupt() 使 !Thread.currentThread().isInterrupted() 为false了// 但是是引得sleep发生了异常,发生的异常清除了 !Thread.currentThread().isInterrupted()的标志位t.start();try {Thread.sleep(5000);} catch (InterruptedException e) {throw new RuntimeException(e);}t.interrupt();}
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
sleep清除的标志位是为了让我们有更多的操作空间(Java希望我们收到要中断的信号后可以自由决定,接下来要做什么)

在这里插入图片描述

线程等待 - join

  1. 线程等待,让一个线程等待另一个线程执行结束,再继续执行,本质上是控制线程结束的顺序
  2. join实现线程等待效果
  3. 主线程中调用 t.join,就是主线程在等待t线程先结束

t.join的工作过程:
1.如果t线程正在执行时,调用join的线程(主线程)就会阻塞,要等待t线程执行完才会解除阻塞
2.如果t线程已经结束执行了,此时调用join线程就会直接返回,不会涉及线程阻塞
3.有一个超时时间的线程等待,如果超过这个线程就不会等待了,不会死等下去

public class Demo10 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{for(int i = 0;i < 5;i++){System.out.println("线程执行中!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();System.out.println("线程开始等待!");// 主线程等待t线程结束// 一旦调用join主线程就会阻塞,此时t线程就会完成后续的工作// 执行到t线程执行完毕之后,join才会解除阻塞,主线程继续执行t.join();System.out.println("线程等待结束!");}
}

线程休眠

  1. Thread.sleep
  2. sleep是有时间误差的
  3. 系统休眠完这个1000ms后就会从阻塞状态变为就绪状态,成了就绪状态后,不是说就能立即回到cpu上执行的,这中间会有调度的开销
public class Demo11 {public static void main(String[] args) throws InterruptedException {long beg = System.currentTimeMillis();Thread.sleep(1000);long end = System.currentTimeMillis();System.out.println("时间:" + (end - beg) + "ms");}
}

线程的状态

  1. 通过三种阻塞的状态可以初步确定线程卡死的原因是什么
    在这里插入图片描述
    TERMINATED:比如t对象还在,但是t线程结束了
public class Demo12 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while(true){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});System.out.println(t.getState());// Newt.start();// 线程正在运行过程中或者是正在等待运行就是 RUNNABLEfor(int i = 0;i < 5;i++){// 第一次还未执行上面的sleep是RUNNABLE// 后面有固定时间的阻塞了都是TIMED_WAITINGSystem.out.println(t.getState());Thread.sleep(1000);}t.join();// t对象还在,但是t线程已经结束了System.out.println(t.getState());// TERMINATED}
}

线程安全问题(最重要,最复杂的部分)

  1. 概念:有些代码在单个线程的环境下能够正确执行,但是同样的代码在多个线程的环境下会出现bug
    例子:
public class Demo13 {static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for(int i = 0;i < 50000;i++){count++;}});Thread t2 = new Thread(()->{for(int i = 0;i < 50000;i++){count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}

上面的代码存在bug
在这里插入图片描述

在这里插入图片描述
由于上述代码在多个线程的情况下执行,会存在线程调度是随机的问题,在某些情况下(不同的调度顺序)的逻辑是不正确的

例子:
在这里插入图片描述
其实是有无数种可能的,因为可能t1执行一次,t2可以执行2次,3次…

产生线程安全的原因:

  1. 操作系统中,线程的调度执行顺序是随机的(抢占式执行)
  2. 两个线程针对同一个变量进行修改
  3. 修改操作不是原子的,count++,就分为三步,(先读,再修改)
    类似的,如果一段逻辑中,需要根据一定的条件来决定是否修改,也会存在类似的问题
  4. 内存可见性问题(当前代码中不存在这种问题)
  5. 指令重排序问题(当前代码也不涉及)

要想解决线程安全问题要从上面的原因入手:
1.第一点是系统内核里实现的解决不了
2.第二点可以通过调整代码结构来规避上述问题,但是有很多情况是调整不了的
3.可以使用第三点 ,把count++变成原子的操作,可以进行加锁

使用加锁可以解决上面的问题,可以使用关键字synchronized

在这里插入图片描述
如果两个进程是在针对同一个对象进行加锁,就会产生锁竞争,如果不是针对同一个对象进行加锁,就不会产生锁竞争,就是并发执行

在这里插入图片描述

加锁的代码:

public class Demo13 {private static int count = 0;public static void main(String[] args) throws InterruptedException {// 加锁Object locker = new Object();Thread t1 = new Thread(()->{for(int i = 0;i < 50000;i++){synchronized(locker) {// 对count这三步操作进行加锁,把它变成一个原子的操作count++;}}});Thread t2 = new Thread(()->{for(int i = 0;i < 50000;i++){synchronized(locker){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);// 10w}
}

文章转载自:

http://KMPTLWVT.tnLnq.cn
http://dPx899sr.tnLnq.cn
http://sQLJ9OWv.tnLnq.cn
http://Fznr7tqL.tnLnq.cn
http://rg49XweP.tnLnq.cn
http://7tTehz48.tnLnq.cn
http://K6HexHwS.tnLnq.cn
http://1gJsmfy1.tnLnq.cn
http://t2ptwMPz.tnLnq.cn
http://fGkJFkdp.tnLnq.cn
http://yNL3Lvxo.tnLnq.cn
http://sq1AcagJ.tnLnq.cn
http://z1VxxTht.tnLnq.cn
http://YOIBhLyX.tnLnq.cn
http://MNvdTEzZ.tnLnq.cn
http://pJHHPGAo.tnLnq.cn
http://mAjkG0JU.tnLnq.cn
http://q7fAKLnF.tnLnq.cn
http://ip4hLuaz.tnLnq.cn
http://HLF20k1a.tnLnq.cn
http://JF1yXTt6.tnLnq.cn
http://6pNw8r67.tnLnq.cn
http://YWWItWXy.tnLnq.cn
http://luCvklXb.tnLnq.cn
http://Sx9Gvs3k.tnLnq.cn
http://g5GmYnlx.tnLnq.cn
http://iR0we0c3.tnLnq.cn
http://frXM57po.tnLnq.cn
http://s5RE6kzq.tnLnq.cn
http://zpUnGdek.tnLnq.cn
http://www.dtcms.com/a/373431.html

相关文章:

  • Excel VBA 自动生成文件夹框架
  • 算法日记---滑动窗口
  • 《嵌入式硬件(四):温度传感器DS1820》
  • 动态规划-学习笔记
  • Java分布式锁详解
  • Docker学习笔记(四):网络管理与容器操作
  • 基于MATLAB的FIR和IIR低通带通滤波器实现
  • SpringMVC 程序开发
  • 深入理解 Linux hostname 命令:从日常操作到运维实战
  • SN码追溯技术全景解析:AI时代的数字身份革命
  • AI 小白入门:探索模型上下文协议(MCP)及其前端应用
  • 代码随想录70期day5
  • Vue3源码reactivity响应式篇之reactive响应式对象的track与trigger
  • GitHub高星标项目:基于大数据的心理健康分析系统Hadoop+Spark完整实现
  • Google Guice @Inject、@Inject、@Singleton等注解的用法
  • 【MATLAB组合导航代码,平面】CKF(容积卡尔曼滤波)作为融合方法,状态量8维,观测量4维,包含二维平面上的严格的INS推导。附完整代码
  • Go Style 代码风格规范
  • Java 16 中引入的 record的基本用法
  • uni-app iOS 性能监控全流程 多工具协作的实战优化指南
  • shell 中 expect 详解
  • 告别低效:构建健壮R爬虫的工程思维
  • Ubuntu中显示英伟达显卡的工具软件或者指令
  • 银行卡号识别案例
  • 【golang学习笔记 gin 】1.2 redis 的使用
  • AI提示词(Prompt)基础核心知识点
  • VTK开发笔记(五):示例Cone2,熟悉观察者模式,在Qt窗口中详解复现对应的Demo
  • Excel 表格 - Excel 减少干扰、专注于内容的查看方式
  • Vue3 + Ant Design Vue 全局配置中文指南
  • CSS in JS 的演进:Styled Components, Emotion 等的深度对比与技术选型指引
  • 哈士奇vs网易高级数仓:数据仓库的灵魂是模型、数据质量还是计算速度?| 易错题