java并发编程

1. 前言

  • 预备知识
    • 线程安全问题,需要你接触过Java Web开发、Jdbc开发、Web服务器、分布式框架时才会遇到
    • 基于JDK8,最好对函数式编程、lambda有一定了解
    • 采用了slf4j打印日志,这是好的实践
    • 采用了lombok简化java bean编写
    • 给每个线程好名字,这也是一项好的实践

学习总纲:
图片

2. 进程与线程

  • 进程和线程的概念
  • 并行和并发的概念
  • 线程基本应用

2.1 进程和线程

进程

  • 概念:

    • 进程是计算机中正在运行的程序的实例,它具有一定的独立功能,是系统进行资源分配和调度的一个独立单位
    • 进程可以包含代码、数据和堆栈等资源,是程序在计算机中执行的基本单位
    • 一个进程可以包含多个线程,线程之间共享进程的资源
    • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360安全卫士等)
  • 特征:并发性、异步性、动态性、独立性、结构性

线程

  • 概念:
    • 线程是进程的一个实体,是CPU 调度和分派的基本单位
    • 线程之间共享进程的内存空间和其他系统资源
    • 线程可以看做是一个轻量级的进程,它允许一个程序在同一时间执行多个任务,从而提高程序的执行效率

进程线程的区别

  1. 资源占用:进程拥有独立的资源,而线程共享进程的资源。
  2. 执行效率:线程比进程更轻量级,线程之间的切换比进程之间的切换更高效
  3. 任务调度:线程是 CPU 调度的基本单位,一个进程内的所有线程共享相同的

2.2 串行,并行,并发

  1. 串行:在计算机系统中,多个任务一个接一个地顺序执行(单核处理器)

  2. 并行:在计算机系统中,多个任务在同一时刻同时执行(多核处理器)

  3. 并发:在计算机系统中,多个任务在同一时间段内执行,但不是同时执行。这意味着并发可以通过任务切换来实现,在单核处理器系统中,CPU 会在不同任务之间进行快速切换,以实现并发执行

案例:

  • 串行:家庭主妇做饭、打扫卫生、给孩子喂奶,做完一件事再去做另一件事
  • 并行:家庭主妇雇了3个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰
  • 并发:家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事

2.3 线程基本应用

  • 同步:需要等待结果返回,才能继续运行就是同步
  • 异步:不需要等待结果返回,就能继续运行就是异步

结论:

  • 在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程
  • tomcat 的异步servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞tomcat 的工作线程
  • ui程序中,开线程进行其他操作,避免阻塞ui线程

结论:

  1. 单核CPU下,多线程不能提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用CPU,不至于一个线程总占用CPU,别的线程没法运行
  2. 多核CPU可以并行跑多个线程,但能否提高程序运行效率还是要分情况。将任务拆分,并行执行,可以提高程>序的运行效率。
    • 注意:不是所有计算任务都能拆分,也不是所有任务都需要拆分
  3. IO操作不占用CPU,只是我们一般拷贝文件使用的是【阻塞TO】,这时相当于线程虽然不用CPU,但需要一直等待IO结束,没能充分利用线程。所以才有后面的【非阻塞IO】和【异步IO】优化

3. 线程

3.1 线程的创建

3.1.1创建线程方法一

  • 匿名内部类,创建线程类
    //匿名内部类,创建线程对象
    Thread t = new Thread(){
    	@Override
    	public void run(){
    		//执行的任务
    	}
    };
    //启动线程
    t.start();
    //创建线程类
    //构造方法的参数是给线程指定名字-->推荐
    Thread t1 = new Thread("t1") {
    	@override
    	public void run( ) {
    		log.debug("hello");
    	}
    };
    t1.start();
    t1.setName("t1");
    

