Java多线程与并发编程基础
Java多线程与并发编程基础1.线程概念2.线程的状态新建线程可运行线程阻塞和等待终止线程3. 线程的属性一、基础标识属性二、运行状态属性三、中断标记属性四、归属与绑定属性五、底层堆栈相关属性调试用多线程同步与通讯synchronized锁ReentrantLock死锁volatile1.线程概念进程操作系统分配资源的最小单位一个独立运行的程序比如打开微信、IDEA、浏览器每个都是进程拥有独立内存、文件句柄、CPU 资源。线程进程内部执行代码的最小单位是 CPU 调度的最小单元依附于进程存在共享进程的内存、资源。一句话总结一个进程至少有 1 个主线程一个进程可以开多个线程多条执行路线同时跑代码。单线程程序从头到尾只有一条执行路线代码串行执行。例Java main 方法就是主线程所有代码按顺序执行前面代码阻塞后面全部卡住。多线程进程内同时启动多条执行分支CPU 快速切换执行不同线程宏观上 “同时运行”。适用场景IO 等待爬虫、文件读写、网络请求并行计算批量数据处理、AI 向量计算异步任务日志、定时任务、消息消费线程的创建线程创建由thread类完成任务类由Runnable接口完成publicinterfaceRunnable{publicabstractvoidrun();}//创建任务Runnabler()-{System.out.println(线程任务);};//创建线程ThreadtnewThread(r);//启动线程t.run();2.线程的状态线程主要有六6种状态New(新建)Runnable(可运行)Blocked(阻塞)Waiting(等待)Timed waiting(计时等待)Terminated(终止)新建线程当使用new Thread®创建一个线程时候这时候线程还没有运行处于新建状态可运行线程当调用start()方法后线程处于可运行状态这时可能正在运行也可能没有运行需要由操作系统进行调度staticvoidyield()//使当前正在执行的线程向另一个线程交出运行权这是一个静态方法阻塞和等待阻塞当一个线程尝试获取一个内部对象锁时候而这个锁目前被其他线程占有这是线程就会进入阻塞状态WAITING 无限等待无超时必须手动唤醒Object.wait()持有 synchronized 锁的对象调用 wait ()释放锁进入 WAITING需其他线程执行 notify()/notifyAll() 唤醒。Thread.join()A 线程调用 B.join()A 等待 B 线程执行完毕A 进入 WAITING。LockSupport.park()底层锁工具如ReentrantLock底层依赖需要 LockSupport.unpark(线程) 唤醒。CountDownLatch.await()计数器没归零计数器不为 0 时线程无限等待。TIMED_WAITING 限时等待带超时时间超时自动苏醒和 WAITING 逻辑一致但设置了等待时长到时间自动恢复无需他人唤醒Thread.sleep(long ms)主动休眠指定毫秒不释放锁到期自动醒来。Object.wait(long timeout)带超时的等待超时 / 被 notify 都能唤醒。Thread.join(long ms)限时等待目标线程结束。LockSupport.parkNanos() / parkUntil()限时阻塞。并发工具限时等待CountDownLatch.await(timeout, unit)、Semaphore.tryAcquire(timeout)、BlockingQueue.poll(timeout)业务开发高频阻塞场景汇总实战常用同步锁竞争synchronized 抢锁失败 → BLOCKED休眠等待sleep() → TIMED_WAITING生产者消费者队列BlockingQueue.take() 无数据 → WAITING线程等待另一个线程结束join() → WAITING被唤醒后→RUNNABLE锁 / 信号量等待ReentrantLock.lock()、semaphore.acquire() → WAITING异步条件等待CountDownLatch/CyclicBarrier → WAITING网络 / 文件读写 IO数据库、爬虫、接口调用 → OS 阻塞RUNNABLE 状态定时等待wait(1000)、poll(5, TimeUnit.SECONDS) → TIMED_WAITINGThread.yield→ RUNNABLE终止线程线程终止有两个场景run方法正常退出线程执行完毕因为一个没有捕获的异常终止了run方法使得线程意外终止3. 线程的属性一、基础标识属性线程名称 name作用区分线程方便日志、堆栈排查读写getName()/setName(String)默认Thread-0、Thread-1 自增命名线程池建议自定义业务名order-task-1线程ID id作用线程唯一标识JVM内全局唯一获取getId()只读不可修改优先级 priority范围1最低~ 10最高默认5NORM_PRIORITY读写getPriority()/setPriority(int)注意只是给操作系统调度的建议不保证严格优先执行二、运行状态属性线程状态 state枚举Thread.StateNEW / RUNNABLE / BLOCKED / WAITING / TIMED_WAITING / TERMINATED获取getState()只读是否存活 isAlive()true线程已启动、未终止NEW/TERMINATED 返回 false是否守护线程 isDaemon()守护线程后台服务线程GC、定时任务所有用户线程结束后JVM直接退出用户线程业务主线程、任务线程JVM会等待全部用户线程结束才退出设置setDaemon(true)必须在 start() 前调用三、中断标记属性中断标志位 interruptedisInterrupted()获取当前线程中断标记不清除标记Thread.interrupted()静态方法获取并重置清除中断标记设置为falseinterrupt()给线程打上中断标记设置为true不会直接停止线程仅作为信号四、归属与绑定属性线程组 ThreadGroup获取getThreadGroup()作用批量管理一组线程统一停止、统计活跃线程数上下文类加载器 contextClassLoader读写getContextClassLoader()/setContextClassLoader()用途框架Spring、MyBatis加载资源、动态类解决父加载器找不到资源问题ThreadLocal 本地存储线程私有不属于Thread对象字段但属于线程私有属性每个线程独有一份数据线程间隔离典型场景存储用户登录信息、请求上下文、数据库连接五、底层堆栈相关属性调试用获取堆栈getStackTrace()返回当前线程执行栈帧数组排查死锁、卡死代码是否持有监视器锁holdsLock(Object obj)判断当前线程是否持有指定对象的synchronized锁多线程同步与通讯多线程同步是当多个线程访问同一资源保证数据的读写一致性常用的同步手段是给资源加锁保证同一时刻只有一个线程能访问。最常用的几种通信方式:wait/notify机制:配合synchronized 使用线程调wait释放锁进入等待其他线程调notify 唤醒它Lock Condition: ReentrantLock的newCondition可以创建多个等待队列比 wait/notify灵活BlockingQueue:生产者消费者模型的首选put和take方法自带阻塞逻辑volatile:轻量级同步适合一个线程写、多个线程读的场景synchronized锁synchronized为Java内置关键字用在方法或者同步代码块上jvm会给你自动加锁// 同步方法publicsynchronizedvoidtransfer(intamount){balance-amount;}// 同步代码块锁粒度更细publicvoidtransfer(intamount){synchronized(this){balance-amount;}}synchronized配合wait()和notify()实现线程间通信:wait():当前线程进入等待状态释放锁直到被其他线程唤醒。必须在同步块里调用。notify():唤醒一个等待的线程。多个线程在等的话随机挑一个。notifyAll():唤醒所有等待的线程让它们重新竞争锁。实现原理synchronized代码块实现原理java字节码中用来实现同步代码块的是monitorenter和monitorexit指令。monitorenter指令执行时当前线程试图获取objectref所存储的对象锁(ObjectMonitor对象)如果取到的monitor对象_count字段为0则将该线程引用存储在monitor对象的_owner字段中并且将_count字段加1线程继续执行直到线程执行monitorexit指令。如果取到的monitor对象_count字段不为0那么首先会判断该线程是否与monitor对象的_owner字段存储的引用相同如果相同_count值加1仍然可以继续运行这保证了synchronized的可重入性如果不同则将该线程放入_WaitSet线程进入 wait 状态等待唤醒。synchronized方法实现原理与synchronized 代码块不同JVM可以从方法常量池中的方法表结构(method_info Structure)中的AcC_SYNCHRONIZED访问标志区分一个方法是否同步方法。当方法调用时调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置如果设置了执行线程将先持有monitor对象然后再执行方法最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。如果一个同步方法执行期间抛出了异常并且在方法内部无法处理此异常那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放ReentrantLockJUC包提供的可重入锁需要手动上锁lock()用完必须在finally块里unlock()解锁否则会死锁privatefinalReentrantLocklocknewReentrantLock();publicvoidtransfer(intamount){lock.lock();try{balance-amount;}finally{lock.unlock();// 必须放 finally保证一定会解锁}}ReenTrantLock与synochronized对比-tryLock():尝试获取锁拿不到立刻返回false不傻等还能设置超时时间-公平锁:构造函数传true就是公平锁谁先来谁先得不会出现线程饿死。-可中断:locklnterruptibly()方法支持响应中断等锁的时候可以被打断。-多条件队列:一个锁可以绑定多个Condition实现分组唤醒比synchronized的单一等待队列灵活多了。 javaprivatefinalConditionnotEmptylock.newCondition();privatefinalConditionnotFulllock.newCondition();// 生产者notFull.await();// 等待不满条件notEmpty.signal();// 通知不空// 消费者notEmpty.await();// 等待不空条件notFull.signal();// 通知不满实现原理ReentrantLock底层就是AQS CAS那套东西。核心是一个int类型的state变量和一个等待队列。加锁的时候线程用CAS尝试把 state 从0改成1改成功了就拿到锁把exclusiveOwnerThread设成自己。改失败了说明锁被别人占着乖乖去同步队列尾部排队然后park 挂起。解锁的时候state减1如果减到О就唤醒队列里的第一个等待线程。可重入的实现很简单:同一个线程再次加锁发现 exclusiveOwnerThread就是自己直接把 state加1不用排队。释放的时候每次减1减到О才真正释放锁。synchronized与ReentrantLock区别死锁什么情况下会发生死锁?一般来说多个线程相互等待对方持有的资源且都不释放自己的资源这种现象称为死锁。具体有四个必要条件必须同时满足才会发生死锁:互斥条件:线程对分配的资源有排他性访问即每一个资源要么分配给一个线程要么是可用的。占有且等待:一个线程已经占有至少一个资源但又在等待另一个资源而此时该资源被其他线程占有。不可剥夺:线程占有的资源不能被剥夺资源只能在使用完后由线程自行释放。环路等待:存在一种资源等待的环形链即线程A在等待线程B占有的资源而线程B在等待线程C占有的资源…直到最后一个线程等待线程A占有的资源从而形成一个等待环路。这四大必要条件只要能够破坏其一就能避免死锁可以采取以下几种措施:避免互斥条件:尽量减少资源的独占性使用非阻塞同步机制。破坏占有且等待:采用资源预分配策略即进程一次性请求所需的所有资源。破坏不可剥夺:如果一个进程得不到所需的资源应释放它所持有的资源或者使用优先级来剥夺资源。破坏环路等待:对系统中的资源进行排序每个线程按序请求资源避免形成环路。volatilevolatile只保证可见性和禁止重排序不保证原子性// 这段代码不是线程安全的privatevolatileintcount0;publicvoidincrement(){count;// 虽然 count 是 volatile但 操作不是原子的}volatile适合的场景是一个线程写、多个线程读的情况比如状态标志位。如果多个线程都要写还是得用锁或者原子类。