进程线程
进程一个程序分配资源的基本单位线程调度运行的基本单位即一个程序里不同的执行路径介绍线程切换OS执行到T1执行到指令2时读数据等待网络传输把T1寄存器的内容执行到什么位置记录下来放到cache里面线程记录然后执行T2然后再执行T1。ALU运算单元专门用来做运算。Register寄存器专门用来存数据。PCProgramControl专门用来存指令。CPU运行速度是内存运行速度的100倍。即ALU访问Register中的数据速度是ALU访问内存进度的100倍加快访问速度可通过缓存的方式实现一级缓存、二级缓存、三级缓存。程序的局部性原理每次从内存中取64bytes(64个字节)cacheline(缓存行)放入缓存中。缓存一致性(硬件级别)对CPU的一种优化。1、cacheline缓存行对齐伪共享缓存行编码技巧任意其他有效数据都不可能和x位于同一行减少了缓存一致性的开销2、CPU指令重排单线程对象的创建过程class T { int m 8; } T t new T();汇编码0 new #2 T 分配空间 m03 dup 栈基于栈的虚拟机4 invoke special #3 Tinit 特殊调用T的init()方法(构造方法)调用完成后m87 astore -1 t与T建立关联8 return不要在构造方法里启动一个线程。实例双击一个程序将代码跟数据放到内存里内存里的一段代码和数据就叫一个进程这时代码还没跑出来。所以进程是一个静态的概念。找到第一行代码开始执行时为进程跑起来。一个进程通常有好几个同时运行的分支主分支为主进程所以线程是动态的进程动态是一个程序分配资源的基本单位双击exe会在内存里占一块空间线程静态是CPU运行时调度运行的基本单位一个线程对应一颗CPU问题电脑只有一个CPU或CPU只有一个核有必要写多线程吗有必要一个线程不是所有时间都在用CPU。比如一个线程接受网络数据一边同时聊天。当一个线程等待的时候可以把另外一个线程切到CPU运行。同一个CPU同一个时间点只能跑一个线程可以让它们轮流使用CPU达到运行多线程的功能。多线程一定比单线程快吗单线程不够就用多线程效率是线性增长的吗不是线程太多线程切换也需要消耗资源。四核8线程超线程一个内核跑2个线程用一个线程池选多少个核心线程池合适呢效率达到最高。看计算和等待时间比例以及想让CPU大概达到百分之多少根据一个经验值来估一个大概的压测值然后进行压测根据实际情况进行调整。创建线程的方法extends Thread 重写run()方法implements Runnable重写run()方法使用lambda表达式使用ExecutorServicepublic class CreateThread { //1、extends Thread重写run方法 static class MyThread extends Thread { Override public void run() { super.run(); } } //2、implements Runnable重写run方法 static class MyRun implements Runnable { Override public void run() { } } public static void main(String[] args) { new MyThread().start(); new Thread(new MyRun()).start(); //3、使用lambda表达式 new Thread(() - { }).start(); } }启动线程的三种方式继承Thread实现Runnable接口ExecutorService结束线程运行完自动结束使用中断Interrupt使用标志位使用Future和ExecutorService对于通过ExecutorService启动的线程public class EndThread { static class MyThread extends Thread { Override public void run() { while (!Thread.currentThread().isInterrupted()) { try { Thread.sleep(5000); } catch (InterruptedException e) { // 响应中断通常在这里重新设置中断状态 Thread.currentThread().interrupt(); break; } } System.out.println(线程被中断); } } static class MyThread02 extends Thread { private volatile boolean running true; public void stopRunning() { running false; } Override public void run() { while (running) { try { Thread.sleep(5000); } catch (InterruptedException e) { // 响应中断通常在这里重新设置中断状态 Thread.currentThread().interrupt(); break; } } System.out.println(线程被中断); } } public static void main(String[] args) throws InterruptedException { MyThread myThread new MyThread(); myThread.start(); // 1、使用interrupt。 假设在某个时间点需要中断线程 myThread.interrupt(); MyThread02 myThread02 new MyThread02(); myThread02.start(); // 2、修改标志位使循环结束 myThread02.stopRunning(); ExecutorService executor Executors.newSingleThreadExecutor(); Future? future executor.submit(() - { while (!Thread.currentThread().isInterrupted()) { // 执行任务 try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 重置中断状态 break; // 退出循环结束任务执行 } } }); // 尝试停止任务但这取决于任务是否响应中断 future.cancel(true); // true 表示即使任务正在运行也尝试停止它通过中断 executor.shutdownNow(); // 3、尝试立即停止所有正在执行的任务并返回一个列表包含那些尚未开始执行的任务。 executor.awaitTermination(1, TimeUnit.SECONDS); // 等待直到所有任务完成或超时。 }线程常用方法Thread.sleep(5000);当前线程暂停5秒让给别的线程运行sleep完回到就绪Thread.yield();退出一会儿回到等待队列进行等待返回就绪状态Thread.join();可保证线程按顺序执行进行等待状态t1执行 -》 t2.join() -》 t2执行完 -》 t1再执行线程的状态新建状态(NEW)运行状态(RUNNABLE)又分为就绪状态READY)和运行状态(RUNNING)阻塞状态(BLOCKED)等待状态(WAITING)超时等待状态(TIMED_WAITING)消亡状态(TERMINATED)public enum State { NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED; }线程状态说明NEW初始状态线程被构建但是还没有调用start()方法RUNNABLE运行状态Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”BLOCKED阻塞状态表示线程阻塞于锁WAITING等待状态表示线程进入等待状态进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)TIME_WAITING超时等待状态该状态不同于WAITING,它是可以在指定的时间自行返回的TERMINATED终止状态表示当前线程已经执行完毕线程状态迁移图参考文章线程迁移的6种状态多线程数据共享问题多个线程访问同一个资源时需要以某种顺序来保证资源在某一个时刻只能被同一个线程使用。例当多个线程同时对一数据进行写操作即A线程需要使用某个资源此时这个资源正在被B线程使用同步机制就会让A线程进行等待直到B线程结束对该资源的使用A线程才能使用该资源。同步机制保证了资源的安全问题。Java语言的同步机制1、同步代码块2、同步方法可使用synchronize关键字来实现同步。异步与同步相反每个线程都包含了自己的数据和方法在进入输入和输出处理时不必关心其他线程的状态或行为也不必等待输入输出处理完毕才返回。什么时候使用异步当程序执行过程中调用了一个需要花费很长时间的方法但我们不希望程序等待这个方法的返回。此时可使用异步编程异步编程提高了程序的效率。区分异步同步同步银行的一个窗口排队按照顺序执行。异步银行的多个窗口排队。并发编程三大特性有序性、可见性、原子性在 Java 中对象的创建过程可以分为以下 3 步分配内存空间为对象分配内存。初始化对象的成员变量调用构造函数初始化对象的成员变量。将对象的引用指向分配的内存地址将对象的引用赋值给变量。这 3 步操作在多线程环境下可能会引发并发编程中的一些典型问题主要涉及以下并发编程特性1.指令重排序问题编译器和处理器为了提高性能可能会对指令进行重排序。例如步骤 3将引用赋值给变量可能会在步骤 2初始化成员变量之前执行。影响如果步骤 3 先于步骤 2 执行其他线程可能会拿到一个未完全初始化的对象从而导致程序行为异常。解决方案使用volatile关键字禁止指令重排序确保对象的创建过程按顺序执行。2.可见性问题在多线程环境下一个线程对共享变量的修改可能不会立即对其他线程可见。例如线程 A 创建了对象并赋值给instance但线程 B 可能看不到instance的最新值仍然认为instance为null。影响这可能导致多个线程同时创建多个实例破坏了单例模式的唯一性。解决方案使用volatile关键字确保变量的可见性或者使用同步机制如synchronized来保证线程间的可见性。3.原子性问题对象的创建过程分配内存、初始化、赋值不是原子操作。如果多个线程同时进入创建对象的逻辑可能会导致多个实例被创建。影响破坏了单例模式的唯一性。解决方案使用同步机制如synchronized确保创建对象的操作是原子的。结合双检锁DCL单例模式的分析public class Singleton { private static volatile Singleton instance; private Singleton() { } public static Singleton getInstance() { //1、减少不必要的同步 if (instance null) { //2、其他线程可以走到这里读中间态因此加把锁 synchronized (Singleton.class) { if (instance null) { instance new Singleton(); //3、实例化到一半的时候也有可能指令重排序走进来因此再加个volatile } } } return instance; } }在双检锁单例模式中对象的创建过程涉及上述所有并发编程特性指令重排序通过volatile关键字禁止重排序。可见性通过volatile关键字确保instance的修改对其他线程立即可见。原子性通过synchronized关键字确保创建对象的操作是原子的。总结对象的创建过程涉及的并发编程特性及其解决方案步骤并发编程特性问题描述解决方案分配内存空间原子性多个线程可能同时分配内存导致多个实例被创建使用synchronized确保原子性初始化成员变量指令重排序初始化操作可能被重排序导致其他线程拿到未完全初始化的对象使用volatile禁止指令重排序将引用赋值给变量可见性一个线程对instance的修改可能对其他线程不可见导致重复创建实例使用volatile确保可见性通过理解这些并发编程特性可以更好地设计和实现线程安全的单例模式。然而指令重排序是编译器和处理器为了提高性能而进行的优化。在某些情况下步骤 2 和步骤 3 可能会被重排序导致对象的引用被赋值给instance但对象还未完全初始化。锁synchronized关键字当多个程序访问同一资源时需要对这个资源进行上锁锁定的是一个对象对应头64位markword拿出2位来记录是否被锁定synchronized方法synchronized(this)synchronized static方法synchronized(T.class)可重入锁一个方法另外一个方法2个方法用的同一把锁加synchronized就没必要加volatile程序过程中如果出现异常情况默认情况下锁会被释放synchronized(object)不能用string常量、Integer、Long基本数据类型synchronized早期实现为JDK重量级锁OS后来改进为锁升级synchronized(object)锁升级markdown记录这个线程id(偏向锁)如果线程争用升级为自旋锁(10次)自旋太久(10次)或线程太多升级为重量级锁OS自旋锁占CPU等待时间短或者线程少用自旋锁其他情况用重量级锁volatile一种轻量级的同步机制。它主要用于确保变量在多个线程之间的可见性和防止指令重排序但不保证操作的原子性。JAVA中有哪些锁悲观锁、乐观锁、公平锁、非公平锁、独占锁、共享锁、分段锁、偏向锁、自旋锁、重量级锁悲观锁每次操作时假定第3方会修改数据所以每次操作时上锁这样别人想拿到这个数据就会阻塞知道它拿到锁。java的同步synchronized关键字就是典型的悲观锁。MySQL就用到了很多这样的锁机制如行锁、表锁都是在做操作之前先加锁悲观锁使用与读少写多的场景。乐观锁每次读数据时都认为不会被第3方修改所以无需加锁。写数据时要判断在此期间是否有第3方数据的修改。乐观锁在java中通过无锁实现最常用的是CAS算法java原子类中的递增操作就通过CAS自旋实现乐观锁适合读多写少的场景数据库中常用版本号控制比如AtomicInteger。公平锁在并发环境中每个线程在获取锁时先查看锁维护的等待队列如果为空或者当前线程是等待队列的第一个就占有锁否则就加入到等待队列中。优点等待的线程不会被饿死。缺点整体吞吐效率比非公平锁低等待队列除了第一个线程其他线程都会阻塞。在 Java 的ReentrantLock中通过将构造方法的参数fair设置为true来启用公平锁ReentrantLock lock new ReentrantLock(true);非公平锁直接尝试占有锁如果尝试失败再采用公平锁的方法。优点减少唤起线程的开销整体的吞吐效率较高因为线程可能无需阻塞就获得线程。缺点处于等待队列的线程可能会被饿死。在 Java 的ReentrantLock中可以指定锁类型默认为非公平锁。通过将构造方法的参数fair设置为false或默认值启用非公平锁ReentrantLock lock new ReentrantLock(false);独占锁同一时间一个锁只能被一个线程占有。也是通过ReentrantLock类实现。共享锁同一时间一个锁可以被多个线程占有。ReentrantLock是独占锁ReentrantReadWriteLock读锁是共享锁写锁是独占锁。独占锁与共享锁通过AQS来实现通过不同的实现方法实现共享和独占。AQS用来构建锁或其他同步组件的基本框架使用一个整型volatile变量(static state)维护同步状态通过内置FIFO队列实现线程排队。分段锁分段锁是锁的设计不是锁的具体实现。分段锁是锁粒度细化当某个操作不需要更新整个数组是就仅仅只对数组的某一项进行加锁操作。ConcurrentHashMap就是通过分段锁的形式来实现的。常见问题什么是死锁、饿锁、饿死死锁是指多进程线程在运行过程中争夺资源造成的一种僵局当进程线程处于这种局面下无外力作用它们将无法向前推进。活锁线程可以使用资源但它很礼貌的让别的线程先用导致谁也无法使用资源。一个线程在等待另一个线程改变某个标志而另一个线程也在等待这个标志被改变结果两个线程都陷入无限循环的等待中。饿死非公平锁多个线程等待资源时有一个很早就等待的线程一直获取不到锁反而让后面的线程获得了锁。发生死锁的条件互斥共享资源被竞争性的访问同一个时间段只能被一个线程使用。执行并等待线程已持有部分分配给它们的资源并在等待其他资源。不可剥夺线程已持有的资源不可被其他线程强制剥夺。循环等待线程之间存在资源的唤醒依赖链每一个线程都依赖于下一个线程释放的必要资源而最后一个线程又依赖于链条头部的线程进入一个循环等待状态。死锁的解决方案分析死锁由上面的四个必要条件导致只破坏其中一个死锁就不会发生。打破互斥条件允许共享资源被多个线程享用受限于实际场景。在线程运行前就申请到所有资源否则不能进入执行状态但会导致资源利用率和线程并发性降低。让线程已持有的资源可以被强制剥夺。避免出现申请资源环路将资源事先编号按号分配这样可以提高资源的利用率和系统的吞吐量但会增加系统的开销增大进程线程对资源的占有时间。死锁的解决方案通过协程来预防避免死锁确保系统不会进入死锁状态可以允许系统进入死锁然后检测修复它忽视这个问题认为死锁不存在Linux内核就这样死锁的检测和修复最简单常用的方法是系统重启不过这个方法代价很大意味着在这之前所有的进程线程已经完成的工作都将付之东流其中包括参与死锁和未参加死锁的线程。撤销线程剥夺资源可行。收回它们的资源终止参与死锁的线程从而解除死锁。线程回退策略让参与死锁的线程回到发生死锁前的某一处并从这处继续允许以求再次执行不会发生死锁理想但系统开销大需要用堆栈记录线程的每一步变化。撤销线程剥夺资源分2种情况一次性撤销参与死锁的全部进程线程剥夺全部资源逐步撤销参与死锁的进程线程逐步收回死锁进程线程占有的资源。一般来说逐步撤销进程线程要按照一定的原则进行目的是撤销那些代价最小的进程线程比如进程线程按一定的优先级确定线程的代价考虑进程线程运行时的代价和此线程相关的外部作业的代价因素。