创建线程方法二:

  • 使用Runnable配合Thread把线程和任务(要执行的代码)分开
    • Thread:线程
    • Runnable:可运行的任务(线程要执行的代码)
    Runnable runnable = new Runnable(){
    	@Override
    	public void run(){
    		//要执行的任务
    	}
    }
    Thread t1 = new Tread(runnable,"t1");
    t1.start();
    
    //java 8 之后是使用lambda 精简代码
    //优化的是创建任务对象
    Runable task = ()->{log.debug("hello");}
    //参数1 是任务对象;参数2 是线程名字
    Thread t2 = new Tread(task2,"t2");
    t2.start();
    

Thread与Runnable的关系

  • 线程(Thread)是 Java 中用于实现多线程的类,它是一个具体的事物,可以被创建、启动、停止等。一个线程可以执行一个或多个任务

  • Runnable接口是一个抽象接口,它只有一个方法run()。任何实现了Runnable接口的类都可以作为一个线程的目标对象。Runnable接口是定义线程执行任务的逻辑,而线程则是具体执行这些任务的实体

  • 线程(Thread)和Runnable接口之间的关系是:线程需要一个目标对象来执行任务,这个目标对象可以是一个实现了Runnable接口的类。

    • Thread是把线程和任务合并在了一起,Runnable是把线程和任务分开了,更容易与线程池等高级API配合

    • 通过实现Runnable接口,我们可以将任务逻辑与线程分离,从而实现多线程编程

创建线程方法三

  • FutureTask能够接收Callable类型的参数,用来处理有返回结果的情况
     FutureTask<Integer> task = new FutureTask(){
         @Override
         public Integer call() throws Exception{
             log.debug("running...");
             return 100;
         }
     };
     Thread thread = new Thread(task,"t1");
     thread.start();
     task.get();
    

3.2 线程的运行

查看进程的方法

  1. windows

    • 任务管理器可以查看进程和线程数,也可以用来杀死进程
    • tasklist查看进程
    • taskkill杀死进程
  2. LIinux

    • ps -fe查看所有进程
    • ps -fT -p 查看某个进程(PID)的所有线程
    • kill杀死进程
    • top按大写H切换是否显示线程
    • top -H -p 查看某个进程(PID)的所有线程
  3. Java

    • jps查看所有Java进程
    • jstack 查看某个Java进程(PID)的所有线程状态

栈与栈帧

  • 栈内存是线程使用,每个线程启动后,虚拟机就会为其分配一块栈内存
    • 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
    • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法,一个栈帧对应一个方法

线程上下文切换(Thread Context Switch)

  • cpu不再执行当前的线程,转而执行另一个线程的代码

  • 原因:

    • 线程的cpu时间片用完
    • 垃圾回收
    • 有更高优先级的线程需要运行
    • 线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法
  • 程序计数器的作用是记住下一条jvm指令的执行地址

    • 当上下文切换发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态
  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等

常见方法

常见方法
常见方法

start()与run()方法

  • run() 方法:run() 方法是线程类(如 Thread 类)中的一个方法,它定义了线程执行的任务。当线程启动时,run() 方法将被自动执行。run() 方法的主要作用是为线程赋予一个任务,告诉线程执行什么操作。

  • start() 方法:start() 方法是线程类(如 Thread 类)中的一个方法,它用于启动新线程。当调用 start() 方法时,线程将开始执行 run() 方法中的代码。start() 方法的主要作用是启动线程,让线程从 run() 方法开始执行。

  • run()方法和start()方法的区别:

    • 方法性质不同:run 是一个普通方法,而 start 是开启新线程的方法。
    • 执行速度不同:调用 run 方法会立即执行任务,调用 start 方法是将线程的状态改为就绪状态,不会立即执行
    • 调用次数不同:run 方法可以被重复调用,而 start 方法只能被调用一次

