多线程

JAVA多线程


多线程基础

线程

定义:
​ 线程是操作系统能够进行运算调度的最小单位。它被包含在进程中,是进程中的实际运作单位
​ 在java中堆内存是唯一的,栈内存不是唯一的,是与线程相关的,一个线程对应一个栈,因此栈也可以叫做线程栈
简单理解: 线程就是软件中互相独立的,可以同时运行的功能
进程: 进程是程序的基本执行实体(一个软件运行之后就是一个进程)
并发:
​ 在同一时刻,有多个指令在单个CPU上交替执行
并行:
​ 在同一时刻,有多个指令在多个CPU上同时执行

创建多线程

​ java虚拟机允许应用程序并发的运行多个线程
​ java中有3中方式可实现多线程

1.继承Thread类:
​ 自己定义一个类继承Thread类,重写run方法,把需要多线程执行的方法写入run方法内,在外部创建该类对象,调用start方法启动

public class MyThread extends Thread {
    @Override
    public void run() {
        for(int i=0; i<100; i++) {
            System.out.println(i); 	}}}
public class MyThreadDemo {
    public static void main(String[] args) {
        MyThread my1 = new MyThread();
        //void start() 导致此线程开始执行; Java虚拟机调用此线程的run方法
        my1.start();		}}

2,实现Runnable接口:
​ 定义一个类实现Runnable接口,并重写run方法,在外部创建一个Thread对象和该类对象,在构造参数里传入该类对象
​ 使用Thread对象调用run方法启动

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for(int i=0; i<100; i++) {
		//这里的Thread.currentThread()方法可获得当前线程的对象
            System.out.println(Thread.currentThread().getName()+":"+i);  }}}
public class MyRunnableDemo {
    public static void main(String[] args) {
        //创建MyRunnable类的对象
        MyRunnable my = new MyRunnable();
        //创建Thread类的对象,把MyRunnable对象作为构造方法的参数
        Thread t1 = new Thread(my,"坦克");
        //启动线程
        t1.start(); }}

3.利用Callable接口和Future接口:
​ 定义一个类实现Callable接口,并定义泛型(该泛型就是返回值类型),重写call方法,将需要多线程执行的语句放入call方法
​ 在外部创建该类对象,FutureTask对象(用于管理多线程运行的结果),以及Thread对象
​ 在FutureTask构造方法内传入该类对象,并定义泛型类型为返回值类型
​ 在Thread构造方法内传入FutureTask对象,调用Thread对象start方法启动线程,调用FutureTask对象get方法获取该线程返回值

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 100; i++) {
            System.out.println("i);
        }
        //返回值就表示线程运行完毕之后的结果
        return "结果";}}
public class Demo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //线程开启之后需要执行里面的call方法
        MyCallable mc = new MyCallable();
        //作为参数传递给Thread对象,获取线程执行完毕之后的结果.
        FutureTask<String> ft = new FutureTask<>(mc);
        //创建线程对象
        Thread t1 = new Thread(ft);
        //开启线程
        t1.start();
        String s = ft.get();
        System.out.println(s);	}}

线程常见方法

屏幕截图 2025-01-04 224035

线程优先级:
线程调度:
​ 分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
​ 抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
​ Java使用的是抢占式调度模型
优先级设置:
​ 可以使用setPriority()方法设置该线程的优先级
​ 优先级有 1到10,默认优先级为5,优先级越高,抢到线程的概率越高
守护线程:
​ 当其他非守护进程执行完毕后,守护进程会陆续结束
​ setDaemon()方法:将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出

插入线程:
​ 在一个线程运行方法内部创建一个新的线程,调用该线程的join()方法
​ 该方法可将调用join方法的这个线程插入到当前线程之前,调用join方法线程执行完毕后,再执行当前线程

中断线程:
​ 在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行
Tip:
​ 如果目标线程处于等待状态,调用 interrupt()方法 join()方法会立即抛出异常
线程的生命周期:
​ New:新创建的线程,尚未执行 //new方法
​ Runnable:运行中的线程,正在执行run()方法的Java代码 //start方法
​ Blocked:运行中的线程,因为某些操作被阻塞而挂起 //无法获得锁对象
​ Waiting:运行中的线程,因为某些操作在等待中 //wait方法
​ Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待 //sleep方法
​ Terminated:线程已终止,因为run()方法执行完毕 //全部代码执行完毕
过程:
​ 创建对象 => 调用start方法开始抢占执行权 => 抢占到执行权 => 线程代码运行完毕,变成垃圾被回收
屏幕截图 2025-01-05 165707
在java中只有6种状态,没有运行状态

线程同步

同步代码块:
​ 利用synchronized关键字声明一个代码块,在该关键字后面声明一个锁对象(该锁对象可以是任何对象)
​ 在线程执行到该代码块时,若锁对象相同,会检测该代码块是否被别的线程使用,
​ 若使用,则等待占用线程运行完毕该代码再执行同步代码块代码
Tip:
​ 若需要改代码块只能被一个线程执行则设置锁对象为唯一对象
​ 一般把锁对象设置为当且类的字节码文件对象, 类名.class

