【Java】线程初阶 中

发布时间:2026/10/22 17:53:32
【Java】线程初阶 中
线程安全线程不安全//定义静态共享变量count , t1和t2两个线程会同时操作这同一个变量 private static int count0; public static void main(String[] args) throws InterruptedException { //创建线程1 Thread t1new Thread(()-{ //循环5000次每次对count执行自增操作 for(int i0;i50000;i) { count; } }); //创建线程2 Thread t2new Thread(()-{ //循环50000次每次对count执行自增操作 for(int i0;i50000;i) { count; } }); t1.start(); //启动线程t1,进入就绪状态等待cpu调度 t2.start(); //启动线程t2,进入就绪状态等待cpu调度 //join() 方法让当前线程阻塞等待调用join的线程执行完毕之后主线程才继续往下执行 t1.join(); t2.join(); // t2.join();主线程等待t2执行结束 System.out.println(count: count); }核心count 不是一条原子指令count在 CPU 层面会拆成3 步独立操作1load把 count 的值从主内存读到 CPU 寄存器2add寄存器里的值 13save把寄存器新值写回主内存的 count原子不可分割一步执行完中间不会被别的线程打断join() 的作用main 线程阻塞等待 t1、t2 执行完毕等两个线程全部自增完再打印 count。如果没有 joinmain 线程会直接打印大概率输出 0。怎么解决这个线程安全问题1加 synchronized 同步锁synchronized (锁对象) { count; }或给自增方法加synchronized保证同一时刻只有一个线程执行 count2使用原子类AtomicInteger它里面的自增是 CAS 原子操作不需要锁private static AtomicInteger count new AtomicInteger(0); count.getAndIncrement(); // 等价 count总结线程不安全根本原因多个线程并发修改同一个共享变量修改操作不是原子的加上线程抢占调度造成指令交错、数据覆盖。线程安全定义多线程环境运行结果和单线程预期结果一样这个代码就是线程安全的线程不安全的几个根源线程是随机抢占式调度前提条件操作系统什么时候切线程是不确定的执行顺序有无数种可能。代码必须保证任意调度顺序下结果都正确不然就线程不安全。多个线程修改同一个共享数据必要条件只有读共享变量没问题并发写 / 读写同时发生才会出问题。原子性1原子性一段操作不可分割执行中途不会被其他线程打断2例子把代码块当成房间原子性 一次只能进去一个人进去之后关上门别人不能中途闯进来打断。锁synchronized就是这个房门锁实现互斥同步。可见性1可见性一个线程修改共享变量其他线程可以立刻看到最新值2JMM 模型规则a共享变量存在主内存b每个线程有自己独立的工作内存寄存器 / 缓存c线程读变量先拷贝一份副本到自己工作内存之后读的是副本d线程改变量先改自己工作内存里的副本不一定立刻刷回主内存3现象线程 1 修改了变量 a但是没有及时同步回主内存线程 2 一直读自己本地旧副本永远看不到最新的值这是可见性问题。4经典场景一个线程修改 flag 标记另一个 while 循环里读 flag循环停不下来。解决方案volatile关键字保证可见性每次读写都和主内存同步。有序性1CPU/JVM 会做指令重排序为了提高执行效率调整代码指令执行顺序单线程没问题多线程下会出现逻辑错乱2volatile 可以禁止指令重排3指令重排JVM、CPU、编译器为了提升执行效率在不改变单线程执行结果的前提下打乱代码执行顺序4多线程下重排会出问题a编译器只保证单线程语义不变无法预判其他线程的行为。b多线程环境指令顺序被打乱就会出现意料之外的错误。总结三大特性1原子性操作不可分割中途不能被打断 → 靠synchronized/ 锁保证2可见性一个线程修改其他线程及时看到最新值→volatile、synchronized 都能保证3有序性代码按照我们写的顺序执行不被 CPU / 编译器乱排→volatile禁止重排4线程不安全 多线程并发修改共享变量 破坏原子性 / 可见性 / 有序性区分两个关键字1synchronized保证原子性 可见性会加锁阻塞其他线程2volatile保证可见性、禁止重排序不能保证原子性解决线程不安全问题private static int count0; //共享变量两个线程都会修改它 public static void main(String[] args)throws InterruptedException { Object lockernew Object(); //锁对象t1,t2必须共用这同一个锁才能实现互斥 // 线程t1: 循环5万次每次对count自增 Thread t1new Thread(()-{ for(int i0;i50000;i) { // 同步代码以locker作为锁 synchronized(locker) { // count 不是原子操作读取1,写回三步 //加锁之后这三步会整体执行不会被其他线程打断 count; } } }); //线程t2同样循环5万次自增 Thread t2new Thread(()-{ for(int i0;i50000;i) { //和t1使用同一个locker锁同一时刻只有一个线程能进入代码块 synchronized (locker){ count; } } }); t1.start(); t2.start(); //启动t1线程 // join()主线程等待t1执行完毕才继续往下走 t1.join(); // join()主线程等待t1执行完毕才继续往下走 t2.join(); //俩线程执行完后打印100000 System.out.println(count: count); }核心原理synchronized 同步锁synchronized(锁对象) 是互斥锁1同一个锁对象 locker同一时刻只能有一个线程拿到锁进入大括号里面的代码执行2其他想要拿这个锁的线程会阻塞等待直到持有锁的线程执行完同步代码块、释放锁3把非原子操作 count 包裹在同步块里面保证 count 这三步load、add、save一次性完整执行中途不会被别的线程打断 → 保证原子性4同时 synchronized 自带内存屏障释放锁的时候会把工作内存的数据刷回主内存加锁的时候从主内存读取最新值自动保证可见性也会禁止指令重排。重点两个线程必须用同一个锁对象如果 t1 锁 A 对象、t2 锁 B 对象两把不同的锁互不影响锁就失效了依然线程不安全。synchronized 监视器锁monitor locksynchronizedsynchronized 是 Java 内置关键字也叫监视器锁、内置锁锁信息保存在 Java 对象头中底层重量级锁依托操作系统 mutex lock 实现两大核心特性互斥、可重入互斥独占同一个对象的锁同一时刻只能有一个线程持有。1进入synchronized修饰的代码块 / 方法加锁2退出synchronized修饰的代码块 / 方法解锁3例子synchronized void increase(){ count; }修饰实例方法锁是当前 this 对象形象理解每个对象自带锁状态类似厕所有人 / 无人1无人 → 线程占用标记为有人2有人 → 其他线程进入阻塞等待队列排队阻塞等待细节重点面试点操作系统为这把锁维护等待队列1锁被占用时其他线程获取锁失败阻塞2线程 A 释放锁后操作系统唤醒等待队列里的线程3synchronized 是非公平锁不遵守先来后到比如 A 先拿到锁B、C 依次排队A 释放锁后B 和 C 重新竞争先排队的 B 不一定优先拿到锁可重入同一条线程已经持有这把锁再次去获取同一把锁可以成功获取不会自己把自己卡死不会死锁对比不可重入锁会自己死锁lock(); // 第一次加锁成功 lock(); // 第二次再加锁锁被自己占用阻塞等待 // 线程卡住永远无法执行解锁死锁synchronized 可重入的底层原理Monitor 内部维护两个信息1持有者 owner记录当前哪个线程持有锁2计数器 count记录重入次数1同一个线程再次获取锁计数器 12退出同步块计数器 - 13只有计数器减到0锁才算真正释放其他线程才能抢锁面试1锁在哪对象头里的 monitor 监视器不是代码不同对象是不同的锁互不干扰2互斥保证原子性同一对象同时只能一个线程执行同步代码3可重入依靠 owner 计数器实现支持嵌套同步4公平性非公平锁释放锁后线程竞争不保证排队顺序5类型JVM 层面隐式锁自动加锁、自动释放不用手动 unlock例子嵌套 synchronized合法不会死锁for (int i 0; i 50000; i) { synchronized (locker) { synchronized (locker) { count; } } }synchronized 的 4 种写法核心synchronized 锁的是【对象】不是代码 / 方法只有争抢同一个锁对象才会互斥阻塞修饰代码块锁任意对象自定义锁对象public class SynchronizedDemo { private Object locker new Object(); public void method() { synchronized (locker) { // 同步代码 } } }锁对象locker 这个 Object 实例锁当前实例对象 thispublic class SynchronizedDemo { public void method() { synchronized (this) { // 同步代码 } } }锁对象当前实例 this等价于同步普通成员方法修饰普通成员方法实例方法public class SynchronizedDemo { public synchronized void method() { // 同步代码 } }1等价写法synchronized(this)2锁对象调用这个方法的实例对象this3不同实例锁互不干扰同一个实例多个synchronized普通方法互斥修饰 static 静态方法public class SynchronizedDemo { public synchronized static void method() { // 同步代码 } }1等价写法synchronized(SynchronizedDemo.class)2锁对象类对象Class 对象这个类全局唯一3静态锁 和 实例锁this是两把完全不同的锁不会互斥面试高频坑点两个线程竞争同一把锁对象才会阻塞等待拿两把不同的锁互不影响并行执行容易错的例子1线程 A 调用同一个对象的synchronized普通方法线程 B 调用同一个对象另一个synchronized普通方法 → 抢 this 锁互斥2线程 A 调用对象 A 的同步方法线程 B 调用对象 B 的同步方法→ 两个 this两把锁不互斥3线程 A 调用静态同步方法锁 class线程 B 调用同一个实例的普通同步方法锁 this→ class 锁 vs this 锁两把锁不互斥Java 标准库 线程安全 、线程不安全类线程不安全类这些类在多线程并发修改共享对象的时候会出现数据错乱、并发异常1ArrayList2LinkedList3HashMap4TreeMap5HashSet6TreeSet7StringBuilder例StringBuilder 方法没有加锁多线程同时 append很容易数据丢失、结果错乱线程安全类依靠 synchronized 加锁实现安全有锁保护并发性能一般1Vector数组集合所有方法synchronized不推荐使用2HashTable哈希表方法synchronized不推荐使用3StringBuffer字符串拼接核心 append 等方法都带synchronizedOverride public synchronized StringBuffer append(Object obj) { toStringCache null; super.append(String.valueOf(obj)); return this; }对比StringBuilder 无锁不安全StringBuffer 加 synchronized安全不可变类不加锁天然线程安全只读不能修改1StringaString 一旦创建内容不能修改所有看似修改的操作substring、 拼接都是生成新对象原对象不变没有写竞争天然线程安全。总结区分1StringBuilder不安全无锁 vsStringBuffer安全synchronized 锁2HashMap不安全 vsHashTable安全过时 vsConcurrentHashMap安全推荐3ArrayList不安全 vsVector安全过时4String不可变天生线程安全不靠锁CopyOnWriteArrayList、CopyOnWriteArraySet写时复制适合读多写少场景