yied()和sleep()

  • sleep
    1. 调用sleep会让当前线程从Ruming进入Timed Waiting状态
    2. 其它线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出工interruptedException
    3. 睡眠结束后的线程未必会立刻得到执行
    4. 建议用TimeUnit的sleep代替Thread 的sleep来获得更好的可读性
  • yield
    1. yield()方法是线程类(如Thread类)中的一个方法,它用于让当前线程放弃 CPU 执行权,将执行机会交给其他线程
    2. 调用yield会让当前线程从Running进入Runnable状态,等待 CPU 分配执行时间片,如果当前线程是优先级最高的线程,它将立即恢复执行
    3. 具体的实现依赖于操作系统的任务调度器

线程优先级

  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
  • 优先级1-10,数值越高,级别越高
  • 如果cpu比较忙,那么优先级高的线程会获得更多的时间片,但cpu闲时,优先级几乎没作用

sleep的应用

  • 放在cpu占百分百

  • 在没有利用cpu来计算时,不要让while(true)空转浪费cpu,这时可以使用yield或 sleep来让出cpu的使用权给其他程序

    while(true){
    	try{
    		Thread.sleep(50);
    	}catch (InterruptedException e){
    		e.printstackTrace();
    	}
    }
    
  • 可以用wait或条件变量达到类似的效果

  • wait和条件变量两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景(加锁同步)

  • sleep适用于无需锁同步的场景

join方法

static int r = 0;
public static void main(string[] args) throws InterruptedException {
	test1();
}
private static void test1() throws InterruptedException {
	log.debug("开始");
	Thread t1 = new Thread(() -> {
		log.debug("开始");
		sleep(1);
		log.debug("结束");
		r = 10;
	},"t1");
	t1.start();
	log.debug("结果为:{}", r);
	log.debug("结束");
}
//r = 0

分析:

  • 因为主线程和线程t1是并行执行的, t1线程需要1秒之后才能算出r=10
  • 而主线程一开始就要打印r的结果,所以只能打印出r=0
    解决方法
  • 用sleep行不行?为什么?
    • 时间不好把握
  • 使用 join,加在t1.start()之后即可t1.join(),等待t1线程运行结束

应用的同步

  • 以调用方角度:
    • 需要等待结果返回,才能继续运行就是同步
    • 不需要等待结果返回,就能继续运行就是异步
  • 多个线程同步的结果就是调用每个线程的join方法
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
	test2();
private static void test2() throws InterruptedException {
	Thread t1 = new Thread(() -> {
		sleep(1);
		r1 = 10;
	});
	Thread t2 = new Thread(() -> {
		sleep(2);
		r2 = 20;
	);
	long start = system.currentTimeMillis();
	t1.start();
	t2.start();
	t1.join();
	t2.join();
	long end = system.currentTimeMillis();
	log.debug("r1:{} r2: {i}cost: {", r1,r2, end - start);
//r1=10,r2=20

并行

join(long n)限时同步

  • 如果线程内部任务需要的时间>n,则线程不会执行完
  • 如果线程内部任务需要的时间<=n,则线程提前结束

interrupt方法

  • interrupt方法打断sleep,wait,join的线程

    private static void test1() throws InterruptedException {
    	Thread t1 = new Thread(()->{
    		sleep(1);
    	},"t1");
    	t1.start();
    	sleep(2);
    	t1.interrupt();//打断线程
    	//isInterrupted方法获取打断标记
    	log.debug("打断状态:{}", t1.isInterrupted());//false
    	//输出打断异常
    
    • isInterrupted方法获取打断标记
      • 线程正常状态被打断:返回true
      • 线程在sleep,wait,join的状态被打断:返回false
  • interrupt方法打断正常线程

  • 正常运行的线程被调用interrupt方法是不会被停止的,所以可以使用isInterrupted方法获得打断标记

    private static void test1() throws InterruptedException {
    	Thread t1 = new Thread(()->{
    		while(true){
    			boolean interrupted = Thread.currentTread().isInterrupted();
    			if(interrupted){
    				break;
    			}
    		}
    	},"t1");
    	t1.start();
    	sleep(2);
    	t1.interrupt();//打断线程
    
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值