//声明
synchronized(任意对象) { 
	多条语句操作共享数据的代码 
}

同步方法:
​ 将synchronized关键字写到方法上
​ 同步方法的锁是java指定的
非静态方法:锁对象为this
静态方法: 锁对象为当前类的字节码文件

//声明
修饰符 synchronized 返回值类型 方法名(方法参数) { 
	方法体;
}
//注:若使用实现Runnable接口线程,则调用该对象形成的线程时,都是调用同一个对象,因此使用非静态同步方法也可以保证锁对象唯一
//	  并且内部的成员变量也是共享的

//Tip: StringBuilder与StringBuffer的不同就在于 StringBuffer内部的方法都是被 synchronized 修饰的同步方法

Lock锁(JDK5新增):
​ 与 synchronized关键字加锁不同,Lock是自己调用方法上锁以及自己调用方法开锁
使用:
​ Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
​ 创建锁对象(用static修饰保证锁对象唯一)
​ lock() | 获得锁
​ unlock() | 释放锁
​ lock相当于synchronized的前{ unlock相当于 }
Tip:
​ 与synchronized修饰不同, Lock锁必须执行unlock方法后才能开锁,因此可以用try..catch..finally方法来保证不会发生阻塞
死锁:
​ 死锁是一种错误
​ 线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
避免: 避免出现2个锁嵌套起来
等待唤醒机制:
方法:

Object类的等待和唤醒方法 说明
void wait() 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法
void notify() 唤醒正在等待对象监视器的单个线程
void notifyAll() 唤醒正在等待对象监视器的所有线程

使用:
​ 在synchronized内部可以调用wait()使线程进入等待状态;
​ 必须在已获得的锁对象上调用wait()方法;
​ 必须在已获得的锁对象上调用notify()notifyAll()方法;
​ 在synchronized内部可以调用notify()notifyAll()唤醒其他等待线程
​ 已唤醒的线程还需要重新获得锁后才能继续执行。
​ 一次只有一个线程可以拥有对象的监视器
Tip:
​ wait()方法必须在当前获取的锁对象上调用,这里获取的是this锁,因此调用this.wait()
​ 调用wait()方法后,线程进入等待状态,wait()方法不会返回,直到将来某个时刻,
​ 线程从等待状态被其他线程唤醒后,wait()方法才会返回,然后,继续执行下一条语句
​ !当一个线程执行 锁对象.wait()方法后,其会立即释放当前的锁
​ 调用sleep()不会释放锁

阻塞队列:
屏幕截图 2025-01-05 152127

BlockingQueue的核心方法:
​ put(anObject): 将参数放入队列,如果放不进去会阻塞
​ take(): 取出第一个数据,取不到会阻塞
​ 阻塞队列内部的方法会自定上锁,尽量避免在外部在加锁造成锁的嵌套
​ ArrayBlockingQueue在创建对象时可以传入一个int参数,代表其可以放入几个数据
线程中断:
Thread.interrupt():中断线程。这里的中断线程并不会立即停止线程,而是设置线程的中断状态为 true(默认是 flase)
Thread.isInterrupted():测试当前线程是否被中断
Thread.interrupted():检测当前线程是否被中断,与 isInterrupted() 方法不同的是,这个方法如果发现当前线程被中断,会清除线程的中断状态。
​ 在线程中断机制里,当其他线程通知需要被中断的线程后,线程中断的状态被设置为 true,
​ 但是具体被要求中断的线程要怎么处理,完全由被中断线程自己决定,可以在合适的时机中断请求,也可以完全不处理继续执行下去。

线程池

线程池的核心原理:
​ 首先会创建一个池子,池子是空的
​ 当提交线程任务时,线程池会创建新的线程对象,任务执行完毕,线程会回到线程池,下次再提交任务时,就不用穿件新的线程对象,
​ 复用原来的线程对象即可。
​ 线程池可以自定义大小,当线程池满时,再次提交任务,该任务会排队等待其他线程执行完毕,回到线程池再继续执行
代码实现:
​ 可以使用Executors中所提供的静态方法来创建线程池

方法名 说明
static ExecutorService newCachedThreadPool() 创建默认线程池(没有上限)
static ExecutorService newFixedThreadPool(int nThreads) 创建有上限的线程池
    public static void main(String[] args) throws InterruptedException {
        //1,创建一个默认的线程池对象.池子中默认是空的.默认最多可以容纳int类型的最大值.
        ExecutorService executorService = Executors.newCachedThreadPool();
        //Executors --- 可以帮助我们创建线程池对象
        //ExecutorService --- 可以帮助我们控制线程池
        executorService.submit("Runnbale实现类或Callbale实现类");  //在调用submit方法后,传入的线程对象就会开始运行(若获得线程的话)
		//销毁线程池
        executorService.shutdown();		}

自定义线程池:
对象创建:
​ ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(核心线程数量,最大线程数量,
​ 空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);
运行细节:
​ 当提交的任务未超过核心线程数量时,直接分配线程
​ 当超过核心线程数量时,会把超过的部分放入任务队列中排队等候
​ 当超过核心线程数列且也超过任务队列大小时,会创建临时线程,没有进入任务队列的任务会被分配临时线程
​ 当超过最大线程数量和任务队列之和时,会把超出部分的任务根据任务拒绝策略进行处理
代码实现:

