多线程的了解

张开发
2026/4/13 18:59:03 15 分钟阅读

分享文章

多线程的了解
文章目录1. 进程2. 线程3. 并发和并行1并发2并行3对比4. java多线程1概述2多线程的实现方式3Thread中常用方法4线程安全问题5同步代码块6同步方法7lock 锁8线程的六种状态9等待唤醒机制10死锁11线程安全集合5. 线程池1概述2核心概念3核心组件4关键参数5工作流程6相关api7线程池使用步骤6. 悲观锁和乐观锁1悲观锁2乐观锁3锁的选择1. 进程1. 应用软件没有运行的时候都是在磁盘中存储着当执行它的运行命令时该软件会进入到内存中执行。 2. 所有的程序都是在内存中运行的在内存中运行的程序就是进程。 3. 进程是指一个内存中运行的应用程序每个程序都有一个独立的内存空间一个应用程序可以同时运行多个进程。 4. 进程是程序中的一次执行过程是系统运行程序的基本单位。系统运行一个程序就是一个进程从创建运行到消亡的过程。2. 线程1. 线程是进程内部的一个独立执行单元每一个线程都可以去执行一个任务一个进程可以有一个(至少有一个)或多个线程。 2. 如果一个程序只有一个线程那么这个程序就是单线程程序如果一个程序有多个线程那么这个程序就是多线程程序。一个程序至少要包含一个线程。 3. 因为每一个线程都可以执行一个任务所以单线程程序同一时间只能执行一个任务而多线程程序可以同时执行多个任务。 4. 多线程程序能够同时执行多个任务但并不是同一时刻能够执行多个任务一个CPU同一时刻最多只能执行一个任务如果想真正意义上同一时刻执行多个任务那么必须有多个CPU。 5. 多线程程序同时执行多个任务指的是同一时间段能够执行多个任务。因为线程是由CPU调度的在同一时间段CPU会在多个线程之间不停切换不停去执行多个任务因为切换的时间非常快所以可以看成是同时执行。 6. 线程调度是由CPU执行的CPU会随意的在多个线程之间不停切换调度没有规律性。 7. 多线程可以提高效率最根本的原因是 可以提高CPU的资源利用率。进程和线程对比图1 地址空间同一个进程的线程共享本进程的地址空间而进程之间则是独立的地址空间。 2资源拥有同一个进程内的线程共享本进程的资源如内存、I/O、CPU等但是进程之间的资源是独立的。 3资源消耗进程切换时消耗的资源大效率高所以涉及到频繁的切换时使用线程要好于进程。 4线程的处理器调度的进本单位进程不是。3. 并发和并行1并发并发指的是 在同一个时间段有多个线程同时执行CPU在多个线程之间不停切换。2并行并行指的是 在同一个时间点有多个线程同时执行。3对比1. 一个CPU 在同一时间点只能调度一个线程所以如果想达到并行的效果那么必须是多CPU才可以。 2. 一个CPU最多只能并发操作不能并行操作。多个CPU才可以并发操作或者并行操作。4. java多线程1概述1. 在java程序中进程里面至少有两个线程一个是main线程另一个是垃圾回收机制线程。 当程序运行时JVM会创建一个main线程并执行程序中的main方法。针对单线程程序同一时间只能执行一个任务如果有多个任务那么只能一个执行完后才能执行另外一个效率比较低。如果想同时执行多个任务那么可以用多线程。 2. JAVA中有一个表示线程的类叫Thread。Thread是程序中执行的线程。Java虚拟机允许应用程序同时执行多个线程。 3. 由于创建一个线程的开销比创建一个进程的开销小的多所以我们在开发多任务运行的时候通常考虑创建多线程而不是创建多进程。 4. 线程之间堆空间是共享的栈空间是独立的每个执行线程都有自己所属的栈内存空间用来运行自己的方法。线程消耗的资源比进程小的多。2多线程的实现方式1. 继承Thread类并重写run() 方法。 具体步骤 1创建一个类继承Thread类重写run()方法。 2在调用的地方创建Thread类的子类对象(自己创建的类)。 3调用 start() 方法启动线程当一个线程的start()方法执行时会做2件事情1.启动线程 2.执行自己的run()方法。 2. 实现Runnable接口并重写run()方法 具体步骤 1创建一个类实现Runnable接口重写接口中的run()方法。 2在调用的地方创建Runnable接口的实现类对象。 3创建Thread对象并且在构造方法位置传递Runnable实现类对象。 4调用Thread类的 start() 方法启动线程线程会执行对应的run()方法。 3. 实现Callable 接口并重写call()方法 具体步骤 1创建一个类实现Callable 接口 2) 重写接口中的call()方法,在call()方法中定义要执行的任务重写的方法必须定义返回值且可以抛出异常。 3在调用的地方创建Callable接口的实现类对象。 4创建线程池【如果要使用Callable接口的实现类那么必须要使用线程池执行这个任务】。 5把callable的实现类对象放到线程池中。 6线程池调用 submit()方法提交任务开始执行并可以调用get()方法获取返回值。 7关闭线程池。 4. 通过线程池创建线程。3Thread中常用方法1. 无参构造 Thread(); 2. 有参构造 Thread(String tName); //分配一个带有指定名字的新的线程对象 Thread(Runnable target); //分配一个带有指定目标的新的线程对象。 Thread(Rnnable target,String name) //配一个带有指定目标的新的线程对象并指定名字。 3. 给线程设置名字 void setName(String name); 4. 获取线程的名字 String getName(); 5. 开启线程线程开始执行 void start( ); 6. 获取正在执行的线程对象 static Thread currentThread(); 7. 线程休眠指定的毫秒数 static void sleep(Long millis);4线程安全问题1. 原因 当多个线程之间存在数据共享的情况 并同时会对数据进行修改操作时就会导致线程安全问题(数据的不一致)。 2. 解决方法 使用关键字synchronized表示的含义为同步。 Synchronized可以修饰代码块也可以修饰方法。如果修饰代码块那么这个代码块就是同步代码块。5同步代码块1. 格式 synchronized ( 锁对象 ){ //代码块内容 // 同步代码块内容其实就是可能造成数据安全问题的代码内容 } 2. 注意点 1锁对象就是一个普通的java对象可以是ArrayListObject, Person 或者其他类型它可以是任意对象没有特殊的含义所起到的作用主要是做一个标记只有持有锁的线程才可以进入到同步代码块多线程时只要保证锁对象的唯一性就可以不太关注锁对象是什么类型。 2同步代码块的作用保证只有持有锁对象的线程才可以进入到同步代码块中这样就可以保证多个线程同时运行时同步代码块中的内容在同一时刻只能被一个线程调用避免了多个线程同时操作共享数据可能造成的数据安全问题。 3被同步代码块修饰的内容其本质上只能进行串行操作也就是说同一时刻只有一个线程能够持有锁并执行同步代码块中的内容在这个线程执行同步代码块的过程中其他线程只能先等待只有当这个线程执行完成并且释放锁之后其他线程才可以继续抢锁当一个线程抢到锁之后那么它可以再执行同步代码块中的内容而没有抢到锁的线程会再次等待一直这样重复操作。6同步方法1. 格式 public synchronized void sell(){ // 方法体内容 // 同步方法其实本质上就是一般方法上加上了一个关键字 synchronized } 2. 注意点 1同步方法可以理解为在整个方法体内容上加上了锁相比同步代码块不用再创建锁对象更加简单方便。 2同步方法也是有锁的如果这个同步方法不是静态的那么这个同步方法的锁就是this,代表的是当前类的对象如果这个同步方法是静态的那么这个锁对象是 当前类名.class 字节码对象 。7lock 锁1. 格式 Lock lock new ReentrankLock(); while(true){ lock.lock(); //方法体内容 lock.unlock(); } 2. 注意点 1在jdk1.5之后提供了Lock接口这个Lock接口表示的就是锁跟同步代码块或者同步方法的区别是同步代码块/同步方法都是自动获取锁自动释放锁而Lock锁需要自己手动的获取锁并且手动的释放锁。 2lock锁用到的方法有 void lock() ; 手动获取锁 void unlock(); 手动的释放锁 3Lock是一个接口一般用到的是它的实现类ReentrankLock 。8线程的六种状态1. 新建(new) 刚刚创建出来的线程处于此状态 比如: new Thread 或者 new Thread的子类对象。 2. 运行(Runnable) 当一个线程调用start()方法之后处于运行状态。 3. 受阻塞(Blocked) 由于同步代码块和同步方法中只有持有锁对象的线程才可以执行而其他线程都处于等待状态当一个线程等待锁对象的时候处于受阻塞状态。 4. 计时等待(Timed Waiting) 当调用线程的sleep(毫秒值)方法或者wait(毫秒值)方法等待指定的毫秒值时这种线程状态叫计时等待。 5. 无限等待(Waiting) 当调用线程的wait()方法而没有传入毫秒值参数的时候此线程处于无限等待状态。 6. 退出(Teminated) 当一个线程的run()方法执行完毕或者调用了线程的stop()方法的时候这个线程处于退出状态。线程状态示意图注意点 1进入计时等待状态的常用情形是调用sleep()方法单独的线程也可以直接调用Thread.currentThread.sleep() 。 2sleep()与锁无关时间到期自动苏醒变成Runnable状态。 3sleep(毫秒值) 方法中的时间毫秒值是线程不会运行的最短时间sleep()方法不能保证线程睡眠到期后就立刻开始执行。 4sleep(方法会释放CPU的执行权但不会释放锁。wait()方法会释放CPU的执行权同时释放锁。 5sleep(毫秒值)方法到期后线程会被自动唤醒唤醒后的线程不需要重新获取锁只要CPU调度该线程该线程就会继续执行下去。9等待唤醒机制1. 相关api 1void wait() //让当前线程等待如果没人唤醒就一直等。 2void wait(long millis) //让当前线程等待如果到了指定毫秒值还没有人唤醒它就自己醒来。 3void notify() //唤醒一个线程唤醒的是当前锁对象下的线程。 44 void notifyAll() //唤醒所有线程唤醒的也是当前锁对象下的线程。 注意 1. 等待唤醒机制中用到的方法是Object中的而不是Thread中。 2. 这些方法虽然是Object中的但是不能直接通过Object去调用这些方法一定要放在同步代码块中去使用而且必须要通过锁对象去调用。 2. 线程通讯 1概念 线程通讯指的是多个线程处理同一个资源但是线程间处理的动作(具体任务)不相同很可能是相反的操作比如生产和消费。 2为什么要处理线程通信 多个线程在默认情况下CPU在多个线程之间是随意调度的没有规律可言当我们需要多个线程来共同完成一个任务并且希望线程之间能够有规律的进行操作时就需要线程通信。 3保证线程通信有效利用资源的方法 等待唤醒机制。 3. 等待唤醒机制的具体操作 等待唤醒机制指的是当一个动作执行的时候另一个其他动作处于等待的状态 而当该动作执行完毕时自身变为等待的状态而刚才处于等待状态的动作(线程)被唤醒开始执行该线程执行完毕时再把其他等待的线程唤醒自身重新等待如此反复进行。这样就可以保证多个线程始终处于一些等待一些执行的状态共同协同操作数据。 4. 特点 1当一个线程进入等待wait()状态的时候它会释放锁不再占用资源。 2当一个线程被唤醒notyfy()的时候它不能马上恢复执行而是需要重新去争夺锁如果该线程抢到了锁那么线程就会从等待变成运行中状态从上次调用wait()方法之后的地方恢复执行如果没有抢到锁那么该线程就处于受阻塞状态。 3wait() 方法和notify()方法必须由同一个锁对象调用因为由同一个锁对象调用的notify()方法可以唤醒之前由该锁对象调用的wait()方法。 4wait() 方法和notify()方法必须都要在同步代码块/同步方法 中使用。10死锁1. 概念 两个或两个以上的线程每个线程都持有一把锁而且每个线程都在等待别的线程释放锁如果这几个线程之间某个线程持有的锁和其他线程等待的锁正好相同那么就会造成无限等待的僵持状态这种状态就叫死锁。11线程安全集合1. ConcurrentHashMap 2. CopyOnWriteArrayList 3. BlockingQueue BlockingQueue是以后个线程安全的队列可以避免线程安全问题。 例 BlockingQueueString queue new LinkedBlockingQueue(); queue.put(data); // 阻塞式插入 String data queue.take(); // 阻塞式取出5. 线程池1概述线程池是一个容器里面放有多个线程这些线程具有可以复用的特点。 当线程要执行任务的时候会直接从线程池中获取线程去执行执行完毕后会将线程再还给线程池这些被还回来的线程可以再去执行其他的任务。 如果线程池中的线程都被使用那么没有被安排执行的任务会处于等待执行的状态当有任务执行完毕释放线程后这些线程会再去执行刚才等待的任务。2核心概念1. 线程复用预先创建一组线程避免频繁创建和销毁线程的开销。 2. 任务队列存储待处理的任务工作线程从队列中获取任务执行。 3. 资源管理通过限制线程数量防止资源耗尽如内存溢出、CPU过载。3核心组件1. 线程池管理器负责创建、销毁线程池及监控状态。 2. 工作线程池中执行任务的线程。 3. 任务队列存放待执行任务支持有界/无界队列、优先级队列等。 4. 任务接口定义任务执行逻辑如Runnable或Callable接口。4关键参数1. 核心线程数Core Pool Size池中保持的最小线程数即使空闲。 2. 最大线程数Maximum Pool Size线程池允许的最大线程数。 3. 任务队列Work Queue 1无界队列如LinkedBlockingQueue可能导致内存溢出。 2有界队列如ArrayBlockingQueue需配合拒绝策略使用。 3同步移交队列如SynchronousQueue直接传递任务不存储。 4. 拒绝策略RejectedExecutionHandler 1抛出异常 2调用者执行任务 3丢弃旧任务 4静默丢弃 5. 空闲线程存活时间KeepAliveTime非核心线程的空闲超时回收时间。5工作流程1. 提交任务时优先创建核心线程执行。 2. 核心线程满后任务进入队列等待。 3. 队列满后创建新线程不超过最大线程数。 4. 达到最大线程数且队列已满触发拒绝策略。6相关api1. 线程池的顶层接口Executor。里面有一个方法execute(Runnable commit):让任务执行。 2. 常用接口ExecutorService接口ExecutorService接口是Executor的子接口里面除了有执行线程的功能外还有管理线程的功能。 ExecutorService 线程池常用方法 1Public Future? submit(Runnable task) //提交一个线程要执行的任务线程池会使用里面的线程去执行这个任务。 2void shutdown() //摧毁线程池。 3. ExecutorService接口的创建方式 ExecutorService接口一般不是直接new而是通过Executors工具类中的方法来创建。 Executors工具类是操作线程池的工具类里面有方法可以直接获取到线程池对象具体方法有 1static ExecutorService newFixedThreadPool (int Threads) //创建一个固定线程个数的线程池。 2static ExecutorService newCachedThreadPool() //获取一个具有缓冲作用的线程池。这个线程池中的线程的数量可以变化。 3static ScheduledExecutorService newScheduledThreadPool (int corePoolSize) //获取一个可以周期性执行任务的线程池。 4static ExecutorService newSingleThreadExecutor () //创建一个单线程的线程池。 一次只能执行一个任务。 可以指定顺序去执行某个任务。7线程池使用步骤1. 通过Executors工具类获取一个线程池对象。 Executor ex Executers.newFixedThreadPool(5); 2. 定义Runnable接口的实现类重写run()方法里面是要执行的任务。 Public class MyRunnableImpl implments Runnable(){ Override Public void run(){ //重写的方法 } } 3. 调用线程池的submit()方法传递Runnable接口的实现类对象为入参表示要执行这个任务。 ex.submit(new MyRunnableImpl()) ; 4. 销毁线程池一般不会 ex.shutdown();6. 悲观锁和乐观锁悲观锁和乐观锁是两种用于解决并发数据冲突比如多个人同时修改同一条数据的常见思路。它们不是指具体的某一把锁而是一种思想和策略。1悲观锁1. 概念 很谨慎觉得并发冲突一定会发生。 所以每次操作数据前都会上锁先把它锁住不让别人动操作完再释放。 传统的关系型数据库里用到了很多这种锁机制比如表锁行锁读锁写锁等。 2. 流程 1A线程开启事务。 2A线程用 SELECT ... FOR UPDATE 锁定要修改的数据行。此时其他任何线程比如B线程想修改或使用 FOR UPDATE 读取这些数据都会被阻塞必须等A提交事务释放锁。 3) A线程进行业务计算然后 UPDATE 数据。 4) A线程 COMMIT 事务锁释放。 3.优点 实现简单能绝对保证数据一致性。 4. 缺点 性能较差容易造成锁等待甚至死锁。不适合高并发、响应要求快的场景。 5. 适用场景 写多读少、并发冲突非常严重、数据修改操作代价很高的场景。比如银行系统中扣减账户余额绝不允许出错且本身并发不会特别高用悲观锁就很稳。2乐观锁1.概念 很乐观觉得并发冲突大概率不会发生。 每次去拿数据的时候都认为别人不会修改所以不会上锁但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。 适用于多读写比较少的情况。 2. 流程 1A线程查询数据同时得到版本号version1。 2) A线程进行业务计算准备更新。 3) A线程执行 UPDATE条件是 id1 且 version1。同时把 version 设置为 2。 4) 如果在A线程操作期间B线程已经改过这条数据那么数据库里的 version 就已经变成了 2。此时A线程的更新条件 version1 无法满足影响行数为0。 5) A线程判断影响行数如果为0说明有人改过通常选择重试整个业务逻辑或者放弃操作。 3. 优点 无锁性能很高并发能力好不会死锁。 4. 缺点 实现稍复杂需要处理重试逻辑。如果并发冲突真的很高会导致大量重试浪费性能。 5. 适用场景 并发冲突较少或者读多写少的场景。例如更新用户信息、商品详情等。也适用于分布式系统因为不依赖单数据库的锁。3锁的选择1 优选考虑乐观锁。 2冲突频繁或者是有明确需要 锁住 的业务需求(比如防止超卖扣库存且能接受一定的等待)、数据库事务隔离级别是可重复读且需要锁定范围防止幻读时使用悲观锁。 3选型建议高并发、低冲突用乐观锁低并发、高冲突用悲观锁。

更多文章