-
InheritableThreadLocal相比ThreadLocal多一个能力:在创建子线程Thread时,子线程Thread会自动继承父线程的InheritableThreadLocal信息到子线程中,进而实现在在子线程获取父线程的InheritableThreadLocal值的目的。关于ThreadLocal详细内容,可以看这篇文章:史上最全ThreadLocal 详解和 ThreadLocal 的区别举个简单的栗子对比下InheritableThreadLocal和ThreadLocal:javapublic class InheritableThreadLocalTest { private static final ThreadLocal<String> threadLocal = new ThreadLocal<>(); private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>(); public static void main(String[] args) { testThreadLocal(); testInheritableThreadLocal(); } /** * threadLocal测试 */ public static void testThreadLocal() { // 在主线程中设置值到threadLocal threadLocal.set("我是父线程threadLocal的值"); // 创建一个新线程并启动 new Thread(() -> { // 在子线程里面无法获取到父线程设置的threadLocal,结果为null System.out.println("从子线程获取到threadLocal的值: " + threadLocal.get()); } ).start(); } /** * inheritableThreadLocal测试 */ public static void testInheritableThreadLocal() { // 在主线程中设置一个值到inheritableThreadLocal inheritableThreadLocal.set("我是父线程inheritableThreadLocal的值"); // 创建一个新线程并启动 new Thread(() -> { // 在子线程里面可以自动获取到父线程设置的inheritableThreadLocal System.out.println("从子线程获取到inheritableThreadLocal的值: " + inheritableThreadLocal.get()); }).start(); } }执行结果:text从子线程获取到threadLocal的值:null从子线程获取到inheritableThreadLocal的值:我是父线程inheritableThreadLocal的值可以看到子线程中可以获取到父线程设置的inheritableThreadLocal值,但不能获取到父线程设置的threadLocal值实现原理InheritableThreadLocal 的实现原理相当精妙,它通过在创建子线程的瞬间,“复制”父线程的线程局部变量,从而实现了数据从父线程到子线程的一次性、创建时的传递 。其核心工作原理可以清晰地通过以下序列图展示,它描绘了当父线程创建一个子线程时,数据是如何被传递的:子线程ThreadLocalMapInheritableThreadLocalThread构造方法父线程子线程ThreadLocalMapInheritableThreadLocalThread构造方法父线程关键步骤:初始化检查父线程的 inheritableThreadLocalsloop[遍历父线程Map中的每个Entry]子线程拥有父线程变量的副本创建 new Thread()调用 init() 方法createInheritedMap(parent.inheritableThreadLocals)新建一个ThreadLocalMap调用 key.childValue(parentValue)返回子线程初始值(默认返回父值,可重写)将 (key, value) 放入新Map返回新的ThreadLocalMap对象将新Map赋给子线程的inheritableThreadLocals属性下面我们来详细拆解图中的关键环节。核心实现机制**数据结构基础:Thread类内部维护了两个 ThreadLocalMap类型的变量 :threadLocals:用于存储普通 ThreadLocal设置的变量副本。inheritableThreadLocals:专门用于存储 InheritableThreadLocal设置的变量副本 。InheritableThreadLocal通过重写 getMap和 createMap方法,使其所有操作都针对 inheritableThreadLocals字段,从而与普通 ThreadLocal分离开 。继承触发时刻:子线程的创建。继承行为发生在子线程被创建(即执行 new Thread())时。在 Thread类的 init方法中,如果判断需要继承(inheritThreadLocals参数为 true)且父线程(当前线程)的 inheritableThreadLocals不为 null,则会执行复制逻辑 。复制过程的核心:createInheritedMap。这是实现复制的核心方法 。它会创建一个新的 ThreadLocalMap,并将父线程 inheritableThreadLocals中的所有条目遍历拷贝到新 Map 中。Key的复制:Key(即 InheritableThreadLocal对象本身)是直接复制的引用。Value的生成:Value 并非直接复制引用,而是通过调用 InheritableThreadLocal的 childValue(T parentValue)方法来生成子线程中的初始值。默认实现是直接返回父值(return parentValue;),这意味着对于对象类型,父子线程将共享同一个对象引用 。关键特性与注意事项创建时复制,后续独立:继承只发生一次,即在子线程对象创建的瞬间。此后,父线程和子线程对各自 InheritableThreadLocal变量的修改互不影响 。在线程池中的局限性:这是 InheritableThreadLocal最需要警惕的问题。线程池中的线程是复用的,这些线程在首次创建时可能已经从某个父线程继承了值。但当它们被用于执行新的任务时,新的任务提交线程(逻辑上的“父线程”)与工作线程已无直接的创建关系,因此之前继承的值不会更新,这会导致数据错乱(如用户A的任务拿到了用户B的信息)或内存泄漏 。对于线程池场景,应考虑使用阿里开源的 TransmittableThreadLocal (TTL) 。浅拷贝与对象共享:由于 childValue方法默认是浅拷贝,如果存入的是可变对象(如 Map、List),父子线程实际持有的是同一个对象的引用。在一个线程中修改该对象的内部状态,会直接影响另一个线程 。若需隔离,可以重写 childValue方法实现深拷贝 。内存泄漏风险:与 ThreadLocal类似,如果线程长时间运行(如线程池中的核心线程),并且未及时调用 remove方法清理,那么该线程的 inheritableThreadLocals会一直持有值的强引用,导致无法被GC回收。良好的实践是在任务执行完毕后主动调用 remove()线程池中局限性一般来说,在真实的业务场景下,没人会直接 new Thread,而都是使用线程池的,因此InheritableThreadLocal在线程池中的使用局限性要额外注意首先,我们先理解 InheritableThreadLocal的继承前提InheritableThreadLocal的继承只发生在 新线程被创建时(即 new Thread()并启动时)。在创建过程中,子线程会复制父线程的 InheritableThreadLocal值。在线程池中,线程是预先创建或按需创建的,并且会被复用。因此,继承只会在线程池创建新线程时发生,而不会在复用现有线程时发生。再看线程池创建新线程的条件,对于标准的 ThreadPoolExecutor,新线程的创建遵循以下规则:当前线程数 < 核心线程数:当提交新任务时,如果当前运行的线程数小于核心线程数,即使有空闲线程,线程池也会创建新线程来处理任务。此时,新线程会继承父线程(提交任务的线程)的 InheritableThreadLocal。当前线程数 >= 核心线程数 && 队列已满 && 线程数 < 最大线程数:当任务队列已满,且当前线程数小于最大线程数时,线程池会创建新线程来处理任务。同样,新线程会继承父线程的 InheritableThreadLocal。不会继承的场景线程复用:当线程池中有空闲线程时(例如,当前线程数 >= 核心线程数,但队列未满),任务会被分配给现有线程执行。此时,没有新线程创建,因此不会发生继承。现有线程的 InheritableThreadLocal值保持不变(可能是之前任务设置的值),这可能导致数据错乱(如用户A的任务看到用户B的数据)。线程数已达最大值:如果线程数已达最大线程数,且队列已满,新任务会被拒绝(根据拒绝策略),也不会创建新线程,因此不会继承。不只是线程池污染,线程池使用 InheritableThreadLocal 还可能存在获取不到值的情况。例如,在执行异步任务的时候,复用了某个已有的线程A,并且当时创建该线程A的时候,没有继承InheritableThreadLocal,进而导致后面复用该线程的时候,从InheritableThreadLocal获取到的值为null:javapublic class InheritableThreadLocalWithThreadPoolTest { private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>(); // 这里线程池core/max数量都只有2 private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 2, 2, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(3000), new ThreadPoolExecutor.CallerRunsPolicy() ); public static void main(String[] args) { // 先执行了不涉及InheritableThreadLocal的子任务初始化线程池线程 testAnotherFunction(); testAnotherFunction(); // 后执行了涉及InheritableThreadLocal testInheritableThreadLocalWithThreadPool("张三"); testInheritableThreadLocalWithThreadPool("李四"); threadPoolExecutor.shutdown(); } /** * inheritableThreadLocal+线程池测试 */ public static void testInheritableThreadLocalWithThreadPool(String param) { // 1. 在主线程中设置一个值到inheritableThreadLocal inheritableThreadLocal.set(param); // 2. 提交异步任务到线程池 threadPoolExecutor.execute(() -> { // 3. 在线程池-子线程里面可以获取到父线程设置的inheritableThreadLocal吗? System.out.println("线程名: " + Thread.currentThread().getName() + ", 父线程设置的inheritableThreadLocal值: " + param + ", 子线程获取到inheritableThreadLocal的值: " + inheritableThreadLocal.get()); }); // 4. 清除inheritableThreadLocal inheritableThreadLocal.remove(); } /** * 模拟另一个独立的功能 */ public static void testAnotherFunction() { // 提交异步任务到线程池 threadPoolExecutor.execute(() -> { // 在线程池-子线程里面可以获取到父线程设置的inheritableThreadLocal吗? System.out.println("线程名: " + Thread.currentThread().getName() + ", 线程池-子线程摸个鱼"); }); }}执行结果:text线程名:pool-1-thread-2,线程池-子线程摸个鱼线程名:pool-1-thread-1,线程池-子线程摸个鱼线程名:pool-1-thread-1,父线程设置的inheritableThreadLocal值:李四,子线程获取到inheritableThreadLocal的值:null线程名:pool-1-thread-2,父线程设置的inheritableThreadLocal值:张三,子线程获取到inheritableThreadLocal的值:null当然了,解决这个问题可以考虑使用阿里开源的 TransmittableThreadLocal (TTL),或者在提交异步任务前,先获取线程数据,再传入。例如:java// 1. 在主线程中先获取inheritableThreadLocal的值String name = inheritableThreadLocal.get(); // 2. 提交异步任务到线程池 threadPoolExecutor.execute(() -> { // 3. 在线程池-子线程里面直接传入数据 System.out.println("线程名: " + Thread.currentThread().getName() + ", 父线程设置的inheritableThreadLocal值: " + param + ", 子线程获取到inheritableThreadLocal的值: " + name); }); 与 ThreadLocal 的对比特性ThreadLocalInheritableThreadLocal数据隔离线程绝对隔离线程绝对隔离子线程继承不支持支持(创建时)底层存储字段Thread.threadLocalsThread.inheritableThreadLocals适用场景线程内全局变量,避免传参父子线程间需要传递上下文数据转载自
-
前言Vector无论是add方法还是get方法都加上了synchronized修饰,当多线程读写List必须排队执行,很显然这样效率比较是低下的,CopyOnWriteArrayList是读写分离的,好处是提高线程访问效率。CopyOnWrite容器即写时复制的容器。通俗的理解是当往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器里的值Copy到新的容器,然后再往新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读 要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。底层原理CopyOnWriteArrayList的动态数组机制 -- 它内部有个volatile数组(array)来保持数据。在“添加/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给volatile数组。这就是它叫做CopyOnWriteArrayList的原因!每一个CopyOnWriteArrayList都和一个监视器锁lock绑定,通过lock,实现了对CopyOnWriteArrayList的互斥添加/删除。类的继承关系CopyOnWriteArrayList实现了List接口,List接口定义了对列表的基本操作;同时实现了RandomAccess接口,表示可以随机访问(数组具有随机访问的特性);同时实现了Cloneable接口,表示可克隆;同时也实现了Serializable接口,表示可被序列化。javapublic class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}类的内部类COWIterator类COWIterator表示迭代器,其也有一个Object类型的数组作为CopyOnWriteArrayList数组的快照,这种快照风格的迭代器方法在创建迭代器时使用了对当时数组状态的引用。此数组在迭代器的生存期内不会更改,因此不可能发生冲突,并且迭代器保证不会抛出 ConcurrentModificationException。创建迭代器以后,迭代器就不会反映列表的添加、移除或者更改。在迭代器上进行的元素更改操作(remove、set 和 add)不受支持。这些方法将抛出 UnsupportedOperationException。javastatic final class COWIterator<E> implements ListIterator<E> { /** Snapshot of the array */ // 快照 private final Object[] snapshot; /** Index of element to be returned by subsequent call to next. */ // 游标 private int cursor; // 构造函数 private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements; } // 是否还有下一项 public boolean hasNext() { return cursor < snapshot.length; } // 是否有上一项 public boolean hasPrevious() { return cursor > 0; } // next项 @SuppressWarnings("unchecked") public E next() { if (! hasNext()) // 不存在下一项,抛出异常 throw new NoSuchElementException(); // 返回下一项 return (E) snapshot[cursor++]; } @SuppressWarnings("unchecked") public E previous() { if (! hasPrevious()) throw new NoSuchElementException(); return (E) snapshot[--cursor]; } // 下一项索引 public int nextIndex() { return cursor; } // 上一项索引 public int previousIndex() { return cursor-1; } /** * Not supported. Always throws UnsupportedOperationException. * @throws UnsupportedOperationException always; {@code remove} * is not supported by this iterator. */ // 不支持remove操作 public void remove() { throw new UnsupportedOperationException(); } /** * Not supported. Always throws UnsupportedOperationException. * @throws UnsupportedOperationException always; {@code set} * is not supported by this iterator. */ // 不支持set操作 public void set(E e) { throw new UnsupportedOperationException(); } /** * Not supported. Always throws UnsupportedOperationException. * @throws UnsupportedOperationException always; {@code add} * is not supported by this iterator. */ // 不支持add操作 public void add(E e) { throw new UnsupportedOperationException(); } @Override public void forEachRemaining(Consumer<? super E> action) { Objects.requireNonNull(action); Object[] elements = snapshot; final int size = elements.length; for (int i = cursor; i < size; i++) { @SuppressWarnings("unchecked") E e = (E) elements[i]; action.accept(e); } cursor = size; }}类的属性属性中有一个可重入锁,用来保证线程安全访问,还有一个Object类型的数组,用来存放具体的元素。当然,也使用到了反射机制和CAS来保证原子性的修改lock域。javapublic class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { // 版本序列号 private static final long serialVersionUID = 8673264195747942595L; // 可重入锁 final transient ReentrantLock lock = new ReentrantLock(); // 对象数组,用于存放元素 private transient volatile Object[] array; // 反射机制 private static final sun.misc.Unsafe UNSAFE; // lock域的内存偏移量 private static final long lockOffset; static { try { UNSAFE = sun.misc.Unsafe.getUnsafe(); Class<?> k = CopyOnWriteArrayList.class; lockOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("lock")); } catch (Exception e) { throw new Error(e); } }}类的构造函数默认构造函数javapublic CopyOnWriteArrayList() { // 设置数组 setArray(new Object[0]);}CopyOnWriteArrayList(Collection<? extends E>)javapublic CopyOnWriteArrayList(Collection<? extends E> c) { Object[] elements; if (c.getClass() == CopyOnWriteArrayList.class) // 类型相同 // 获取c集合的数组 elements = ((CopyOnWriteArrayList<?>)c).getArray(); else { // 类型不相同 // 将c集合转化为数组并赋值给elements elements = c.toArray(); // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elements.getClass() != Object[].class) // elements类型不为Object[]类型 // 将elements数组转化为Object[]类型的数组 elements = Arrays.copyOf(elements, elements.length, Object[].class); } // 设置数组 setArray(elements);}该构造函数的处理流程如下判断传入的集合c的类型是否为CopyOnWriteArrayList类型,若是,则获取该集合类型的底层数组(Object[]),并且设置当前CopyOnWriteArrayList的数组(Object[]数组),进入步骤③;否则,进入步骤②将传入的集合转化为数组elements,判断elements的类型是否为Object[]类型(toArray方法可能不会返回Object类型的数组),若不是,则将elements转化为Object类型的数组。进入步骤③设置当前CopyOnWriteArrayList的Object[]为elements。CopyOnWriteArrayList(E[]):该构造函数用于创建一个保存给定数组的副本的列表。javapublic CopyOnWriteArrayList(E[] toCopyIn) { // 将toCopyIn转化为Object[]类型数组,然后设置当前数组 setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));}核心函数分析对于CopyOnWriteArrayList的函数分析,主要明白Arrays.copyOf方法即可理解CopyOnWriteArrayList其他函数的意义。copyOf函数该函数用于复制指定的数组,截取或用 null 填充(如有必要),以使副本具有指定的长度。javapublic static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { @SuppressWarnings("unchecked") // 确定copy的类型(将newType转化为Object类型,将Object[].class转化为Object类型; // 判断两者是否相等,若相等,则生成指定长度的Object数组 // 否则,生成指定长度的新类型的数组) T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); // 将original数组从下标0开始,复制长度为(original.length和newLength的较小者),复制到copy数组中(也从下标0开始) System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy;}add函数javapublic boolean add(E e) { // 可重入锁 final ReentrantLock lock = this.lock; // 获取锁 lock.lock(); try { // 元素数组 Object[] elements = getArray(); // 数组长度 int len = elements.length; // 复制数组 Object[] newElements = Arrays.copyOf(elements, len + 1); // 存放元素e newElements[len] = e; // 设置数组 setArray(newElements); return true; } finally { // 释放锁 lock.unlock(); }}此函数用于将指定元素添加到此列表的尾部,处理流程如下获取锁(保证多线程的安全访问),获取当前的Object数组,获取Object数组的长度为length,进入步骤②。根据Object数组复制一个长度为length+1的Object数组为newElements(此时,newElements[length]为null),进入下一步骤。将下标为length的数组元素newElements[length]设置为元素e,再设置当前Object[]为newElements,释放锁,返回。这样就完成了元素的添加。addIfAbsent方法该函数用于添加元素(如果数组中不存在,则添加;否则,不添加,直接返回),可以保证多线程环境下不会重复添加元素。javaprivate boolean addIfAbsent(E e, Object[] snapshot) { // 重入锁 final ReentrantLock lock = this.lock; // 获取锁 lock.lock(); try { // 获取数组 Object[] current = getArray(); // 数组长度 int len = current.length; if (snapshot != current) { // 快照不等于当前数组,对数组进行了修改 // Optimize for lost race to another addXXX operation // 取较小者 int common = Math.min(snapshot.length, len); for (int i = 0; i < common; i++) // 遍历 if (current[i] != snapshot[i] && eq(e, current[i])) // 当前数组的元素与快照的元素不相等并且e与当前元素相等 // 表示在snapshot与current之间修改了数组,并且设置了数组某一元素为e,已经存在 // 返回 return false; if (indexOf(e, current, common, len) >= 0) // 在当前数组中找到e元素 // 返回 return false; } // 复制数组 Object[] newElements = Arrays.copyOf(current, len + 1); // 对数组len索引的元素赋值为e newElements[len] = e; // 设置数组 setArray(newElements); return true; } finally { // 释放锁 lock.unlock(); }}该函数的流程如下:获取锁,获取当前数组为current,current长度为len,判断数组之前的快照snapshot是否等于当前数组current,若不相等,则进入步骤2;否则,进入步骤4不相等,表示在snapshot与current之间,对数组进行了修改(如进行了add、set、remove等操作),获取长度(snapshot与current之间的较小者),对current进行遍历操作,若遍历过程发现snapshot与current的元素不相等并且current的元素与指定元素相等(可能进行了set操作),进入步骤5,否则,进入步骤3在当前数组中索引指定元素,若能够找到,进入步骤5,否则,进入步骤4复制当前数组current为newElements,长度为len+1,此时newElements[len]为null。再设置newElements[len]为指定元素e,再设置数组,进入步骤5释放锁,返回。set函数此函数用于用指定的元素替代此列表指定位置上的元素,也是基于数组的复制来实现的。javapublic E set(int index, E element) { // 可重入锁 final ReentrantLock lock = this.lock; // 获取锁 lock.lock(); try { // 获取数组 Object[] elements = getArray(); // 获取index索引的元素 E oldValue = get(elements, index); if (oldValue != element) { // 旧值等于element // 数组长度 int len = elements.length; // 复制数组 Object[] newElements = Arrays.copyOf(elements, len); // 重新赋值index索引的值 newElements[index] = element; // 设置数组 setArray(newElements); } else { // Not quite a no-op; ensures volatile write semantics // 设置数组 setArray(elements); } // 返回旧值 return oldValue; } finally { // 释放锁 lock.unlock(); }}remove函数此函数用于移除此列表指定位置上的元素。javapublic E remove(int index) { // 可重入锁 final ReentrantLock lock = this.lock; // 获取锁 lock.lock(); try { // 获取数组 Object[] elements = getArray(); // 数组长度 int len = elements.length; // 获取旧值 E oldValue = get(elements, index); // 需要移动的元素个数 int numMoved = len - index - 1; if (numMoved == 0) // 移动个数为0 // 复制后设置数组 setArray(Arrays.copyOf(elements, len - 1)); else { // 移动个数不为0 // 新生数组 Object[] newElements = new Object[len - 1]; // 复制index索引之前的元素 System.arraycopy(elements, 0, newElements, 0, index); // 复制index索引之后的元素 System.arraycopy(elements, index + 1, newElements, index, numMoved); // 设置索引 setArray(newElements); } // 返回旧值 return oldValue; } finally { // 释放锁 lock.unlock(); }}处理流程如下获取锁,获取数组elements,数组长度为length,获取索引的值elements[index],计算需要移动的元素个数(length - index - 1),若个数为0,则表示移除的是数组的最后一个元素,复制elements数组,复制长度为length-1,然后设置数组,进入步骤③;否则,进入步骤②先复制index索引前的元素,再复制index索引后的元素,然后设置数组。释放锁,返回旧值 CopyOnWriteArrayList是Fail Safe的采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。Vector无论是add方法还是get方法都加上了synchronized修饰,当多线程读写List必须排队执行,很显然这样效率比较是低下的,CopyOnWriteArrayList是读写分离的,好处是提高线程访问效率。缺陷和使用场景CopyOnWriteArrayList的写效率比Vector慢。当CopyOnWriteArrayList写元素时是通过备份数组的方式实现的,当多线程同步激烈,数据量较大时会不停的复制数组,内存浪费严重。如果原数组的内容比较多的情况下,可能导致young gc或者full gc弱一致性:不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;小结: CopyOnWriteArrayList合适读多写少的场景,例如黑名单白名单等转载自https://www.cnblogs.com/sevencoding/p/19525347
-
如何得到⼀个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使⽤ Insert() ⽅法读取数据流,使⽤ GetMedian() ⽅法获取当前读取数据的中位数。思路及解答排序列表法维护一个列表,每次获取中位数前进行排序javaimport java.util.ArrayList;import java.util.Collections;import java.util.List;public class MedianFinder1 { private List<Integer> data; public MedianFinder1() { data = new ArrayList<>(); } // 插入数字到数据流 public void Insert(Integer num) { data.add(num); // 每次插入后排序,保持列表有序 Collections.sort(data); } // 获取当前数据流的中位数 public Double GetMedian() { int size = data.size(); if (size == 0) return 0.0; if (size % 2 == 1) { // 奇数个元素,返回中间值 return (double) data.get(size / 2); } else { // 偶数个元素,返回中间两个数的平均值 int mid = size / 2; return (data.get(mid - 1) + data.get(mid)) / 2.0; } }}插入操作:每次插入需要排序,时间复杂度O(n log n)获取中位数:直接通过索引访问,时间复杂度O(1)空间复杂度:O(n),需要存储所有数据插入排序法在方法一基础上优化,在插入时就找到正确位置,避免每次都完整排序。同时利用二分查找找到插入位置,减少排序开销javaimport java.util.ArrayList;import java.util.List;public class MedianFinder2 { private List<Integer> data; public MedianFinder2() { data = new ArrayList<>(); } public void Insert(Integer num) { // 使用二分查找找到合适的插入位置 int left = 0, right = data.size() - 1; while (left <= right) { int mid = left + (right - left) / 2; if (data.get(mid) < num) { left = mid + 1; } else { right = mid - 1; } } // 在找到的位置插入元素 data.add(left, num); } public Double GetMedian() { int size = data.size(); if (size == 0) return 0.0; if (size % 2 == 1) { return (double) data.get(size / 2); } else { int mid = size / 2; return (data.get(mid - 1) + data.get(mid)) / 2.0; } }}插入操作:二分查找O(log n) + 插入操作O(n) = O(n)获取中位数:O(1),通过索引直接访问优化效果:比方法一有明显提升,特别适合部分有序的数据双堆法是最高效的解法,利用大顶堆和小顶堆的特性来动态维护中位数,使用大顶堆存较小一半,小顶堆存较大一半⽤⼀个数字来不断统计数据流中的个数,并且创建⼀个最⼤堆,⼀个最⼩堆如果插⼊的数字的个数是奇数的时候,让最⼩堆⾥⾯的元素个数⽐最⼤堆的个数多 1 ,这样⼀来中位数就是⼩顶堆的堆顶如果插⼊的数字的个数是偶数的时候,两个堆的元素保持⼀样多,中位数就是两个堆的堆顶的元素相加除以2 。javapublic class Solution { private int count = 0; private PriorityQueue<Integer> min = new PriorityQueue<Integer>(); private PriorityQueue<Integer> max = new PriorityQueue<Integer>(new Comparator<Integer>() { public int compare(Integer o1, Integer o2) { return o2 - o1; } }); public void Insert(Integer num) { count++; if (count % 2 == 1) { // 奇数的时候,需要最⼩堆的元素⽐最⼤堆的元素多⼀个。 // 先放到最⼤堆⾥⾯,然后弹出最⼤的 max.offer(num); // 把最⼤的放进最⼩堆 min.offer(max.poll()); } else { // 放进最⼩堆 min.offer(num); // 把最⼩的放进最⼤堆 max.offer(min.poll()); } } public Double GetMedian() { if (count % 2 == 0) { return (min.peek() + max.peek()) / 2.0; } else { return (double) min.peek(); } }}插入操作:堆的插入操作O(log n),平衡操作O(log n),总体O(log n)获取中位数:直接访问堆顶元素,O(1)时间复杂度空间复杂度:O(n),需要存储所有数据为什么这种方法有效?大顶堆(maxHeap):存储数据流中较小的一半数字,堆顶是这一半中的最大值小顶堆(minHeap):存储数据流中较大的一半数字,堆顶是这一半中的最小值平衡维护:确保两个堆的大小相差不超过1,这样中位数就只与两个堆顶有关转载自https://www.cnblogs.com/sevencoding/p/19468596
-
1、 使用Git实现revert的完整操作步骤【转载】cid:link_02、C++中new关键字用法示例详解【转载】cid:link_13、在C# WinForm项目中跨.cs文件传值的六种常用方案【转载】cid:link_24、 一文带你搞懂Java中Error和Exception的区别【转载】cid:link_35、 Java中实现Word和TXT之间互相转换的实用教程【转载】cid:link_46、MyBatis-Plus 默认不更新null的4种方法【转载】cid:link_57、SpringBoot接口防抖的5种高效方案【转载】cid:link_68、 Java中锁分类及在什么场景下使用【转载】cid:link_79、 Java中锁的全面解析之类型、使用场景、优缺点及实现方式(示例代码【转载】cid:link_810、 Caffeine结合Redis空值缓存实现多级缓存【转载】cid:link_911、在PostgreSQL中优雅高效地进行全文检索的完整过程【转载】cid:link_1012、MySQL CDC原理解析及实现方案【转载】cid:link_1113、 PostgreSQL优雅的进行递归查询的实战指南【转载】cid:link_1214、Redis 常用命令之基础、进阶与场景化实战案例【转载】https://bbs.huaweicloud.com/forum/thread-0212720487861500817-1-1.html15、Git中忽略文件机制的.gitignore与.git/info/exclude两种方式详解【转载】https://bbs.huaweicloud.com/forum/thread-0212720487688092711-1-1.html
-
Java中锁的全面解析:类型、使用场景、优缺点及实现方式在多线程编程中,锁是保证数据一致性和线程安全的核心机制。Java 提供了丰富的锁机制来应对不同的并发场景。本文将从锁的基本概念出发,详细讲解 Java 中常见的锁类型、它们的使用场景、优缺点以及底层实现原理,并通过代码示例帮助读者深入理解。1. 锁的基本概念锁是一种同步机制,用于控制多个线程对共享资源的访问。当一个线程获取锁后,其他线程必须等待该锁被释放才能继续执行,从而避免竞态条件(Race Condition)。2. Java 中常见的锁类型2.1 互斥锁(Mutex Lock)特点:一次只允许一个线程持有锁。保证临界区的独占访问。常见实现:synchronized 关键字(内置锁)ReentrantLock(可重入锁)代码示例:使用 synchronized12345678910111213141516public class Counter { private int count = 0; // 同步方法,使用对象锁(this) public synchronized void increment() { count++; } // 同步代码块,使用指定对象锁 public void decrement() { synchronized (this) { count--; } } public int getCount() { return count; }}优点:简单易用,无需手动释放锁。JVM 自动管理锁的获取与释放。缺点:无法中断等待中的线程。不能设置超时时间。只能是非公平锁(默认)。2.2 可重入锁(Reentrant Lock)特点:支持同一个线程多次获取同一把锁(即“可重入”)。提供更灵活的控制能力。代码示例:使用 ReentrantLock12345678910111213141516171819202122232425import java.util.concurrent.locks.ReentrantLock;public class ReentrantExample { private final ReentrantLock lock = new ReentrantLock(); private int count = 0; public void increment() { lock.lock(); try { count++; // 可以再次获取锁(可重入) if (count == 1) { System.out.println("Thread " + Thread.currentThread().getName() + " is re-entering the lock."); } } finally { lock.unlock(); } } public int getCount() { lock.lock(); try { return count; } finally { lock.unlock(); } }}优点:支持可中断的锁获取(lockInterruptibly())。支持超时锁获取(tryLock(timeout))。支持公平锁和非公平锁(通过构造函数选择)。缺点:必须手动释放锁,容易忘记 unlock() 导致死锁。语法比 synchronized 复杂。2.3 读写锁(ReadWriteLock)特点:分离读操作和写操作的锁。多个读线程可以同时访问共享资源,但写操作必须独占。常见实现:ReentrantReadWriteLock代码示例:读写锁的应用1234567891011121314151617181920212223242526import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;public class ReadWriteExample { private final ReadWriteLock lock = new ReentrantReadWriteLock(); private String data = "Default"; // 读操作:多个线程可同时读 public String readData() { lock.readLock().lock(); try { System.out.println(Thread.currentThread().getName() + " is reading data: " + data); return data; } finally { lock.readLock().unlock(); } } // 写操作:独占访问,其他读写均被阻塞 public void writeData(String newData) { lock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName() + " is writing data: " + newData); data = newData; } finally { lock.writeLock().unlock(); } }}优点:读多写少的场景下性能显著提升(减少锁竞争)。提高并发效率。缺点:写操作会阻塞所有读操作,可能导致“写饥饿”(Writer Starvation)。逻辑复杂度增加。2.4 原子锁(Atomic Lock)特点:使用原子操作(CAS)实现无锁编程。避免传统锁带来的上下文切换开销。常见类:AtomicInteger,AtomicReference,StampedLock(分段锁)代码示例:使用 AtomicInteger12345678910import java.util.concurrent.atomic.AtomicInteger;public class AtomicCounter { private final AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); // CAS 操作,原子递增 } public int getCount() { return count.get(); }}优点:无锁机制,避免线程阻塞。性能高,适合高并发场景。缺点:无法处理复杂的复合操作(如“读-修改-写”)。存在 ABA 问题(可通过 AtomicStampedReference 解决)。2.5 StampedLock(戳记锁)特点:JDK 8 引入,支持乐观读锁(Optimistic Read)。读写分离,支持读写锁和乐观读锁三种模式。代码示例:StampedLock 用法12345678910111213141516171819202122232425262728293031import java.util.concurrent.locks.StampedLock;public class StampedLockExample { private final StampedLock stampedLock = new StampedLock(); private double x, y; // 乐观读:不加锁,先尝试读取,失败则升级为悲观读 public double distanceFromOrigin() { long stamp = stampedLock.tryOptimisticRead(); double currentX = x, currentY = y; // 检查是否在读期间发生了写操作(版本变化) if (!stampedLock.validate(stamp)) { stamp = stampedLock.readLock(); try { currentX = x; currentY = y; } finally { stampedLock.unlockRead(stamp); } } return Math.sqrt(currentX * currentX + currentY * currentY); } // 写操作:独占锁 public void move(double deltaX, double deltaY) { long stamp = stampedLock.writeLock(); try { x += deltaX; y += deltaY; } finally { stampedLock.unlockWrite(stamp); } }}优点:读操作性能极高(乐观读无需阻塞)。适用于读多写少且对读性能要求极高的场景。缺点:API 复杂,需要显式验证版本。容易出错(如忘记验证或释放锁)。3. 锁的使用场景总结锁类型适用场景推荐程度synchronized简单同步,小范围临界区⭐⭐⭐⭐⭐ReentrantLock需要超时、中断、公平性控制⭐⭐⭐⭐☆ReentrantReadWriteLock读多写少的共享数据⭐⭐⭐⭐☆AtomicXXX简单计数器、状态标志⭐⭐⭐⭐⭐StampedLock极高读性能需求,读多写少⭐⭐⭐☆☆4. 锁的底层实现原理(简述)synchronized:基于 JVM 的对象头(Mark Word)实现,通过 Monitor 机制管理锁状态。ReentrantLock:基于 AQS(AbstractQueuedSynchronizer)实现,使用 CAS+FIFO 队列管理线程排队。StampedLock:基于版本戳(Stamp)和状态位管理,支持乐观读。5. 最佳实践建议优先使用 synchronized,除非有特殊需求。避免在 finally 块外调用 unlock(),防止死锁。读写锁适用于读多写少的场景,避免“写饥饿”。原子类适合简单操作,复杂逻辑仍需锁保护。StampedLock 适合高性能读场景,但需谨慎使用。
-
一、基础分类(按实现方式)这是最核心的分类维度,直接决定锁的使用方式和核心能力。1. 内置锁(synchronized)- 隐式锁核心定义Java 关键字,JVM 层面实现的隐式锁(无需手动释放),是最基础、使用最广泛的锁。JDK1.6 后引入「锁升级」机制,性能大幅提升。核心特点可重入、默认非公平锁;自动加锁 / 解锁(方法 / 代码块执行完自动释放,无需手动处理);底层依赖对象头的Mark Word + 监视器锁(ObjectMonitor);支持锁升级(偏向锁→轻量级锁→重量级锁),适配不同并发场景。适用场景简单互斥场景(如方法 / 代码块的线程安全);并发度不高、代码简洁性优先的场景;不需要灵活特性(如可中断、超时获取锁)的场景;绝大多数普通业务场景(JVM 优化后性能接近显式锁)。代码示例1234567891011121314151617181920212223242526public class SynchronizedDemo { // 1. 实例方法锁(对象锁):锁当前实例对象 public synchronized void objectLock() { System.out.println(Thread.currentThread().getName() + "获取对象锁"); try { Thread.sleep(100); } catch (InterruptedException e) {} } // 2. 静态方法锁(类锁):锁当前类的Class对象 public static synchronized void classLock() { System.out.println(Thread.currentThread().getName() + "获取类锁"); try { Thread.sleep(100); } catch (InterruptedException e) {} } // 3. 代码块锁:自定义锁对象(灵活度最高) private final Object lockObj = new Object(); public void blockLock() { synchronized (lockObj) { System.out.println(Thread.currentThread().getName() + "获取代码块锁"); try { Thread.sleep(100); } catch (InterruptedException e) {} } } public static void main(String[] args) { SynchronizedDemo demo = new SynchronizedDemo(); // 竞争同一对象锁,串行执行 new Thread(demo::objectLock, "线程1").start(); new Thread(demo::objectLock, "线程2").start(); }}2. 显式锁(Lock 接口)- 手动锁核心定义JUC 包下java.util.concurrent.locks.Lock接口的实现类,手动加锁 / 释放锁(需在finally中释放,避免死锁),是 synchronized 的补充和增强。核心实现类 & 特点实现类核心特性ReentrantLock可重入、支持公平 / 非公平、可中断、超时获取锁ReentrantReadWriteLock读写分离(读共享、写独占)、可重入StampedLock支持乐观读、读写锁、写锁,性能优于读写锁适用场景需要灵活锁控制(如可中断、超时获取锁、公平锁)的场景;读多写少的高并发场景(选 ReentrantReadWriteLock/StampedLock);高并发、需要精细控制锁生命周期的场景。代码示例(ReentrantLock)1234567891011121314151617181920212223242526import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockDemo { // 公平锁(按请求顺序获取),默认非公平锁(性能更高) private static final ReentrantLock lock = new ReentrantLock(true); public static void doTask() { // 1. 加锁(可替换为lockInterruptibly():可中断锁) lock.lock(); try { System.out.println(Thread.currentThread().getName() + "获取锁"); // 模拟业务操作 Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 恢复中断状态 } finally { // 2. 释放锁(必须放finally,否则死锁) if (lock.isHeldByCurrentThread()) { lock.unlock(); System.out.println(Thread.currentThread().getName() + "释放锁"); } } } public static void main(String[] args) { new Thread(ReentrantLockDemo::doTask, "线程A").start(); new Thread(ReentrantLockDemo::doTask, "线程B").start(); }}二、进阶分类(按锁的核心特性)基于锁的行为和并发特性分类,帮你理解锁的底层逻辑和适用场景。1. 可重入锁 vs 不可重入锁类型定义示例适用场景可重入锁同一线程可多次获取同一把锁,不会死锁synchronized、ReentrantLock所有业务场景(递归调用、同一线程多次操作共享资源)不可重入锁同一线程多次获取同一锁会死锁自定义简单自旋锁(未处理重入)极少使用(仅严格限制锁获取次数的特殊场景)可重入锁示例(synchronized 递归调用)123456789101112public class ReentrantDemo { public synchronized void outer() { System.out.println("外层方法获取锁"); inner(); // 同一线程再次获取同一锁,无死锁 } public synchronized void inner() { System.out.println("内层方法获取锁"); } public static void main(String[] args) { new ReentrantDemo().outer(); // 正常执行,无死锁 }}2. 乐观锁 vs 悲观锁这是并发设计思想的分类,而非具体锁实现。类型核心思想实现方式适用场景悲观锁假设必有竞争,先锁后执行synchronized、ReentrantLock高冲突、写多读少(如库存扣减、转账)乐观锁假设无竞争,先执行后检测CAS(Atomic 类)、版本号低冲突、读多写少(如计数器、缓存更新)乐观锁示例(CAS 实现)12345678910111213141516171819import java.util.concurrent.atomic.AtomicInteger;public class OptimisticLockDemo { // CAS是乐观锁的核心实现 private static final AtomicInteger count = new AtomicInteger(0); // 原子自增(无锁,冲突时重试) public static void increment() { count.incrementAndGet(); } public static void main(String[] args) throws InterruptedException { Runnable task = () -> { for (int i = 0; i < 1000; i++) increment(); }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("最终计数:" + count.get()); // 2000(线程安全) }}3. 公平锁 vs 非公平锁类型定义示例适用场景公平锁按请求顺序获取锁,先到先得ReentrantLock(true)对公平性要求高(如任务排队、避免线程饥饿)非公平锁不按顺序,线程可插队获取锁synchronized、ReentrantLock()大部分场景(优先性能,容忍轻微饥饿)4. 读写锁(ReentrantReadWriteLock)- 共享 + 独占锁核心定义将锁拆分为「读锁(共享锁)」和「写锁(独占锁)」,核心规则:读 - 读共享:多个线程可同时获取读锁;读 - 写互斥:读锁和写锁不能同时持有;写 - 写互斥:多个线程不能同时获取写锁。适用场景读多写少的场景(如缓存、配置读取、商品详情页、数据查询),相比普通独占锁,能大幅提升读并发效率。代码示例12345678910111213141516171819202122232425262728293031323334353637383940import java.util.concurrent.locks.ReentrantReadWriteLock;public class ReadWriteLockDemo { private static final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); private static final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock(); private static final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock(); private static int cacheData = 0; // 模拟缓存数据 // 读操作(共享锁,多线程同时执行) public static void readCache() { readLock.lock(); try { System.out.println(Thread.currentThread().getName() + "读取缓存:" + cacheData); Thread.sleep(200); // 模拟读耗时 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { readLock.unlock(); } } // 写操作(独占锁,串行执行) public static void updateCache(int newData) { writeLock.lock(); try { System.out.println(Thread.currentThread().getName() + "更新缓存:" + newData); cacheData = newData; Thread.sleep(200); // 模拟写耗时 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { writeLock.unlock(); } } public static void main(String[] args) { // 5个读线程(可同时执行,并发效率高) for (int i = 0; i < 5; i++) { new Thread(ReadWriteLockDemo::readCache, "读线程" + i).start(); } // 1个写线程(独占,所有读线程等待) new Thread(() -> updateCache(100), "写线程").start(); }}5. synchronized 的锁升级(偏向锁→轻量级锁→重量级锁)JDK1.6 为优化 synchronized 引入的自适应锁机制,锁级别从低到高升级(不可逆),适配不同并发场景:锁类型核心特点适用场景偏向锁锁偏向第一个线程,无竞争时零开销单线程执行同步代码(如初始化资源)轻量级锁多线程交替竞争,CAS 自旋获取锁少量线程(2-3 个)交替执行重量级锁多线程激烈竞争,线程阻塞(OS 层面)大量线程同时竞争锁6. 自旋锁 vs 阻塞锁类型定义示例适用场景自旋锁获取锁失败时循环重试(自旋),不阻塞CAS、synchronized 轻量级锁锁持有时间短、低冲突(如简单变量更新)阻塞锁获取锁失败时线程阻塞,释放 CPUsynchronized 重量级锁、ReentrantLock锁持有时间长、高冲突(如复杂业务逻辑)7. 分段锁(ConcurrentHashMap JDK1.7)核心定义将数据拆分为多个「段(Segment)」,每个段独立加锁,不同段的操作互不阻塞,并发度 = 段数(默认 16)。适用场景JDK1.7 的 ConcurrentHashMap(JDK1.8 后用 CAS+Synchronized 替代),适用于高并发读写 Map 的场景。三、实战选型指南(核心)业务场景推荐锁类型选型原因简单互斥、代码简洁synchronized隐式锁,无需手动释放,JVM 优化优异可中断 / 超时 / 公平锁ReentrantLock支持灵活的锁控制特性读多写少(缓存 / 查询)ReentrantReadWriteLock/StampedLock读共享,大幅提升读并发效率低冲突、简单变量更新CAS(AtomicInteger/AtomicLong)无锁,性能最高,避免自旋消耗高冲突、写多读少(转账)synchronized/ReentrantLock悲观锁,避免高冲突下的 CAS 自旋 CPU 开销公平性要求高(任务排队)ReentrantLock(true)按请求顺序获取锁,避免线程饥饿高并发 Map 操作ConcurrentHashMapJDK1.8 用 CAS+Synchronized,兼顾性能和安全性总结关键点回顾基础核心:synchronized(简单、通用)和 Lock 接口(灵活、高级)是 Java 锁的两大基石,前者适用于普通场景,后者适用于需要灵活控制的场景;特性选型:读多写少选读写锁,低冲突选乐观锁 / CAS,高冲突选悲观锁,公平性要求高选公平锁;性能原则:优先选择「开销低」的锁(如偏向锁、CAS),高冲突场景才用「高开销」的阻塞锁(如重量级锁);实战建议:90% 的普通业务场景用 synchronized 即可,仅在需要可中断、超时、读写分离时才用 Lock 接口实现类。
-
Spire.Doc for Java:Word 与 TXT 转换的利器在 Java 生态中,处理 Word 文档的库并不少见,但 Spire.Doc for Java 凭借其强大的功能和易用性脱颖而出。它是一个专业的 Word 文档处理组件,支持创建、读写、编辑、转换和打印 Word 文档,并且兼容多种 Word 版本。其中,对 Word 和 TXT 格式的互相转换提供了非常便捷的 API。引入 Spire.Doc for Java要开始使用 Spire.Doc,您需要将其作为依赖添加到您的 Maven 项目中。Maven 配置示例:1234567891011121314 <repositories> <repository> <id>com.e-iceblue</id> <name>e-iceblue</name> <url>https://repo.e-iceblue.cn/repository/maven-public/</url> </repository></repositories><dependencies> <dependency> <groupId>e-iceblue</groupId> <artifactId>spire.doc</artifactId> <version>14.1.3</version> </dependency></dependencies>请确保您使用的版本是最新的稳定版本,以获取最佳的兼容性和功能。从 Word 到 TXT:逐步实现文档内容提取将 Word 文档转换为纯文本(TXT)是一个常见的需求,例如用于内容提取、文本分析或跨平台传输。Spire.Doc for Java 提供了一行代码即可完成此操作。实现步骤加载 Word 文档: 使用 Document 类的 loadFromFile() 方法加载目标 Word 文档。保存为 TXT 格式: 调用 saveToFile() 方法,并指定输出路径和 FileFormat.Txt 格式。释放资源: 调用 dispose() 方法释放文档对象占用的资源。Java 代码示例1234567891011121314151617181920import com.spire.doc.Document;import com.spire.doc.FileFormat; public class ConvertWordtoText { public static void main(String[] args) { // 创建 Document 对象 Document doc = new Document(); // 加载 Word 文件 doc.loadFromFile("示例.docx"); // 将文档保存为 TXT 格 doc.saveToFile("Word转文本.txt", FileFormat.Txt); // 释放资源 doc.dispose(); }}代码解析:document.loadFromFile(inputWordPath): 负责读取指定路径的 Word 文档内容。document.saveToFile(outputTxtPath, FileFormat.Txt): 这是转换的核心。它将加载的 Word 文档内容以纯文本格式写入到 outputTxtPath 指定的文件中。FileFormat.Txt 枚举值明确指示了目标格式。document.dispose(): 释放资源,用于关闭文件流并释放内存,特别是在处理大量文档时。从 TXT 到 Word:构建富文本格式文档将纯文本(TXT)文件转换为 Word 文档,通常是为了对其进行格式化、添加图片、表格或其他富文本元素。Spire.Doc 同样能轻松实现这一目标。实现步骤创建或加载 Word 文档: 对于从 TXT 创建新的 Word 文档,直接创建 Document 对象即可。加载 TXT 内容: 使用 Document 类的 loadFromFile() 方法加载 TXT 文件。保存为 Word 格式: 调用 saveToFile() 方法,并指定输出路径和 FileFormat.Docx(或 FileFormat.Doc)格式。释放资源: 调用 dispose() 方法释放文档对象占用的资源。Java 代码示例1234567891011121314151617181920import com.spire.doc.Document;import com.spire.doc.FileFormat; public class ConvertTextToWord { public static void main(String[] args) { // 创建 Document 对象 Document txt = new Document(); // 加载 .txt 文本文件 txt.loadFromFile("介绍.txt"); // 将文件保存为 Word 格式 txt.saveToFile("TXT转Word.docx", FileFormat.Docx); // 释放资源 txt.dispose(); }}代码解析:document.loadFromFile(inputTxtPath): 这里巧妙地利用了 spire.doc for java 的 loadFromFile 方法不仅可以加载 Word 文档,还能加载 TXT 文件并将其内容导入到 Document 对象中。document.saveToFile(outputWordPath, FileFormat.Docx): 将包含 TXT 内容的 Document 对象保存为 Word 格式。FileFormat.Docx 是现代 Word 文档的默认格式,您也可以选择 FileFormat.Doc。格式调整建议:将 TXT 转换为 Word 后,默认情况下可能只是简单的文本导入。如果需要更复杂的格式,例如设置字体、段落样式、页眉页脚等,Spire.Doc 也提供了丰富的 API 来实现这些功能,您可以在 loadFromFile 之后、saveToFile 之前,对 document 对象进行进一步的编辑操作。
-
Error 和 Exception 的基本概念在开始之前,我们先来理解一下这两个概念的基本含义。Error(错误):通常指的是系统级的错误,这些错误往往是程序无法恢复的,或者恢复起来非常困难。比如内存溢出、栈溢出、系统资源耗尽等。这些错误一般不是由程序逻辑问题引起的,而是由系统环境、硬件资源等外部因素导致的。Exception(异常):则是指程序运行过程中出现的异常情况,这些异常通常是可以被程序捕获和处理的。比如数组越界、空指针引用、文件不存在、网络连接失败等。这些异常往往是由程序的设计问题、逻辑错误或者外部输入导致的。简单来说,Error 是"系统说不行",Exception 是"程序说有问题"。两者的核心区别虽然 Error 和 Exception 都是程序运行时的异常情况,但它们有几个关键的区别:严重程度不同Error 通常比 Exception 更严重。Error 往往意味着系统级别的故障,比如内存溢出(OutOfMemoryError)、栈溢出(StackOverflowError)等。这些错误一旦发生,程序通常无法继续正常运行,甚至可能导致整个应用崩溃。Exception 相对来说就没那么严重了。虽然有些 Exception 也会导致程序崩溃(比如未捕获的运行时异常),但大多数 Exception 都是可以被程序捕获和处理的。比如文件读取失败,我们可以提示用户重新选择文件;网络请求失败,我们可以重试或者显示错误信息。处理方式不同对于 Error,我们通常不应该尝试捕获和处理。因为 Error 往往意味着系统资源已经耗尽或者系统环境出现了严重问题,这时候程序已经无法正常工作了,强行处理可能会让问题变得更糟。对于 Exception,我们应该主动捕获和处理。这是程序健壮性的重要体现。比如在读取文件时,我们应该捕获 IOException,然后给用户一个友好的提示,而不是让程序直接崩溃。来源不同Error 通常来自系统层面,比如 JVM 运行时错误、操作系统错误等。这些错误不是由我们的业务代码直接引起的,而是由底层系统或环境问题导致的。Exception 通常来自应用层面,比如我们的业务逻辑、API 调用、数据处理等。这些异常往往可以通过改进代码逻辑、添加校验、重试机制等方式来处理。实际应用场景让我们通过几个实际的场景来理解 Error 和 Exception 的区别:场景一:内存溢出假设你正在开发一个图片处理应用,用户上传了一张非常大的图片。如果你的程序试图将整个图片加载到内存中,而系统内存不足,就会抛出 OutOfMemoryError。这是一个典型的 Error。因为内存不足是系统资源问题,不是你的程序逻辑问题。虽然你可以通过优化代码(比如分块处理图片)来避免这个问题,但一旦内存真的耗尽了,程序就很难恢复了。正确的做法是:在程序设计阶段就考虑内存限制,避免一次性加载过大的数据。如果真的遇到了 OutOfMemoryError,最好的处理方式可能是记录错误日志,然后优雅地退出程序,而不是试图捕获和处理这个错误。场景二:文件读取失败假设你的应用需要读取一个配置文件。如果文件不存在,或者文件被其他程序占用,就会抛出 FileNotFoundException 或 IOException。这是一个典型的 Exception。因为文件读取失败是可以通过程序逻辑来处理的。你可以捕获这个异常,然后给用户一个友好的提示,比如"配置文件不存在,请检查文件路径",或者使用默认配置。123456789101112try { File configFile = new File("config.properties"); // 读取配置文件} catch (FileNotFoundException e) { // 文件不存在,使用默认配置 logger.warn("配置文件不存在,使用默认配置"); loadDefaultConfig();} catch (IOException e) { // 文件读取失败,提示用户 logger.error("读取配置文件失败", e); showErrorDialog("无法读取配置文件,请检查文件权限");}场景三:网络请求超时假设你的应用需要调用一个远程 API。如果网络连接不稳定,或者服务器响应慢,可能会抛出 SocketTimeoutException 或 ConnectException。这也是一个典型的 Exception。你可以捕获这个异常,然后实现重试机制,或者给用户一个友好的提示。1234567891011121314int maxRetries = 3;for (int i = 0; i < maxRetries; i++) { try { // 发送网络请求 return httpClient.execute(request); } catch (SocketTimeoutException e) { if (i == maxRetries - 1) { // 最后一次重试也失败了 throw new ApiException("网络请求超时,请检查网络连接"); } // 等待一段时间后重试 Thread.sleep(1000 * (i + 1)); }}场景四:空指针引用假设你的代码中有一个对象可能为 null,但你没有做空值检查就直接使用了它,就会抛出 NullPointerException。这也是一个典型的 Exception。虽然 NullPointerException 是运行时异常,不需要强制捕获,但我们应该在代码中主动避免这种情况。1234567891011// 不好的做法String name = user.getName(); // 如果 user 为 null,会抛出 NullPointerExceptionSystem.out.println(name.length()); // 好的做法if (user != null) { String name = user.getName(); if (name != null) { System.out.println(name.length()); }}不同语言中的实现虽然 Error 和 Exception 的概念是相通的,但不同语言的实现方式可能不太一样:Java 中,Error 和 Exception 都是 Throwable 的子类。Error 包括 OutOfMemoryError、StackOverflowError 等系统级错误;Exception 包括 RuntimeException(运行时异常)和 CheckedException(检查异常)。Python 中,所有的异常都继承自 BaseException。系统退出异常(SystemExit、KeyboardInterrupt)类似于 Error,其他异常类似于 Exception。Swift 中,Error 是一个协议,任何遵循 Error 协议的类型都可以被抛出。Swift 没有严格区分 Error 和 Exception,但我们可以通过命名和文档来区分系统级错误和应用级异常。最佳实践在实际开发中,我们应该遵循以下原则:对于 Error(系统级错误):不要尝试捕获和处理系统级错误在程序设计阶段就考虑资源限制,避免触发系统错误如果真的遇到了系统错误,记录日志并优雅退出对于 Exception(应用级异常):主动捕获和处理可能出现的异常给用户提供友好的错误提示实现重试机制、降级方案等容错处理记录详细的异常日志,方便问题排查代码设计建议:使用防御性编程,提前检查可能的问题合理使用异常处理,不要过度捕获异常区分可恢复的异常和不可恢复的异常对于关键操作,实现重试和降级机制总结Error 和 Exception 虽然都是程序运行时的异常情况,但它们有本质的区别:Error 是系统级的错误,通常无法恢复,不应该被捕获处理Exception 是应用级的异常,可以被捕获和处理,是程序健壮性的重要体现在实际开发中,我们应该:通过合理的设计避免系统级错误主动捕获和处理应用级异常给用户提供友好的错误提示实现完善的容错和降级机制
-
一、为什么需要异常处理? 想象一下:你写的 Java 程序运行时突然因 “数组下标越界”“空指针” 崩溃,没有任何提示,用户根本不知道问题出在哪。异常处理的核心目的就是: 捕获程序运行时的错误,避免程序直接崩溃;清晰提示错误原因,便于定位和修复问题;保证程序在出错后仍能优雅地继续执行(或正常退出)。 二、Java 异常体系的核心结构 Java 中所有异常都继承自Throwable类,主要分为两大分支: Error(错误):JVM 级别的严重错误,如OutOfMemoryError、StackOverflowError,程序无法捕获和处理,只能通过优化代码 / 配置避免;Exception(异常):程序可处理的错误,又分为:受检异常(Checked Exception):编译时必须处理,如IOException、SQLException;非受检异常(Unchecked Exception):运行时异常,编译时无需处理,如NullPointerException、ArrayIndexOutOfBoundsException。 三、异常处理的核心语法:try-catch-finally 这是 Java 处理异常的基础语法,先看一个完整示例: java 运行 import java.io.FileReader;import java.io.IOException;public class ExceptionDemo { public static void readFile(String path) { FileReader fr = null; try { // 可能抛出异常的代码块 fr = new FileReader(path); int ch = fr.read(); System.out.println("读取到字符:" + (char) ch); } catch (IOException e) { // 捕获并处理异常 System.out.println("文件读取失败:" + e.getMessage()); e.printStackTrace(); // 打印异常堆栈,便于调试 } finally { // 无论是否抛出异常,都会执行的代码(常用于释放资源) try { if (fr != null) { fr.close(); // 关闭文件流 } } catch (IOException e) { e.printStackTrace(); } } } public static void main(String[] args) { readFile("test.txt"); // 若文件不存在,会捕获IOException }} 关键说明: try:包裹可能抛出异常的代码,必须有;catch:捕获指定类型的异常,可多个(按异常子类到父类顺序);finally:可选,但常用于释放资源(如文件流、数据库连接),即使try/catch中有return,finally仍会执行。 四、Java 7+ 简化资源释放:try-with-resources 手动关闭资源容易遗漏,try-with-resources可以自动关闭实现了AutoCloseable接口的资源(如 FileReader、Connection): java 运行 public class TryWithResourcesDemo { public static void readFile(String path) { // 资源声明在try()中,执行完自动关闭 try (FileReader fr = new FileReader(path)) { int ch = fr.read(); System.out.println("读取到字符:" + (char) ch); } catch (IOException e) { System.out.println("文件读取失败:" + e.getMessage()); } } public static void main(String[] args) { readFile("test.txt"); }} 五、自定义异常:让异常更贴合业务场景 系统自带的异常无法满足业务需求时,可自定义异常。例如,用户登录时 “密码错误” 异常: java 运行 // 自定义受检异常(继承Exception)public class PasswordErrorException extends Exception { // 构造方法 public PasswordErrorException() { super(); } public PasswordErrorException(String message) { super(message); }}// 使用自定义异常public class LoginService { public static void login(String username, String password) throws PasswordErrorException { if (!"123456".equals(password)) { // 抛出自定义异常 throw new PasswordErrorException("密码错误,请重新输入"); } System.out.println("登录成功!"); } public static void main(String[] args) { try { login("admin", "111111"); } catch (PasswordErrorException e) { System.out.println("登录失败:" + e.getMessage()); } }} 六、异常处理的避坑指南(新手必看) 不要捕获所有异常(catch (Exception e)):会掩盖真正的错误,应精准捕获具体异常;不要空 catch 块:catch (Exception e) {}会让异常 “消失”,无法定位问题,至少要打印异常信息;不要滥用 throws:把异常抛给上层却不处理,最终会导致程序崩溃;finally 中避免返回值:会覆盖 try/catch 中的 return 结果,造成逻辑混乱。 七、总结 Java 异常体系分为 Error(不可处理)和 Exception(可处理),Exception 又分受检 / 非受检异常;try-catch-finally是基础异常处理语法,Java 7 + 的try-with-resources可自动释放资源;异常处理要精准、有意义,避免捕获所有异常或空 catch 块,自定义异常可贴合业务场景。 整体总结 第一篇文章聚焦 Java 核心类String,从不可变性、常量池、实战避坑三个维度讲解,适合初学者夯实基础;第二篇文章聚焦异常处理,从体系结构、核心语法、简化写法、自定义异常到避坑指南,覆盖新手到进阶的核心知识点;两篇文章均包含可直接运行的代码示例,兼顾原理讲解和实战应用,符合 Java 学习的认知逻辑。
-
一、为什么 String 是 Java 中最特殊的类? 对于 Java 初学者来说,String类是接触最早、使用最频繁的类,但也是最容易踩坑的类。它的特殊性体现在三个核心点: 不可变性:String 对象一旦创建,其内部的字符序列就无法被修改;字符串常量池:Java 为 String 设计了常量池机制,用于节省内存、提高性能;重载的 + 运算符:String 是 Java 中唯一重载了 + 运算符的类,但其底层实现容易被误解。 二、String 的不可变性到底是什么? 先看一段简单的代码: java 运行 public class StringImmutableDemo { public static void main(String[] args) { String str = "Hello"; str = str + " Java"; // 看似修改了str,实则创建了新对象 System.out.println(str); // 输出:Hello Java } } 这段代码中,str = str + " Java"并不是修改了原来的 "Hello" 对象,而是创建了一个新的 String 对象 "Hello Java",并让 str 指向这个新对象。 String 的不可变性由其底层实现保证: java 运行 // String类的核心源码(简化版)public final class String { private final char value[]; // 存储字符的数组被final修饰 // 构造方法、方法等省略} final修饰的String类:无法被继承,避免子类破坏不可变性;final修饰的value数组:数组引用无法被修改(但数组本身的元素理论上可通过反射修改,不推荐)。 三、字符串常量池:提升性能的关键 字符串常量池(String Pool)是 JVM 为 String 专门开辟的一块内存区域(位于堆中),核心作用是缓存字符串常量,避免重复创建。 1. 两种创建 String 对象的方式对比 java 运行 public class StringPoolDemo { public static void main(String[] args) { // 方式1:直接赋值,会先检查常量池,存在则复用,不存在则创建 String s1 = "Java"; String s2 = "Java"; System.out.println(s1 == s2); // true,指向常量池同一个对象 // 方式2:new关键字,会先在堆中创建新对象,常量池若没有则创建常量 String s3 = new String("Java"); String s4 = new String("Java"); System.out.println(s3 == s4); // false,堆中不同对象 System.out.println(s3.equals(s4)); // true,内容相等 // 手动入池:intern()方法 String s5 = s3.intern(); System.out.println(s1 == s5); // true,s5指向常量池对象 }} 四、String 常用操作避坑指南 避免频繁使用 + 拼接字符串:循环中使用+拼接会创建大量临时对象,推荐使用StringBuilder(单线程)或StringBuffer(多线程): java 运行 // 低效写法String result = "";for (int i = 0; i < 1000; i++) { result += i;}// 高效写法StringBuilder sb = new StringBuilder();for (int i = 0; i < 1000; i++) { sb.append(i);}String result = sb.toString(); equals 和 == 的区别:==比较对象地址,equals比较字符串内容,判断字符串相等务必用equals;空字符串判断:优先使用str.isEmpty()或str.length() == 0,避免str == ""(地址比较)。 五、总结 String 的不可变性是其核心特性,由final关键字和底层数组实现;字符串常量池可减少内存占用,直接赋值和new创建对象的逻辑不同;频繁拼接字符串优先用StringBuilder,避免不必要的性能损耗。
-
随着HarmonyOS NEXT全面转向ArkTS,大量存量Java应用与Android生态开发者面临核心技术栈重构的挑战。本方案旨在系统性解决Java开发者向ArkTS迁移过程中的核心痛点,提供可实操的路径指引。1.1 问题说明Java开发者在迁移过程中,通常在以下场景面临编译错误、运行时异常或性能/行为不一致问题:语法与类型不兼容:首次编译大量ArkTS代码时,出现“类型不匹配”“属性不存在”等静态检查错误。并发模型重构:原有依赖synchronized、ReentrantLock实现的多线程共享内存模型直接迁移后,出现数据错乱、程序崩溃。异步编程重构:Thread、Runnable、ExecutorService等代码无法直接运行,需适配异步模型。跨语言交互异常:与Java云服务端进行加密解密、签名验签等操作时,结果不一致。运行时行为差异:单例对象在不同线程(如StartupTask与EntryAbility)中并非同一实例;this指向异常导致程序崩溃。具体表现为:项目编译失败,控制台输出大量ArkTS语法错误码;应用运行时出现数据竞争、UI卡顿或跨平台交互失败。1.2 原因分析核心原因在于ArkTS与Java在设计哲学与运行时模型上存在本质差异。静态类型 vs 动态类型:ArkTS是强静态类型语言,禁止使用any/unknown,且不支持Java中“结构类型”(仅形状相似即可赋值)。Java的灵活类型转换在ArkTS中需显式声明。Actor并发模型 vs 共享内存模型:ArkTS采用内存隔离的Actor模型(通过TaskPool和Worker通信),不支持多线程直接读写同一对象。而Java的synchronized机制构建于共享内存之上,两者范式冲突。异步编程范式:Java通过多线程和Future处理并发,而ArkTS基于事件循环,使用Promise/async/await进行单线程异步调度,避免阻塞UI线程。数值与编码差异:数字精度:ArkTS的number基于IEEE 754双精度浮点数,仅能精确表示53位整数。与C++侧交互64位大整数(如指针)时,需使用BigInt。字节符号:ArkTS的Uint8Array为无符号字节(0-255),而Java的byte为有符号(-128-127)。加解密、哈希等操作若直接传输原始字节数组,会导致云侧解密/验签失败。运行时上下文(this)绑定:Java中this在编译时确定,指向当前实例。ArkTS中this是动态绑定的,取决于调用时的上下文,若将对象方法作为回调传递,可能导致this指向错误而崩溃。1.3 解决思路解决问题的核心是“范式转换”,而非简单的语法翻译。总体遵循以下优化方向:类型显式化与结构定义化:将模糊的Java类型(尤其是Object)替换为ArkTS明确的接口、类或联合类型,杜绝使用any。并发任务化与通信消息化:将“共享内存+锁”的线程逻辑,拆解为独立的可序列化任务,通过TaskPool分发,或通过Worker进行消息通信。共享数据需通过@Sendable类或SharedArrayBuffer传递。异步包装与Promise化:将阻塞式或后台线程逻辑,重构为async函数,利用TaskPool执行CPU密集型任务。数据格式中间层对齐:与Java端进行二进制数据交互时,必须主动进行符号转换(Uint8Array <-> Int8Array)或使用标准格式(如Base64)作为中间层。上下文绑定显式化:传递对象方法时,使用.bind(this)或箭头函数固定this指向。1.4 解决方案方案一:并发模型迁移(共享内存 -> 内存隔离)场景:将后台计算任务移至子线程。使用@Concurrent装饰函数import { taskpool } from '@kit.ArkTS'; // 原Java: new Thread(() -> { calculate(); }).start(); @Concurrent function calculate(data: number): number { // 执行独立计算,注意不能访问外部非Sendable变量 return data * 2; } async function doTask() { let task = new taskpool.Task(calculate, 42); // 创建任务 let result = await taskpool.execute(task); // 执行并等待结果 console.info(`Result: ${result}`); } 使用@Sendable类传递对象import { taskpool } from '@kit.ArkTS'; @Sendable // 标记为可跨线程传递 class CalculationTask { private factor: number = 2; run(input: number): number { return input * this.factor; } } @Concurrent function runner(task: CalculationTask, value: number): number { return task.run(value); } async function doComplexTask() { let myTask = new CalculationTask(); let task = new taskpool.Task(runner, myTask, 21); let result = await taskpool.execute(task); } 方案二:异步编程迁移(Thread -> Promise)场景:网络请求等I/O操作。 // 原Java: new Thread(() -> { httpRequest(); }).start(); async function fetchData(): Promise<void> { try { let response = await http.request('https://api.example.com/data'); // 假设的API let data = response.result as string; // 更新UI (会自动回到UI线程) this.uiData = data; } catch (error) { console.error('Request failed:', error); } } // 在UI事件中直接调用: fetchData(); 方案三:跨语言数据兼容性处理场景:HMAC-SHA1加密,保证ArkTS结果Java可解密。 import { cryptoFramework } from '@kit.CryptoArchitecture'; import { util } from '@kit.ArkTS'; async function hmacSha1(key: string, data: string): Promise<Int8Array> { // ... 省略加密步骤,得到 Uint8Array 结果 ... let uint8Result: Uint8Array = await doHmacSha1(key, data); // *** 关键转换:将无符号字节数组转换为有符号字节数组 *** let int8Result = new Int8Array(uint8Result.length); for (let i = 0; i < uint8Result.length; i++) { let value = uint8Result[i]; // 将 >127 的值转换为负数表示 int8Result[i] = value > 127 ? value - 256 : value; } // 也可以使用更简洁的方式: let int8Result = new Int8Array(uint8Result); // 通常再转换为Base64字符串传输 let base64Str = util.Base64.encodeToString(int8Result); return int8Result; } Java服务端需使用byte[]接收,并对Base64字符串进行解码。方案四:解决this绑定问题 class MyClass { private name: string = 'MyClass'; handleClick() { console.info(this.name); } registerCallback() { // 错误:直接传递方法,this会丢失 // someComponent.setCallback(this.handleClick); // 正确:使用箭头函数或bind绑定this someComponent.setCallback(() => this.handleClick()); // 或 someComponent.setCallback(this.handleClick.bind(this)); } } 方案五:单例模式适配(跨线程)由于ArkTS线程内存隔离,要实现真正的进程内单例,需借助共享模块(HSP)。将单例类定义在HSP模块中。确保类被标记为@Sendable(如果需要在线程间传递)。// 在HSP模块中定义 @Sendable export class GlobalDataManager { private static instance: GlobalDataManager; private data: Map<string, Object> = new Map(); private constructor() {} public static getInstance(): GlobalDataManager { if (!GlobalDataManager.instance) { GlobalDataManager.instance = new GlobalDataManager(); } return GlobalDataManager.instance; } // ... 其他方法 ... } 在不同模块或线程中,通过GlobalDataManager.getInstance()访问的是HSP提供的唯一实例。1.5 结果展示通过实施本解决方案:成功编译与运行:项目可顺利升级至compatibleSdkVersion ≥ 10的标准模式,通过严格的ArkTS语法检查,构建出符合NEXT标准的应用。稳定性与性能提升:基于Actor模型的并发设计从根本上避免了数据竞争和死锁,提高了应用稳定性。静态类型检查和优化的异步模型减少了运行时开销,提升了性能。生态无缝对接:通过规范的数据转换(如字节符号处理),确保ArkTS应用与既有Java服务端生态(如加解密、签名)的无缝、正确交互。提供标准范式:为团队后续所有Java向ArkTS的迁移项目提供清晰的、经过验证的架构改造指南和代码范例,大幅降低迁移过程中的试错成本和学习曲线,将迁移效率提升50%以上。结论:从Java到ArkTS的迁移是一次从“语言”到“范式”的升级。理解并接受内存隔离、事件驱动、强静态类型等新范式,是成功迁移的关键。本方案提供的系统性思路和具体“配方”,能有效引导开发者跨越鸿沟,构建更稳健、高性能的HarmonyOS应用。
-
Java作为一门拥有近30年历史的编程语言,凭借其 跨平台特性、丰富的生态系统 和 广泛的应用场景,始终占据编程语言排行榜前列。面对云原生、微服务、AI等技术的蓬勃发展,Java也在持续进化(如虚拟线程、GraalVM)。本文将为初学者和进阶者梳理一条清晰的2025年Java学习路线,涵盖 基础语法到云原生微服务架构 的全流程,并提供实用的学习资源推荐。一、Java基础阶段(4-8周)1. 开发环境搭建 JDK安装:深入理解JDK/JRE/JVM的关系,掌握环境变量(如JAVA_HOME, PATH)配置。推荐JDK 17或21(LTS)。 IDE使用:IntelliJ IDEA (社区版) 是首选,熟练掌握快捷键、调试(Debugging)、版本控制集成。 第一个Java程序:Hello World,理解编译(javac)、运行(java)过程及JVM作用。2. 核心语法基础 数据类型:严格区分基本类型(Primitive)与引用类型(Reference)。 流程控制:if-else, switch表达式(Java 12+), for/while/do-while循环。 数组与字符串:数组操作,String API 核心方法,StringBuilder/StringBuffer区别。 方法:定义、参数传递(值传递)、重载(Overload)、可变参数(Varargs)。3. 面向对象编程(OOP) 类与对象:理解面向对象的基本抽象单元。 四大基石: 封装 (Encapsulation):访问控制修饰符(private, protected, public)。 继承 (Inheritance):extends关键字,方法重写(Override),super。 多态 (Polymorphism):向上转型、向下转型、动态绑定。 抽象 (Abstraction):抽象类(abstract class)、接口(interface)。 接口 vs 抽象类:深刻理解应用场景与设计考量。 内部类:成员内部类、静态内部类、局部内部类、匿名内部类(Lambda前身)。4. 核心类库 (Java SE API) 集合框架 (Collections Framework): 核心接口:List (ArrayList, LinkedList), Set (HashSet, TreeSet), Map (HashMap, LinkedHashMap, TreeMap)。 HashMap 原理(哈希表、冲突解决、扩容)是面试重点。 迭代器(Iterator)、比较器(Comparator/Comparable)。 异常处理 (Exception Handling): 异常体系:Throwable -> Error/Exception(Checked/Unchecked)。 try-catch-finally / try-with-resources (Java 7+)。 自定义异常。 IO/NIO: 字节流(InputStream/OutputStream)、字符流(Reader/Writer)。 File类、序列化(Serializable)。 NIO 基础概念:Buffer, Channel, Selector。 多线程 (Multithreading): 创建线程:继承Thread vs 实现Runnable/Callable + Future。 线程池:ExecutorService 及其实现(ThreadPoolExecutor)。 线程同步:synchronized关键字、Lock接口(ReentrantLock)及其条件变量。 线程通信:wait()/notify()/notifyAll()。 反射 (Reflection): Class对象获取。 动态创建对象、访问字段/方法。 动态代理(Proxy)。5. 关联技术基础理解Web开发全貌所需,不必精通,但需知晓概念与交互。前端三剑客 (HTML, CSS, JavaScript): HTML:基础结构、常用标签。 CSS:基础样式、选择器、盒模型。 JavaScript (JS):基础语法、DOM操作、事件处理、Ajax (Fetch API)。 主流前端框架:了解Vue.js / React.js 的作用和基本概念。拓展阅读:HTML 与 CSS 基础教程Web基础协议: HTTP/HTTPS:请求/响应结构、方法(GET/POST/PUT/DELETE)、状态码、Header、Cookie。 TCP/IP:了解OSI模型、TCP三次握手/四次挥手。会话管理技术: Cookie & Session:原理、区别、应用场景。 JWT (JSON Web Token):现代无状态会话方案。拓展阅读:会话技术:Cookie 与 Session 详解推荐资源: 书籍:《Java核心技术 卷Ⅰ》(Core Java Volume I) - 经典权威。 视频:B站【狂神说Java】基础篇 - 通俗易懂。 练习:LeetCode简单题目(两数之和、反转链表等),牛客网Java基础题。二、Java 进阶阶段(6-10周)1. JVM 深度理解 内存模型 (Runtime Data Areas):堆(Heap)、栈(Stack/VM Stack/Native Method Stack)、方法区(Metaspace)、程序计数器(PC Register)。 垃圾回收 (GC): 对象存活判断(引用计数、可达性分析)。 GC算法:标记-清除、标记-整理、复制;分代收集思想(Young/Old)。 垃圾收集器:Serial, Parallel, CMS, G1, ZGC, Shenandoah。掌握G1原理与调优。 GC日志分析。 类加载机制: 过程:加载 -> 链接(验证、准备、解析) -> 初始化。 双亲委派模型 (Parents Delegation Model) 及其打破。 类加载器:Bootstrap, Extension/Platform, Application。 JVM调优实战: 常用JVM参数 (-Xms, -Xmx, -XX:NewRatio, -XX:SurvivorRatio, -XX:+UseG1GC等)。 使用工具:VisualVM, JConsole, jstat, jmap, jstack 分析内存、线程、GC。2. 并发编程 - 应对高并发挑战 线程池进阶: ThreadPoolExecutor 核心参数(corePoolSize, maxPoolSize, workQueue, keepAliveTime, handler)详解。 线程池状态流转。 合理配置线程池。 并发工具类 (java.util.concurrent): CountDownLatch:等待多个任务完成。 CyclicBarrier:线程到达屏障点等待。 Semaphore:控制并发访问数。 Exchanger:线程间交换数据。 原子类 (java.util.concurrent.atomic): AtomicInteger, AtomicReference等。 CAS (Compare-And-Swap) 原理 及 ABA 问题。 volatile 关键字:保证可见性、禁止指令重排序(部分),理解其局限性。 并发容器:ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue(ArrayBlockingQueue, LinkedBlockingQueue)。3. Java新特性 - 拥抱现代化 Lambda表达式:语法、作用域。 函数式接口 (Functional Interface):@FunctionalInterface, java.util.function包 (Predicate, Function, Consumer, Supplier)。 Stream API:流式操作 (filter, map, reduce, collect)、并行流。 模块化系统 (Java Platform Module System - JPMS, Java 9+):module-info.java,理解模块化带来的变化。 记录类 (Record, Java 16):简化不可变数据载体。 模式匹配 (Pattern Matching, Java 17 preview, 后续版本增强):instanceof模式匹配、switch模式匹配(预览特性,关注进展)。4. 设计模式 创建型:单例模式(多种实现及适用场景)、工厂方法模式、抽象工厂模式、建造者模式。 结构型:适配器模式、装饰器模式、代理模式(静态、JDK动态、CGLIB)、外观模式。 行为型:策略模式、观察者模式、责任链模式、模板方法模式。 理解Spring等框架中设计模式的应用(如IoC容器是工厂模式,AOP是代理模式)。推荐资源: 书籍:《Effective Java (3rd)》 - Java编程最佳实践圣经,《Java并发编程实战》 - 并发领域权威。 工具:JProfiler (付费强大), VisualVM, Arthas (阿里开源在线诊断工具)。 实践:动手实现简易线程池、内存泄漏检测Demo、应用设计模式重构小项目。三、数据库与MySQL(2-3周)1. 环境搭建 MySQL 8.x安装与配置:掌握Linux/Windows下安装,理解关键配置项。 客户端工具:熟练使用MySQL Workbench (官方)、Navicat Premium (流行商业工具)。 字符集与排序规则:统一使用utf8mb4和utf8mb4_0900_ai_ci (或合适校对规则) 避免乱码。2. SQL核心与进阶拓展阅读:MySQL 基础入门指南 SQL深度掌握: DDL (数据定义语言):CREATE/ALTER/DROP (DATABASE, TABLE, INDEX)。 DML (数据操作语言):INSERT/UPDATE/DELETE。 DQL (数据查询语言):SELECT语句精髓,聚合函数(COUNT, SUM, AVG, MAX, MIN, GROUP_CONCAT),分组(GROUP BY),过滤(HAVING)。 DCL (数据控制语言):GRANT/REVOKE。 复杂查询:深入理解7种JOIN (INNER, LEFT/RIGHT [OUTER], FULL[OUTER], CROSS, SELF),子查询优化技巧。 窗口函数 (Window Functions):ROW_NUMBER(), RANK(), DENSE_RANK(), NTILE(), 聚合函数+OVER子句。解决复杂分析问题利器。 存储过程与函数:开发流程,理解其优缺点(维护性、性能)并谨慎使用。 触发器 (Triggers):原理,极其谨慎使用。3. 数据库设计与性能优化拓展阅读:MySQL 进阶知识数据库设计: 范式理论:1NF/2NF/3NF/Boyce-Codd NF (BCNF) 的理解与权衡,反范式设计优化查询性能。 ER建模:使用工具(如MySQL Workbench, PowerDesigner)设计表结构及关系。 索引优化: B+树原理是理解索引的基石。 索引类型:主键、唯一、普通、全文、空间、组合索引。 最左前缀原则是组合索引生效的关键。 索引覆盖、索引下推 (ICP)。 何时建索引?索引失效场景排查(函数、运算、类型转换、OR条件、!=/<>、LIKE通配符开头)。 执行计划 (EXPLAIN):必须精通解读 type, key, rows, Extra 等字段含义。 慢查询日志:开启、分析(mysqldumpslow, pt-query-digest)、优化。事务与锁机制: ACID特性:理解InnoDB如何通过redo log(重做日志)保证持久性(D)、undo log(回滚日志)保证原子性(A)和一致性©、锁和MVCC保证隔离性(I)。 隔离级别 (Isolation Levels): READ UNCOMMITTED -> 脏读 READ COMMITTED -> 不可重复读 REPEATABLE READ (MySQL默认) -> 幻读 (InnoDB通过Next-Key Locking解决) SERIALIZABLE 锁机制:共享锁(S锁)、排他锁(X锁)、意向锁(IS/IX)。行锁、间隙锁(Gap Lock)、临键锁(Next-Key Lock)、死锁检测与处理 (SHOW ENGINE INNODB STATUS)。 MVCC (Multi-Version Concurrency Control):ReadView、undo log版本链,理解REPEATABLE READ和READ COMMITTED下可见性规则差异。高可用与扩展架构: 主从复制 (Replication):原理(基于binlog)、配置(异步/半同步)、GTID模式。 读写分离:应用层实现、中间件实现(ShardingSphere-JDBC, MyCat)。 分库分表 (Sharding): 垂直拆分、水平拆分。 分片策略:Range(范围)、Hash(哈希)、取模、一致性Hash。 中间件:ShardingSphere (Apache顶级项目,生态完善)、MyCat。 高可用集群 (HA):MHA (Master High Availability), MGR (MySQL Group Replication, InnoDB Cluster基础)。JDBC (Java Database Connectivity):理解驱动、Connection、Statement/PreparedStatement (防SQL注入)、ResultSet、事务管理。 拓展阅读:JDBC 讲解全面教程推荐资源: 书籍:《高性能MySQL(第4版)》 官方文档:MySQL 8.0 Reference Manual 在线练习:LeetCode数据库题库、SQLZoo、HackerRank SQL。 工具:pt-query-digest (Percona Toolkit), MySQL Workbench Performance Reports。四、开发框架与中间件(8-12周)1. Spring 生态 Spring Framework Core: IoC (控制反转) / DI (依赖注入):核心思想,ApplicationContext, Bean生命周期、作用域(singleton, prototype等)、配置方式(XML, Java Config, Annotation)。 AOP (面向切面编程):核心概念(切面Aspect、连接点Joinpoint、通知Advice、切入点Pointcut),实现原理(动态代理),常用场景(日志、事务、安全)。 Spring MVC: 核心组件:DispatcherServlet, HandlerMapping, Controller, ViewResolver。 请求处理流程。 RESTful API 设计规范与实践,常用注解 (@RestController, @RequestMapping, @GetMapping等)。 Spring Boot: 核心理念:约定优于配置、自动配置 (@EnableAutoConfiguration, spring.factories)、嵌入式容器(Tomcat, Jetty, Undertow)。 Starter:理解机制,常用Starter (spring-boot-starter-web, spring-boot-starter-data-jpa, spring-boot-starter-data-redis, spring-boot-starter-security, spring-boot-starter-actuator)。 外部化配置 (application.properties/.yml), Profile, 日志配置。 打包 (spring-boot-maven-plugin) 与运行。 Spring Cloud (构建分布式/微服务系统): 服务注册与发现:Nacos (推荐,阿里开源,功能丰富), Eureka (逐步淡出)。 配置中心:Nacos Config, Spring Cloud Config Server。 服务调用:OpenFeign (声明式REST客户端)。 负载均衡:Spring Cloud LoadBalancer (替代Ribbon)。 服务熔断与降级:Sentinel (推荐,阿里开源), Hystrix (维护模式)。 API网关:Spring Cloud Gateway (高性能, 异步), Netflix Zuul (1.x维护模式, 2.x不活跃)。 分布式链路追踪:Sleuth + Zipkin / SkyWalking。2. 持久层框架 MyBatis (半自动ORM, 灵活性强):拓展阅读:MyBatis 使用详解 XML映射文件:<select>, <insert>, <update>, <delete>, 动态SQL (<if>, <choose>, <foreach>, <where>, <set>, <trim>)。 核心对象:SqlSessionFactory, SqlSession。与Spring整合:SqlSessionTemplate。 缓存机制:一级缓存(Session级别)、二级缓存(Mapper/Namespace级别)配置与注意事项。 插件开发:原理(拦截器Interceptor),示例(如PageHelper分页实现)。 逆向工程:MyBatis Generator / MyBatis-Plus (国内流行,提供强大CRUD封装和代码生成)。 Spring Data JPA (基于JPA标准的全自动ORM, 开发效率高): JPA (Java Persistence API) 规范,Hibernate 是其最流行实现。 核心概念:Entity, EntityManager, Repository (JpaRepository及其方法命名查询)。 关联关系映射:@OneToOne, @OneToMany/@ManyToOne, @ManyToMany。 查询方式:JPQL (Java Persistence Query Language), Criteria API (类型安全), 方法名推导。 N+1 查询问题:原因分析,解决方案(@EntityGraph, JOIN FETCH in JPQL)。 乐观锁:@Version注解实现并发控制。3. 常用中间件 缓存 (Cache): Redis:绝对主流选择。数据类型(String, Hash, List, Set, Sorted Set, Bitmaps, HyperLogLog, Geospatial, Stream)。持久化(RDB, AOF)。主从复制、哨兵(Sentinel)、集群(Cluster)。客户端(Jedis, Lettuce, Redisson - 分布式对象和锁)。 消息队列 (Message Queue): 核心概念:生产者(Producer)、消费者(Consumer)、Broker、主题(Topic)、队列(Queue)。 Kafka:高吞吐、分布式、持久化、分区(Partition)、副本(Replica)、ISR机制、可靠性保证(ACK机制)。适用日志、大数据流处理。 RocketMQ (阿里开源):金融级稳定性、顺序消息、事务消息、延时消息。国内广泛应用。 RabbitMQ:基于AMQP协议,功能丰富(Exchange, Binding, Routing Key),消息确认机制灵活。 RPC框架 (Remote Procedure Call): Dubbo (阿里开源):高性能Java RPC框架。核心:服务注册发现(Zookeeper, Nacos)、负载均衡、集群容错、服务治理。推荐工具链: 构建工具:Maven (主流) / Gradle (更灵活,Android/Kotlin首选)。掌握依赖管理、生命周期、插件使用。 版本控制:Git 是绝对标准。掌握基本命令(clone, add, commit, push, pull, branch, merge, rebase)、理解工作流(Feature Branching, Git Flow, GitHub Flow)。 容器化:Docker:核心概念(镜像Image、容器Container、仓库Repository)、基本命令(run, build, push, pull, ps, logs)、Dockerfile编写。Docker Compose:定义和运行多容器应用。五、项目实战与面试准备将所学知识融会贯通,构建简历亮点,针对性攻克面试难关。1. 项目实战 (选择1-2个深度打磨) 初级项目 (夯实基础): 电商系统核心模块: 商品中心:SPU/SKU设计、类目管理、商品搜索(SQL LIKE/简单ES)。 用户中心:注册登录、认证授权 (Authentication & Authorization)。 购物车:数据结构设计(Redis/DB)。 订单系统:状态机设计、下单流程、事务管理 (@Transactional)、库存扣减。 支付模块:模拟对接(支付宝/微信沙箱)。 性能关注点:商品列表页SQL优化 (EXPLAIN), 热点数据缓存(Redis)。 博客/内容管理系统 (CMS): 文章管理:CRUD、富文本编辑(集成UEditor/Kindeditor)、分类标签(多对多关系)。 评论系统:设计(嵌套评论/楼层评论)、审核。 用户与权限:角色管理、内容权限控制。 数据统计:访问量、热门文章。 进阶点:全文检索集成 (Elasticsearch / Solr)。 进阶项目 (挑战难点, 应对面试): 秒杀/高并发系统: 核心挑战:瞬时高并发、超卖、数据库压力。 解决方案: 分层校验:活动校验、用户资格校验前置。 缓存预热:活动信息、库存加载到Redis。 库存扣减:Redis Lua脚本保证原子性、异步扣减DB库存。 请求削峰:MQ(如RocketMQ/Kafka)排队处理。 限流熔断:网关层/应用层限流(Sentinel/Guava RateLimiter)、熔断降级。 页面静态化:CDN加速。 技术栈:Spring Boot + Redis + MQ + (Sentinel) + (ShardingSphere-JDBC)。 微服务实践: 服务拆分:按业务域(用户服务、商品服务、订单服务、支付服务)。 技术栈:Spring Boot + Spring Cloud Alibaba (Nacos, Sentinel, Seata, RocketMQ) / Spring Cloud Netflix (Eureka, Hystrix, Zuul) + MyBatis/Spring Data JPA。 核心问题解决: 服务治理:注册发现(Nacos)、负载均衡(LoadBalancer)、熔断降级(Sentinel)。 分布式事务:Seata AT模式 (2PC)、可靠消息最终一致性 (基于MQ)、TCC模式。理解优缺点。 数据一致性:分库分表 (ShardingSphere-JDBC)、数据同步(Canal监听MySQL binlog到ES/Redis)、分布式ID生成(Snowflake, Leaf)。 配置管理:Nacos Config。 链路追踪:Sleuth + Zipkin。 部署:Docker容器化。2. 面试专项准备 Java核心: HashMap (1.7 vs 1.8+ 结构、put流程、扩容、线程安全ConcurrentHashMap)、ArrayList vs LinkedList。 JVM:内存区域、GC算法与收集器(G1重点)、类加载、OOM排查、JVM参数调优。 并发:线程状态、线程池原理与参数、synchronized/Lock/volatile/CAS、并发容器、ThreadLocal、AQS原理(理解ReentrantLock/CountDownLatch等实现基础)。 新特性:Lambda、Stream API、Optional、模块化、Record。 数据库 (MySQL 为主): 索引:B+树、聚簇/非聚簇索引、覆盖索引、最左前缀、索引失效场景、索引优化。 事务:ACID、隔离级别与问题、MVCC原理(ReadView)、锁机制(行锁、间隙锁、临键锁、死锁)、redo log/undo log/binlog作用与两阶段提交(2PC)。 性能优化:EXPLAIN解读、慢查询优化、分库分表方案(ShardingSphere)、主从复制与读写分离、高可用(MHA/MGR)。 设计:三范式、ER图、大表优化策略。 框架 (Spring为主): Spring:IoC/DI原理、Bean生命周期、AOP原理与应用、事务传播行为与隔离级别、Spring MVC流程。 Spring Boot:自动配置原理、Starter原理、常用注解。 Spring Cloud:核心组件作用与原理(Nacos注册发现配置、Feign、Gateway、Sentinel、Seata)、服务治理、熔断降级策略、分布式事务方案对比。 ORM:MyBatis #{} vs ${}、缓存、动态SQL; JPA/Hibernate N+1问题、懒加载、关联映射。 中间件: Redis:数据类型与应用场景、持久化、过期策略、内存淘汰策略、集群模式、分布式锁实现(setnx + lua)、缓存穿透/击穿/雪崩解决方案。 MQ (Kafka/RocketMQ):架构设计(Broker/Topic/Partition)、可靠性保证(ACK、副本)、顺序消息、事务消息、积压处理。 分布式:CAP/BASE理论、分布式ID、分布式锁、分布式Session、一致性协议(Raft/Paxos了解概念)。 系统设计:设计一个短链系统、设计一个抢红包系统、设计一个朋友圈/微博Feed流。掌握常见设计模式应用场景。 算法与数据结构: 基础:数组、链表、栈、队列、哈希表、堆、二叉树(遍历、BST、AVL/红黑树概念)、图(遍历、最短路径概念)。 算法:排序(快排、归并、堆排)、二分查找、DFS/BFS、递归、滑动窗口、双指针、动态规划(经典背包、路径问题)、贪心。 复杂度分析 (Big O Notation)。 刷题:LeetCode (按标签/公司/频率刷)、剑指Offer、牛客网。重点:链表、树、数组、字符串、动态规划、回溯。六、前沿探索、持续学习以及资源分享1. 前沿技术技术日新月异,保持好奇心和学习动力是关键。 云原生 (Cloud Native): Kubernetes (K8s):容器编排事实标准,学习Pod、Deployment、Service、Ingress等核心概念。 Service Mesh:Istio / Linkerd,理解Sidecar模式、服务治理下沉。 Serverless:FaaS (Function as a Service) 概念与实践(AWS Lambda, 阿里云函数计算)。 云数据库:AWS RDS, Azure SQL Database, 阿里云PolarDB的优化与管理。 大数据基础: Hadoop:HDFS (分布式存储), MapReduce (计算模型), YARN (资源调度)。 Spark:内存计算引擎,比MapReduce更高效,学习RDD/DataFrame/Dataset API。 流处理:Spark Streaming, Flink (低延迟、高吞吐)。 前沿Java技术: Quarkus:为GraalVM和Kubernetes量身定制的超音速亚原子Java框架(启动快、内存小)。 GraalVM:高性能跨语言运行时,支持AOT编译(提前编译成本地镜像)。 Project Loom (虚拟线程 - Java 19+):轻量级线程,旨在显著提高高并发应用吞吐量,简化并发编程(java.lang.VirtualThread)。 Project Panama (外部函数与内存API - incubating):改进Java与原生代码(特别是C库)的互操作性。 Project Valhalla (值对象与泛型特化 - in progress):引入值类型(inline class),优化内存布局和性能。 数据库扩展: 时序数据库 (TSDB):InfluxDB, TimescaleDB,物联网(IoT)、监控场景应用。 图数据库 (Graph Database):Neo4j,处理复杂关系。 架构演进:深入理解DDD(领域驱动设计)、CQRS(命令查询职责分离)、Event Sourcing(事件溯源)、微服务治理最佳实践、可观测性(Observability - Metrics, Logging, Tracing)。2. 学习建议与资源 代码量是硬道理: 初期:保证每天200+行有效代码,动手敲,不要只看。 中后期:通过项目持续积累,目标:1-2个完整项目(含文档)写在简历上。 构建知识体系: 使用思维导图(XMind, MindMaster)或 双链笔记 (Obsidian, Logseq) 梳理技术脉络和关联。 定期回顾总结,形成自己的技术博客/笔记库。 善用优质资源: 官方文档永远是第一手资料(JDK, Spring, MySQL, Redis等)。 经典书籍(见各阶段推荐)。 高质量视频教程(B站、慕课网、极客时间)。 技术博客/社区:掘金、InfoQ、开发者头条、Stack Overflow (解决问题)、GitHub (学习源码、参与开源)、CSDN(精选文章)。 积极参与社区: GitHub:Star优质项目,尝试提交PR修复小issue,阅读源码。 技术论坛:提问、回答问题、参与讨论。 线下/线上技术沙龙、Meetup。 避免常见误区: 不要过早陷入框架细节:先打好Java核心、数据结构算法、计算机网络、操作系统基础。 重视单元测试:熟练使用JUnit 5 / TestNG,培养TDD/BDD思维。 理解设计原则:SOLID (单一职责、开闭、里氏替换、接口隔离、依赖倒置) 比死记设计模式更重要。 关注原理,不止于会用:理解框架、中间件、JVM背后的工作原理。 英语能力很重要:大量一手资料、官方文档、前沿技术资讯都是英文。推荐资源汇总: 书籍: 基础/核心:《Java核心技术 卷I》《Effective Java》《Java编程思想》 并发:《Java并发编程实战》 JVM:《深入理解Java虚拟机》 数据库:《高性能MySQL》《SQL必知必会》《Redis设计与实现》 设计模式:《Head First设计模式》《设计模式:可复用面向对象软件的基础》 架构/Spring:《Spring实战》《Spring Boot实战》《微服务架构设计模式》 在线平台: 学习:B站、慕课网、极客时间、Coursera/edX (CS基础) 刷题:LeetCode、牛客网、LintCode 问答:Stack Overflow、SegmentFault 社区/博客:掘金、InfoQ、开发者头条、美团技术团队、阿里技术 文档: Oracle Java Documentation Spring Framework Documentation Spring Boot Reference Documentation MySQL Reference Manual Redis Documentation Kafka Documentation Nacos Documentation 工具: IDE: IntelliJ IDEA (Ultimate/Community) 构建: Maven, Gradle 版本控制: Git (GitHub, GitLab, Gitee) 数据库: MySQL Workbench, DBeaver, Navicat 接口测试: Postman, Insomnia 容器: Docker, Docker Desktop 监控/诊断: VisualVM, JConsole, Arthas, JProfiler (付费)Java 学习是一场马拉松而非短跑。 技术栈会更新,但扎实的基础、持续学习的热情、解决问题的能力和工程化的思维是长久立足之本。保持耐心,循序渐进,动手实践是掌握编程的唯一捷径。记住:The best time to start was yesterday. The next best time is now. (开始的最佳时间是昨天,其次是现在!) 踏上你的Java精进之旅吧!————————————————原文链接:https://blog.csdn.net/m0_64355285/article/details/155104433
-
什么是继承在 Java 中,使用类来对现实中的实体进行描述,例如定义一个 Cat 类来描述猫:public class Cat { String name; int age; public void eat() { System.out.println("吃饭"); }}一键获取完整项目代码java定义一个 Dog 类来描述狗:public class Dog { String name; int age; public void eat() { System.out.println("吃饭"); }}一键获取完整项目代码java而上述代码中的 name、age 以及 eat 方法,都是重复的此时,就可以对这些共性进行抽取而在面向对象思想中提出了 继承 的概念,专门用来进行共性抽取,实现代码复用继承(inhertance):是面向对象编程的特征,它允许在保持原有类的基础上进行扩展,增加新的功能,这样产生的新类,称之为派生类。通过继承,能够实现共性的抽取,从而实现代码的复用我们将上述 Cat 和 Dog 的共性进行抽取,使用继承的思想来达到复用效果: 如上图所示,Dog 和 Cat 都继承了 Animal 类,其中,Animal 称为 父类(或 超类、基类),Dog 和 Cat 称之为 Animal 的 子类(或 派生类),继承之后,子类可以复用父类中的成员,子类在实现时只需关心自己新增的成员即可继承的语法在 Java 中若要表示类之间的继承关系,需要使用 extends 关键字:修饰符 class 子类 extends 父类{ ...}public class Animal { String name; int age; public void eat() { System.out.println("吃饭"); }}一键获取完整项目代码javaCat 类继承 Animal,还可以新增需要的成员变量或方法public class Cat extends Animal{ public void miaow() { System.out.println("喵喵叫"); }}一键获取完整项目代码java子类会将父类中的成员变量或成员方法继承到子类中子类在继承父类之后,可以添加自己特有的成员父类成员的访问子类将父类中的方法和变量继承之后,那么,子类中能否直接访问父类中继承下来的成员呢?访问父类的成员变量当子类和父类不存在同名对象时public class A { int a = 10;}一键获取完整项目代码javapublic class B extends A { int b = 20; public static void main(String[] args) { B b = new B(); System.out.println(b.a); System.out.println(b.b); // System.out.println(b.c); // 编译失败,子类和父类中都不存在 c b.method(); }}一键获取完整项目代码java运行结果:子类自己有就访问自己的成员变量,若没有,就访问父类的成员变量 当子类和父类存在同名对象时public class A { int a = 10; int b = 20;}一键获取完整项目代码javapublic class B extends A { int a = 100; // 与父类中的成员同名,且类型相同 char b = 'b'; // 与父类中的成员同名,但类型不同 public void method() { System.out.println("a: " + a + " b: " + b); } public static void main(String[] args) { B b = new B(); b.method(); }}一键获取完整项目代码java运行结果:存在同名变量时,优先访问子类的成员变量 从上述两个示例中,可以看出:若访问的成员变量子类中有,则访问子类自己的成员变量若访问的成员变量子类中没有,则访问父类中继承下来的,若父类中也没有,则会编译报错若访问的成员变量,父类子类中都有,则优先访问自己的成员变量即 自己有就优先自己的,若没有再向父类中找访问父类的成员方法成员方法名不同public class A { int a = 10; int b = 20; public void methodA() { System.out.println("methodA..."); }}一键获取完整项目代码javapublic class B extends A { int a = 100; // 与父类中的成员同名,且类型相同 char b = 'b'; // 与父类中的成员同名,但类型不同 public void methodB() { System.out.println("methodB..."); } public static void main(String[] args) { B b = new B(); b.methodA(); b.methodB();// b.methodC(); // 编译失败,在继承体系中没有发现 methodC() }}一键获取完整项目代码java运行结果:当成员方法没有同名时,在子类方法中或通过子类对象访问方法时,若自己有,访问自己的,若自己没有,则在父类中找,若父类中也没有,则报错成员方法名相同public class B extends A { int a = 100; // 与父类中的成员同名,且类型相同 char b = 'b'; // 与父类中的成员同名,但类型不同 public void methodA(int a) { System.out.println("methodA..." + a); } public static void main(String[] args) { B b = new B(); b.methodA(); b.methodA(10); }}一键获取完整项目代码java 运行结果:当子类方法名和父类方法名相同,但其参数列表不同时,就构成了重载,根据调用方法时传递的参数选择合适的方法访问,若不存在该方法,则报错public class B extends A { int a = 100; // 与父类中的成员同名,且类型相同 char b = 'b'; // 与父类中的成员同名,但类型不同 @Override public void methodA() { System.out.println("B methodA..."); } public static void main(String[] args) { B b = new B(); b.methodA(); }}一键获取完整项目代码java 当子类方法名和父类方法名相同,且其参数列表也相同时,就构成了重写,此时,会访问子类的方法那么,当子类中存在和父类相同的成员时,如何在子类中访问父类同名成员呢?super 关键字当子类和父类中存在相同名称的成员时,若要在子类方法中访问父类同名成员时,是不能直接访问的。Java 提供了 super 关键字,super 的主要作用:在子类方法中访问父类成员访问父类成员变量public class B extends A { int a = 100; // 与父类中的成员同名,且类型相同 char b = 'b'; // 与父类中的成员同名,但类型不同 public void methodB() { System.out.println(super.a); System.out.println(super.b); } public static void main(String[] args) { B b = new B(); b.methodB(); }}一键获取完整项目代码java访问父类成员方法public class B extends A { int a = 100; // 与父类中的成员同名,且类型相同 char b = 'b'; // 与父类中的成员同名,但类型不同 public void methodB() { super.methodA(); System.out.println("methodB..."); } public static void main(String[] args) { B b = new B(); b.methodB(); }}一键获取完整项目代码java需要注意的是:只能在非静态方法中使用 super访问父类构造方法在子类对象构造时,会先调用父类构造方法,然后再执行子类构造方法为 A 添加带一个参数的构造方法public class A { int a = 10; int b = 20; public A(int a) { System.out.println("a"); } public void methodA() { System.out.println("methodA..."); }}一键获取完整项目代码java 编译报错:A 中没有可用的默认构造函数为什么会报错呢?这是因为在 A 中没有定义的构造方法时,编译器提供了默认的无参构造方法子类 B 中也没有定义构造方法,编译器也提供了默认的无参构造方法,且在 B 的构造方法中第一行默认有隐含的 super() 调用父类的无参构造方法当添加了带有一个参数的构造方法后,编译器也就不再提供无参的构造方法,此时也就会报错我们可以在 A 中添加无参构造方法或是在 B 的无参构造方法中调用 A 带有一个参数的构造方法 public B(int a) { super(a); }一键获取完整项目代码java当父类中有多个构造方法时,就需要在子类构造方法中选择一个合适的父类构造方法调用,否则编译失败为什么要先调用父类的构造方法呢?子类对象中的成员是由两部分组成的:父类继承下来的成员 和 子类新增的成员 因此,在构造子类对象时,就需要先调用父类的构造方法,将从父类继承下来的成员构造完整,然后再调用子类自己的构造方法,将子类自己新增的成员初始化完整在子类构造方法中,通过 super(...) 调用父类构造,且 super(...) 必须是子类构造函数中的第一条语句因此,super(...) 只能在子类构造方法中出现一次,且必须在第一行而若需要在构造方法中使用 this 关键字调用类中的其他构造方法,也必须在第一行使用,也就是说,super(...) 和 this(...) 不能同时出现super 和 this相同点(1)都是 Java 中的关键字(2)都可以在成员方法中用来访问成员变量和调用其他成员方法,都可以作为构造方法的第一条语句(3)只能在类的非静态方法中使用,用来访问非静态成员方法和变量(4)在构造方法中调用时,必须是构造方法的第一条语句,并且不能同时存在不同点(1)this 表示当前对象(实例方法的对象)的引用,而 super 表示子类对象重父类继承下来部分成员的引用(2)在非静态成员方法中,this 用来访问本类的方法和属性,super 用来访问父类继承下来的方法和属性(3)在构造方法中,this(...) 用于调用本类构造方法,super(...) 用于调用父类构造方法,两种调用不能同时在构造方法中出现(4)构造方法中一定会存在 super(...) 的调用,但 this(...) 若没有写则没有初始化在前面的文章 Java 代码块-CSDN博客 中,我们学习了代码块,重点学习了 实例代码块 和 静态代码块,以及它们的执行顺序,接下来,我们就来看存在继承关系时,它们的执行顺序public class A { int a; int b; { a = 10; b = 20; System.out.println("A 构造代码块执行..."); } static { System.out.println("A 静态代码块执行..."); } public A(int a) { System.out.println("A 构造方法执行..."); } public void methodA() { System.out.println("methodA..."); }}一键获取完整项目代码javapublic class B extends A { int a = 100; // 与父类中的成员同名,且类型相同 char b = 'b'; // 与父类中的成员同名,但类型不同 static { System.out.println("B 静态代码块执行..."); } { System.out.println("B 构造代码块执行..."); } public B(int a) { super(a); System.out.println("B 构造方法执行..."); } public void methodA() { System.out.println("methodB..."); } public static void main(String[] args) { B b1 = new B(10); System.out.println("---------------------"); B b2 = new B(20); }}一键获取完整项目代码java运行结果: 通过运行结果,可以看到:静态代码块先执行,并且只执行一次,在类加载阶段执行父类静态代码块优先于子类静态代码块执行,且最早执行当有对象创建时,才会执行实例代码块父类实例代码块和父类构造方法先执行,然后再执行子类的实例代码块和子类的构造方法访问限定符为了实现封装性,Java 引入了访问限定符,主要限定:类或类中成员能否在类外或其他包中被访问范围 private default protected public同一个包中的同一个类 √ √ √ √同一个包中的不同类 √ √ √不同包中的子类 √ √不同包中的非子类 √public:在任何类都可以访问protected:在同一个包中的类或不同包中的子类可以访问default:同一个包中的类可以访问private:只有该类内部可以访问当 A 和 B 位于同一个包中时:public class A { public int a; int b; protected int c; private int d;}一键获取完整项目代码javapublic class B extends A { public void method() { super.a = 10; super.b = 20; super.c = 30; super.d = 40; }}一键获取完整项目代码java由于变量 d 是 A 私有的,即只能在 A 类中访问,因此,子类中不能直接访问但是,父类中的 private 成员变量虽然不能在子类中直接访问,但是也继承到子类中了子类可以通过父类提供的方法来进行修改public class A { public int a; int b; protected int c; private int d; public int getD() { return d; } public void setD(int d) { this.d = d; }}一键获取完整项目代码javapublic class B extends A { public void method() { super.a = 10; super.b = 20; super.c = 30; super.setD(40); }}一键获取完整项目代码java当 A 和 B 位于不同包中:public class B extends A { public void method() { super.a = 10; // 父类中 public 修饰的成员在不同包子类中可以直接访问 super.b = 20; // 父类中 protected 修饰的成员在不同包子类中可以直接访问 // super.c = 30; // 编译报错,父类中默认访问权限修饰的成员在不同包子类中不能直接访问 // super.d = 40; // 编译报错,父类中默认访问权限修饰的成员在不同包子类中不能直接访问 }}一键获取完整项目代码java继承方式在 Java 中,支持的继承方式有:(1)单继承(2)多层继承(3)不同类继承同一个类(4)多继承(不支持)Java 中不支持多继承类是对现实事物的抽象,而当情况比较复杂时,涉及到的类也会比较多,类之间的关系也会比较复杂但即使如此,我们并不希望类之间的继承层次太复杂,一般不希望出现超过三层的继承关系此时,若想从语法上限制继承,就可以使用 final 关键字final 关键字final 可以用来修饰变量、成员方法以及类当修饰变量时,表示该变量为常量(不可变)当修饰方法时,表示该方法不能被重写当修饰类时,表示该类不能被继承继承和组合与继承类似,组合也是一种表达类之间关系的方式,也能够达到代码重用的效果。但组合并没有涉及到特殊的语法,仅仅是将一个类的实例作为另一个类的字段继承表示的对象之间是 is a 的关系,如:猫 是 动物组合表示的对象之间是 has a 的关系,如:电脑 有 键盘组合:public class Person { private Student[] students;}一键获取完整项目代码java此时 Student 属于 Person 的一部分, 在 Person 中,可以复用 Student 中的属性和方法继承: public class Student extends Person{ }一键获取完整项目代码javaStudent 是 Person 的子类,继承了父类的成员,能够复用父类中的属性和方法组合和继承都可以实现代码复用,是使用组合还是继承,需要根据具体的情况来进行选择————————————————原文链接:https://blog.csdn.net/2301_76161469/article/details/142621979
-
一、体会线程安全问题当我们编写一个多线程程序,要求两个线程对同一个变量(共享变量)进行修改,得到的结果是否与预期一致?创建两个线程,分别对共享变量(count)进行自增5万次操作,最后输出的结果理论上应为10万,但是实际上输出的结果是一个小于10万且不确定的数。读者可以自行实现一下该多线程程序,运行后看看结果是否符合预期。public class Demo14_threadSafety { private static int count = 0; public static void main1(String[] args) { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t2-结束"); }); t1.start(); t2.start(); // 理论上输出的结果应是100000,实际输出的结果是0 // 原因是主线程 main 运行太快了,当 t1 和 t2 线程还在计算时,主线程已经打印结果、运行完毕了 System.out.println(count); } // 让主线程等待 t1 和 t2 线程,等到它们两个都执行完成再打印,故使用 join 方法 public static void main2(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t2-结束"); }); t1.start(); t2.start(); // 在主线程中,通过 t1 和 t2 对象调用 join 方法 // 表示让主线程 main 等待 t1 线程和 t2 线程 t1.join(); t2.join(); // 当两个线程都执行完毕后主线程再继续执行打印操作 System.out.println(count); // 实际输出的结果小于100000,仍不符合预期 }}一键获取完整项目代码java二、线程安全的概念通过上面的一个例子,想必读者已经体会到线程安全问题了吧?那究竟什么是线程安全问题呢?其原因是什么?如何解决线程安全问题呢?不要急,且听小编慢慢道来~如果在多线程环境下运行的程序其结果符合预期或与在单线程环境下运行的结果一致,就说这个程序是线程安全的,否则是线程不安全的。上面的例子在单线程环境下运行——比如来两个循环对共享变量进行自增操作,那么结果是符合预期的;但是在多线程环境下运行就不符合预期。因此该程序是线程不安全的,也可以说该程序存在线程安全问题。三、线程安全问题的原因究竟是哪里出问题导致程序出现线程安全问题呢?究其根本,罪魁祸首是 操作系统的线程调度有随机性/抢占式执行 。由于操作系统的线程调度是有随机性的,这就会存在这种情况:某一个线程还没执行完呢,就调度到其他线程去执行了,从而导致数据不正确。当然了,一个巴掌拍不响,还有以下三个导致线程不安全的原因:原子性:指 Java 语句,一条 Java 语句可能对应不止一条指令,若对应一条指令,就是原子的。可见性:一个线程对主内存(共享变量)的修改,可以及时被其他线程看到。有序性:一个线程观察其他线程中指令的执行顺序,由于 JVM 对指令进行了重排序,观察到的顺序一般比较杂乱。因其原理与 CPU 及编译器的底层原理有关,暂不讨论。之前的例子就是由于原子性没有得到保障而出现线程安全问题:public static void main2(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t2-结束"); }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count);}一键获取完整项目代码java1. “count ++” 这条语句对应多条指令:读取数据、计算结果、存储数据。2. t1 线程和 t2 线程分别执行一次“count++”语句,期望的结果是“count = 2”,其过程如下: 初始情况: 当线程执行“count ++”时,总共分三个步骤:load、update、save,由于线程调度的随机性/抢占式执行,可能会出现以下情况(可能出现的情况有很多种,这里只是其中一种): 这时候 t1 正在执行“count ++”这条语句,执行了“load 和 update”指令后,t1 的工作内存(寄存器)存着更新后的值,但是还未被写回内存中: 接着调度到 t2 线程并开始执行“count ++”语句,并且语句中包含的三条指令都执行。此时由于 t1 更新后的 count 的值还未写回内存,因此 t2 执行 load 操作所获取到的 count 仍是 0。接着 t2 执行 update 和 save 指令: 当 t2 执行完成,内存的 count 已被修改为 1 。此时调度回 t1 线程并继续执行 save 指令,但是 t1 线程寄存器中 count 的值也是 1 ,此时写回内存更新后 count 的值依然是 1 。 结果 count = 1,与预期的 count = 2 不符,因此存在线程安全问题,其原因是操作系统的随机线程调度和 count 语句存在非原子性。四、解决线程安全问题的方法从上面的例子我们知道,当一条语句的指令被拆开来执行的话是存在线程安全问题的,但是,当我们将“count ++”这条语句的三个指令都放在一起执行怎么样? 当线程调度的情况如下: 此时 t1 线程开始执行“count ++”语句的 load、update 和 save 指令。内存中的 count 为 0,t1 读取到内存中的 count 之后更新至 1 并写回内存中。当 t1 执行完成后内存的 count 由 0 更新至 1: 接着调度至 t2 线程,开始执行“count ++”语句的 load、update 和 save 指令。经过更新后内存中的 count 为 1,此时 t2 读取 count 并更新为 2,然后写回内存中。当 t2 执行完成,内存中的 count 就更新成 2 了:可以发现,结果与预期相符!说明这个方法可行。可以将操作顺序改成先让 t1 线程完成“count ++”操作,再让 t2 线程完成该操作——即串行执行。现在我们对之前的例子进行优化:// 可以试着让 t1 线程先执行完后,再让 t2 线程执行,改成串行执行public static void main3(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { count++; } System.out.println("t2-结束"); }); t1.start(); t1.join(); t2.start(); t2.join(); System.out.println(count);}一键获取完整项目代码java刚刚是让一个线程一次性执行“count ++”这条语句的三个指令,也就是说,我们是通过这样操作将原本是非原子的三条指令打包成了一个原子指令(即执行过程中不可被打断——调度走)。这样就有效的解决了线程安全问题。而上述的操作,其实就是 Java 中的 加锁操作 。当一个线程执行一个非原子的语句时,通过加锁操作可以防止在执行过程中被调度走或被其他线程打断,若其他线程想要执行该语句,则要进入 阻塞等待 的状态,当线程执行完毕并将锁释放,操作系统这时唤醒等待中的线程,才可以执行该语句。就相当于上厕所:当厕所内没有人时(没有线程加锁),就可以使用;当厕所内有人时(已经有线程加锁了),那么就必须等里面的人出来后才能使用。注意:前一个线程解锁之后,并不是后一个线程立刻获取到锁。而是需要靠操作系统唤醒阻塞等待中的线程的。若 t1、t2 和 t3 三个线程竞争同一个锁,当 t1 线程获取到锁,t2 线程再尝试获取锁,接着 t3 线程尝试获取锁,此时 t2 和 t3 线程都因获取锁失败而处于阻塞等待状态。当 t1 线程释放锁之后,t2 线程并不会因为先进入阻塞状态在被唤醒后比 t3 先拿到锁,而是和 t3 进行公平竞争。(不遵循先来后到原则)4.1 synchronized 关键字在处理由原子性导致的线程安全问题时,通常采用加锁操作。加锁 / 解锁这些操作本身是在操作系统所提供的 API 中的,很多编程语言对其进行了封装,Java 中使用 synchronized 关键字来进行加锁 / 解锁操作,其底层是使用操作系统的 mutex lock 来实现的。Java 中的任何一个对象都可以用作“锁”。synchronized (锁对象){ ——> 进入代码块,相当于加锁操作 // 一些需要保护的逻辑} ——> 出了代码块,相当于解锁操作当多个线程针对同一个锁对象竞争的时候,加锁操作才有意义。对之前的例子进行加锁操作:public class Demo15_synchronized { private static int count = 0; public static void main1(String[] args) throws InterruptedException { Object locker = new Object(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized (locker) { count++; } } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { synchronized (locker) { count++; } } System.out.println("t2-结束"); }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); }}一键获取完整项目代码javasynchronized 关键字用来修饰 普通方法 时,相当于给 this 加锁;synchronized 关键字用来修饰 静态方法 时,相当于给 类对象 加锁。于是可以使用另一种写法:// 写法二:// 将 count++ 所包含的三个操作封装成一个 add 方法// 使用 synchronized 修饰 add 方法 class Counter { private int count = 0; synchronized public void add () { // synchronized 修饰普通方法相当于给 this 加锁 count++; } // 相当于: // public void add () { // synchronized (this) { // count++; // } // } public int get () { return count; }} public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { counter.add(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { counter.add(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.get());}一键获取完整项目代码java这样一来,就成功解决了多线程程序中由原子性导致的线程安全问题。4.2 volatile 关键字我们再来一个例子:让 t1 线程读取共享变量的值,然后让 t2 线程修改共享变量的值,我们可以写出如下程序:public class Demo17_volatile { private static int flag = 0; public static void main1(String[] args) { Thread t1 = new Thread(() -> { while (flag == 0) { // 当 flag 为 0 时一直循环 } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { // 对 flag 进行修改 Scanner in = new Scanner(System.in); System.out.println("请输入 flag 的值:"); flag = in.nextInt(); }); t1.start(); t2.start(); // 运行后发现即使输入1,t1 线程并不会结束 }}一键获取完整项目代码java我们期望该程序运行后,输入非零的数如 1,t1 线程能够结束,实际并非如此,很显然,这也是出现了线程安全问题。这次出现问题的原因并非原子性,而是 可见性 。我们说过,可见性是一个线程对主内存(共享变量)的修改能够被其他线程及时看到,若不能被其他线程及时看到,就会出现数据错误,从而导致线程安全问题。如果我们使用锁来处理的话,好像并不能解决。我们先来认识一个东西:编译器优化。由于不能保证写代码的人每一次写代码都不会出错,所以开发 JDK 的先人们就让编译器 / JVM 能够根据代码的原有逻辑进行优化。在我们这个程序中:while {...load...cmp...} 先将数据从内存中 load 到寄存器中,然后在寄存器进行 “条件比较” 指令 cmp 。在短时间内用户还没来得及输入呢,但是这个循环语句能够执行成万上亿次且内存中的 flag 的值一直都是 0。那么这时候编译器 / JVM 就察觉到:既然一直读取 flag 都是同一个值,那干脆我直接读取寄存器算了(读取寄存器开销更小)。这样一来,当用户输入 1 的时候(存入内存),t1 线程就无法读取通过 t2 线程更新之后的值了,也就不会结束。在上面的程序中,t2 线程修改后的值无法被 t1 线程看到,这就是可见性导致的线程安全问题,由于编译器我们没办法更改,所以只好另寻他法。如果我们能让循环执行的慢一点,是不是就能解决问题了?尝试优化该程序:public static void main2(String[] args) { Thread t1 = new Thread(() -> { while (flag1 == 0) { // 当 flag1 为 0 时一直循环 try { Thread.sleep(1); // 放慢读取速度 } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { // 对 flag1 进行修改 Scanner in = new Scanner(System.in); System.out.println("请输入 flag1 的值:"); flag1 = in.nextInt(); }); t1.start(); t2.start();}一键获取完整项目代码java在运行优化版的程序后,发现问题被解决了,其原因是:使用了 sleep(1)—— 表示每一次读取的时候休眠 1毫秒,这对于读取操作的速度来说已经很慢了。因此不会触发编译器优化,也就不会出现可见性导致的线程安全问题了。但是,在实际开发环境中频繁使用 sleep 的话会导致程序效率下降,这样的话用户体验就会变差。Java 中使用 volatile 关键字来处理 可见性 导致的线程安全问题。使用 volatile 关键字来修饰共享变量,这样一来这个变量无论如何都不会被编译器优化。具体流程是:当 t1 线程读取被修饰的变量时,会强制读取主内存中变量最新的值;当 t2 线程修改被修饰的变量时,在工作内存(寄存器)中更新变量的值之后,立即将改变的值刷新至主内存。加上 volatile 关键字强制读取内存,虽然速度慢了,但是保证数据不出错。使用 volatile 关键字优化后的程序:// 使用 volatile 关键字来修饰被读取的变量,此时无论读取速度怎样,JVM 都不会对该变量进行优化private volatile static int flag = 0; public static void main(String[] args) { Thread t1 = new Thread(() -> { while (flag == 0) { // 当 flag 为 0 时一直循环 } System.out.println("t1-结束"); }); Thread t2 = new Thread(() -> { // 对 flag 进行修改 Scanner in = new Scanner(System.in); System.out.println("请输入 flag 的值:"); flag = in.nextInt(); }); t1.start(); t2.start();}一键获取完整项目代码java注意:volatile 关键字只能保证可见性,不能保证原子性,因此不能完全解决线程安全问题。当多线程中存在 ++、--、+=、-=、*= 和 /= 或者类似的操作符时就不能够使用 volatile 关键字来处理,需要使用 synchronized 关键字。五、死锁那么话又说回来,如果对锁使用不当,就很容易出现死锁。如果一个线程已经获取了锁,再次获取这个锁,能成功吗?会出现死锁吗?Object locker = new Object(); synchronized (locker) { synchronized (locker) { // 一些需要保护的逻辑 }}一键获取完整项目代码java按照前面的逻辑,第一次获取锁对象后,该锁对象已被占有,下一次获取该锁对象时应该会触发阻塞等待。而要想解除阻塞等待就得往下继续执行,但是要想往下执行就得将锁解开。这样不就构成死锁了嘛?不妨试试在你的 IDE 上运行一下~5.1 synchronized 的可重入性如果你在 IDE 上运行了刚刚的程序,可以发现程序是没问题的。按照我们这之前的逻辑,当锁对象第一次被获取到,其他的线程再想获取锁对象时就只能阻塞等待。但是,第二次获取锁对象的是同一个线程呀~ 因此并不会触发阻塞等待。Java 中的 synchronized 关键字是具有可重入性的,即对于同一个线程可以重复获取同一个锁对象。可重入锁内部包含了“线程持有者”和“计数器”两个信息,若线程加锁时发现锁已被占有且恰好是自己,此时仍可以获取到锁,让计数器自增即可;当计数器为 0 时,将锁释放,其他线程可以才获取到锁。这样就可以避免死锁的出现——开发 JDK 的前人们真的为我们考虑了太多😭5.2 死锁的概念当有两个线程为了保护两个不同的共享资源而使用两个不同的锁且这两个锁使用不当时,就会造成两个线程都在等待对方解锁,在没有外界干扰的情况下他们会一直相互等待,此时就是发生了死锁。死锁需要同时具备以下四个条件:互斥条件:要求同一个资源不能被多个线程同时占有。不可剥夺条件:当资源已被某个线程占有,其他线程只能等到该线程使用完并释放后才能获取,不可以强行打断并获取。持有并等待条件:有三个线程两个资源,当线程 1 已经占有资源 A(线程 2 尝试获取资源 A 但触发阻塞等待)且尝试获取资源 B 时(此时资源 B 已被线程 3 占有),线程 1 就会触发阻塞等待且不释放手中持有的资源 A 。循环等待/环路等待条件:有两个线程两个资源,线程 1 已占有资源 A 并想要获取资源 B ,但是资源 B 已被线程 2 占有,并且线程 2 在占有资源 B 的同时想要获取资源 A 。比如,有两个线程 t1 和 t2 以及两个锁 locker1 和 locker2,t1 先获取 locker1 并尝试获取 locker2;t2 先获取 locker2 并尝试获取 locker1:// 两个线程两把锁,每个线程获取到一把锁之后尝试获取对方的锁public static void main2(String[] args) throws InterruptedException { Object locker1 = new Object(); Object locker2 = new Object(); Thread t1 = new Thread(() -> { synchronized (locker1) { // t1 线程获取到锁对象 locker1 try { Thread.sleep(1000); // 确保 t2 线程拿到 locker2 } catch (InterruptedException e) { throw new RuntimeException(e); } // 尝试获取锁对象 locker2 synchronized (locker2) { System.out.println("t1 线程获取到两把锁"); } } }); Thread t2 = new Thread(() -> { synchronized (locker2) { // t2 线程获取到锁对象 locker2 try { Thread.sleep(1000); // 确保 t1 线程拿到 locker1 } catch (InterruptedException e) { throw new RuntimeException(e); } // 尝试获取锁对象 locker1 synchronized (locker1) { System.out.println("t2 线程获取到两把锁"); } } }); t1.start(); t2.start(); t1.join(); t2.join();}一键获取完整项目代码java运行该程序会发现什么也没输出,程序却还在运行。这就是发生了死锁。我们借助第三方工具来观察线程的状态:5.3 如何避免死锁要想避免死锁,我们就要从死锁的四个条件入手,其中,互斥条件和不可剥夺条件基本上是无法打破的,因为这两个是 synchronized 锁的基本特性,因此我们选择从后两个条件入手。1. 持有并等待条件:通常是由于代码中的嵌套加锁导致的,因此选择将嵌套的加锁代码改成串行的加锁代码(先将已占有的资源释放掉,然后去获取另一个资源):// 避免死锁的写法:打破持有并等待条件——将嵌套加锁改成串行加锁public static void main(String[] args) throws InterruptedException { Object locker1 = new Object(); Object locker2 = new Object(); Thread t1 = new Thread(() -> { synchronized (locker1) { // t1 线程获取到锁对象 locker1 try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } // t1 线程释放已占有的锁对象 locker1 // 尝试获取锁对象 locker2 synchronized (locker2) { System.out.println("t1 线程获取到两把锁"); // t1 获取到两把锁之后结束执行,此时 locker1 和 locker2 均被释放 } }); Thread t2 = new Thread(() -> { synchronized (locker2) { // t2 线程获取到锁对象 locker2 try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } // t2 线程释放已占有的锁对象 locker2 // 尝试获取锁对象 locker1 synchronized (locker1) { System.out.println("t2 线程获取到两把锁"); // t2 获取到两把锁后结束执行 } }); t1.start(); t2.start(); t1.join(); t2.join();}一键获取完整项目代码java2. 循环等待/环路等待条件:这种情况一般都是双方都持有对方想要的锁但是都不肯释放,所以我们就调整一下顺序:让 t1 和 t2 获取锁的顺序都是 locker1 和 locker2,当 t1 已占有 locker1 时 t2 想获取只能阻塞等待,但这时 t1 可以获取 locker2,等待 t1 两把锁都获取到并释放之后,t2 被唤醒并且获取两把锁。// 防止死锁的写法:打破循环等待条件——调整加锁顺序public static void main(String[] args) throws InterruptedException { Object locker1 = new Object(); Object locker2 = new Object(); Thread t1 = new Thread(() -> { synchronized (locker1) { // t1 线程获取到锁对象 locker1 try { Thread.sleep(1000); // 此时 t2 因 locker1 被 t1 获取而处于阻塞状态 BLOCKED // t1 休眠结束后,获取 locker2 } catch (InterruptedException e) { throw new RuntimeException(e); } // 尝试获取锁对象 locker2 synchronized (locker2) { System.out.println("t1 线程获取到两把锁"); // t1 获取到两把锁之后结束执行,此时 locker1 和 locker2 均被释放 } } }); Thread t2 = new Thread(() -> { synchronized (locker1) { // t2 线程获取到锁对象 locker1 // 因 t1 先获取到 locker1 ,t2 此时处于阻塞状态 BLOCKED // 当 t1 结束执行后,t2 恢复执行并获取 locker1 try { Thread.sleep(1000); } catch (InterruptedException e) { // 休眠结束后继续获取 locker2 throw new RuntimeException(e); } // 尝试获取锁对象 locker2 synchronized (locker2) { System.out.println("t2 线程获取到两把锁"); // t2 获取到两把锁后结束执行 } } }); t1.start(); t2.start(); t1.join(); t2.join();}一键获取完整项目代码java今天暂且到这吧~————————————————原文链接:https://blog.csdn.net/REGARD712/article/details/155878355
-
一、下载 JDK 1.8 安装包JDK 1.8 是企业级应用的经典稳定版本,优先从官方渠道下载适配 Linux 64 位的压缩包:官方下载地址:Java Downloads | Oracle小技巧:Oracle 官网下载需登录,若嫌麻烦,可选择华为云 / 阿里云镜像站(如 https://mirrors.huaweicloud.com/openjdk),下载速度更快且无需登录。 二、清理系统自带 JDK(关键避坑步骤)CentOS 系统默认可能预装 OpenJDK,与 Oracle JDK 冲突,需彻底清理:# 1. 检查已安装的 Java 相关包(忽略大小写,避免漏查)rpm -qa | grep -i java # 2. 强制卸载所有 Java 包(无依赖检查,彻底清理)rpm -qa | grep -i java | xargs -n1 rpm -e --nodeps一键获取完整项目代码bash命令解析:rpm -qa:列出系统所有已安装的 RPM 包;grep -i:忽略大小写匹配 java/Java/JAVA;xargs -n1:逐个传递包名,避免批量卸载报错;rpm -e --nodeps:强制卸载,忽略包间依赖(清理更彻底)。三、安装 JDK 1.8(规范目录 + 高效操作)Linux 软件建议统一安装在 /usr/local 目录,便于管理:# 1. 将下载的压缩包移动到 /usr/local(替换为实际文件路径)sudo mv ~/jdk-8u421-linux-x64.tar.gz /usr/local/ # 2. 进入安装目录cd /usr/local # 3. 解压压缩包(xf 组合:x=解压,f=指定文件,无需加 v 减少冗余输出)sudo tar xf jdk-8u421-linux-x64.tar.gz # 4. 删除压缩包(节省磁盘空间,可选)sudo rm -f jdk-8u421-linux-x64.tar.gz # 5. 重命名目录(可选,简化后续配置,避免版本号过长)sudo mv jdk1.8.0_421 jdk1.8一键获取完整项目代码bash注意:解压后目录名默认是 jdk1.8.0_421(与压缩包版本对应),重命名为 jdk1.8 可避免后续环境变量因版本号变更出错。四、配置全局环境变量(永久生效)环境变量写入 /etc/profile 可对所有用户生效,避免仅当前用户可用:# 1. 编辑 profile 文件(新手推荐用 nano,比 vi 更易操作)sudo vi /etc/profile# 或 sudo nano /etc/profile # 2. 在文件末尾添加以下配置(复制粘贴即可,注意 JAVA_HOME 路径)export JAVA_HOME=/usr/local/jdk1.8export JRE_HOME=${JAVA_HOME}/jreexport CLASSPATH=.:${JAVA_HOME}/lib/dt.jar:${JAVA_HOME}/lib/tools.jar:${JRE_HOME}/libexport PATH=${JAVA_HOME}/bin:${JRE_HOME}/bin:$PATH一键获取完整项目代码bash配置解析:JAVA_HOME:JDK 根目录(核心配置,必须与实际安装路径一致);CLASSPATH:补充 dt.jar 和 tools.jar,解决部分项目类加载失败问题;PATH:将 JDK/bin 加入系统路径,使 java/javac 命令全局可用。# 3. 刷新配置文件(立即生效,无需重启服务器)source /etc/profile # 4. 验证环境变量(可选,确认配置无误)echo $JAVA_HOME# 输出 /usr/local/jdk1.8 则说明配置正确一键获取完整项目代码bash五、验证安装结果(一步确认)执行以下命令,若输出如下版本信息,说明安装成功:java -version一键获取完整项目代码bash成功输出示例:java version "1.8.0_421"Java(TM) SE Runtime Environment (build 1.8.0_421-b09)Java HotSpot(TM) 64-Bit Server VM (build 25.421-b09, mixed mode)一键获取完整项目代码bash六、常见问题速解(99% 的人会踩的坑)问题现象 解决方法执行 java -version 提示 “命令未找到” 1. 检查 JAVA_HOME 路径是否与实际解压目录一致;2. 重新执行 source /etc/profile,或退出当前终端重新登录;3. 确认命令以 root/sudo 权限执行。解压后无 jre 目录 部分 JDK 1.8 版本需手动生成 JRE:cd $JAVA_HOME && ./bin/jlink --module-path jmods --add-modules java.desktop --output jre环境变量配置后仅 root 用户可用 避免将配置写入 ~/.bash_profile,必须写入 /etc/profile;执行 chmod 644 /etc/profile 确保普通用户可读取。解压时报 “权限不足” 执行 sudo chmod 755 jdk-8u421-linux-x64.tar.gz 赋予文件执行权限。七、进阶优化(生产环境推荐)配置软链接:避免后续升级 JDK 需修改环境变量sudo ln -s /usr/local/jdk1.8 /usr/local/java# 环境变量中 JAVA_HOME 改为 /usr/local/java 即可一键获取完整项目代码bash检查依赖库:部分系统缺少 libc 库导致 JDK 启动失败sudo yum install -y glibc.i686 glibc.x86_64一键获取完整项目代码bash总结JDK 1.8 安装核心是 “清理旧版本 + 路径准确 + 全局配置”,按本教程操作可避开 90% 的常见问题。安装完成后,可直接部署 Tomcat、Spring Boot 等 Java 项目,适配绝大多数企业级应用场景。————————————————原文链接:https://blog.csdn.net/m0_73483832/article/details/156015201
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签