• 深入了解线程:并发编程的核心要点和应用


    什么是线程?

    线程(Thread)是计算机科学中的一个基本概念,它是进程内的执行单元。线程是操作系统或进程内部的轻量级执行流,具有独立的程序计数器(PC)和栈,但共享相同进程的资源,包括内存空间、文件句柄等。线程是进程内的多个线程共享相同的进程上下文,因此它们可以更高效地协同工作。

    Linux中使用 top -H -p <进程ID> 能得到这个进程下的线程
    在这里插入图片描述

    特点

    1.轻量级: 与进程相比,线程通常更轻量级,因为它们共享大部分进程的资源,如内存空间。这使得线程的创建、销毁和切换成本较低。

    2.并发执行: 多个线程可以在同一进程中并发执行。这意味着它们可以同时执行不同的任务,从而提高了程序的性能和响应时间。

    3.共享资源: 线程在同一进程内共享相同的资源,包括内存、文件、打开的网络连接等。这可以简化资源管理和共享,但也需要进行适当的同步和互斥控制,以防止竞态条件和数据冲突。

    4.独立执行流: 每个线程有自己的程序计数器(PC)和栈,可以独立执行不同的代码路径。这使得线程能够同时执行不同的函数或任务。

    5.通信和同步: 在多线程应用程序中,线程之间需要进行通信和同步以协调它们的工作。常见的同步机制包括互斥锁、信号量、条件变量等。

    6.并行性: 线程是实现并行计算的一种方式。多个线程可以同时执行不同的计算任务,从而加速程序的执行。

    优点

    1.并发性和并行性: 线程允许多个任务同时执行,从而提高程序的并发性和并行性。这有助于更有效地利用多核处理器和提高系统性能。在多核系统中,单线程程序无法充分利用所有可用的处理器核心,而多线程程序可以并行执行不同任务。

    2.提高响应性: 多线程可以使应用程序更加响应用户输入或外部事件。例如,在图形用户界面(GUI)应用程序中,单线程可能会导致界面冻结,而多线程可以使界面保持响应,即使某个线程正在执行耗时的操作。

    3.简化程序设计: 多线程可以使程序更模块化和易于理解。任务可以分解为多个线程,每个线程负责执行特定的工作。这有助于提高代码的可维护性和可扩展性。

    4.资源共享: 线程在同一进程内共享相同的资源,包括内存、文件、网络连接等。这降低了资源分配的复杂性,减少了资源浪费。例如,一个数据库服务器可以使用多线程来处理多个客户端请求,而不是为每个请求创建一个独立的进程。

    快的任务完成: 并行执行不同的任务可以加速任务的完成。多线程可以用于加速计算密集型任务、并行化数据处理、提高网络通信的效率等。

    6.实现复杂性: 在某些情况下,使用多线程可以更容易地处理复杂的问题。例如,在模拟、游戏开发和科学计算中,多线程可以帮助分解复杂任务,简化代码。

    7.提高系统稳定性: 多线程应用程序更容易实现错误恢复和故障隔离。如果一个线程崩溃,不会导致整个进程崩溃,从而提高了系统的稳定性。

    线程的类型

    用户级线程

    什么是用户级线程

    用户级线程(User-Level Threads)是在用户空间(用户程序的地址空间)中由线程库(Thread Library)提供支持的线程。这种线程的创建、调度和管理都在用户空间进行,而不需要内核(操作系统)的干预。用户级线程是与操作系统无关的,操作系统并不知道它们的存在。

    在Java中,用户级线程通常是通过使用线程库或第三方框架来实现的,而不是使用标准的Thread类。一个常见的用户级线程库是协程库,如Quasar或Project Loom。这些库提供了在用户空间中实现用户级线程的功能
    以下是使用Project Loom库创建用户级线程的示例代码:

    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.Future;
    
    public class UserLevelThreadExample {
        public static void main(String[] args) {
            // 创建一个用户级线程
            Future<String> fiber = Fiber.schedule(() -> {
                return "Hello from a user-level thread!";
            });
    
            try {
                // 等待用户级线程执行完成并获取结果
                String result = fiber.get();
                System.out.println(result);
            } catch (ExecutionException | InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    优点

    1.轻量级: 用户级线程相对于内核级线程来说更轻量级,因为它们不需要内核级别的上下文切换。线程的创建和销毁以及线程之间的切换都可以在用户空间中完成,这样的操作更快速、更高效。

    2.独立调度: 用户级线程的调度不依赖于操作系统的调度算法,而是由线程库中的用户代码来决定。这使得开发者可以更灵活地控制线程的调度行为。

    3.无需内核支持: 用户级线程不需要内核的支持,因此在一些特定场景下,可以更好地适应特定的需求。不依赖于内核,也意味着用户级线程可以在不同的操作系统上运行,而不需要修改。

    4.适用于特定应用场景: 由于用户级线程的轻量级特性,它们通常用于特定的应用场景,例如需要快速响应、高度并发但不涉及复杂I/O操作的应用程序。

    缺点

    1.无法利用多核处理器: 由于用户级线程的调度是在用户空间完成的,无法充分利用多核处理器的并行性能。所有线程仍然运行在同一个核心上,无法实现真正的并行处理。

    2.阻塞问题: 如果一个用户级线程在进行阻塞式I/O操作时,会导致整个进程的所有线程被阻塞,因为操作系统不知道线程的存在,无法将其他线程切换到运行态。

    3.不稳定性: 用户级线程的稳定性受限于线程库的实现,不同的线程库可能存在不同的稳定性问题。

    4.无法实现真正的并发控制: 用户级线程的并发控制受限于线程库,无法像内核级线程那样实现更高级别的并发控制。

    内核级线程

    什么是内核级线程

    内核级线程(Kernel-Level Threads),也称为内核线程或系统线程,是由操作系统内核直接管理和调度的线程。这些线程在操作系统内核的支持下执行

    在Java中,使用Thread类创建的线程通常是内核级线程。这是因为Thread类是Java的标准线程类,它直接映射到操作系统级别的线程。这些线程由Java虚拟机(JVM)管理,并受操作系统的调度和资源管理。

    当使用Thread类创建线程时,JVM会将线程映射到底层操作系统的原生线程(通常是内核级线程),这允许Java应用程序利用多核处理器和操作系统的并发支持。这些线程具有操作系统级别的上下文切换,因此它们可以实现真正的并行执行,利用多核处理器的性能。

    优点

    1.多核支持: 内核级线程可以充分利用多核处理器的并行性,允许线程在不同的处理器核心上并行执行,提高了系统性能。

    2.硬件抽象: 内核级线程提供了对硬件的抽象,使线程可以直接访问硬件资源,如文件系统、网络、设备驱动程序等。这使得内核级线程适用于需要直接与硬件交互的应用程序。

    3.操作系统支持: 内核级线程依赖于操作系统提供的支持,因此可以利用操作系统的丰富功能,包括强大的进程调度、资源管理和安全性功能。

    4.稳定性和隔离: 由于操作系统内核管理内核级线程,它们通常具有较高的稳定性和安全性。操作系统能够确保线程之间的隔离,从而防止一个线程的错误影响整个系统。

    缺点

    1.创建和销毁开销较大: 内核级线程的创建和销毁通常涉及较大的开销,因为它们需要操作系统内核的介入。这与用户级线程相比,用户级线程的创建和销毁开销较小。

    2.线程切换开销: 内核级线程的切换通常涉及内核态到用户态的切换,这会引入较大的线程切换开销,包括上下文保存和恢复。这对于一些轻量级任务来说可能是昂贵的。

    3.复杂性: 内核级线程的管理和调度由操作系统内核负责,因此具有更高的复杂性。这使得内核级线程的使用和调试可能相对复杂。

    4.可伸缩性问题: 内核级线程的数量受限于操作系统内核的限制,因此当需要大量线程时,可能会遇到可伸缩性问题。

    5.操作系统依赖性: 内核级线程的行为和性能在不同操作系统上可能会有所不同,因此它们对操作系统的依赖性较高。这可能会导致跨平台应用程序开发方面的挑战。

    守护线程

    什么是守护线程

    守护线程是一种特殊类型的线程,它在后台运行,不会阻止程序的退出。当所有非守护线程结束时,守护线程会自动终止。它们通常用于执行后台任务,如垃圾回收。

    使用场景

    1.jvm 垃圾回收器
    2.心跳检测

    Java创建守护线程

            Thread thread = new Thread(new  ThreadDemo());
            //设置线程为守护线程
            thread.setDaemon(true);
    
    • 1
    • 2
    • 3

    线程的生命周期

    1.新建(New): 当线程对象被创建但尚未启动时,线程处于新建状态。在这个阶段,线程对象已经被分配内存,但尚未开始执行。

    2.就绪(Runnable): 线程处于就绪状态时,它已经被启动,但尚未分配到CPU执行时间片,等待操作系统的调度。在就绪状态下,线程可以随时开始执行。

    3.运行(Running): 线程在运行状态时,它正在执行任务代码,占用CPU时间片。一个时刻只能有一个线程处于运行状态,即使有多个线程在就绪状态。

    4.阻塞(Blocked): 当线程因某种原因而无法继续执行时,它进入阻塞状态。这些原因包括等待I/O操作完成、等待获取锁或等待其他资源。线程在阻塞状态下不会占用CPU时间,直到它可以继续执行。

    5.等待(Waiting): 等待状态是阻塞状态的一种特殊情况。线程通常在等待某个条件满足时进入等待状态。例如,通过调用Object.wait()方法或Thread.sleep()方法。

    6.定时等待(Timed Waiting): 定时等待状态是等待状态的一种变体,线程在等待一段指定的时间后会自动从定时等待状态恢复到就绪状态。例如,通过调用Thread.sleep()方法指定等待时间。

    7.终止(Terminated): 线程的生命周期在终止状态下结束。线程可以因任务执行完毕、异常终止或手动终止而进入终止状态。一旦线程进入终止状态,它不能再切换到其他状态。

    Java创建线程的方式

    继承Thread

    public class ThreadDemo extends Thread  {
    
        @Override
        public void run() {
            System.out.println("ThreadDemo start");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    public class ThreadMain {
    
        public static void main(String[] args) {
            System.out.println("ThreadMain start");
            Thread thread = new Thread(new ThreadDemo());
            //当调用thread.start方法的时候会执行ThreadDemo 的run方法打印ThreadDemo start
            thread.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    实现Runnable接口

    public class RunnableDemo implements Runnable{
        @Override
        public void run() {
            System.out.println("RunnableDemo start");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
        public static void main(String[] args) {
            System.out.println("ThreadMain start");
            Thread thread = new Thread(new RunnableDemo());
              //当调用thread.start方法的时候会执行RunnableDemo 的run方法打印RunnableDemo start
            thread.start();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    实现Callable接口

    因为Thread对象的参数不接收Callable对象,所以要定义一个既可以接收Callable对象的,又可以被Thread对象接收的对象------->FutureTask对象。FutureTask调用时会调用run方法,而FutureTask类中又定义的有Callable对象,所以会调用call()方法。

    public class CallableDemo {
    	public static void main(String[] args) throws InterruptedException, ExecutionException {
    		//Callable接口的实现类:不同数据范围的计算任务
    		SumCalu sumCalu1 = new SumCalu(1,300);//1-300
    		SumCalu sumCalu2 = new SumCalu(301,500);//301-500
    		SumCalu sumCalu3 = new SumCalu(501,1000);//501-1000
    		
    		//FutureTask类间接也是Callable的实现类
    		FutureTask<Integer> futureTask1 = new FutureTask<Integer>(sumCalu1);
    		FutureTask<Integer> futureTask2 = new FutureTask<Integer>(sumCalu2);
    		FutureTask<Integer> futureTask3 = new FutureTask<Integer>(sumCalu3);
    		
    		//创建线程对象,传入
    		Thread t1 = new Thread(futureTask1);
    		Thread t2 = new Thread(futureTask2);
    		Thread t3 = new Thread(futureTask3);
    		
    		//启动线程
    		t1.start();
    		t2.start();
    		t3.start();
    		
    		//线程执行结束,分别获取各自线程的返回结果
    		System.out.println("开始分别获取......");
    		Integer sum1 = futureTask1.get();
    		Integer sum2 = futureTask2.get();
    		Integer sum3 = futureTask3.get();
    		
    		//汇总结果
    		System.out.println("汇总各自计算结果");
    		Integer sum = sum1+sum2+sum3;
    		System.out.println("汇总结果:"+sum);
    	}
    }
     
    //通过Callable接口实现类SumCalu封装某个范围内数据的累加和
    class SumCalu implements Callable<Integer>{
     
    	private int begin,end;
    	//有参构造方法
    	public SumCalu(int begin,int end) {
    		this.begin = begin;
    		this.end = begin;
    	}
    	
    	//执行逻辑
    	@Override
    	public Integer call() throws Exception {
    		int total = 0;
    		for(int i=begin;i<=end;i++) {
    			total += i;
    		}
    		//获取当前线程的名称
    		System.out.println(Thread.currentThread().getName());
    		return total;
    	}
    	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    通过线程池创建

    执行线程任务有两种方式:executer()和submit()。execute()只能提交Runnable类型的任务,且没有返回结果;submit()既能提交Runnable类型的任务,又能提交Callable类型的任务,且可以返回Future类型的结果。

    public class ExecutorDemo {
    	public static void main(String[] args) {
    		//创建固定数量(10)的线程池
    		ExecutorService executorService =  Executors.newFixedThreadPool(10);
    		
    		//不确定数量的线程池请求
    		while(true) {
    			//向线程池提交一个执行任务(Runnable接口实现类对象)
    			//线程池分配一个"空闲线程"执行该任务
    			//如果没有空闲线程,则该任务进入"等待队列(工作队列)"
    			executorService.execute(new Runnable() {
    				@Override
    				public void run() {
    					System.out.println(Thread.currentThread().getName()+"执行了一次任务!");
    					try {
    						//当前线程休眠1000毫秒
    						Thread.sleep(1000);
    					} catch (InterruptedException e) {
    						e.printStackTrace();
    					}
    				}
    			});
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    引入多线程导致的问题

    1.死锁(Deadlocks): 死锁是多个线程相互等待对方释放资源的情况,导致所有线程无法继续执行。死锁的预防和解决需要小心的资源分配和死锁检测。

    2.上下文切换开销: 频繁的线程上下文切换可能导致性能下降,因为上下文切换本身需要时间和资源。合理设计线程数量和调度策略可以减少这种开销。

    3.线程安全问题: 多线程应用可能涉及到共享数据,开发者需要确保多线程操作不会导致数据损坏或不一致。这包括正确使用锁、原子操作和线程安全的数据结构。

    线程上下文切换

    什么是线程的上下文切换

    线程的上下文切换是指在多线程环境下,操作系统或线程调度器暂停当前执行的线程,并将其执行上下文(包括寄存器状态、程序计数器、栈指针等)保存到内存中,然后选择另一个线程来执行。这个过程可以理解为从一个线程的执行状态切换到另一个线程的执行状态,以便并发执行多个线程。

    线程为什么需要上下文切换

    1.多任务并发执行: 在多线程应用中,多个线程共享同一个CPU。为了实现并发执行,操作系统需要在不同的线程之间进行切换,以确保它们都有机会执行。上下文切换是实现这种并发的方式之一。

    2.抢占式调度: 在多线程环境中,操作系统通常会采用抢占式调度策略。这意味着更高优先级的线程可以在任何时刻抢占 CPU 时间片,以确保紧急任务能够立即执行。上下文切换是在这些抢占操作中的一部分,以实现线程的切换。

    3.阻塞和等待: 当线程执行阻塞操作,如等待I/O完成、等待锁或等待条件变量满足时,它会被暂停,以便其他线程可以执行。一旦阻塞操作完成,线程需要从之前的位置继续执行,这需要上下文切换。

    4.线程间通信和同步: 在多线程应用中,线程之间需要协调和同步操作,以避免竞争条件和数据访问冲突。上下文切换允许线程在不同的时间点执行,以进行协调和同步操作。

    5.多核处理器利用: 在多核处理器中,多个线程可以并行执行,以充分利用硬件资源。上下文切换允许不同的线程在不同的核心上执行,以实现并行性。

    引发上下文切换的条件

    1.抢占式调度: 当操作系统内核决定暂停当前运行的线程,并将 CPU 时间片分配给另一个线程时,发生抢占式的上下文切换。这通常基于线程的优先级和调度策略,以确保各个线程都有公平的机会执行。

    2.阻塞和等待: 当一个线程执行阻塞操作(如等待I/O操作、获取锁、等待条件满足等)时,它会被暂停,并让出 CPU。一旦阻塞操作完成,线程会重新进入可运行状态,并可能被调度执行。

    3.线程主动让出 CPU: 线程可以通过调用 yield() 或 sleep() 等方法来显式地让出 CPU,以触发上下文切换。

    频繁上下文切换的成本及代价

    成本

    时间成本: 上下文切换需要保存当前线程的执行上下文,并加载新线程的执行上下文。这些操作需要一定的时间,可能会导致延迟。

    资源成本: 上下文切换需要分配和管理内存资源,包括保存和恢复执行上下文所需的数据结构。这可能占用额外的内存和操作系统资源。

    导致的问题

    性能下降: 过多的上下文切换会导致性能下降。这是因为线程上下文切换本身需要时间,而且它可能会导致CPU缓存失效,从而降低了执行效率。在CPU密集型应用中,频繁的上下文切换可能会降低应用的整体性能。

    竞争条件: 上下文切换时,线程状态可能会被暂停,这可能会导致竞争条件。竞争条件是一种在多线程应用中可能导致数据不一致或错误的情况。

    死锁: 不正确的线程同步和阻塞操作可能导致死锁。如果线程在等待资源时被上下文切换,而其他线程持有它需要的资源,可能会导致死锁。

    资源争用: 上下文切换可能导致线程争用系统资源,如锁、内存、文件句柄等。这可能会导致资源瓶颈和性能问题。

    不公平调度: 不恰当的线程调度策略可能导致某些线程获得更多的CPU时间,而其他线程受到忽视。这可能会降低系统的公平性。

    线程安全问题

    什么是线程安全问题

    线程安全问题是指在多线程环境下,当多个线程同时访问和修改共享的数据或资源时可能导致的问题。这些问题包括数据竞争、不一致的状态和其他可能破坏应用程序正确性的情况。

    导致线程不安全问题的原因

    1.竞争条件(Race Conditions): 多个线程尝试同时访问和修改共享数据,但没有适当的同步机制来保护数据的完整性。这可能导致不确定的结果。
    2.未加锁的访问: 如果多个线程同时访问共享数据而没有获取正确的锁,可能会导致数据不一致性。例如,一个线程可能在另一个线程修改数据时读取数据。
    3.共享可变状态: 多个线程共享可变状态(例如对象的属性或全局变量),而没有适当的同步机制。一个线程可能会在另一个线程尚未完成修改之前读取或修改数据。
    4.不一致的内存访问: 多核处理器上,不同的CPU核心可能会访问不一致的内存副本,导致线程间的数据不一致性。这通常需要使用内存屏障和同步操作来解决。
    5.不同线程的执行顺序: 线程的执行顺序是不确定的,因此某些操作可能在不同线程中以不同的顺序发生,导致问题。

    如何解决线程不安全问题

    1.使用锁机制: 最常见的方法是使用锁,如互斥锁(Mutex)或信号量(Semaphore),以确保在任何时刻只有一个线程可以访问共享资源。通过获取锁,线程可以互斥地执行关键代码段,从而避免竞争条件。
    2.使用原子操作: 原子操作是不可分割的操作,它们保证在单个操作中执行多个线程之间的操作。编程语言和库通常提供原子操作,如原子递增或原子交换,以确保线程安全。

    死锁

    形成死锁的原因

    它发生在多个线程或进程之间相互等待对方释放资源的情况下,导致所有线程或进程无法继续执行,形成了僵局。
    典型的死锁情况涉及多个线程或进程,每个线程或进程都在等待其他线程或进程释放它们占用的资源。这种等待是很持久的,因为每个线程都无法继续执行,直到它所需的资源被释放。

    A线程在等待B线程释放锁 B线程在等待A线程释放锁 然后谁都获取不到对方的锁 直接造成死锁

    导致死锁的条件

    1. 互斥条件:所谓互斥就是进程在某一时间内独占资源。
    2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    3. 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
    4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

    排查死锁问题案例

    package com.alipay.alibabademo.rocketmq;
    
    public class ThreadMain {
    
        public static void main(String[] args) {
            final Object lock1 = new Object();
            final Object lock2 = new Object();
    
            Thread thread1 = new Thread(() -> {
                synchronized (lock1) {
                    System.out.println("Thread 1: Holding lock1...");
                    try { Thread.sleep(100); } catch (InterruptedException e) {}
                    System.out.println("Thread 1: Waiting for lock2...");
                    synchronized (lock2) {
                        System.out.println("Thread 1: Acquired lock2!");
                    }
                }
            });
    
            Thread thread2 = new Thread(() -> {
                synchronized (lock2) {
                    System.out.println("Thread 2: Holding lock2...");
                    try { Thread.sleep(100); } catch (InterruptedException e) {}
                    System.out.println("Thread 2: Waiting for lock1...");
                    synchronized (lock1) {
                        System.out.println("Thread 2: Acquired lock1!");
                    }
                }
            });
    
            thread1.start();
            thread2.start();
    
            // 等待两个线程执行完成
            try {
                thread1.join();
                thread2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    在这里插入图片描述
    1.通过jps命令得到正在运行的Java程序的PID
    2.通过通过 jstack查看这个PID下的日志
    如图可以看出
    Thread-0" 线程正在等待锁 <0x0000000716e1f848>,并且已经锁定锁 <0x0000000716e1f838>。
    “Thread-1” 线程正在等待锁 <0x0000000716e1f838>,并且已经锁定锁 <0x0000000716e1f848>。
    这种情况下,“Thread-0” 持有锁 <0x0000000716e1f848>,但它需要锁 <0x0000000716e1f838> 来继续执行。同时,“Thread-1” 持有锁 <0x0000000716e1f838>,但它需要锁 <0x0000000716e1f848> 来继续执行。由于它们相互等待对方释放锁,导致了死锁。

    多线程的三大特性

    1.原子性(Atomicity): 原子性是指一个操作是不可中断的,要么全部执行,要么都不执行。在多线程编程中,原子操作是线程安全的,多个线程同时执行原子操作不会导致数据不一致或竞态条件。Java 提供了一些原子操作的机制,如 synchronized 关键字、java.util.concurrent.atomic 包中的原子类等,用于确保原子性。

    2.可见性(Visibility): 可见性指的是一个线程对共享变量的修改能够被其他线程立即看到。在多核处理器和多线程环境中,由于缓存和指令重排等因素,共享变量的可见性问题可能会导致线程不一致。为了确保可见性,Java 提供了 volatile 关键字和锁机制(如 synchronized)来同步线程之间的内存访问。

    3.有序性(Ordering): 有序性指的是线程执行操作的顺序要与程序中的顺序一致。在多线程环境中,编译器和处理器可能对指令进行重排,但这些重排不能改变程序的原始语义。Java 通过 volatile 和锁机制来确保有序性。

    线程休眠(Sleep)

    sleep 是一个在多线程编程中常用的方法,它用于让线程休眠(暂停执行)一段指定的时间。 但在休眠期间,它不会释放已经持有的锁,并且会放弃 CPU 的执行权。

    try {
        // 让当前线程休眠500毫秒(半秒)
        Thread.sleep(500);
    } catch (InterruptedException e) {
        // 处理中断异常
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    上述代码会让当前线程休眠500毫秒。在这段时间内,线程不会执行任何任务,然后它会自动唤醒并继续执行。

    线程控制(yield)

    yield 是一个线程控制的方法,用于暗示当前线程愿意放弃 CPU 的执行权,使其他具有相同或更高优先级的线程有机会运行。但它不会释放锁,只是放弃 CPU 的执行权。

    Thread.yield();
    
    • 1

    yield 方法的调用会导致当前线程从运行状态切换到就绪状态,让操作系统的线程调度器决定下一个运行的线程。通常情况下,如果没有更高优先级的线程需要执行,yield 方法可能会让当前线程继续运行。

    Join

    Join用于等待另一个线程执行完成。当一个线程调用另一个线程的 join 方法时,它会等待目标线程执行完毕后再继续执行。这通常用于协调多个线程的执行顺序。

    Thread threadToJoin = new Thread(() -> {
        // 线程执行的任务
    });
    
    // 启动目标线程
    threadToJoin.start();
    
    try {
        // 等待目标线程执行完毕
        threadToJoin.join();
    } catch (InterruptedException e) {
        // 处理中断异常
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在上述代码中,主线程启动了一个目标线程 threadToJoin,然后通过 threadToJoin.join() 来等待 threadToJoin 线程执行完毕。一旦 threadToJoin 线程执行完成,主线程才会继续执行。

    线程阻塞(Wait)

    当一个线程调用 wait 方法时,它会释放之前获得的锁,并进入等待状态。在等待期间,线程不会占用 CPU 的执行权,它会让出 CPU 给其他线程执行。只有当另一个线程通过 notify 或 notifyAll 方法通知等待的线程条件已满足时,等待的线程才会被唤醒,重新尝试获取锁,并继续执行。

    synchronized (lockObject) {
        while (conditionIsNotMet) {
            try {
                lockObject.wait(); // 当条件不满足时,线程释放锁并等待
            } catch (InterruptedException e) {
                // 处理中断异常
            }
        }
        // 执行线程需要的操作
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    上述代码中,wait 方法通常在一个同步块内部使用,以确保线程对共享资源的访问是同步的。while 循环用于检查某个条件是否满足,如果条件不满足,线程调用 wait 方法释放锁并等待,直到另一个线程通过 notify 或 notifyAll 方法通知它条件已经满足。一旦线程被唤醒,它会重新尝试获取锁并继续执行

    线程唤醒

    notify

    notify 是 Java 多线程编程中用于线程通信和同步的方法之一,它用于唤醒等待在对象监视器上的一个线程。当一个线程调用某个对象的 notify 方法时,它会通知等待在这个对象上的某个线程,使其从等待状态进入就绪状态,然后等待 CPU 调度执行。

    notifyAll

    它用于唤醒等待在对象监视器上的所有线程。当一个线程调用某个对象的 notifyAll 方法时,它会通知所有等待在这个对象上的线程,使它们从等待状态进入就绪状态,然后等待 CPU 调度执行。

  • 相关阅读:
    【axios】axios的基本使用
    动态规划矩阵连乘算法(C/C++)
    软测人面试 ,HR 会问到哪些问题?学会涨薪3000+
    深度学习之基于YoloV5抽烟检测系统
    顺序表(数据结构与算法)
    MySQL基础(2)
    Elastic 发布 Elasticsearch Relevance Engine™ — 为 AI 革命提供高级搜索能力
    2022 全球 AI 模型周报
    记一个,生产遇到的redission锁,释放问题:lock.tryLock(0, 0, TimeUnit.SECONDS)
    Unity编辑器运行时设置GameView分辨率
  • 原文地址:https://blog.csdn.net/qq_41956309/article/details/133717408