//使用列子
//    参数一:核心线程数量
//    参数二:最大线程数
//    参数三:空闲线程最大存活时间
//    参数四:时间单位	用TimeUnit指定
//    参数五:任务队列
//    参数六:创建线程工厂
//    参数七:任务的拒绝策略
    public static void main(String[] args) {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(2,5,2,TimeUnit.SECONDS,new ArrayBlockingQueue<>(10), 					
                                                         	   Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        pool.submit(new MyRunnable());
        pool.shutdown();	}

//创建对象
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
//		corePoolSize:   核心线程的最大值,不能小于0
//		maximumPoolSize:最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize
//		keepAliveTime:  空闲线程最大存活时间,不能小于0
//		unit:           时间单位
//		workQueue:      任务队列,不能为null
//		threadFactory:  创建线程工厂,不能为null      
//		handler:        任务的拒绝策略,不能为null  

//拒绝策略
//	RejectedExecutionHandler是jdk提供的一个任务拒绝策略接口,它下面存在4个子类。
//		ThreadPoolExecutor.AbortPolicy: 		    丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
//		ThreadPoolExecutor.DiscardPolicy: 		   丢弃任务,但是不抛出异常 这是不推荐的做法。
//		ThreadPoolExecutor.DiscardOldestPolicy:    抛弃队列中等待最久的任务 然后把当前任务加入队列中。
//		ThreadPoolExecutor.CallerRunsPolicy:        调用任务的run()方法绕过线程池直接执行。

线程池大小:
CPU密集型计算:
​ 最大并行数 + 1
I/O密集型计算:
​ 最大并行数 * 期望CPU利用率 * ( 总时间(CPU计算时间 + 等待时间) / CPU计算时间)
最大并行数:
​ 当前java虚拟机可使用的线程数量
​ 调用 Runtime.getRuntime().avaliableProcessors() 获取

原子性

JMM模型:
​ JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型。
特点:
​ 1.所有的共享变量都存储于主内存(计算机的RAM)这里所说的变量指的是实例变量和类变量。
​ 不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
​ 2.每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本
​ 3.线程对变量的所有的操作(读,写)都必须在工作内存中完成,而不能直接读写主内存中的变量,
​ 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成
简单解释:
​ 每个线程都有一个工作内存,每个工作内存都保留了一份主内存的副本,副本内记录者共享变量的值
​ 每次线程操作变量时,都是操作自己的工作内存,然后再把工作内存写入主内存中达到同步
volatile关键字:
​ 为了保证多线程操作共享变量的可见性,可用volatile修饰变量
​ 当一个共享变量被 volatile 修饰时,它会保证修改的值立即更新到主存当中,这样的话,当有其他线程需要读取时,就会从内存中读到新值。
​ 普通的共享变量不能保证可见性,因为变量被修改后什么时候刷回到主存是不确定的,因此另外一个线程读到的可能就是旧值。
​ Java 的锁机制如 synchronized 和 lock 也是可以保证可见性的
作用:
​ 当写一个 volatile 变量时,JMM 会把该线程在本地内存中的变量强制刷新到主内存中去;
​ volatile会禁止指令重排
​ 当我们使用 volatile 关键字来修饰一个变量时,Java 内存模型会插入内存屏障
​ (一个处理器指令,可以对 CPU 或编译器重排序做出约束)来确保以下两点
​ 写屏障(Write Barrier):当一个 volatile 变量被写入时,写屏障确保在该屏障之前的所有变量的写入操作都提交到主内存
​ 读屏障(Read Barrier):当读取一个 volatile 变量时,读屏障确保在该屏障之后的所有读操作都从主内存中读取
​ 也就是说执行到 volatile 变量时,其前面的所有语句都必须执行完,后面所有得语句都未执行。且前面语句的结果对 volatile 变量及其后面语句可见
注:
​ volatile只保证单个变量的原子性,以及其读和写的内存可见性
​ 但不保证一整个过程的内存可见性,
​ 如 线程1获取了 被volatile修饰的变量a,b也获取了a,此时2者获取的a值相同,若2者都对其修改,则不能实现预测的结果

JUC

JMM模型

概述

​ JMM: JAVA 内存模型
​ 在Java中,使用的是共享内存并发模型。

PixPin_2025-03-08_15-30-59

重排序

概述:
​ 编译器为了优化代码的执行,会对代码的执行顺序进行重排,但最后的结果和java代码顺序结果相同
​ 但重排序对代码顺序的保证仅限于 单个线程, 多个线程的重排不进行保证

happens-before

概述:
​ 为了解决对多线程重排的乱序,java提供了happens-before关系,只要遵从happens-before,重排也对多线程保证

​ 在Java中,有以下天然的happens-before关系:

  • 程序顺序规则:一个线程中的每一个操作,happens-before于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  • start规则:如果线程A执行操作ThreadB.start()启动线程B,那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作、
  • join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
posted @ 2025-06-14 00:09  LittleD-  阅读(14)  评论(0)    收藏  举报