十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

第20章 多线程入门

第20章 多线程入门 第20章 多线程入门让程序同时做多件事就是多线程。本节学习线程的创建方式、生命周期和简单同步。多线程是并发编程的基础也是面试重点。一、进程与线程进程正在运行的程序是资源分配的基本单位。一个进程内有独立的内存空间。线程进程内的执行单元是 CPU 调度的基本单位。一个进程至少有一个线程主线程可以有多个线程共享进程的内存。进程和线程的区别对比项进程线程资源独立内存空间共享所属进程的内存切换开销大要切换内存空间小通信跨进程麻烦直接读写共享变量崩溃影响进程间互不影响一个线程出问题可能带崩整个进程多线程的好处充分利用多核 CPU4 核 CPU 上 4 个线程可以真并行耗时任务下载、查询不阻塞主流程提升程序响应速度界面不卡为什么需要多线程想象单线程下载大文件下载的几秒里整条程序都卡住用户点哪都没反应。把下载扔进子线程主线程继续响应点击、刷新界面——等待和干活并行互不阻塞。二、创建线程的两种方式方式一继承 Thread 类classMyThreadextendsThread{Overridepublicvoidrun(){for(inti1;i5;i){System.out.println(getName() 运行中i);}}}MyThreadt1newMyThread();t1.start();// 启动线程不要直接调用 run()两个线程同时 start输出会交替出现顺序不固定——这就是并发的直观感受A、B 的打印穿插在一起谁先谁后由 CPU 调度决定。方式二实现 Runnable 接口推荐classMyRunnableimplementsRunnable{Overridepublicvoidrun(){for(inti1;i5;i){System.out.println(Thread.currentThread().getName()i);}}}ThreadtnewThread(newMyRunnable(),工作线程);t.start();为什么推荐 RunnableJava 单继承实现接口不影响继承其他类任务run 逻辑和线程Thread分离更符合把任务交给线程执行的思想——同一份任务可以交给多个线程执行而继承 Thread 做不到后面学的线程池就基于 Runnable/Callable方式三Lambda 简化JDK 8ThreadtnewThread(()-{System.out.println(Lambda 线程);});t.start();Runnable只有一个抽象方法是函数式接口所以能用 Lambda 简化。这个写法从 JDK 8 引入后JDK 11、17 里用法完全一样放心用。三、start() 与 run() 的区别高频考点MyThreadtnewMyThread();t.start();// 开启新线程run() 在新线程中异步执行t.run();// 直接调用 run()只是普通方法调用不新开线程start()才创建新线程run()只是普通方法。忘了 start 直接调 run代码会在主线程顺序执行看不到并发效果。用输出顺序一眼看出区别t.run();// run()main 线程顺序执行输出一定是先 A 后 Bt.start();// start()新线程与主线程抢 CPU输出顺序不固定另外start() 只能调用一次重复 start 会抛IllegalThreadStateException易错点再展开。四、线程的生命周期新建(New) → 就绪(Runnable) → 运行(Running) → 死亡(Terminated) ↑ ↓ 阻塞/等待(Blocked/Waiting) → 就绪新建new Thread() 创建就绪start() 后等待 CPU 调度运行获得 CPU 时间片执行阻塞/等待sleep、wait、IO 等待等条件满足后回到就绪死亡run() 执行完或异常终止用t.getState()可以亲眼观察状态流转start 前是 NEWstart 后是 RUNNABLE大概率执行完是 TERMINATEDsleep 期间是 TIMED_WAITING。常用方法Thread.sleep(1000);// 静态方法当前线程休眠 1000 毫秒t.join();// 等待 t 线程执行完当前线程再继续主线程等子线程收尾的典型用法Thread.currentThread();// 获取当前线程对象t.getName();// 线程名t.setName(名字);// 设置线程名也可以构造器传五、线程安全问题多个线程同时操作共享数据会产生数据不一致classCounter{intcount0;voidincrement(){count;// 问题所在count 不是原子操作}}count在 CPU 层面其实是三步读 count → 加 1 → 写回。两个线程同时执行可能都读到 0都写成 1导致结果少了。这属于并发三大问题里的原子性问题另外两个是可见性、有序性。CountercnewCounter();// 开 10 个线程每个自增 1000 次for(inti0;i10;i){newThread(()-{for(intj0;j1000;j){c.increment();}}).start();}// 主线程等待所有线程执行完简化写法Thread.sleep(2000);System.out.println(c.count);// 期望 10000实际可能不到// 输出9387参考值每次运行都不同多数小于 10000为什么会丢假设 count 现在是 5线程 A 读到 5 还没写回线程 B 也读到 5两个都写 6——加了两次结果只加了一次。线程越多、竞争越激烈丢得越厉害。六、解决线程安全synchronized用synchronized给方法加锁同一时刻只有一个线程能执行classCounter{intcount0;synchronizedvoidincrement(){count;}}加锁后再跑上一节的 10×1000 测试结果永远是 10000。synchronized两种用法// 1. 修饰方法锁住整个方法publicsynchronizedvoidmethod(){}// 2. 修饰代码块只锁关键部分粒度更细、性能更好publicvoidmethod(){// 非共享代码不用锁synchronized(this){// 操作共享数据的代码}}锁的本质线程进入同步代码前必须先获得对象锁没拿到就阻塞等待。注意锁的对象是实例而不是代码——两个线程必须锁同一个对象才互斥各自 new 一个对象再锁等于没锁。七、线程安全的集合开发中常直接用线程安全容器省去手动加锁// 老式的线程安全集合VectorStringvnewVector();HashtableString,StringhtnewHashtable();// 并发包 java.util.concurrent推荐高性能线程安全集合// 从 JDK 8 起就是标准库标配JDK 11、17 里同样内置ConcurrentHashMapString,StringmapnewConcurrentHashMap();CopyOnWriteArrayListStringlistnewCopyOnWriteArrayList();并发容器帮你把锁写好了直接用10 个线程各往 CopyOnWriteArrayList 加 100 个元素结果稳定是 1000换成普通 ArrayList 会小于 1000甚至抛并发修改异常。老式 Vector/Hashtable 给每个方法都加了锁并发高时性能差是历史遗留现代项目优先用 concurrent 包。八、线程池进阶预告频繁 new Thread 开销大正式开发用线程池复用线程importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;ExecutorServicepoolExecutors.newFixedThreadPool(4);// 4 个线程的池for(inti0;i10;i){inttaskIdi;pool.execute(()-System.out.println(任务 taskId 执行中));}pool.shutdown();// 关闭线程池Runnable 没有返回值想让线程算个结果回来用 Callable Futurepool.submit(() - 42)返回 Futurefuture.get()阻塞取结果。线程池核心参数核心线程数、最大线程数、任务队列、拒绝策略——并发进阶再详讲。九、扩展知识9.1 sleep vs wait 初识两者都能让线程歇一会儿但差别很大对比项Thread.sleep(ms)Object.wait()谁的静态方法Thread 的静态方法Object 的实例方法要不要锁不需要任何地方能调必须在 synchronized 内是否释放锁不释放抱着锁睡释放锁让别人进来怎么醒来到时间自动醒等 notify/notifyAll 唤醒典型配合生产者/消费者。厨师做好菜就 wait 等通知服务员取走菜就 notifyclassKitchen{synchronizedvoidcook()throwsInterruptedException{System.out.println(厨师做好菜等待取走);wait();// 释放锁并等待通知System.out.println(菜被取走继续做下一道);}synchronizedvoidtake(){System.out.println(服务员取走菜);notify();// 唤醒一个等待该锁的线程}}publicstaticvoidmain(String[]args)throwsInterruptedException{KitchenknewKitchen();newThread(()-{try{k.cook();}catch(InterruptedExceptione){}}).start();Thread.sleep(100);// 保证厨师先拿到锁k.take();}// 输出厨师做好菜等待取走// 服务员取走菜// 菜被取走继续做下一道9.2 守护线程用户线程主线程结束不影响它会自己跑完如业务线程守护线程所有用户线程结束后守护线程自动结束如 GC 垃圾回收线程、后台日志用法daemon.setDaemon(true)必须在start()之前设置之后主线程一结束守护线程自动退出不会无限跑下去。适合后台心跳、日志清理这类陪跑任务。9.3 线程优先级setPriority(1~10)给线程设置优先级默认 5Thread.MIN_PRIORITY1、MAX_PRIORITY10。注意这只是给调度器的建议具体由操作系统决定不能依赖它保证执行顺序——了解即可别在业务里依赖优先级。9.4 死锁初识两个线程各自持有一把锁又都想拿对方的锁互相等对方释放——谁都不让程序卡死ObjectlockAnewObject();ObjectlockBnewObject();newThread(()-{synchronized(lockA){System.out.println(线程1拿到锁A);try{Thread.sleep(100);}catch(InterruptedExceptione){}synchronized(lockB){// 等锁B但锁B在线程2手里System.out.println(线程1拿到锁B);}}}).start();newThread(()-{synchronized(lockB){System.out.println(线程2拿到锁B);try{Thread.sleep(100);}catch(InterruptedExceptione){}synchronized(lockA){// 等锁A但锁A在线程1手里System.out.println(线程2拿到锁A);}}}).start();// 输出线程1拿到锁A// 线程2拿到锁B// 之后互相等程序卡死这就是死锁避免死锁的入门准则所有线程按同样的顺序拿锁都先 A 后 B就永远不会互相等。十、易错点1. 直接调用 run() 而不是 start()// ❌ run() 只是普通方法调用没有并发效果t.run();// ✅ start() 才创建新线程t.start();2. start() 重复调用t.start();t.start();// ❌ 抛 IllegalThreadStateException// ✅ 一个线程只能 start 一次要再跑就 new 一个新线程3. 共享变量不加同步数据错乱// ❌ count 不是原子操作多线程下结果变少// ✅ 加 synchronized或直接用原子类JDK 8 自带importjava.util.concurrent.atomic.AtomicInteger;AtomicIntegercountnewAtomicInteger(0);count.incrementAndGet();// 原子自增无需加锁4. sleep 抛 InterruptedException 未处理编译报错// ❌ Thread.sleep(1000); // 编译错误Unhandled exception type InterruptedException// ✅ 捕获或声明抛出try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}5. Lambda 里直接使用循环变量// ❌ 编译报错i 不是 effectively final循环中会变for(inti0;i5;i){newThread(()-System.out.println(i)).start();}// ✅ 先复制到局部变量for(inti0;i5;i){inttaskIdi;newThread(()-System.out.println(taskId)).start();}6. 主线程不等子线程就结束// ❌ main 跑完就退出子线程可能没跑完// ✅ 用 join() 等子线程完成再收尾worker.join();十一、小结进程是资源分配的基本单位线程是 CPU 调度的基本单位线程共享进程内存创建线程继承 Thread / 实现 Runnable推荐/ LambdaJDK 811、17 用法一致start()开新线程且只能调一次run()只是普通方法调用生命周期新建 → 就绪 → 运行 → 阻塞/等待 → 死亡多线程操作共享数据有安全风险count 不是原子操作synchronized 加锁解决或用原子类/并发容器sleep 不释放锁wait 释放锁必须在 synchronized 内靠 notify 唤醒生产环境用线程池和并发容器不手写裸线程多把锁注意按相同顺序拿避免死锁下一节最后一节用综合项目串起前面所有知识。下一篇第21章 实战学生信息管理系统综合应用
返回列表