-
在JAVA程序中,性能问题的大部分原因并不在于JAVA语言,而是程序本身。养成良好的编码习惯非常重要,能够显著地提升程序性能。1. 尽量在合适的场合使用单例使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面:第一,控制资源的使用,通过线程同步来控制资源的并发访问;第二,控制实例的产生,以达到节约资源的目的;第三,控制数据共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信。2. 尽量避免随意使用静态变量当某个对象被定义为static变量所引用,那么GC通常是不会回收这个对象所占有的内存,如public class A{private static B b = new B();}此时静态变量b的生命周期与A类同步,如果A类不会卸载,那么b对象会常驻内存,直到程序终止。3. 尽量避免过多过常地创建Java对象尽量避免在经常调用的方法,循环中new对象,由于系统不仅要花费时间来创建对象,而且还要花时间对这些对象进行垃圾回收和处理,在我们可以控制的范围内,最大限度地重用对象,最好能用基本的数据类型或数组来替代对象。4. 尽量使用final修饰符带有final修饰符的类是不可派生的。在JAVA核心API中,有许多应用final的例子,例如java、lang、String,为String类指定final防止了使用者覆盖length()方法。另外,如果一个类是final的,则该类所有方法都是final的。java编译器会寻找机会内联(inline)所有的final方法(这和具体的编译器实现有关),此举能够使性能平均提高50%。如:让访问实例内变量的getter/setter方法变成”final:简单的getter/setter方法应该被置成final,这会告诉编译器,这个方法不会被重载,所以,可以变成”inlined”,例子:class MAF {public void setSize (int size) {_size = size;}private int _size;}更正:class DAF_fixed {final public void setSize (int size) {_size = size;}private int _size;}5. 尽量使用局部变量调用方法时传递的参数以及在调用中创建的临时变量都保存在栈(Stack)中,速度较快;其他变量,如静态变量、实例变量等,都在堆(Heap)中创建,速度较慢。6. 尽量处理好包装类型和基本类型两者的使用场所虽然包装类型和基本类型在使用过程中是可以相互转换,但它们两者所产生的内存区域是完全不同的,基本类型数据产生和处理都在栈中处理,包装类型是对象,是在堆中产生实例。在集合类对象,有对象方面需要的处理适用包装类型,其他的处理提倡使用基本类型。7. 慎用synchronized,尽量减小synchronize的方法都知道,实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。synchronize方法被调用时,直接会把当前对象锁了,在方法执行完之前其他线程无法调用当前对象的其他方法。所以,synchronize的方法尽量减小,并且应尽量使用方法同步代替代码块同步。9. 尽量不要使用finalize方法实际上,将资源清理放在finalize方法中完成是非常不好的选择,由于GC的工作量很大,尤其是回收Young代内存时,大都会引起应用程序暂停,所以再选择使用finalize方法进行资源清理,会导致GC负担更大,程序运行效率更差。10. 尽量使用基本数据类型代替对象String str = "hello";上面这种方式会创建一个“hello”字符串,而且JVM的字符缓存池还会缓存这个字符串;String str = new String("hello");此时程序除创建字符串外,str所引用的String对象底层还包含一个char[]数组,这个char[]数组依次存放了h,e,l,l,o11. 多线程在未发生线程安全前提下应尽量使用HashMap、ArrayListHashTable、Vector等使用了同步机制,降低了性能。12. 尽量合理的创建HashMap当你要创建一个比较大的hashMap时,充分利用这个构造函数public HashMap(int initialCapacity, float loadFactor);避免HashMap多次进行了hash重构,扩容是一件很耗费性能的事,在默认中initialCapacity只有16,而loadFactor是 0.75,需要多大的容量,你最好能准确的估计你所需要的最佳大小,同样的Hashtable,Vectors也是一样的道理。13. 尽量减少对变量的重复计算如:for(int i=0;i<list.size();i++)应该改为:for(int i=0,len=list.size();i<len;i++)并且在循环中应该避免使用复杂的表达式,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快。14. 尽量避免不必要的创建如:A a = new A();if(i==1){list.add(a);}应该改为:if(i==1){A a = new A();list.add(a);}15. 尽量在finally块中释放资源程序中使用到的资源应当被释放,以避免资源泄漏,这最好在finally块中去做。不管程序执行的结果如何,finally块总是会执行的,以确保资源的正确关闭。16. 尽量使用移位来代替’a/b’的操作“/”是一个代价很高的操作,使用移位的操作将会更快和更有效如:int num = a / 4;int num = a / 8;应该改为:int num = a >> 2;int num = a >> 3;但注意的是使用移位应添加注释,因为移位操作不直观,比较难理解。17.尽量使用移位来代替’a*b’的操作同样的,对于’*’操作,使用移位的操作将会更快和更有效如:int num = a * 4;int num = a * 8;应该改为:int num = a << 2;int num = a << 3;18. 尽量确定StringBuffer的容量StringBuffer 的构造器会创建一个默认大小(通常是16)的字符数组。在使用中,如果超出这个大小,就会重新分配内存,创建一个更大的数组,并将原先的数组复制过来,再丢弃旧的数组。在大多数情况下,你可以在创建 StringBuffer的时候指定大小,这样就避免了在容量不够的时候自动增长,以提高性能。如:StringBuffer buffer = new StringBuffer(1000);19. 尽量早释放无用对象的引用大部分时,方法局部引用变量所引用的对象会随着方法结束而变成垃圾,因此,大部分时候程序无需将局部,引用变量显式设为null。例如:Public void test(){Object obj = new Object();……Obj=null;}上面这个就没必要了,随着方法test()的执行完成,程序中obj引用变量的作用域就结束了。但是如果是改成下面:Public void test(){Object obj = new Object();……Obj=null;//执行耗时,耗内存操作;或调用耗时,耗内存的方法……}这时候就有必要将obj赋值为null,可以尽早的释放对Object对象的引用。20. 尽量避免使用二维数组二维数据占用的内存空间比一维数组多得多,大概10倍以上。21. 尽量避免使用split除非是必须的,否则应该避免使用split,split由于支持正则表达式,所以效率比较低,如果是频繁的几十,几百万的调用将会耗费大量资源,如果确实需要频繁的调用split,可以考虑使用apache的StringUtils.split(string,char),频繁split的可以缓存结果。22. ArrayList & LinkedList一个是线性表,一个是链表,一句话,随机查询尽量使用ArrayList,ArrayList优于LinkedList,LinkedList还要移动指针,添加删除的操作LinkedList优于ArrayList,ArrayList还要移动数据,不过这是理论性分析,事实未必如此,重要的是理解好2者得数据结构,对症下药。23. 尽量使用System.arraycopy ()代替通过来循环复制数组System.arraycopy() 要比通过循环来复制数组快的多。24. 尽量缓存经常使用的对象尽可能将经常使用的对象进行缓存,可以使用数组,或HashMap的容器来进行缓存,但这种方式可能导致系统占用过多的缓存,性能下降,推荐可以使用一些第三方的开源工具,如EhCache,Oscache进行缓存,他们基本都实现了FIFO/FLU等缓存算法。25. 尽量避免非常大的内存分配有时候问题不是由当时的堆状态造成的,而是因为分配失败造成的。分配的内存块都必须是连续的,而随着堆越来越满,找到较大的连续块越来越困难。26. 慎用异常当创建一个异常时,需要收集一个栈跟踪(stack track),这个栈跟踪用于描述异常是在何处创建的。构建这些栈跟踪时需要为运行时栈做一份快照,正是这一部分开销很大。当需要创建一个 Exception 时,JVM 不得不说:先别动,我想就您现在的样子存一份快照,所以暂时停止入栈和出栈操作。栈跟踪不只包含运行时栈中的一两个元素,而是包含这个栈中的每一个元素。如果您创建一个 Exception ,就得付出代价,好在捕获异常开销不大,因此可以使用 try-catch 将核心内容包起来。从技术上讲,你甚至可以随意地抛出异常,而不用花费很大的代价。招致性能损失的并不是 throw 操作——尽管在没有预先创建异常的情况下就抛出异常是有点不寻常。真正要花代价的是创建异常,幸运的是,好的编程习惯已教会我们,不应该不管三七二十一就抛出异常。异常是为异常的情况而设计的,使用时也应该牢记这一原则。27. 尽量重用对象特别是String对象的使用中,出现字符串连接情况时应使用StringBuffer代替,由于系统不仅要花时间生成对象,以后可能还需要花时间对这些对象进行垃圾回收和处理。因此生成过多的对象将会给程序的性能带来很大的影响。28. 不要重复初始化变量默认情况下,调用类的构造函数时,java会把变量初始化成确定的值,所有的对象被设置成null,整数变量设置成0,float和double变量设置成0.0,逻辑值设置成false。当一个类从另一个类派生时,这一点尤其应该注意,因为用new关键字创建一个对象时,构造函数链中的所有构造函数都会被自动调用。这里有个注意,给成员变量设置初始值但需要调用其他方法的时候,最好放在一个方法。比如initXXX()中,因为直接调用某方法赋值可能会因为类尚未初始化而抛空指针异常,如:public int state = this.getState()。29. 在java+Oracle的应用系统开发中,java中内嵌的SQL语言应尽量使用大写形式,以减少Oracle解析器的解析负担。30. 在java编程过程中,进行数据库连接,I/O流操作,在使用完毕后,及时关闭以释放资源。因为对这些大对象的操作会造成系统大的开销。31. 过分的创建对象会消耗系统的大量内存,严重时,会导致内存泄漏,因此,保证过期的对象的及时回收具有重要意义。JVM的GC并非十分智能,因此建议在对象使用完毕后,手动设置成null。32. 在使用同步机制时,应尽量使用方法同步代替代码块同步。33. 不要在循环中使用Try/Catch语句,应把Try/Catch放在循环最外层Error是获取系统错误的类,或者说是虚拟机错误的类。不是所有的错误Exception都能获取到的,虚拟机报错Exception就获取不到,必须用Error获取。34. 通过StringBuffer的构造函数来设定它的初始化容量,可以明显提升性能StringBuffer的默认容量为16,当StringBuffer的容量达到最大容量时,它会将自身容量增加到当前的2倍+2,也就是2*n+2。无论何时,只要StringBuffer到达它的最大容量,它就不得不创建一个新的对象数组,然后复制旧的对象数组,这会浪费很多时间。所以给StringBuffer设置一个合理的初始化容量值,是很有必要的!35. 合理使用java.util.VectorVector与StringBuffer类似,每次扩展容量时,所有现有元素都要赋值到新的存储空间中。Vector的默认存储能力为10个元素,扩容加倍。vector.add(index,obj) 这个方法可以将元素obj插入到index位置,但index以及之后的元素依次都要向下移动一个位置(将其索引加 1)。 除非必要,否则对性能不利。同样规则适用于remove(int index)方法,移除此向量中指定位置的元素。将所有后续元素左移(将其索引减 1)。返回此向量中移除的元素。所以删除vector最后一个元素要比删除第1个元素开销低很多。删除所有元素最好用removeAllElements()方法。如果要删除vector里的一个元素可以使用 vector.remove(obj);而不必自己检索元素位置,再删除,如int index = indexOf(obj);vector.remove(index)。38. 不用new关键字创建对象的实例用new关键词创建类的实例时,构造函数链中的所有构造函数都会被自动调用。但如果一个对象实现了Cloneable接口,我们可以调用它的clone()方法。clone()方法不会调用任何类构造函数。下面是Factory模式的一个典型实现:public static Credit getNewCredit(){return new Credit();}改进后的代码使用clone()方法:private static Credit BaseCredit = new Credit();public static Credit getNewCredit(){return (Credit)BaseCredit.clone();}39. 不要将数组声明为:public static final40. HaspMap的遍历:Map<String, String[]> paraMap = new HashMap<String, String[]>();for( Entry<String, String[]> entry : paraMap.entrySet() ){String appFieldDefId = entry.getKey();String[] values = entry.getValue();}利用散列值取出相应的Entry做比较得到结果,取得entry的值之后直接取key和value。41. array(数组)和ArrayList的使用array 数组效率最高,但容量固定,无法动态改变,ArrayList容量可以动态增长,但牺牲了效率。42. 单线程应尽量使用 HashMap, ArrayList,除非必要,否则不推荐使用HashTable,Vector,它们使用了同步机制,而降低了性能。43. StringBuffer,StringBuilder的区别在于:java.lang.StringBuffer 线程安全的可变字符序列。一个类似于String的字符串缓冲区,但不能修改。StringBuilder与该类相比,通常应该优先使用StringBuilder类,因为它支持所有相同的操作,但由于它不执行同步,所以速度更快。为了获得更好的性能,在构造StringBuffer或StringBuilder时应尽量指定她的容量。当然如果不超过16个字符时就不用了。 相同情况下,使用StringBuilder比使用StringBuffer仅能获得10%~15%的性能提升,但却要冒多线程不安全的风险。综合考虑还是建议使用StringBuffer。44. 尽量使用基本数据类型代替对象。45. 使用具体类比使用接口效率高,但结构弹性降低了,但现代IDE都可以解决这个问题。46. 考虑使用静态方法,如果你没有必要去访问对象的外部,那么就使你的方法成为静态方法。它会被更快地调用,因为它不需要一个虚拟函数导向表。这同时也是一个很好的实践,因为它告诉你如何区分方法的性质,调用这个方法不会改变对象的状态。47. 应尽可能避免使用内在的GET,SET方法。48.避免枚举,浮点数的使用。以下举几个实用优化的例子:一、避免在循环条件中使用复杂表达式在不做编译优化的情况下,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快。例子:import java.util.Vector;class CEL {void method (Vector vector) {for (int i = 0; i < vector.size (); i++) // Violation; // ...}}更正:class CEL_fixed {void method (Vector vector) {int size = vector.size ()for (int i = 0; i < size; i++); // ...}}二、为’Vectors’ 和 ‘Hashtables’定义初始大小JVM为Vector扩充大小的时候需要重新创建一个更大的数组,将原原先数组中的内容复制过来,最后,原先的数组再被回收。可见Vector容量的扩大是一个颇费时间的事。通常,默认的10个元素大小是不够的。你最好能准确的估计你所需要的最佳大小。例子:import java.util.Vector;public class DIC {public void addObjects (Object[] o) {// if length > 10, Vector needs to expandfor (int i = 0; i< o.length;i++) {v.add(o); // capacity before it can add more elements.}}public Vector v = new Vector(); // no initialCapacity.}更正:自己设定初始大小。public Vector v = new Vector(20);public Hashtable hash = new Hashtable(10);三、在finally块中关闭Stream程序中使用到的资源应当被释放,以避免资源泄漏。这最好在finally块中去做。不管程序执行的结果如何,finally块总是会执行的,以确保资源的正确关闭。四、使用’System.arraycopy ()’代替通过来循环复制数组例子:public class IRB{void method () {int[] array1 = new int [100];for (int i = 0; i < array1.length; i++) {array1 [i] = i;}int[] array2 = new int [100];for (int i = 0; i < array2.length; i++) {array2 [i] = array1 [i]; // Violation}}}更正:public class IRB{void method () {int[] array1 = new int [100];for (int i = 0; i < array1.length; i++) {array1 [i] = i;}int[] array2 = new int [100];System.arraycopy(array1, 0, array2, 0, 100);}}五、让访问实例内变量的getter/setter方法变成”final”简单的getter/setter方法应该被置成final,这会告诉编译器,这个方法不会被重载,所以,可以变成”inlined”,例子:class MAF {public void setSize (int size) {_size = size;}private int _size;}更正:class DAF_fixed {final public void setSize (int size) {_size = size;}private int _size;}六、对于常量字符串,用’String’ 代替 ‘StringBuffer’常量字符串并不需要动态改变长度。例子:public class USC {String method () {StringBuffer s = new StringBuffer ("Hello");String t = s + "World!";return t;}}更正:把StringBuffer换成String,如果确定这个String不会再变的话,这将会减少运行开销提高性能。七、在字符串相加的时候,使用 ’ ’ 代替 ” “,如果该字符串只有一个字符的话例子:public class STR {public void method(String s) {String string = s + "d" // violation.string = "abc" + "d" // violation.}}更正:将一个字符的字符串替换成’ ‘public class STR {public void method(String s) {String string = s + 'd'string = "abc" + 'd'}}以上仅是Java方面编程时的性能优化,性能优化大部分都是在时间、效率、代码结构层次等方面的权衡,各有利弊,不要把上面内容当成教条,或许有些对我们实际工作适用,有些不适用,还望根据实际工作场景进行取舍,活学活用,变通为宜。———————————————— 原文链接:https://blog.csdn.net/qq_42894896/article/details/82256770
-
如何优化 Java 应用的性能随着应用程序复杂度的不断增加,性能优化已成为每位开发者必备的技能之一。尤其是对于 Java 应用程序,随着数据量和并发用户的增加,如何保证系统的高效运行,减少延迟和资源消耗,是确保系统可持续性和用户体验的关键。本文将探讨几个常见的 Java 性能优化方法,涵盖代码优化、JVM 调优和数据库性能优化等方面。一、代码优化优化代码是提升 Java 应用性能的首要步骤。良好的代码设计不仅提高了应用的响应速度,还可以减少内存占用和计算资源的浪费。1. 避免不必要的对象创建Java 中频繁创建对象,尤其是小的临时对象,可能会增加垃圾回收的压力,导致性能下降。为了提高性能,可以考虑:复用对象:在合适的地方,尽量避免在循环中频繁创建新对象。使用对象池:对于高频创建的对象,可以考虑使用对象池(如 Apache Commons Pool 或自定义对象池)来复用对象。2. 选择合适的数据结构不同的数据结构在执行特定操作时有不同的性能表现。例如:对于查找操作,HashMap 和 HashSet 的性能优于 ArrayList 和 LinkedList,尤其是在数据量大的时候。如果不需要频繁插入删除元素,使用 ArrayList 比 LinkedList 更高效。根据操作类型选择适当的数据结构,可以显著提升应用性能。3. 减少同步开销在多线程环境中,过度使用 synchronized 关键字会导致线程阻塞,降低系统的吞吐量。可以考虑以下优化:锁粒度优化:减少同步范围,避免不必要的锁竞争。使用更高效的并发工具:Java 5 引入了 java.util.concurrent 包,提供了如 ReentrantLock、CountDownLatch、Semaphore 等高效的并发工具,可以替代传统的 synchronized 锁。4. 优化字符串操作Java 中的字符串操作是性能优化的热点之一,因为每次对字符串进行修改时,都会生成新的对象。为了优化字符串的使用,可以:使用 StringBuilder 或 StringBuffer 来代替字符串拼接,尤其是在循环中。对于多次拼接的场景,使用 StringJoiner 或 Collectors.joining() 来提高效率。二、JVM 调优JVM 是 Java 程序的运行时环境,合理配置 JVM 参数可以显著提升应用性能。常见的 JVM 调优措施包括内存管理和垃圾回收调优。1. 内存设置JVM 的堆内存设置对 Java 应用的性能至关重要。可以通过调整堆内存的大小来优化性能:调整堆内存大小:使用 -Xms 和 -Xmx 参数设置初始堆大小和最大堆大小,确保 JVM 堆大小足够大,以减少频繁的垃圾回收。堆内存分代:JVM 堆内存分为年轻代(Young Generation)、老年代(Old Generation)和持久代(Permanent Generation 或 MetaSpace)。合理设置这些区域的大小可以优化垃圾回收过程,减少频繁的 Full GC。-Xms2g -Xmx4g2. 垃圾回收调优Java 的垃圾回收(GC)是自动的,但也可以通过调整垃圾回收器和相关参数来提升性能:选择合适的垃圾回收器:不同的 GC 算法适用于不同的应用场景。常见的 GC 算法包括:Serial GC:适用于小型单线程应用。Parallel GC:适用于多核机器,能够并行回收年轻代。CMS(Concurrent Mark-Sweep)GC:适用于低延迟应用,能够减少停顿时间。G1 GC:适用于大内存应用,提供更好的延迟控制和吞吐量。-XX:+UseG1GC3. JVM 参数监控可以使用 jconsole 或 jvisualvm 等工具来监控应用的内存使用情况、垃圾回收日志、线程状态等,从而找出瓶颈并进行优化。三、数据库性能优化Java 应用与数据库之间的交互通常是性能瓶颈的一个重要来源。优化数据库性能可以大幅提高应用的响应速度和吞吐量。1. 优化 SQL 查询避免 N+1 查询:N+1 查询问题是指在查询一个实体的同时,又对其相关的实体执行多次查询。使用 JOIN 或 EAGER 加载可以避免多次查询。索引优化:创建合理的索引可以显著提高查询性能,特别是对于频繁查询的字段(如主键、外键)。查询优化:通过 EXPLAIN 语句分析 SQL 查询计划,识别并优化性能差的查询。2. 连接池的使用数据库连接池可以减少创建数据库连接的开销,提升数据库访问的效率。常用的连接池有:HikariCP:一个高性能的 JDBC 连接池,广泛应用于高并发的 Java 应用中。C3P0 和 DBCP:也是常见的数据库连接池。HikariDataSource dataSource = new HikariDataSource();dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");dataSource.setUsername("user");dataSource.setPassword("password");3. 缓存机制使用缓存可以显著减少对数据库的访问频率,减少延迟。常见的缓存方案包括:本地缓存:如 Guava 缓存或 Caffeine,适用于单机应用。分布式缓存:如 Redis 或 Memcached,适用于分布式应用,可以缓存数据库查询结果或计算密集型数据。四、异步和并发优化在高并发场景下,使用异步处理可以显著提高应用的吞吐量和响应速度。Java 提供了多种并发工具来处理异步任务。1. 使用线程池Java 提供了 ExecutorService 来管理线程池,合理配置线程池参数,避免过多的线程上下文切换和线程泄漏。ExecutorService executor = Executors.newFixedThreadPool(10);executor.submit(() -> { // 执行任务});2. 使用 CompletableFutureCompletableFuture 是 Java 8 引入的异步编程工具,支持非阻塞式的任务执行和组合,适用于多个异步操作的执行。CompletableFuture.supplyAsync(() -> "Hello") .thenApplyAsync(result -> result + " World") .thenAccept(System.out::println);五、总结Java 应用的性能优化涉及多个层面,包括代码优化、JVM 调优、数据库优化和并发优化等。通过细致地分析应用中的瓶颈,并采取针对性的优化措施,能够大大提升系统的响应速度和处理能力。如果你正在开发高性能的 Java 应用,建议定期使用性能分析工具进行监控,及时发现并解决性能问题。同时,不要忽视基础设施层面的优化,如数据库索引、缓存使用和多线程/异步编程等。通过上述优化方法,你可以有效提升 Java 应用的性能,让你的系统更加高效、稳定,并能够应对更高的负载。———————————————— 原文链接:https://blog.csdn.net/m0_54187478/article/details/144084814
-
接口性能优化对于从事后端开发的同学来说,肯定再熟悉不过了,因为它是一个跟开发语言无关的公共问题。该问题说简单也简单,说复杂也复杂。有时候,只需加个索引就能解决问题。有时候,需要做代码重构。有时候,需要增加缓存。有时候,需要引入一些中间件,比如mq。有时候,需要需要分库分表。有时候,需要拆分服务。等等。。。导致接口性能问题的原因千奇百怪,不同的项目不同的接口,原因可能也不一样。本文我总结了一些行之有效的,优化接口性能的办法,给有需要的朋友一个参考。1.索引接口性能优化大家第一个想到的可能是:优化索引。没错,优化索引的成本是最小的。你通过查看线上日志或者监控报告,查到某个接口用到的某条sql语句耗时比较长。这时你可能会有下面这些疑问:该sql语句加索引了没?加的索引生效了没?mysql选错索引了没?1.1 没加索引sql语句中where条件的关键字段,或者order by后面的排序字段,忘了加索引,这个问题在项目中很常见。项目刚开始的时候,由于表中的数据量小,加不加索引sql查询性能差别不大。后来,随着业务的发展,表中数据量越来越多,就不得不加索引了。可以通过命令:show index from `order`;能单独查看某张表的索引情况。也可以通过命令:show create table `order`;查看整张表的建表语句,里面同样会显示索引情况。通过ALTER TABLE命令可以添加索引:ALTER TABLE `order` ADD INDEX idx_name (name);也可以通过CREATE INDEX命令添加索引:CREATE INDEX idx_name ON `order` (name);不过这里有一个需要注意的地方是:想通过命令修改索引,是不行的。目前在mysql中如果想要修改索引,只能先删除索引,再重新添加新的。删除索引可以用DROP INDEX命令:ALTER TABLE `order` DROP INDEX idx_name;用DROP INDEX命令也行:DROP INDEX idx_name ON `order`;1.2 索引没生效通过上面的命令我们已经能够确认索引是有的,但它生效了没?此时你内心或许会冒出这样一个疑问。那么,如何查看索引有没有生效呢?答:可以使用explain命令,查看mysql的执行计划,它会显示索引的使用情况。例如:explain select * from `order` where code='002';结果:通过这几列可以判断索引使用情况,执行计划包含列的含义如下图所示: 说实话,sql语句没有走索引,排除没有建索引之外,最大的可能性是索引失效了。下面说说索引失效的常见原因:如果不是上面的这些原因,则需要再进一步排查一下其他原因。 1.3 选错索引此外,你有没有遇到过这样一种情况:明明是同一条sql,只有入参不同而已。有的时候走的索引a,有的时候却走的索引b?没错,有时候mysql会选错索引。必要时可以使用force index来强制查询sql走某个索引。至于为什么mysql会选错索引,后面有专门的文章介绍的,这里先留点悬念。2. sql优化如果优化了索引之后,也没啥效果。接下来试着优化一下sql语句,因为它的改造成本相对于java代码来说也要小得多。下面给大家列举了sql优化的15个小技巧:由于这些技巧在我之前的文章中已经详细介绍过了,在这里我就不深入了。 3. 远程调用很多时候,我们需要在某个接口中,调用其他服务的接口。比如有这样的业务场景:在用户信息查询接口中需要返回:用户名称、性别、等级、头像、积分、成长值等信息。而用户名称、性别、等级、头像在用户服务中,积分在积分服务中,成长值在成长值服务中。为了汇总这些数据统一返回,需要另外提供一个对外接口服务。于是,用户信息查询接口需要调用用户查询接口、积分查询接口 和 成长值查询接口,然后汇总数据统一返回。调用过程如下图所示:调用远程接口总耗时 530ms = 200ms + 150ms + 180ms 显然这种串行调用远程接口性能是非常不好的,调用远程接口总的耗时为所有的远程接口耗时之和。那么如何优化远程接口性能呢?3.1 并行调用上面说到,既然串行调用多个远程接口性能很差,为什么不改成并行呢?如下图所示:调用远程接口总耗时 200ms = 200ms(即耗时最长的那次远程接口调用) 在java8之前可以通过实现Callable接口,获取线程返回结果。java8以后通过CompleteFuture类实现该功能。我们这里以CompleteFuture为例:public UserInfo getUserInfo(Long id) throws InterruptedException, ExecutionException { final UserInfo userInfo = new UserInfo(); CompletableFuture userFuture = CompletableFuture.supplyAsync(() -> { getRemoteUserAndFill(id, userInfo); return Boolean.TRUE; }, executor); CompletableFuture bonusFuture = CompletableFuture.supplyAsync(() -> { getRemoteBonusAndFill(id, userInfo); return Boolean.TRUE; }, executor); CompletableFuture growthFuture = CompletableFuture.supplyAsync(() -> { getRemoteGrowthAndFill(id, userInfo); return Boolean.TRUE; }, executor); CompletableFuture.allOf(userFuture, bonusFuture, growthFuture).join(); userFuture.get(); bonusFuture.get(); growthFuture.get(); return userInfo;}温馨提醒一下,这两种方式别忘了使用线程池。示例中我用到了executor,表示自定义的线程池,为了防止高并发场景下,出现线程过多的问题。3.2 数据异构上面说到的用户信息查询接口需要调用用户查询接口、积分查询接口 和 成长值查询接口,然后汇总数据统一返回。那么,我们能不能把数据冗余一下,把用户信息、积分和成长值的数据统一存储到一个地方,比如:redis,存的数据结构就是用户信息查询接口所需要的内容。然后通过用户id,直接从redis中查询数据出来,不就OK了?如果在高并发的场景下,为了提升接口性能,远程接口调用大概率会被去掉,而改成保存冗余数据的数据异构方案。 但需要注意的是,如果使用了数据异构方案,就可能会出现数据一致性问题。用户信息、积分和成长值有更新的话,大部分情况下,会先更新到数据库,然后同步到redis。但这种跨库的操作,可能会导致两边数据不一致的情况产生。4. 重复调用重复调用在我们的日常工作代码中可以说随处可见,但如果没有控制好,会非常影响接口的性能。不信,我们一起看看。4.1 循环查数据库有时候,我们需要从指定的用户集合中,查询出有哪些是在数据库中已经存在的。实现代码可以这样写:public List<User> queryUser(List<User> searchList) { if (CollectionUtils.isEmpty(searchList)) { return Collections.emptyList(); } List<User> result = Lists.newArrayList(); searchList.forEach(user -> result.add(userMapper.getUserById(user.getId()))); return result;}这里如果有50个用户,则需要循环50次,去查询数据库。我们都知道,每查询一次数据库,就是一次远程调用。如果查询50次数据库,就有50次远程调用,这是非常耗时的操作。那么,我们如何优化呢?具体代码如下:public List<User> queryUser(List<User> searchList) { if (CollectionUtils.isEmpty(searchList)) { return Collections.emptyList(); } List<Long> ids = searchList.stream().map(User::getId).collect(Collectors.toList()); return userMapper.getUserByIds(ids);}提供一个根据用户id集合批量查询用户的接口,只远程调用一次,就能查询出所有的数据。这里有个需要注意的地方是:id集合的大小要做限制,最好一次不要请求太多的数据。要根据实际情况而定,建议控制每次请求的记录条数在500以内。4.2 死循环有些小伙伴看到这个标题,可能会感到有点意外,死循环也算?代码中不是应该避免死循环吗?为啥还是会产生死循环?有时候死循环是我们自己写的,例如下面这段代码:while(true) { if(condition) { break; } System.out.println("do samething");}这里使用了while(true)的循环调用,这种写法在CAS自旋锁中使用比较多。当满足condition等于true的时候,则自动退出该循环。如果condition条件非常复杂,一旦出现判断不正确,或者少写了一些逻辑判断,就可能在某些场景下出现死循环的问题。出现死循环,大概率是开发人员人为的bug导致的,不过这种情况很容易被测出来。还有一种隐藏的比较深的死循环,是由于代码写的不太严谨导致的。如果用正常数据,可能测不出问题,但一旦出现异常数据,就会立即出现死循环。4.3 无限递归如果想要打印某个分类的所有父分类,可以用类似这样的递归方法实现:public void printCategory(Category category) { if(category == null || category.getParentId() == null) { return; } System.out.println("父分类名称:"+ category.getName()); Category parent = categoryMapper.getCategoryById(category.getParentId()); printCategory(parent);}正常情况下,这段代码是没有问题的。但如果某次有人误操作,把某个分类的parentId指向了它自己,这样就会出现无限递归的情况。导致接口一直不能返回数据,最终会发生堆栈溢出。建议写递归方法时,设定一个递归的深度,比如:分类最大等级有4级,则深度可以设置为4。然后在递归方法中做判断,如果深度大于4时,则自动返回,这样就能避免无限循环的情况。5. 异步处理有时候,我们接口性能优化,需要重新梳理一下业务逻辑,看看是否有设计上不太合理的地方。比如有个用户请求接口中,需要做业务操作,发站内通知,和记录操作日志。为了实现起来比较方便,通常我们会将这些逻辑放在接口中同步执行,势必会对接口性能造成一定的影响。接口内部流程图如下:这个接口表面上看起来没有问题,但如果你仔细梳理一下业务逻辑,会发现只有业务操作才是核心逻辑,其他的功能都是非核心逻辑。 在这里有个原则就是:核心逻辑可以同步执行,同步写库。非核心逻辑,可以异步执行,异步写库。上面这个例子中,发站内通知和用户操作日志功能,对实时性要求不高,即使晚点写库,用户无非是晚点收到站内通知,或者运营晚点看到用户操作日志,对业务影响不大,所以完全可以异步处理。通常异步主要有两种:多线程 和 mq。5.1 线程池使用线程池改造之后,接口逻辑如下:发站内通知和用户操作日志功能,被提交到了两个单独的线程池中。 这样接口中重点关注的是业务操作,把其他的逻辑交给线程异步执行,这样改造之后,让接口性能瞬间提升了。但使用线程池有个小问题就是:如果服务器重启了,或者是需要被执行的功能出现异常了,无法重试,会丢数据。那么这个问题该怎么办呢?5.2 mq使用mq改造之后,接口逻辑如下:对于发站内通知和用户操作日志功能,在接口中并没真正实现,它只发送了mq消息到mq服务器。然后由mq消费者消费消息时,才真正的执行这两个功能。 这样改造之后,接口性能同样提升了,因为发送mq消息速度是很快的,我们只需关注业务操作的代码即可。6. 避免大事务很多小伙伴在使用spring框架开发项目时,为了方便,喜欢使用@Transactional注解提供事务功能。没错,使用@Transactional注解这种声明式事务的方式提供事务功能,确实能少写很多代码,提升开发效率。但也容易造成大事务,引发其他的问题。下面用一张图看看大事务引发的问题。从图中能够看出,大事务问题可能会造成接口超时,对接口的性能有直接的影响。 我们该如何优化大事务呢?少用@Transactional注解将查询(select)方法放到事务外事务中避免远程调用事务中避免一次性处理太多数据有些功能可以非事务执行有些功能可以异步处理关于大事务问题我的另一篇文章《让人头痛的大事务问题到底要如何解决?》,它里面做了非常详细的介绍,如果大家感兴趣可以看看。7. 锁粒度在某些业务场景中,为了防止多个线程并发修改某个共享数据,造成数据异常。为了解决并发场景下,多个线程同时修改数据,造成数据不一致的情况。通常情况下,我们会:加锁。但如果锁加得不好,导致锁的粒度太粗,也会非常影响接口性能。7.1 synchronized在java中提供了synchronized关键字给我们的代码加锁。通常有两种写法:在方法上加锁 和 在代码块上加锁。先看看如何在方法上加锁:public synchronized doSave(String fileUrl) { mkdir(); uploadFile(fileUrl); sendMessage(fileUrl);}这里加锁的目的是为了防止并发的情况下,创建了相同的目录,第二次会创建失败,影响业务功能。但这种直接在方法上加锁,锁的粒度有点粗。因为doSave方法中的上传文件和发消息方法,是不需要加锁的。只有创建目录方法,才需要加锁。我们都知道文件上传操作是非常耗时的,如果将整个方法加锁,那么需要等到整个方法执行完之后才能释放锁。显然,这会导致该方法的性能很差,变得得不偿失。这时,我们可以改成在代码块上加锁了,具体代码如下:public void doSave(String path,String fileUrl) { synchronized(this) { if(!exists(path)) { mkdir(path); } } uploadFile(fileUrl); sendMessage(fileUrl);}这样改造之后,锁的粒度一下子变小了,只有并发创建目录功能才加了锁。而创建目录是一个非常快的操作,即使加锁对接口的性能影响也不大。最重要的是,其他的上传文件和发送消息功能,任然可以并发执行。当然,这种做在单机版的服务中,是没有问题的。但现在部署的生产环境,为了保证服务的稳定性,一般情况下,同一个服务会被部署在多个节点中。如果哪天挂了一个节点,其他的节点服务任然可用。多节点部署避免了因为某个节点挂了,导致服务不可用的情况。同时也能分摊整个系统的流量,避免系统压力过大。同时它也带来了新的问题:synchronized只能保证一个节点加锁是有效的,但如果有多个节点如何加锁呢?答:这就需要使用:分布式锁了。目前主流的分布式锁包括:redis分布式锁、zookeeper分布式锁 和 数据库分布式锁。由于zookeeper分布式锁的性能不太好,真实业务场景用的不多,这里先不讲。下面聊一下redis分布式锁。7.2 redis分布式锁在分布式系统中,由于redis分布式锁相对于更简单和高效,成为了分布式锁的首先,被我们用到了很多实际业务场景当中。使用redis分布式锁的伪代码如下:public void doSave(String path,String fileUrl) { try { String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime); if ("OK".equals(result)) { if(!exists(path)) { mkdir(path); uploadFile(fileUrl); sendMessage(fileUrl); } return true; } } finally{ unlock(lockKey,requestId); } return false;}跟之前使用synchronized关键字加锁时一样,这里锁的范围也太大了,换句话说就是锁的粒度太粗,这样会导致整个方法的执行效率很低。其实只有创建目录的时候,才需要加分布式锁,其余代码根本不用加锁。于是,我们需要优化一下代码:public void doSave(String path,String fileUrl) { if(this.tryLock()) { mkdir(path); } uploadFile(fileUrl); sendMessage(fileUrl);}private boolean tryLock() { try { String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime); if ("OK".equals(result)) { return true; } } finally{ unlock(lockKey,requestId); } return false;}上面代码将加锁的范围缩小了,只有创建目录时才加了锁。这样看似简单的优化之后,接口性能能提升很多。说不定,会有意外的惊喜喔。哈哈哈。redis分布式锁虽说好用,但它在使用时,有很多注意的细节,隐藏了很多坑,如果稍不注意很容易踩中。7.3 数据库分布式锁mysql数据库中主要有三种锁:表锁:加锁快,不会出现死锁。但锁定粒度大,发生锁冲突的概率最高,并发度最低。行锁:加锁慢,会出现死锁。但锁定粒度最小,发生锁冲突的概率最低,并发度也最高。间隙锁:开销和加锁时间界于表锁和行锁之间。它会出现死锁,锁定粒度界于表锁和行锁之间,并发度一般。并发度越高,意味着接口性能越好。所以数据库锁的优化方向是:优先使用行锁,其次使用间隙锁,再其次使用表锁。赶紧看看,你用对了没?8.分页处理有时候我会调用某个接口批量查询数据,比如:通过用户id批量查询出用户信息,然后给这些用户送积分。但如果你一次性查询的用户数量太多了,比如一次查询2000个用户的数据。参数中传入了2000个用户的id,远程调用接口,会发现该用户查询接口经常超时。调用代码如下:List<User> users = remoteCallUser(ids);众所周知,调用接口从数据库获取数据,是需要经过网络传输的。如果数据量太大,无论是获取数据的速度,还是网络传输受限于带宽,都会导致耗时时间比较长。那么,这种情况要如何优化呢?答:分页处理。将一次获取所有的数据的请求,改成分多次获取,每次只获取一部分用户的数据,最后进行合并和汇总。其实,处理这个问题,要分为两种场景:同步调用 和 异步调用。8.1 同步调用如果在job中需要获取2000个用户的信息,它要求只要能正确获取到数据就好,对获取数据的总耗时要求不太高。但对每一次远程接口调用的耗时有要求,不能大于500ms,不然会有邮件预警。这时,我们可以同步分页调用批量查询用户信息接口。具体示例代码如下:List<List<Long>> allIds = Lists.partition(ids,200);for(List<Long> batchIds:allIds) { List<User> users = remoteCallUser(batchIds);}代码中我用的google的guava工具中的Lists.partition方法,用它来做分页简直太好用了,不然要巴拉巴拉写一大堆分页的代码。8.2 异步调用如果是在某个接口中需要获取2000个用户的信息,它考虑的就需要更多一些。除了需要考虑远程调用接口的耗时之外,还需要考虑该接口本身的总耗时,也不能超时500ms。这时候用上面的同步分页请求远程接口,肯定是行不通的。那么,只能使用异步调用了。代码如下:List<List<Long>> allIds = Lists.partition(ids,200);final List<User> result = Lists.newArrayList();allIds.stream().forEach((batchIds) -> { CompletableFuture.supplyAsync(() -> { result.addAll(remoteCallUser(batchIds)); return Boolean.TRUE; }, executor);})使用CompletableFuture类,多个线程异步调用远程接口,最后汇总结果统一返回。9.加缓存解决接口性能问题,加缓存是一个非常高效的方法。但不能为了缓存而缓存,还是要看具体的业务场景。毕竟加了缓存,会导致接口的复杂度增加,它会带来数据不一致问题。在有些并发量比较低的场景中,比如用户下单,可以不用加缓存。还有些场景,比如在商城首页显示商品分类的地方,假设这里的分类是调用接口获取到的数据,但页面暂时没有做静态化。如果查询分类树的接口没有使用缓存,而直接从数据库查询数据,性能会非常差。那么如何使用缓存呢?9.1 redis缓存通常情况下,我们使用最多的缓存可能是:redis和memcached。但对于java应用来说,绝大多数都是使用的redis,所以接下来我们以redis为例。由于在关系型数据库,比如:mysql中,菜单是有上下级关系的。某个四级分类是某个三级分类的子分类,这个三级分类,又是某个二级分类的子分类,而这个二级分类,又是某个一级分类的子分类。这种存储结构决定了,想一次性查出这个分类树,并非是一件非常容易的事情。这就需要使用程序递归查询了,如果分类多的话,这个递归是比较耗时的。所以,如果每次都直接从数据库中查询分类树的数据,是一个非常耗时的操作。这时我们可以使用缓存,大部分情况,接口都直接从缓存中获取数据。操作redis可以使用成熟的框架,比如:jedis和redisson等。用jedis伪代码如下:String json = jedis.get(key);if(StringUtils.isNotEmpty(json)) { CategoryTree categoryTree = JsonUtil.toObject(json); return categoryTree;}return queryCategoryTreeFromDb();先从redis中根据某个key查询是否有菜单数据,如果有则转换成对象,直接返回。如果redis中没有查到菜单数据,则再从数据库中查询菜单数据,有则返回。此外,我们还需要有个job每隔一段时间,从数据库中查询菜单数据,更新到redis当中,这样以后每次都能直接从redis中获取菜单的数据,而无需访问数据库了。这样改造之后,能快速的提升性能。 但这样做性能提升不是最佳的,还有其他的方案,我们一起看看下面的内容。9.2 二级缓存上面的方案是基于redis缓存的,虽说redis访问速度很快。但毕竟是一个远程调用,而且菜单树的数据很多,在网络传输的过程中,是有些耗时的。有没有办法,不经过请求远程,就能直接获取到数据呢?答:使用二级缓存,即基于内存的缓存。除了自己手写的内存缓存之后,目前使用比较多的内存缓存框架有:guava、Ehcache、caffine等。我们在这里以caffeine为例,它是spring官方推荐的。第一步,引入caffeine的相关jar包<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId></dependency><dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>2.6.0</version></dependency>第二步,配置CacheManager,开启EnableCaching@Configuration@EnableCachingpublic class CacheConfig { @Bean public CacheManager cacheManager(){ CaffeineCacheManager cacheManager = new CaffeineCacheManager(); //Caffeine配置 Caffeine<Object, Object> caffeine = Caffeine.newBuilder() //最后一次写入后经过固定时间过期 .expireAfterWrite(10, TimeUnit.SECONDS) //缓存的最大条数 .maximumSize(1000); cacheManager.setCaffeine(caffeine); return cacheManager; }}第三步,使用Cacheable注解获取数据@Servicepublic class CategoryService { @Cacheable(value = "category", key = "#categoryKey") public CategoryModel getCategory(String categoryKey) { String json = jedis.get(categoryKey); if(StringUtils.isNotEmpty(json)) { CategoryTree categoryTree = JsonUtil.toObject(json); return categoryTree; } return queryCategoryTreeFromDb(); }}调用categoryService.getCategory()方法时,先从caffine缓存中获取数据,如果能够获取到数据,则直接返回该数据,不进入方法体。如果不能获取到数据,则再从redis中查一次数据。如果查询到了,则返回数据,并且放入caffine中。如果还是没有查到数据,则直接从数据库中获取到数据,然后放到caffine缓存中。具体流程图如下:该方案的性能更好,但有个缺点就是,如果数据更新了,不能及时刷新缓存。此外,如果有多台服务器节点,可能存在各个节点上数据不一样的情况。 由此可见,二级缓存给我们带来性能提升的同时,也带来了数据不一致的问题。使用二级缓存一定要结合实际的业务场景,并非所有的业务场景都适用。但上面我列举的分类场景,是适合使用二级缓存的。因为它属于用户不敏感数据,即使出现了稍微有点数据不一致也没有关系,用户有可能都没有察觉出来。10. 分库分表有时候,接口性能受限的不是别的,而是数据库。当系统发展到一定的阶段,用户并发量大,会有大量的数据库请求,需要占用大量的数据库连接,同时会带来磁盘IO的性能瓶颈问题。此外,随着用户数量越来越多,产生的数据也越来越多,一张表有可能存不下。由于数据量太大,sql语句查询数据时,即使走了索引也会非常耗时。这时该怎么办呢?答:需要做分库分表。如下图所示:图中将用户库拆分成了三个库,每个库都包含了四张用户表。 如果有用户请求过来的时候,先根据用户id路由到其中一个用户库,然后再定位到某张表。路由的算法挺多的:根据id取模,比如:id=7,有4张表,则7%4=3,模为3,路由到用户表3。给id指定一个区间范围,比如:id的值是0-10万,则数据存在用户表0,id的值是10-20万,则数据存在用户表1。一致性hash算法分库分表主要有两个方向:垂直和水平。说实话垂直方向(即业务方向)更简单。在水平方向(即数据方向)上,分库和分表的作用,其实是有区别的,不能混为一谈。分库:是为了解决数据库连接资源不足问题,和磁盘IO的性能瓶颈问题。分表:是为了解决单表数据量太大,sql语句查询数据时,即使走了索引也非常耗时问题。此外还可以解决消耗cpu资源问题。分库分表:可以解决 数据库连接资源不足、磁盘IO的性能瓶颈、检索数据耗时 和 消耗cpu资源等问题。如果在有些业务场景中,用户并发量很大,但是需要保存的数据量很少,这时可以只分库,不分表。如果在有些业务场景中,用户并发量不大,但是需要保存的数量很多,这时可以只分表,不分库。如果在有些业务场景中,用户并发量大,并且需要保存的数量也很多时,可以分库分表。11. 辅助功能优化接口性能问题,除了上面提到的这些常用方法之外,还需要配合使用一些辅助功能,因为它们真的可以帮我们提升查找问题的效率。11.1 开启慢查询日志通常情况下,为了定位sql的性能瓶颈,我们需要开启mysql的慢查询日志。把超过指定时间的sql语句,单独记录下来,方面以后分析和定位问题。开启慢查询日志需要重点关注三个参数:slow_query_log 慢查询开关slow_query_log_file 慢查询日志存放的路径long_query_time 超过多少秒才会记录日志通过mysql的set命令可以设置:set global slow_query_log='ON'; set global slow_query_log_file='/usr/local/mysql/data/slow.log';set global long_query_time=2;设置完之后,如果某条sql的执行时间超过了2秒,会被自动记录到slow.log文件中。当然也可以直接修改配置文件my.cnf[mysqld]slow_query_log = ONslow_query_log_file = /usr/local/mysql/data/slow.loglong_query_time = 2但这种方式需要重启mysql服务。很多公司每天早上都会发一封慢查询日志的邮件,开发人员根据这些信息优化sql。11.2 加监控为了出现sql问题时,能够让我们及时发现,我们需要对系统做监控。目前业界使用比较多的开源监控系统是:Prometheus。它提供了 监控 和 预警 的功能。架构图如下: 我们可以用它监控如下信息:接口响应时间调用第三方服务耗时慢查询sql耗时cpu使用情况内存使用情况磁盘使用情况数据库使用情况等等。。。它的界面大概长这样子:可以看到mysql当前qps,活跃线程数,连接数,缓存池的大小等信息。 如果发现数据量连接池占用太多,对接口的性能肯定会有影响。这时可能是代码中开启了连接忘了关,或者并发量太大了导致的,需要做进一步排查和系统优化。截图中只是它一小部分功能,如果你想了解更多功能,可以访问Prometheus的官网:https://prometheus.io/11.3 链路跟踪有时候某个接口涉及的逻辑很多,比如:查数据库、查redis、远程调用接口,发mq消息,执行业务代码等等。该接口一次请求的链路很长,如果逐一排查,需要花费大量的时间,这时候,我们已经没法用传统的办法定位问题了。有没有办法解决这问题呢?用分布式链路跟踪系统:skywalking。架构图如下:通过skywalking定位性能问题: 在skywalking中可以通过traceId(全局唯一的id),串联一个接口请求的完整链路。可以看到整个接口的耗时,调用的远程服务的耗时,访问数据库或者redis的耗时等等,功能非常强大。 之前没有这个功能的时候,为了定位线上接口性能问题,我们还需要在代码中加日志,手动打印出链路中各个环节的耗时情况,然后再逐一排查。 如果你用过skywalking排查接口性能问题,不自觉的会爱上它的。如果你想了解更多功能,可以访问skywalking的官网。如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下,您的支持是我坚持写作最大的动力———————————————— 原文链接:https://blog.csdn.net/m0_72088858/article/details/126699810
-
由于之前看的容易忘记,因此特记录下来,以便学习总结与更好理解,该系列博文也是第一次记录,所有有好多不完善之处请见谅与留言指出,如果有幸大家看到该博文,希望报以参考目的看浏览,如有错误之处,谢谢大家指出与留言。这里只是讲解下锁优化思路以及方法的总结,具体技术深究以后慢慢补充一、锁优化的思路和方法锁优化是指:在多线程的并发中当用到锁时,尽可能让性能有所提高。一般并发中用到锁,就是阻塞的并发,前面讲到一般并发级别分为阻塞的和非阻塞的(非阻塞的包含:无障碍的,无等待的,无锁的等等),一旦用到锁,就是阻塞的,也就是一般最糟糕的并发,因此锁优化就是在堵塞的情况下去提高性能;所以所锁的优化就是让性能尽可能提高,不管怎么提高,堵塞的也没有无锁的并发底。让锁定的障碍降到最低,锁优化并不是说就能解决锁堵塞造成的性能问题。这是做不到的。方法如下: 减少锁持有时间 减小锁粒度 锁分离 锁粗化 锁消除二、减少锁持有时间举例:public synchronized void syncMethod(){othercode1();mutextMethod();othercode2();}使用这个锁会造成其他线程进行等待,因此让锁的的持有时间减少和锁的范围,锁的零界点就会降低,其他线程就会很快获取锁,尽可能减少了冲突时间。改进优化如下:public void syncMethod2(){othercode1();synchronized(this){mutextMethod();}othercode2();}三、减小锁粒度 将大对象,拆成小对象,好处是:大大增加并行度,降低锁竞争(同时偏向锁,轻量级锁成功率提高) 提高偏向锁,轻量级锁成功率 HashMap的同步实现( HashMap他是非线程安全的实现) – Collections.synchronizedMap(Map m)(多线程下使用时:用该synchronizedMap封装方式先封装让他实现线程同步的) – 返回SynchronizedMap对象 内部实现如下:就是实现对set与get进行加锁,进行互斥上同步,不管读还是写都会拿到这个互斥对象。他变成很重的对象,不管读还是写,都会互斥阻塞,读堵塞写,写堵塞读,当多个读和写时线程会一个一个的进来。public V get(Object key) { synchronized (mutex) {return m.get(key);} }public V put(K key, V value) { synchronized (mutex) {return m.put(key, value);}} ConcurrentHashMap(高性能的hash表,他就是做了减少锁粒度的实现,他被拆分好像16个Segment,每个Segment就是一个个小的hashmap.。就是把大的hash表拆成若干个小的hash表。) – 若干个Segment :Segment[] segments – Segment中维护HashEntry – put操作时• 先定位到Segment,锁定一个Segment,执行put在减小锁粒度后, ConcurrentHashMap允许若干个线程同时进入五、锁分离就是把读堵塞写,写堵塞读,读读堵塞,写写堵塞就可以使用所分离;锁分离,就是读写锁分离,读不用改变数据,所以所有的读不会产生堵塞。当写的时候才去进行堵塞。一般读情况大于锁,所以使用读写锁会有所提高系统性能。如下图1、 ReadWriteLock : 维护了一对锁,读锁可允许多个读线程并发使用,写锁是独占的。 根据功能进行锁分离 所有读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。 读写锁比互斥锁允许对于共享数据更大程度的并发。每次只能有一个写线程,但是同时可以有多个线程并发地读数据。ReadWriteLock适用于读多写少的并发情况。 读多写少的情况,可以提高性能(根据功能模块是进行不同锁,读锁跟读锁同时进入情况其实就属于无等待的并发,因此这种操作就是把堵塞的变成非堵塞的,性能就是有所改变) ReadWriteLock源码剖析:https://blog.csdn.net/qq_19431333/article/details/705684782、 读写分离思想可以延伸,只要操作互不影响,锁就可以分离 LinkedBlockingQueue LinkedBlockingQueue 用法:https://www.cnblogs.com/edgedance/p/7082078.html – 队列 – 链表思想也可以理解为:在forkjioning有所提到,就是任务work的偷窃,当线程执行自己的任务,和一个线程去盗取别人的任务,他们的任务队列中的数据他们是从两个不同的端去拿的,这就是热点分离基本思想,一个从头部拿,一个从尾部拿。如下: 头部和尾部之间的操作是不冲突的,所以可以进行高并发操作,当然当队列中只有一个数据情况就另当别论你了。六、锁粗化(一)、通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行任务。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化。因此可以把很多次请求的锁拿到一个锁里面,但前提是:中间不需要的同步的代码块很很快的执行完。1.举例如下:public void demoMethod(){synchronized(lock){//do sth.}//做其他不需要的同步的工作,但能很快执行完毕synchronized(lock){//do sth.}}改进优化如下:public void demoMethod(){//整合成一次锁请求synchronized(lock){//do sth.//做其他不需要的同步的工作,但能很快执行完毕}}2.举例如下:for(int i=0;i<CIRCLE;i++){synchronized(lock){}}该进入下:synchronized(lock){for(int i=0;i<CIRCLE;i++){}}七、锁消除在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。有时候对完全不可能加锁的代码执行了锁操作,因为些锁并不是我们加的,是JDK的类引用进来的,当我们使用的时候,会自动引进来,所以我们会在不可能出现在多线程需要同步的情况就执行了锁操作。在某些条件成熟下,系统会消除这些锁。如下:public static void main(String args[]) throws InterruptedException {long start = System.currentTimeMillis();for (int i = 0; i < CIRCLE; i++) {craeteStringBuffer("JVM", "Diagnosis");} long bufferCost = System.currentTimeMillis() - start; System.out.println("craeteStringBuffer: " + bufferCost + " ms");}public static String craeteStringBuffer(String s1, String s2) {StringBuffer sb = new StringBuffer(); //他就是实现的多线程同步功能sb.append(s1); 这两个就是同步操作sb.append(s2);return sb.toString();}sb是线程安全的。但事实上sb他在栈空间引用的,他是局部变量,他就是在线程内部才会有的,在局部变量表中,只有一个线程可以执行他,其他线程是不可靠,能访问到他的因此对sb进行所有同步操作都是无意义的。因此对些情况,虚拟机提供了一些优化,就是如下操作,虚拟机开启server模式同时进行开启逃逸分析DoEscapeAnalysis,如果没有逃逸的就把锁去掉(EliminateLocks)。逃逸分析是指:看sb是否有可能逃出StringBuffer的作用域。变成sb公有的,全局的变量,变成其他线程可访问的了。 进行逃逸分析的执行时间,(同时加上去掉锁操作), 进行逃逸分析的执行时间,(没有加上去掉锁操作)。server模式用法简单讲解:与client模式相比,server模式的启动比较慢,因为server模式会尝试收集更多的系统性能信息,使用更复杂的优化算法对程序进行优化。因此当系统完全启动并进入运行稳定期后,server模式的执行速度会远远快于client模式,所以在对于后台长期运行的系统,使用-server参数启动对系统的整体性能可以有不小的帮助,但对于用户界面程序,运行时间不长,又追求启动速度建议使用-client模式启动。未来发展64位系统必然取代32位系统,而64位系统中的虚拟机更倾向于server模式。八、虚拟机内的锁优化 偏向锁 轻量级锁 自旋锁1.首先看下:对象头Mark 详细讲解:https://blog.csdn.net/zhoufanyang_china/article/details/54601311 Mark Word,对象头的标记,32位 (对象头部保存一些对象的一些信息,32位是指系统的位数) 描述对象的hash、锁信息,垃圾回收标记,年龄 – 指向锁记录的指针 – 指向monitor的指针 – GC标记 – 偏向锁线程ID2、偏向锁(偏心,就是偏向当前占有锁的线程,他的思想是悲观的思想,一般我们都是杞人忧天的,大多情况是没有竞争的,就可以使用偏向锁,可以对一个线程操作提高性能)思想:那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步,退出同步也,无需每次加锁解锁都去CAS更新对象头,如果不一致意味着发生了竞争,锁已经不是总是偏向于同一个线程了,这时候需要锁膨胀为轻量级锁,才能保证线程间公平竞争锁。在没有实际竞争的情况下,还能够针对部分场景继续优化。如果不仅仅没有实际竞争,自始至终,使用锁的线程都只有一个,那么,维护轻量级锁都是浪费的。偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。“偏向”的意思是,偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此,只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁。偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定。(1.)大部分情况是没有竞争的,所以可以通过偏向来提高性能(2.)所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程(3.)将对象头Mark的标记设置为偏向,并将线程ID写入对象头Mark(4.) 只要没有竞争,获得偏向锁的线程,在将来进入同步块,不需要做同步(5.)当其他线程请求相同的锁时,偏向模式结束(6.) -XX:+UseBiasedLocking – 默认启用(6.) 在竞争激烈的场合,偏向锁会增加系统负担(每次偏向模式都会失败,因为线程竞争,就会是偏向锁结束;所以每一次都很容易结束偏向锁,就加大了偏向锁的每一次判断,偏向锁就没有任何效果)public static List<Integer> numberList =new Vector<Integer>(); //Vector带有锁public static void main(String[] args) throws InterruptedException {long begin=System.currentTimeMillis();int count=0;int startnum=0;while(count<10000000){numberList.add(startnum);startnum+=2;count++;}long end=System.currentTimeMillis();System.out.println(end-begin);}在系统起来时虚拟机默认启用偏向时间是4,因为开始的竞争是很激烈的。3.轻量级锁(就是如果在偏向锁失败时,系统就会有可能去进行轻量级锁,目的是尽可能不要动用操作系统中层面的互斥,性能差,因为对于操作系统来说,虚拟机本身就是应用,所以我们在应用层面去解决线程同步问题。)自旋锁的目标是降低线程切换的成本。如果锁竞争激烈,我们不得不依赖于重量级锁,让竞争失败的线程阻塞;如果完全没有实际的锁竞争,那么申请重量级锁都是浪费的。轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。Mark Word是对象头的一部分;每个线程都拥有自己的线程栈(虚拟机栈),记录线程和函数调用的基本信息。二者属于JVM的基础内容,此处不做介绍。当然,由于轻量级锁天然瞄准不存在锁竞争的场景,如果存在锁竞争但不激烈,仍然可以用自旋锁优化,自旋失败后再膨胀为重量级锁。思想就是:判断线程是否持有某个对象锁,去看他的头部是否设置了这个对象的mark值,如果有,就说明线程拥有了锁。 BasicObjectLock – 嵌入在线程栈中的对象 普通的锁处理性能不够理想,轻量级锁是一种快速的锁定方法。 如果对象没有被锁定(判断步骤) – 将对象头的Mark指针保存到锁对象中 – 将对象头设置为指向锁的指针(在线程栈空间中)如下操作:在虚拟机层面去进行快速持有锁与非持有锁判断操作,其实就是CAS操作。cas成功,说明你持有锁,费则则没有。lock->set_displaced_header(mark);if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)){ TEVENT (slow_enter: release stacklock) ; return ;}lock位于线程栈中产生问题:1. 如果轻量级锁获取失败(CAS失败),表示存在竞争,升级为重量级锁(常规锁)2. 在没有锁竞争的前提下,减少传统锁使用OS(操作系统)互斥量产生的性能损耗3.在竞争激烈时,轻量级锁会多做很多额外操作,导致性能下降扩展CAS:CAS:Compare and Swap, 翻译成比较并交换。 java.util.concurrent包中借助CAS实现了区别于synchronouse同步锁的一种乐观锁。CAS操作包含三个操作数——内存位置(V),预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器将会自动将该位置值更新为新值,否则,不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。通过以上定义我们知道CAS其实是有三个步骤的1.读取内存中的值2.将读取的值和预期的值比较3.如果比较的结果符合预期,则写入新值https://blog.csdn.net/liu88010988/article/details/50799978https://blog.csdn.net/qq_35357656/article/details/786573734.自旋锁(可以防止在操作系统层面线程被挂起)当轻量级锁没有拿到失败时,他就有可能动用操作系统方面的互斥,有可能动用是指,他还可能进行自旋锁操作。当竞争存在时,如果线程可以很快获得锁,那么可以不在OS层挂起线程,让线程做几个空操作(自旋);当拿不到锁时,不立即去挂掉线程,而是做空循环,尝试再去拿到锁,当别人释放锁时,你就可以拿到锁。避免线程在操作系统层面挂起。避免8万个时间周期的浪费。 JDK1.6中-XX:+UseSpinning开启 1.6可关闭和开启操作, JDK1.7中,去掉此参数,改为内置实现 1.7则把他改为内置开启 如果同步块很长,自旋失败,会降低系统性能 如果同步块很短,自旋成功,节省线程挂起切换时间,提升系统性能因此减少锁的持有时间也会增加自旋成功率。ConcurrentHashMap就可以使用这个自旋锁,hashmap的操作是非常快的,所以自旋等待的可能性就会提高。5.偏向锁,轻量级锁,自旋锁总结(这些都是在虚拟机层面的优化,不是java层面的方式) 他们不是Java语言层面的锁优化方法,是虚拟机层面的方法 内置于JVM中的获取锁的优化方法和获取锁的步骤 – 偏向锁可用会先尝试偏向锁 – 轻量级锁可用会先尝试轻量级锁 – 以上都失败,尝试自旋锁 – 再失败,尝试普通锁,使用OS互斥量在操作系统层挂起 OS互斥量: (1)、偏向锁、轻量级锁、重量级锁适用于不同的并发场景:偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。重量级锁:有实际竞争,且锁竞争时间长。另外,如果锁竞争时间短,可以使用自旋锁进一步优化轻量级锁、重量级锁的性能,减少线程切换。如果锁竞争程度逐渐提高(缓慢),那么从偏向锁逐步膨胀到重量锁,能够提高系统的整体性能。三种锁的详细解析:https://blog.csdn.net/zqz_zqz/article/details/70233767https://blog.csdn.net/noble510520/article/details/788342246.一个错误使用锁的案例-对不变模式的数据类型进行加锁操作public class IntegerLock {static Integer i=0; public static class AddThread extends Thread{public void run(){for(int k=0;k<100000;k++){synchronized(i){i++;}}}}public static void main(String[] args) throws InterruptedException {AddThread t1=new AddThread();AddThread t2=new AddThread();t1.start();t2.start();t1.join();t2.join();System.out.println(i);}}interge 是不变模式的,也就是i值不会发生变化,变化的是i的引用。static Integer i=0; 是不变的,Interge是不可变的,对他i++是不会改变的,因此这里i++实际的动作是对原始的int做操作,对Interge做++其内部是对他自动拆箱成int进行i++的,这时候改变的不是interge对象的值,而是改变i本身的引用,当i++时,会生成新的Interge,并复到i上,而不是把原来i进行操作,如果每一次都对i做同步,但不同的线程操作的i对象可能不是同一个i,第一个可能执行原来的i,下一个线程可能执行新的i对象。(可以用上面代码测试)7.ThreadLocal用法案例ThreadLocal跟锁是没有关系,ThreadLocal是最彻底的,可以把锁完全给替代的东西。基本思想是:多线程中对有数据冲突的对象进行加锁操作,那么去掉锁的简单方法是,为每一个线程都提供一个对象的实例,不同的线程访问自己的对象。他是线程局部的变量private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static class ParseDate implements Runnable{int i=0;public ParseDate(int i){this.i=i;}public void run() {try {Date t=sdf.parse("2015-03-29 19:29:"+i%60); //sdf对象他不是线程安全的System.out.println(i+":"+t);} catch (ParseException e) {e.printStackTrace();}}}public static void main(String[] args) {ExecutorService es=Executors.newFixedThreadPool(10);for(int i=0;i<1000;i++){es.execute(new ParseDate(i));}}SimpleDateFormat被多线程访问优化:线程安全的static ThreadLocal<SimpleDateFormat> tl=new ThreadLocal<SimpleDateFormat>();public static class ParseDate implements Runnable{int i=0;public ParseDate(int i){this.i=i;}public void run() {try {if(tl.get()==null){tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));//每一次要new一个对象}Date t=tl.get().parse("2015-03-29 19:29:"+i%60);System.out.println(i+":"+t);} catch (ParseException e) {e.printStackTrace();}}}public static void main(String[] args) {ExecutorService es=Executors.newFixedThreadPool(10);for(int i=0;i<1000;i++){es.execute(new ParseDate(i));}}为每一个线程分配一个实例另外一个错误案例:他不会去维护每一个对象的拷贝,实际上tl.get()是把ThreadLocal对象指向同一个对象实例,对所有线程来说他还是同一个对象。static ThreadLocal<SimpleDateFormat> tl=new ThreadLocal<SimpleDateFormat>();private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static class ParseDate implements Runnable{int i=0;public ParseDate(int i){this.i=i;}public void run() {try {if(tl.get()==null){tl.set(sdf);}Date t=tl.get().parse("2015-03-29 19:29:"+i%60);//这个还不是线程安全的,操作还是同一个线程,ThreadLocal指定的还是同一个对象,System.out.println(i+":"+t);} catch (ParseException e) {e.printStackTrace();}}}public static void main(String[] args) {ExecutorService es=Executors.newFixedThreadPool(10);for(int i=0;i<1000;i++){es.execute(new ParseDate(i));}}如果使用共享实例,起不到效果总结:对于工具等api对象类,数据库连接实例等希望对每个线程持单独有一个对象,就会减少线程的开销,比如SimpleDateFormat不需要线程之间相互影响,不会产生冲突,就可以使用他。———————————————— 原文链接:https://blog.csdn.net/gududedabai/article/details/80911855
-
干货分享,感谢您的阅读!在多线程编程中,锁是保证线程安全的重要手段之一,但如何选择合适的锁并进行优化,一直是我们面临的挑战。本博客探讨Java中同步锁的性能分析与优化之路,从使用同步锁和不使用同步锁的性能对比入手,逐步展开对锁的优化手段和技术原理的解析,帮助读者更好地理解和应用Java中的锁机制。一、同步锁性能分析同步锁在多线程编程中是保证线程安全的重要工具,其性能开销一直是不可忽视的存在。(一)性能验证说明为了直观说明我们可以直接先准备两个Java代码用例,我们通过高并发环境下的计数器递增操作来对比使用同步锁和不使用同步锁的性能差异。1. 使用同步锁的代码示例使用ReentrantLock来保护对共享资源(counter)的访问,确保同一时间只有一个线程可以对计数器进行操作。具体代码如下:package org.zyf.javabasic.thread.lock.opti; import java.util.concurrent.locks.ReentrantLock; /** * @program: zyfboot-javabasic * @description: 使用了ReentrantLock来保护对共享资源(counter)的访问,确保同一时间只有一个线程可以对计数器进行操作。 * @author: zhangyanfeng * @create: 2024-06-05 22:54 **/public class SyncLockExample { private static int counter = 0; private static final ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { long startTime = System.currentTimeMillis(); Thread[] threads = new Thread[100]; for (int i = 0; i < 100; i++) { threads[i] = new Thread(new IncrementWithLock()); threads[i].start(); } for (Thread thread : threads) { thread.join(); } long endTime = System.currentTimeMillis(); System.out.println("Time with lock: " + (endTime - startTime) + " ms"); } static class IncrementWithLock implements Runnable { @Override public void run() { for (int i = 0; i < 1000000; i++) { lock.lock(); try { counter++; } finally { lock.unlock(); } } } }}2. 不使用同步锁的代码示例不使用任何同步机制,直接操作共享资源。具体代码如下:package org.zyf.javabasic.thread.lock.opti; /** * @program: zyfboot-javabasic * @description: 不使用任何同步机制,直接操作共享资源。 * @author: zhangyanfeng * @create: 2024-06-05 22:55 **/public class NoSyncLockExample { private static int counter = 0; public static void main(String[] args) throws InterruptedException { long startTime = System.currentTimeMillis(); Thread[] threads = new Thread[100]; for (int i = 0; i < 100; i++) { threads[i] = new Thread(new IncrementWithoutLock()); threads[i].start(); } for (Thread thread : threads) { thread.join(); } long endTime = System.currentTimeMillis(); System.out.println("Time without lock: " + (endTime - startTime) + " ms"); } static class IncrementWithoutLock implements Runnable { @Override public void run() { for (int i = 0; i < 1000000; i++) { counter++; } } }}3. 结果与讨论运行以上代码,我当前的机器上可以直观的看到使用同步锁的时间: 1314 ms不使用同步锁的时间: 20 ms从结果中可以明显看出,同步锁会带来显著的性能开销。同步锁的存在增加了线程间的等待时间和上下文切换的开销,从而降低了程序的整体运行效率。所以在使用锁时,对锁的优化使用是必不可少的。(二)案例初步优化分析说明在开始讲解一些常用的优化手段的时候,我们先就现在这个用例来谈谈可能我们一般可以想到的直观优化手段。1. 使用AtomicInteger原子类尝试优化分析Java的java.util.concurrent.atomic包提供了一些原子类,可以在并发编程中避免显式加锁。最简单的我们可以使用AtomicInteger来替代显式的锁。package org.zyf.javabasic.thread.lock.opti; import java.util.concurrent.atomic.AtomicInteger; /** * @program: zyfboot-javabasic * @description: 使用AtomicInteger来替代显式的锁 * @author: zhangyanfeng * @create: 2024-06-05 23:07 **/public class AtomicExample { private static AtomicInteger counter = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { long startTime = System.currentTimeMillis(); Thread[] threads = new Thread[100]; for (int i = 0; i < 100; i++) { threads[i] = new Thread(new IncrementAtomic()); threads[i].start(); } for (Thread thread : threads) { thread.join(); } long endTime = System.currentTimeMillis(); System.out.println("Time with AtomicInteger: " + (endTime - startTime) + " ms"); } static class IncrementAtomic implements Runnable { @Override public void run() { for (int i = 0; i < 1000000; i++) { counter.incrementAndGet(); } } }}理论上这样优化后性能上必会上升,但实际上运行后其耗时为 6714 ms,性能反而变差了。这里其实我们之前的博客中也有讲过,其主要的原因有两个:原子类的开销:AtomicInteger的incrementAndGet方法虽然是无锁的,但它依赖于底层的CAS(Compare-And-Swap)操作。CAS操作虽然是无锁的,但在高并发情况下,多个线程同时尝试更新同一个变量时,CAS操作可能会频繁地失败并重试,从而导致性能下降。相比之下,ReentrantLock在某些情况下可能反而表现更好,尤其是在锁争用不是特别激烈的时候。高并发下的内存争用:在高并发情况下,多个线程同时访问和修改共享变量会导致内存争用。这种争用在使用AtomicInteger时表现得更为明显,因为每次操作都需要与主内存同步,可能会导致缓存一致性协议的开销。2. 对AtomicInteger原子类进一步优化我们可以尝试以下方法来进一步优化:减少线程数量:在本示例中,我们使用了100个线程同时访问共享变量,这可能导致过多的上下文切换和争用。可以尝试减少线程数量,看看性能是否有所改善。使用更高效的同步机制:可以尝试使用其他同步机制,如LongAdder或ConcurrentLinkedQueue等,这些工具在高并发场景下通常表现更好。这里我们直接用LongAdder验证,具体代码如下:package org.zyf.javabasic.thread.lock.opti; import java.util.concurrent.atomic.LongAdder; /** * @program: zyfboot-javabasic * @description: LongAdder在高并发情况下比AtomicInteger有更好的性能 * @author: zhangyanfeng * @create: 2024-06-05 23:26 **/public class LongAdderExample { private static LongAdder counter = new LongAdder(); public static void main(String[] args) throws InterruptedException { long startTime = System.currentTimeMillis(); Thread[] threads = new Thread[100]; for (int i = 0; i < 100; i++) { threads[i] = new Thread(new IncrementLongAdder()); threads[i].start(); } for (Thread thread : threads) { thread.join(); } long endTime = System.currentTimeMillis(); System.out.println("Time with LongAdder: " + (endTime - startTime) + " ms"); } static class IncrementLongAdder implements Runnable { @Override public void run() { for (int i = 0; i < 1000000; i++) { counter.increment(); } } }}运行后发现这个时候的耗时基本在204 ms,优化还是很明显的。3. 结论说明(LongAdder原理理解体会)在实际应用中,选择合适的优化方法需要根据具体的业务逻辑和并发需求进行权衡和调整。这里我们针对LongAdder的优化进行说明一下,它是基于了 CAS 分段锁的思想实现的。线程去读写一个 LongAdder 类型的变量时,流程如下: 基于 Unsafe 提供的 CAS 操作 +valitale 去实现的。在 LongAdder 的父类 Striped64 中维护着一个 base 变量和一个 cell 数组,当多个线程操作一个变量的时候,先会在这个 base 变量上进行 cas 操作,当它发现线程增多的时候,就会使用 cell 数组。比如当 base 将要更新的时候发现线程增多(也就是调用 casBase 方法更新 base 值失败),那么它会自动使用 cell 数组,每一个线程对应于一个 cell ,在每一个线程中对该 cell 进行 cas 操作,这样就可以将单一 value 的更新压力分担到多个 value 中去,降低单个 value 的 “热度”,同时也减少了大量线程的空转,提高并发效率,分散并发压力。这种分段锁需要额外维护一个内存空间 cells ,不过在高并发场景下,这点成本几乎可以忽略。我觉得可以把 LongAdder 想象成一个超市收银台系统:base 变量:一个主收银台,所有顾客最开始都会排队在这里付款。cell 数组:多个备用收银台,当主收银台忙不过来时,顾客会被分配到不同的备用收银台去付款。这样做的好处是,当有大量顾客(高并发)时,大家不会都挤在一个收银台前,避免了长时间的等待,提高了结账效率。二、回顾Java锁优化Java 中的 synchronized 关键字和 java.util.concurrent.locks 包中的 Lock 接口(如 ReentrantLock)是两种常见的加锁方式,它们各有优缺点,针对这两种锁,JDK 自身做了很多的优化,它们的实现方式也是不同的。我们不妨进行简单的回顾一下。(一)synchronized 关键字synchronized 关键字给代码或者方法上锁时,都有显示或者隐藏的上锁对象。当一个线程试图访问同步代码块时,它首先必须得到锁,而退出或抛出异常时必须释放锁(注意是 synchronized 关键字的内置机制,由 Java 语言和 JVM 自动管理。我们在使用 synchronized 关键字时,无需手动编写获取和释放锁的代码,synchronized 会自动处理这些细节)。给普通方法加锁时:锁定对象是实例对象 (this)。相同实例的 synchronized 方法之间是串行的,不同实例之间则不会互相影响。给静态方法加锁时:锁定对象是类的 Class 对象。所有实例共享同一个类锁,因此静态 synchronized 方法在所有实例上都是串行的。给代码块加锁时:可以指定任意对象作为锁。锁的粒度更细,可以针对不同的对象分别加锁,灵活控制并发。synchronized 的实现依赖于 JVM 和操作系统的原语,如监视器锁和信号量。JVM 在执行同步块时,会插入相应的加锁和解锁指令。1. monitor 锁的实现原理synchronized 关键字在 Java 字节码中通过监视器(Monitor)指令来实现。具体来说,当 Java 编译器编译包含 synchronized 关键字的代码时,会在相应的位置插入 monitorenter 和 monitorexit 指令。JVM 在运行这些字节码时,会负责管理锁的获取和释放。对于同步实例方法,编译器在字节码中并不会显式插入 monitorenter 和 monitorexit 指令。相反,它会在方法的访问标志(access flag)中添加 ACC_SYNCHRONIZED 标志。JVM 在调用该方法时会自动获取实例的监视器锁,并在方法返回或抛出异常时自动释放锁。对于同步静态方法,编译器同样会在方法的访问标志中添加 ACC_SYNCHRONIZED 标志。JVM 在调用该方法时会自动获取类的监视器锁,并在方法返回或抛出异常时自动释放锁。对于同步代码块,编译器会在进入同步块时插入 monitorenter 指令,在退出同步块时插入 monitorexit 指令。这些指令用于显式地获取和释放指定的锁对象。现在我们直观的验证一下:package org.zyf.javabasic.thread.lock.opti; /** * @program: zyfboot-javabasic * @description: 同步方法和同步代码块的示例类,以及它们在字节码中的表现。 * @author: zhangyanfeng * @create: 2024-06-06 07:56 **/public class SynchronizedExample { private int count = 0; private final Object lock = new Object(); // 同步实例方法 public synchronized void increment() { count++; } // 同步静态方法 public static synchronized void staticIncrement() { // 静态方法体 } // 同步代码块 public void blockIncrement() { synchronized (lock) { count++; } }}可以使用 javap -c SynchronizedExample 命令查看字节码如下:public synchronized void increment(); flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=2, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field count:I 4: iconst_1 5: iadd 6: aload_0 7: swap 8: putfield #2 // Field count:I 11: return public static synchronized void staticIncrement(); flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED Code: stack=0, locals=0, args_size=0 0: return public void blockIncrement(); flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: aload_0 1: getfield #3 // Field lock:Ljava/lang/Object; 4: dup 5: astore_1 6: monitorenter 7: aload_0 8: dup 9: getfield #2 // Field count:I 12: iconst_1 13: iadd 14: putfield #2 // Field count:I 17: aload_1 18: monitorexit 19: goto 27 22: astore_2 23: aload_1 24: monitorexit 25: aload_2 26: athrow 27: return Exception table: from to target type 7 19 22 any 22 25 22 any正如我们上面总结的一样,整体来说:使用 ACC_SYNCHRONIZED 标志,JVM 自动处理锁的获取和释放。使用 monitorenter 和 monitorexit 指令显式处理锁的获取和释放,确保在正常退出和异常退出时都能正确释放锁。这两者虽然显示效果不同,但他们都是通过 monitor 来实现同步的。当一个线程进入一个同步块或同步方法时,它会尝试获取与该对象关联的 Monitor。如果 Monitor 已经被其他线程持有,当前线程将会阻塞,直到 Monitor 被释放。虽然具体实现可能会根据不同的 JVM 实现有所差异,但通常可以用以下示意来表示 Monitor 的结构:Monitor { Thread owner; // 当前持有锁的线程 int recursionCount; // 递归计数器 List<Thread> entryList; // 进入列表,等待获取锁的线程队列 List<Thread> waitSet; // 等待列表,调用 wait() 方法等待的线程队列 List<Thread> exitList; // 退出列表,等待退出的线程队列(可能的实现)}锁相关主要的流程如下: 初始状态:多个线程尝试进入同步代码块或方法,所有线程首先进入 EntryList 队列。这些线程处于“Waiting for monitor entry”状态( jstack 命令可查看)。获取锁的过程:某个线程成功获取 Monitor 后,_owner 变量设置为当前线程,表示该线程持有锁。Monitor 的 _count 计数器自增 1,表示锁被持有的次数。等待和释放锁:持有 Monitor 的线程调用 wait() 方法时,会释放锁,并进入 WaitSet 队列。释放锁后,_owner 变量恢复为 null,_count 计数器自减 1。线程状态变为“in Object.wait()”,等待被其他线程唤醒。执行完成释放锁:持有 Monitor 的线程执行完同步代码块或方法时,会释放锁。释放锁后,_owner 变量恢复为 null,_count 计数器自减 1。唤醒等待的线程:线程调用 notify() 或 notifyAll() 方法时,会唤醒 WaitSet 中的一个或多个线程。被唤醒的线程重新进入 EntryList 队列,等待再次获取 Monitor 锁。2.分级锁在 JDK 1.8 中,synchronized 关键字的性能得到了显著提升,这主要得益于 JVM 对锁机制进行了一系列优化:锁的分级及其优化路径(大体可以按照下面的路径进行升级:偏向锁 — 轻量级锁 — 重量级锁,锁只能升级,不能降级,所以一旦升级为重量级锁,就只能依靠操作系统进行调度)。要想了解锁升级的过程,需要先看一下对象在内存里的结构。 在 Java 中,对象的内存布局中包含了 MarkWord、Class Pointer、Instance Data 和 Padding 等部分。而锁的升级过程主要与对象头的 MarkWord 有关。要知道MarkWord 是对象头中用于存储对象的运行时信息的。在 64 位 JVM 中,MarkWord 的长度为 64 位。在 32 位 JVM 中,MarkWord 的长度为 32 位(如上图)。偏向锁(Biased Locking)单线程高效使用锁:在只有一个线程使用锁的情况下,偏向锁效率最高。获取锁:第一个线程访问同步块时,会检查对象头 Mark Word 的标志位 Tag 是否为 01。如果是,线程将自己的线程 ID 写入 Mark Word,锁进入偏向锁状态。撤销偏向锁:当其他线程尝试获取锁时,如果 Mark Word 中的线程 ID 不匹配,偏向锁会被撤销,升级为轻量级锁。适合单线程场景,高效。轻量级锁自旋锁获取:参与竞争的线程会在自己的线程栈中生成一个 LockRecord (LR),通过 CAS(自旋)的方式,将锁对象头中的 Mark Word 设置为指向自己的 LR 的指针。设置成功的线程获得锁。自旋失败:如果自旋多次失败,锁会升级为重量级锁。适合短期竞争,自旋获取锁。重量级锁系统调度:重量级锁会导致线程挂起,进入操作系统内核态,等待操作系统调度,然后再映射回用户态。系统调用的开销很高,锁的膨胀到重量级锁就意味着性能下降。激烈竞争:如果共享变量竞争激烈,锁会迅速膨胀为重量级锁。如果并发竞争严重,可以使用 -XX:-UseBiasedLocking 禁用偏向锁,可能会有一些性能提升。适合长期竞争,但性能开销大。3. 锁升级一览(二)concurrent 包里面的 Locksynchronized 是 Java 提供的最基本的同步机制,通过简单易用的语法确保线程安全。然而,随着并发需求的复杂化,Java 的并发包 (java.util.concurrent) 提供了更多高级和高效的并发工具,如 ReentrantLock、ReadWriteLock、Atomic 类等,来应对更复杂的并发场景。在实际开发中,应根据具体情况选择合适的同步机制。现在我们聚焦在Lock进行分析。1. 锁机制基于线程而不是基于调用(可重入锁)“这种锁机制基于线程而不是基于调用” 的意思是说,当一个线程持有锁时,它可以在同一个线程的不同调用链中多次获取同一把锁,而不会被阻塞或引发死锁。这是因为锁是跟线程关联的,而不是跟调用栈关联的。假设有一个对象 example,它有三个同步方法 a、b 和 c,每个方法都被 synchronized 关键字修饰:package org.zyf.javabasic.thread.lock.opti; /** * @program: zyfboot-javabasic * @description: 锁机制基于线程而不是基于调用 * @author: zhangyanfeng * @create: 2024-06-07 19:04 **/public class LockExample { public synchronized void a() { System.out.println("In method a"); b(); // 调用 b 方法 } public synchronized void b() { System.out.println("In method b"); c(); // 调用 c 方法 } public synchronized void c() { System.out.println("In method c"); } public static void main(String[] args) { LockExample example = new LockExample(); example.a(); }}在 main 方法中,我们调用了 example.a()。在 a 方法内部,又调用了 b 方法,而 b 方法内部又调用了 c 方法。这种调用关系如下:example.a() -> example.b() -> example.c()当线程调用 example.a() 时,由于 a 方法是同步方法,线程必须先获得对象 example 的锁。在 a 方法内部,线程调用 example.b()。虽然 b 方法也是同步方法,但是由于当前线程已经持有了 example 对象的锁,所以它可以继续执行,不需要再次获取锁。类似地,在 b 方法内部,线程调用 example.c() 时,也不需要再次获取锁。这个过程中锁是跟线程关联的,而不是跟每次调用关联的。一个线程持有锁之后,可以在它的调用栈中多次获取同一把锁,而不需要重新获取锁,也不会被阻塞。如果 Java 的锁机制不是基于线程的,而是基于每次调用的,那么在上面的示例中,线程在调用 b 方法时会尝试再次获取 example 对象的锁,但是由于它已经持有这个锁,这将导致死锁。因此,基于线程的锁机制(即可重入锁)避免了这种情况,使得一个线程在持有锁时,可以多次获取同一把锁。Java 的 ReentrantLock 类也支持可重入性,将以上synchronized替换成其效果也是一样的。像上面这样,在并发编程中,可重入锁(Reentrant Lock)指的是一个线程可以多次获得同一把锁。可重入锁的作用在于避免线程死锁,当一个线程已经持有了一个锁,再次请求该锁时可以直接获取,而无需再次等待。这种锁机制基于线程而不是基于调用。2. Lock 主要方法在 Java 的并发编程中,Lock 接口提供了比 synchronized 更加灵活和强大的锁机制。Lock 与 synchronized 的使用方法不同,它需要手动加锁,然后在 finally 中解锁。Lock 接口是基于 AQS(AbstractQueuedSynchronizer)实现的,而 AQS 又依赖于 volatile 和 CAS(Compare-And-Swap)操作来实现线程的同步控制。其中AQS基本原理可见文章从ReentrantLock理解AQS的原理及应用总结,我们这里暂时增加一张原文图片进行体会理解: 现在我们来看一下几个关键方法:lock()获取锁。如果锁已经被其他线程持有,则当前线程将被阻塞,直到获取到锁。和 synchronized 没什么区别,如果获取不到锁,都会被阻塞;lock.lock();try { // critical section} finally { lock.unlock();}unlock()释放锁。通常在 finally 块中调用,以确保锁在使用之后总是被释放。重申需要手动加锁,然后在 finally 中解锁,在 finally 中解锁,在 finally 中解锁。tryLock()尝试获取锁。如果锁可用,则获取锁并返回 true,否则返回 false。该方法不会阻塞线程。if (lock.tryLock()) { try { // critical section } finally { lock.unlock(); }} else { // handle lock not acquired}tryLock(long timeout, TimeUnit unit)尝试在给定的时间范围内获取锁。如果在指定时间内获取到锁,则返回 true,否则返回 false。if (lock.tryLock(1, TimeUnit.SECONDS)) { try { // critical section } finally { lock.unlock(); }} else { // handle lock not acquired}lockInterruptibly()获取锁,但与 lock() 不同的是,这个方法允许响应中断。如果线程在等待锁的过程中被中断,则抛出 InterruptedException。try { lock.lockInterruptibly(); try { // critical section } finally { lock.unlock(); }} catch (InterruptedException e) { // handle interrupt}平时开发中建议在需要及时响应的业务场景下使用带超时时间的 tryLock 方法。基本建议如下:普通场景:使用 lock() 方法即可,确保线程安全。高并发、及时响应场景:使用带超时时间的 tryLock 方法,保证服务的高可用性和快速响应能力。3. 读写锁ReentrantReadWriteLock在高并发场景下,对于一些业务来说,使用 Lock 这种粗粒度的锁可能会导致性能瓶颈。例如,对于一个 HashMap,如果业务场景是读多写少,给读操作加上和写操作一样的锁会大大降低效率。因为在这种情况下,读操作会频繁发生,而每次读操作都被迫等待写锁的释放,这样就大大降低了系统的吞吐量。为了解决这类问题,我们可以使用 ReentrantReadWriteLock(一种读写分离的锁机制)。ReentrantReadWriteLock 提供了两种锁:读锁(ReadLock)和写锁(WriteLock),其核心思想是将读操作和写操作分离开来,从而提高系统的并发性能。基本内容说明读锁:允许多个线程同时获取读锁,进行并发读操作。读锁之间是共享的,多个读线程可以同时读取而不会相互阻塞。写锁:只有一个线程可以获取写锁,进行写操作。写锁之间是互斥的,写线程会阻塞其他写线程。读写互斥:读操作和写操作之间是互斥的,当一个线程持有写锁时,其他线程不能获取读锁。这样保证了数据的一致性和线程安全。性能验证说明为了更直观地对比 ReentrantReadWriteLock 在读多写少场景中的性能优势,我们可以编写一个性能测试用例,比较使用 ReentrantReadWriteLock 和 ReentrantLock 的性能差异。private static final int NUM_OPERATIONS = 10000;private static final int NUM_THREADS = 10; public static void main(String[] args) throws InterruptedException { // Test with ReentrantLock long startTime = System.currentTimeMillis(); Thread[] threads = new Thread[NUM_THREADS]; for (int i = 0; i < NUM_THREADS; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < NUM_OPERATIONS / NUM_THREADS; j++) { reentrantLock.lock(); try { map.put("key" + j, "value" + j); } finally { reentrantLock.unlock(); } } }); } for (Thread thread : threads) { thread.start(); } for (Thread thread : threads) { thread.join(); } long endTime = System.currentTimeMillis(); System.out.println("ReentrantLock Write Time: " + (endTime - startTime) + " ms"); // Test with ReentrantReadWriteLock startTime = System.currentTimeMillis(); for (int i = 0; i < NUM_THREADS; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < NUM_OPERATIONS / NUM_THREADS; j++) { writeLock.lock(); try { map.put("key" + j, "value" + j); } finally { writeLock.unlock(); } } }); } for (Thread thread : threads) { thread.start(); } for (Thread thread : threads) { thread.join(); } endTime = System.currentTimeMillis(); System.out.println("ReentrantReadWriteLock Write Time: " + (endTime - startTime) + " ms");}运行结果如下:ReentrantLock 的写入时间为 63 ms;ReentrantReadWriteLock 的写入时间为 32 ms这个结果看起来ReentrantReadWriteLock 的写入时间比 ReentrantLock 要快,这表明在这种情况下,读写分离的锁确实能够提高性能。4.乐观读取、悲观读取和写入的机制:StampedLockStampedLock 是在 Java 8 中引入的,它提供了一种乐观读取、悲观读取和写入的机制,可以比 ReentrantReadWriteLock 更高效地支持读写分离。StampedLock 不支持重入,因此在使用时需要特别注意避免死锁的情况。基本内容说明StampedLock 主要有三种锁模式:写锁(writeLock):与普通的互斥锁类似,一次只能被一个线程持有。当一个线程持有写锁时,任何其他线程试图获取写锁或者悲观读锁都会被阻塞。悲观读锁(readLock):与 ReentrantReadWriteLock 中的读锁类似,用于读取共享数据。当一个线程持有悲观读锁时,其他线程试图获取写锁的请求会被阻塞,但不会阻塞其他悲观读锁的获取请求。乐观读锁(tryOptimisticRead):是一种乐观的读取模式,允许多个线程同时访问共享数据,不会阻塞其他线程的写入操作。但在使用乐观读锁时,需要通过 validate 方法来验证读取操作是否有效。通过合理地选择锁模式,可以使 StampedLock 在某些情况下比传统的读写锁更高效。性能验证说明使用 StampedLock 和 ReentrantReadWriteLock 来实现读写分离,并比较它们的性能:package org.zyf.javabasic.thread.lock.opti; import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantReadWriteLock;import java.util.concurrent.locks.StampedLock; /** * @program: zyfboot-javabasic * @description: 使用 StampedLock 和 ReentrantReadWriteLock 来实现读写分离,并比较它们的性能。 * @author: zhangyanfeng * @create: 2024-06-07 22:35 **/public class StampedLockVsReentrantReadWriteLock { private static final int NUM_THREADS = 200; private static final int NUM_OPERATIONS = 5000000; private static volatile int sharedVariable = 0; public static void main(String[] args) throws InterruptedException { long start, end; // Test with StampedLock StampedLock stampedLock = new StampedLock(); start = System.currentTimeMillis(); Thread[] stampedLockWriteThreads = new Thread[NUM_THREADS]; Thread[] stampedLockReadThreads = new Thread[NUM_THREADS]; for (int i = 0; i < NUM_THREADS; i++) { stampedLockWriteThreads[i] = new Thread(() -> { for (int j = 0; j < NUM_OPERATIONS; j++) { long stamp = stampedLock.writeLock(); try { sharedVariable++; } finally { stampedLock.unlockWrite(stamp); } } }); stampedLockWriteThreads[i].start(); stampedLockReadThreads[i] = new Thread(() -> { for (int j = 0; j < NUM_OPERATIONS; j++) { long stamp = stampedLock.readLock(); try { int value = sharedVariable; } finally { stampedLock.unlockRead(stamp); } } }); stampedLockReadThreads[i].start(); } for (Thread thread : stampedLockWriteThreads) { thread.join(); } for (Thread thread : stampedLockReadThreads) { thread.join(); } end = System.currentTimeMillis(); System.out.println("StampedLock Write and Read Time: " + (end - start) + " ms"); // Test with ReentrantReadWriteLock ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); Lock writeLock = rwLock.writeLock(); Lock readLock = rwLock.readLock(); start = System.currentTimeMillis(); Thread[] rwLockWriteThreads = new Thread[NUM_THREADS]; Thread[] rwLockReadThreads = new Thread[NUM_THREADS]; for (int i = 0; i < NUM_THREADS; i++) { rwLockWriteThreads[i] = new Thread(() -> { for (int j = 0; j < NUM_OPERATIONS; j++) { writeLock.lock(); try { sharedVariable++; } finally { writeLock.unlock(); } } }); rwLockWriteThreads[i].start(); rwLockReadThreads[i] = new Thread(() -> { for (int j = 0; j < NUM_OPERATIONS; j++) { readLock.lock(); try { int value = sharedVariable; } finally { readLock.unlock(); } } }); rwLockReadThreads[i].start(); } for (Thread thread : rwLockWriteThreads) { thread.join(); } for (Thread thread : rwLockReadThreads) { thread.join(); } end = System.currentTimeMillis(); System.out.println("ReentrantReadWriteLock Write and Read Time: " + (end - start) + " ms"); }}运行结果如下:StampedLock 的写入和读取时间为 21200 ms;ReentrantReadWriteLock 的写入和读取时间为 26779 ms结果来看StampedLock 的性能优于 ReentrantReadWriteLock。StampedLock 在这种情况下的表现更好,这与其内部实现机制有关。StampedLock 使用乐观读锁来提高并发性,而 ReentrantReadWriteLock 使用悲观读锁。在适当的情况下,StampedLock 能够更高效地支持读写分离,这通常在读操作远远多于写操作的情况下更为明显。5. 公平锁与非公平锁非公平锁允许在释放锁时不考虑等待队列中的其他线程的情况,新的线程可以立即争抢锁。这种情况下,可能存在某些线程总是无法获取锁的情况,造成线程饥饿。相反,公平锁确保等待时间最长的线程会被优先选择来获取锁,这样每个线程都有机会获取到锁,避免了饥饿现象。在公平锁中,线程按照请求锁的顺序进入队列,并按顺序获取锁。synchronized关键字 vs Lock接口在Java中,synchronized关键字使用的是非公平锁,而在Lock接口的实现类中,可以通过构造函数来选择使用公平锁或非公平锁。public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this);}公平锁的实现需要维护一个有序的等待队列,因此在多核场景下,可能会降低吞吐量。这个在一开始讲Lock的时候我们就已经提过了,这里图在放一下: 功能验证package org.zyf.javabasic.thread.lock.opti; import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock; /** * @program: zyfboot-javabasic * @description: 比较非公平锁和公平锁的性能 * @author: zhangyanfeng * @create: 2024-06-08 00:07 **/public class LockPerformanceComparison { private static final int NUM_THREADS = 10; private static final int NUM_ITERATIONS = 1000000; private static final Lock unfairLock = new ReentrantLock(); private static final Lock fairLock = new ReentrantLock(true); // 公平锁 public static void main(String[] args) { System.out.println("Unfair Lock Performance Test"); testLock(unfairLock); System.out.println("Fair Lock Performance Test"); testLock(fairLock); } private static void testLock(Lock lock) { long startTime = System.currentTimeMillis(); Thread[] threads = new Thread[NUM_THREADS]; for (int i = 0; i < NUM_THREADS; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < NUM_ITERATIONS; j++) { lock.lock(); try { // 模拟一些计算或任务 Math.pow(Math.random(), Math.random()); } finally { lock.unlock(); } } }); } for (Thread thread : threads) { thread.start(); } for (Thread thread : threads) { try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } long endTime = System.currentTimeMillis(); System.out.println("Execution Time: " + (endTime - startTime) + " ms"); }}运行结果如下:Unfair Lock Performance Test:Execution Time: 3338 msFair Lock Performance Test:Execution Time: 26218 ms可以看到非公平锁(Unfair Lock)的性能明显优于公平锁(Fair Lock)。这是因为公平锁需要维护一个有序的等待队列,以确保线程按照它们的请求顺序获得锁。相比之下,非公平锁允许线程在锁可用时立即获取锁,而不考虑它们的请求顺序,因此效率更高。在实际应用中,如果不需要严格的线程调度顺序,通常会选择使用非公平锁来获得更好的性能。(三)Java 中两种加锁方式对比和建议类别 Synchronized Lock实现方式 monitor AQS底层细节 JVM优化 Java API分级锁 是 否功能特性 单一 丰富锁分离 无 读写锁锁超时 无 带超时时间的 tryLock可中断 否 lockInterruptiblyLock 的功能是比 Synchronized 多的,能够对线程行为进行更细粒度的控制。但如果只是用最简单的锁互斥功能,建议直接使用 Synchronized,有两个原因:Synchronized 的编程模型更加简单,更易于使用Synchronized 引入了偏向锁,轻量级锁等功能,能够从 JVM 层进行优化,同时JIT 编译器也会对它执行一些锁消除动作。三、锁的优化手段Java 中有两种加锁的方式:一种就是常见的synchronized 关键字,另外一种,就是使用 concurrent 包里面的 Lock。针对这两种锁,JDK 自身做了很多的优化,它们的实现方式也是不同的。这个在上文中已经讲了很多,我们体会的已经比较深了,现在站在巨人的肩膀上总结一下优化的一般思路:减少锁的粒度、减少锁持有的时间、锁分级、锁分离 、锁消除、乐观锁、无锁等。 (一)减少锁的粒度锁粒度(Lock Granularity)指的是锁定的资源范围大小。锁的粒度越大,意味着锁定的资源范围越广,可能会导致更多的线程阻塞等待锁的释放;锁的粒度越小,意味着锁定的资源范围越窄,可以减少线程的阻塞和等待时间。减少锁粒度是指通过细化锁的范围和控制的资源,来减少线程之间的冲突和竞争,从而提高并发性和系统性能。假设我们有一个大型整数数组,多个线程需要同时对该数组进行读写操作。我们可以将数组分成多个段,每个段使用一个独立的锁,从而允许不同线程并行访问不同段的数据。先看使用单个锁的 SingleLockArray:使用一个全局锁来保护整个数组。所有线程在访问数组时都需要获取这把锁,因此会导致更多的锁竞争和阻塞。package org.zyf.javabasic.thread.lock.opti; import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock; /** * @program: zyfboot-javabasic * @description: 使用一个全局锁来保护整个数组。所有线程在访问数组时都需要获取这把锁,因此会导致更多的锁竞争和阻塞。 * @author: zhangyanfeng * @create: 2024-06-08 01:02 **/public class SingleLockArray { private static final int ARRAY_SIZE = 10000; private final int[] array = new int[ARRAY_SIZE]; private final Lock lock = new ReentrantLock(); public void increment(int index) { lock.lock(); try { array[index]++; } finally { lock.unlock(); } } public int get(int index) { lock.lock(); try { return array[index]; } finally { lock.unlock(); } }}在看使用分段锁的 SegmentLockArray:将数组分成多个段,每个段使用一个独立的锁。这样,当多个线程访问不同段的数据时,可以并行执行,而不会相互阻塞。package org.zyf.javabasic.thread.lock.opti; import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock; /** * @program: zyfboot-javabasic * @description: 将数组分成多个段,每个段使用一个独立的锁。这样,当多个线程访问不同段的数据时,可以并行执行,而不会相互阻塞 * @author: zhangyanfeng * @create: 2024-06-08 01:06 **/public class SegmentLockArray { private static final int NUM_SEGMENTS = 10; private static final int SEGMENT_SIZE = 1000; private static final int ARRAY_SIZE = NUM_SEGMENTS * SEGMENT_SIZE; private final int[] array = new int[ARRAY_SIZE]; private final Lock[] locks = new ReentrantLock[NUM_SEGMENTS]; public SegmentLockArray() { for (int i = 0; i < NUM_SEGMENTS; i++) { locks[i] = new ReentrantLock(); } } public void increment(int index) { int segment = index / SEGMENT_SIZE; locks[segment].lock(); try { array[index]++; } finally { locks[segment].unlock(); } } public int get(int index) { int segment = index / SEGMENT_SIZE; locks[segment].lock(); try { return array[index]; } finally { locks[segment].unlock(); } } }进行验证说明package org.zyf.javabasic.thread.lock.opti; import java.util.concurrent.ThreadLocalRandom; /** * @program: zyfboot-javabasic * @description: 对比 * @author: zhangyanfeng * @create: 2024-06-08 01:07 **/public class LockArrayPerComparison { private static final int NUM_SEGMENTS = 10; private static final int SEGMENT_SIZE = 1000; private static final int ARRAY_SIZE = NUM_SEGMENTS * SEGMENT_SIZE; public static void main(String[] args) throws InterruptedException { SegmentLockArray array = new SegmentLockArray(); int numThreads = 100; Thread[] threads = new Thread[numThreads]; // Writing threads for (int i = 0; i < numThreads / 2; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < 10000; j++) { int index = ThreadLocalRandom.current().nextInt(ARRAY_SIZE); array.increment(index); } }); } // Reading threads for (int i = numThreads / 2; i < numThreads; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < 10000; j++) { int index = ThreadLocalRandom.current().nextInt(ARRAY_SIZE); array.get(index); } }); } long startTime = System.currentTimeMillis(); for (Thread thread : threads) { thread.start(); } for (Thread thread : threads) { thread.join(); } long endTime = System.currentTimeMillis(); System.out.println("Execution Time with Segment Locks: " + (endTime - startTime) + " ms"); // Compare with single lock SingleLockArray singleLockArray = new SingleLockArray(); Thread[] singleLockThreads = new Thread[numThreads]; // Writing threads for (int i = 0; i < numThreads / 2; i++) { singleLockThreads[i] = new Thread(() -> { for (int j = 0; j < 10000; j++) { int index = ThreadLocalRandom.current().nextInt(ARRAY_SIZE); singleLockArray.increment(index); } }); } // Reading threads for (int i = numThreads / 2; i < numThreads; i++) { singleLockThreads[i] = new Thread(() -> { for (int j = 0; j < 10000; j++) { int index = ThreadLocalRandom.current().nextInt(ARRAY_SIZE); singleLockArray.get(index); } }); } startTime = System.currentTimeMillis(); for (Thread thread : singleLockThreads) { thread.start(); } for (Thread thread : singleLockThreads) { thread.join(); } endTime = System.currentTimeMillis(); System.out.println("Execution Time with Single Lock: " + (endTime - startTime) + " ms"); }}运行结果如下:Execution Time with Segment Locks: 94 msExecution Time with Single Lock: 40 ms可以看到使用分段锁的 SegmentLockArray 的执行时间明显优于使用单个锁的 SingleLockArray,特别是在高并发的场景下。这样,通过减少锁粒度,我们可以显著提高系统的并发性能。(二)减少锁持有时间通过让锁资源尽快地释放,减少锁持有的时间,其他线程可更迅速地获取锁资源,进行其他业务的处理。我们使用两个计数器类,一个是未优化的版本,另一个是优化后的版本。未优化的版本(UnoptimizedCounter):具体代码如下展示,在锁定的代码块中进行了模拟的工作(Thread.sleep(1)),导致锁的持有时间较长,从而增加了锁竞争和线程阻塞的时间。package org.zyf.javabasic.thread.lock.opti; import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock; /** * @program: zyfboot-javabasic * @description: 未优化的版本 * @author: zhangyanfeng * @create: 2024-06-08 01:22 **/public class UnoptimizedCounter { private int count = 0; private final Lock lock = new ReentrantLock(); public void increment() { lock.lock(); try { // Simulate some work that doesn't need to be locked Thread.sleep(1); count++; } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public int getCount() { lock.lock(); try { return count; } finally { lock.unlock(); } }}优化后的版本(OptimizedCounter):将模拟的工作(`Thread.sleep(1))移到了锁定代码块之外。锁的持有时间大幅减少,锁定代码块仅包含了必要的操作(count++),减少了锁竞争和线程阻塞的时间,从而提高了系统的并发性能。package org.zyf.javabasic.thread.lock.opti; import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock; /** * @program: zyfboot-javabasic * @description: 优化后的版本 * @author: zhangyanfeng * @create: 2024-06-08 01:23 **/public class OptimizedCounter { private int count = 0; private final Lock lock = new ReentrantLock(); public void increment() { try { // Simulate some work that doesn't need to be locked Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } lock.lock(); try { count++; } finally { lock.unlock(); } } public int getCount() { lock.lock(); try { return count; } finally { lock.unlock(); } }}我们将通过创建多个线程对这两个计数器进行并发操作,并测量它们的执行时间:package org.zyf.javabasic.thread.lock.opti; /** * @program: zyfboot-javabasic * @description: LockPerformanceTest * @author: zhangyanfeng * @create: 2024-06-08 01:24 **/public class LockTimePerformanceTest { private static final int NUM_THREADS = 10; private static final int NUM_ITERATIONS = 1000; public static void main(String[] args) throws InterruptedException { System.out.println("UnoptimizedCounter Performance Test"); UnoptimizedCounter unoptimizedCounter = new UnoptimizedCounter(); testCounterPerformance(unoptimizedCounter); System.out.println("OptimizedCounter Performance Test"); OptimizedCounter optimizedCounter = new OptimizedCounter(); testCounterPerformance(optimizedCounter); } private static void testCounterPerformance(Object counter) throws InterruptedException { Thread[] threads = new Thread[NUM_THREADS]; long startTime = System.currentTimeMillis(); for (int i = 0; i < NUM_THREADS; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < NUM_ITERATIONS; j++) { if (counter instanceof UnoptimizedCounter) { ((UnoptimizedCounter) counter).increment(); } else if (counter instanceof OptimizedCounter) { ((OptimizedCounter) counter).increment(); } } }); threads[i].start(); } for (Thread thread : threads) { thread.join(); } long endTime = System.currentTimeMillis(); System.out.println("Execution Time: " + (endTime - startTime) + " ms"); if (counter instanceof UnoptimizedCounter) { System.out.println("Final count (Unoptimized): " + ((UnoptimizedCounter) counter).getCount()); } else if (counter instanceof OptimizedCounter) { System.out.println("Final count (Optimized): " + ((OptimizedCounter) counter).getCount()); } }}运行结果如下:UnoptimizedCounter Performance Test:Execution Time: 12656 msOptimizedCounter Performance Test:Execution Time: 1254 ms优化后的版本(OptimizedCounter)应该会显示更短的执行时间,从而验证了减少锁持有时间对性能的优化效果。(三)锁分级JVM中的锁分级机制主要包括偏向锁、轻量级锁和重量级锁。这些锁的分级和升级是为了在不同的线程竞争情况下,尽可能地减少锁的开销和提升性能。锁的升级过程是不可逆的,即从偏向锁到轻量级锁,再到重量级锁。如果一个锁已经升级为重量级锁,即使之后的竞争减少,也不会降级回轻量级锁或偏向锁。这样做是为了简化JVM的实现,避免复杂的锁降级逻辑带来的额外开销。这个在上文第二部分的开头就已经讲过了。(四)锁分离锁分离是一种针对不同类型操作进行区分的优化技术,通过使用不同的锁来分别处理读操作和写操作,来提高并发性能。读写锁(ReentrantReadWriteLock)就是一种典型的锁分离技术的实现。锁分离的核心思想在于,读操作和写操作对资源的影响不同,可以采用不同的锁策略来优化性能。其优化版本StampedLock 的性能优于 ReentrantReadWriteLock,StampedLock 使用乐观读锁来提高并发性,而 ReentrantReadWriteLock 使用悲观读锁。在适当的情况下,StampedLock 能够更高效地支持读写分离,这通常在读操作远远多于写操作的情况下更为明显。具体验证代码在上方第二部分的Lock中已经给出了样例,请使用中自行选择。(五)锁消除锁消除(Lock Elimination)是指 JVM 通过分析代码的运行范围,判断出某些锁在多线程环境下没有竞争,因此可以去掉这些锁操作。这个过程是由 JVM 在运行时通过即时编译器(JIT 编译器)和逃逸分析来决定的。逃逸分析(Escape Analysis)是锁消除的关键技术。它分析对象的作用范围,判断对象是否会逃逸出方法或线程。如果某个对象只在方法内部使用,并且不会逃逸出这个方法,则认为这个对象是线程私有的,锁操作就没有实际意义,可以消除。考虑以下两个字符串拼接的示例:package org.zyf.javabasic.thread.lock.opti; /** * @program: zyfboot-javabasic * @description: 两个字符串拼接的示例 * @author: zhangyanfeng * @create: 2024-06-08 01:49 **/public class LockEliminationExample { public static void main(String[] args) { long startTime; long endTime; // Test with StringBuffer startTime = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { concatenateStringBuffer("Hello", "World"); } endTime = System.currentTimeMillis(); System.out.println("StringBuffer Execution Time: " + (endTime - startTime) + " ms"); // Test with StringBuilder startTime = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { concatenateStringBuilder("Hello", "World"); } endTime = System.currentTimeMillis(); System.out.println("StringBuilder Execution Time: " + (endTime - startTime) + " ms"); } public static String concatenateStringBuffer(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb.toString(); } public static String concatenateStringBuilder(String s1, String s2) { StringBuilder sb = new StringBuilder(); sb.append(s1); sb.append(s2); return sb.toString(); }}StringBuffer 和 StringBuilder 都用于字符串拼接,但 StringBuffer 是线程安全的,通过内部使用的同步机制(锁)来保证线程安全,而 StringBuilder 则是非线程安全的,没有同步机制。当 JVM 通过逃逸分析发现 StringBuffer 对象没有逃逸出方法时,就会将其锁操作消除,从而使得它的性能和 StringBuilder 接近。(六)乐观锁乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试) 在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。具体的理论和使用验证可见:Java中常用的锁总结与理解当然最直接的可见:超越并发瓶颈:CAS与乐观锁的智慧应用(七)无锁无锁队列是一种在多线程环境下访问共享资源的方式,它不会阻塞其他线程的执行。在 Java 中,最典型的无锁队列实现是 ConcurrentLinkedQueue,它使用CAS(Compare and Swap)指令来处理对数据的并发访问。CAS指令是一种非阻塞的原子操作,不会引起上下文切换和线程调度,因此是一种非常轻量级的多线程同步机制。ConcurrentLinkedQueue的实现基于CAS机制,它将队列的入队和出队等操作拆分为更细粒度的步骤,进一步减小了CAS控制的范围,提高了并发性能。与之相对应的是阻塞队列LinkedBlockingQueue,它内部使用锁机制来实现同步,因此性能没有无锁队列高。除了ConcurrentLinkedQueue外,还有一种无锁队列框架叫做Disruptor,它是一个无锁、有界的队列框架,具有极高的性能。Disruptor使用RingBuffer、无锁和缓存行填充等技术来实现高效的并发访问,适用于极高并发的场景,比如日志、消息等中间件。尽管Disruptor的编程模型相对复杂,但在需要极致性能的场景下,可以取代传统的阻塞队列。四、总结在《Java 同步锁性能的最佳实践:从理论到实践的完整指南》中,我们深入探讨了Java中同步锁的性能及其优化手段,从基础理论到实际应用案例,为开发者提供了全面的指导。首先,我们分析了同步锁的性能,使用代码示例验证了使用与不使用同步锁对程序性能的影响,明确了同步锁在多线程环境中的必要性和影响因素。通过案例初步优化分析,运用AtomicInteger进行性能优化,进一步引入LongAdder的原理,使读者在实际开发中能够更好地应对高并发场景下的性能问题。接着,我们回顾了Java锁的基本概念与实现机制,包括synchronized关键字的监视器锁实现原理、锁的分级以及锁升级的过程。对比了Java concurrent包中的Lock接口,包括可重入锁、读写锁和StampedLock的机制,强调了不同锁机制在性能上的差异及其适用场景。最后,我们总结了锁的优化手段,包括减少锁的粒度、持有时间、锁分级、锁分离、锁消除、乐观锁以及无锁技术等。这些优化策略不仅提升了程序的性能,也为并发编程提供了更为灵活的解决方案。通过本指南,开发者不仅能够掌握Java中同步锁的基本使用,还能深入理解各种锁的优缺点及其适用场景,为在多线程环境下构建高效、稳定的应用程序奠定了坚实的基础。在未来的开发中,灵活运用这些优化手段,将为提高系统性能提供更多可能性。———————————————— 原文链接:https://blog.csdn.net/xiaofeng10330111/article/details/139611703
-
SSL/TLS(弱加密)抓包解密办法
-
大家好,首先祝大家新年快乐,身体健康,代码没bug,投产顺利。本次整理带来的有关于mybatis,java,spring,springboot,linux,shell,Python,HarmonyOS,算法等多种类技术干货,希望可以帮到大家。 1.MyBatis 探秘之#{} 与 ${} 参传差异解码(数据库连接池筑牢数据交互根基)【转】 https://bbs.huaweicloud.com/forum/thread-0296171007849327168-1-1.html 2.scala中正则表达式的使用详解【转】 https://bbs.huaweicloud.com/forum/thread-02119171007725887139-1-1.html 3.SpringBoot实现websocket服务端及客户端的详细过程【转】 https://bbs.huaweicloud.com/forum/thread-0217171007411153148-1-1.html 4.spring 参数校验Validation示例详解【转】 https://bbs.huaweicloud.com/forum/thread-02109171007351771185-1-1.html 5.java集成kafka实例代码【转】 https://bbs.huaweicloud.com/forum/thread-02112171007196458168-1-1.html 6.SpringBoot中Get请求和POST请求接收参数示例详解【转】 https://bbs.huaweicloud.com/forum/thread-02112171007106397167-1-1.html 7.Java中StopWatch工具类的用法详解【转】 https://bbs.huaweicloud.com/forum/thread-0263171006880471176-1-1.html 8.Linux下shell基本命令之grep用法及示例小结【转】 https://bbs.huaweicloud.com/forum/thread-02119170989547937131-1-1.html 9.Python使用PIL库拼接图片的详细教程【转】 https://bbs.huaweicloud.com/forum/thread-02119170989479126130-1-1.html 10.bash shell的条件语句详解【转】 https://bbs.huaweicloud.com/forum/thread-0217170989387999141-1-1.html 11.pandas数据缺失的两种处理办法【转】 https://bbs.huaweicloud.com/forum/thread-02112170989326183158-1-1.html 12.Python使用PyQt5实现中英文切换功能【转】 https://bbs.huaweicloud.com/forum/thread-0263170989204908173-1-1.html 13.python螺旋数字矩阵的实现示例【转】 https://bbs.huaweicloud.com/forum/thread-0241170989116650156-1-1.html 14.使用Python实现文件查重功能【转】 https://bbs.huaweicloud.com/forum/thread-02109170989032191180-1-1.html 15.Linux内核验证套件(LKVS) https://bbs.huaweicloud.com/forum/thread-0217170613884758126-1-1.html 16.三大排序算法:插入排序、希尔排序、选择排序 https://bbs.huaweicloud.com/forum/thread-0263170613042983158-1-1.html 17.【Linux】多用户协作-转载 https://bbs.huaweicloud.com/forum/thread-02109170612919963145-1-1.html 18.SSH可以连接但sftp确无法链接,有可能是防火墙的问题吗-转载 https://bbs.huaweicloud.com/forum/thread-0217170612856550125-1-1.html 19.【Linux】线程同步与互斥 (生产者消费者模型-转载 https://bbs.huaweicloud.com/forum/thread-02112170612824528138-1-1.html 20.【HarmonyOS】公司鸿蒙项目收工总结之《组件》 https://bbs.huaweicloud.com/forum/thread-0263170475389496136-1-1.html 21.【HarmonyOS】高仿华为阅读翻页 https://bbs.huaweicloud.com/forum/thread-02109170473860592128-1-1.html 22.【HarmonyOS】仿iOS线性渐变实现 https://bbs.huaweicloud.com/forum/thread-0241170473759150128-1-1.html 23.【HarmonyOS】利用emitter封装工具类 https://bbs.huaweicloud.com/forum/thread-0276170473613513148-1-1.html 24.【HarmonyOS】多Toast显示工具类 https://bbs.huaweicloud.com/forum/thread-0217170472563245114-1-1.html 25.【HarmonyOS】头像裁剪圆形遮罩效果实现demo https://bbs.huaweicloud.com/forum/thread-0276170472467118147-1-1.html
-
stopWatch 是org.springframework.util 包下的一个工具类,使用它可直观的输出代码执行耗时,以及执行时间百分比,下面就跟随小编一起来看看它的具体用法吧stopWatch 是org.springframework.util 包下的一个工具类,使用它可直观的输出代码执行耗时,以及执行时间百分比。在未使用这个工具类之前,如果我们需要统计某段代码的耗时,我们会这样写:public static void main(String[] args) throws InterruptedException { test0(); } public static void test0() throws InterruptedException { long start = System.currentTimeMills(); // do something Thread.sleep(100); long end = System.currentTimeMills(); long start2 = System.currentTimeMills(); // do somethind long end2 = System.currentTimeMills(); System.out.println("某某1执行耗时:" + (end -start)); System.out.println("某某2执行耗时:" + (end2 -start2)); }如果改用stopWatch 实现的一个示例public class StopWatchDemo { public static void main(String[] args) throws InterruptedException { test1(); } public static void test1() throws InterruptedException { StopWatch sw = new StopWatch("test"); sw.start("task1"); // do something Thread.sleep(100); sw.stop(); sw.start("task2"); // do someting Thread.sleep(200); sw.stop(); System.out.println("sw.prettyPrint()-------"); System.out.println(sw.prettyPrint()); } }运行结果如下:sw.prettyPrint()------StopWatch 'test': running time (millis) = 310-----------------------------------------ms % Task name-----------------------------------------00110 035% task100200 065% task2通过start 与stop 方法分别记录开始时间与结束时间,其中在记录结束时间的时候,会维护一个链表类型的taskList 属性,从而时该类可记录多个任务,最后的输出页仅仅是对之前的记录信息做了一个统一的归纳输出。
-
本文详细介绍了SpringBoot的核心特性,包括约定优于配置、自动配置机制、日志框架的使用,以及源码分析部分,如自动配置的加载过程、SpringBoot启动流程、数据源和Mybatis的自动配置。此外,还涉及到了SpringBoot的缓存管理和数据访问解析,包括JSR107标准和Spring缓存注解的使用。 一、SpringBoot简介 1.1 原有Spring优缺点分析 1.1.1 Spring的优点分析 Spring是Java企业版(Java Enterprise Edition,JEE,也称J2EE)的轻量级代替品。无需开发重量级的Enterprise JavaBean(EJB),Spring为企业级Java开发提供了一种相对简单的方法,通过依赖注入和面向切面编程,用简单的Java对象(Plain Old Java Object,POJO)实现了EJB的功能。 1.1.2 Spring的缺点分析 虽然Spring的组件代码是轻量级的,但它的配置却是重量级的。一开始,Spring用XML配置,而且是很多XML配置。Spring 2.5引入了基于注解的组件扫描,这消除了大量针对应用程序自身组件的显式XML配置。Spring 3.0引入了基于Java的配置,这是一种类型安全的可重构配置方式,可以代替XML。 所有这些配置都代表了开发时的损耗。因为在思考Spring特性配置和解决业务问题之间需要进行思维切换,所以编写配置挤占了编写应用程序逻辑的时间。和所有框架一样,Spring实用,但与此同时它要求的回报也不少。 除此之外,项目的依赖管理也是一件耗时耗力的事情。在环境搭建时,需要分析要导入哪些库的坐标,而且还需要分析导入与之有依赖关系的其他库的坐标,一旦选错了依赖的版本,随之而来的不兼容问题就会严重阻碍项目的开发进度。 1.2 SpringBoot的概述 1.2.1 SpringBoot解决上述Spring的缺点 SpringBoot对上述Spring的缺点进行的改善和优化,基于约定优于配置的思想,可以让开发人员不必在配置与逻辑业务之间进行思维的切换,全身心的投入到逻辑业务的代码编写中,从而大大提高了开发的效率,一定程度上缩短了项目周期。 1.2.2 SpringBoot的特点 为基于Spring的开发提供更快的入门体验 开箱即用,没有代码生成,也无需XML配置。同时也可以修改默认值来满足特定的需求 提供了一些大型项目中常见的非功能性特性,如嵌入式服务器、安全、指标,健康检测、外部配置等 SpringBoot不是对Spring功能上的增强,而是提供了一种快速使用Spring的方式 1.2.3 SpringBoot的核心功能 起步依赖 起步依赖本质上是一个Maven项目对象模型(Project Object Model,POM),定义了对其他库的传递依赖,这些东西加在一起即支持某项功能。 简单的说,起步依赖就是将具备某种功能的坐标打包到一起,并提供一些默认的功能。 自动配置 Spring Boot的自动配置是一个运行时(更准确地说,是应用程序启动时)的过程,考虑了众多因素,才决定Spring配置应该用哪个,不该用哪个。该过程是Spring自动完成的。 简单项目结构 SpringBoot项目简单结构如下: ├── demo1 │ ├── pom.xml # 依赖管理 │ ├── src │ │ ├── main │ │ │ ├── java # 核心代码 │ │ │ │ └── online │ │ │ │ └── yuluo │ │ │ │ └── demo1 │ │ │ │ ├── Demo1Application.java │ │ │ │ ├── controller │ │ │ │ ├── dao │ │ │ │ └── service │ │ │ └── resources #项目资源文件 │ │ │ ├── application.properties #项目配置文件 可用application.yml代替 │ │ │ ├── static #静态资源文件 │ │ │ └── templates #模板文件,可存放邮件、SMS等模板 │ │ └── test #测试 │ │ └── java │ │ └── online │ │ └── yuluo │ │ └── demo1 │ │ └── Demo1ApplicationTests.java │ └── target #打包目标生成文件 ———————————————— 原文链接:https://blog.csdn.net/weixin_44935456/article/details/108475035
-
一、简介 1.1、什么是SpringBoot 我们知道,从 2002 年开始,Spring 一直在飞速的发展,如今已经成为了在Java EE(Java Enterprise Edition)开发中真正意义上的标准,但是随着技术的发展,Java EE使用 Spring 逐渐变得笨重起来,大量的 XML 文件存在于项目之中。繁琐的配置,整合第三方框架的配置问题,导致了开发和部署效率的降低。 2012 年 10 月,Mike Youngstrom 在 Spring jira 中创建了一个功能请求,要求在 Spring 框架中支持无容器 Web 应用程序体系结构。他谈到了在主容器引导 spring 容器内配置 Web 容器服务。这是 jira 请求的摘录: 我认为 Spring 的 Web 应用体系结构可以大大简化,如果它提供了从上到下利用 Spring 组件和配置模型的工具和参考体系结构。在简单的 main()方法引导的 Spring 容器内嵌入和统一这些常用Web 容器服务的配置。 这一要求促使了 2013 年初开始的 Spring Boot 项目的研发,到今天,Spring Boot 的版本已经到了 2.0.3 RELEASE。Spring Boot 并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。 它集成了大量常用的第三方库配置,Spring Boot应用中这些第三方库几乎可以是零配置的开箱即用(out-of-the-box),大部分的 Spring Boot 应用都只需要非常少量的配置代码(基于 Java 的配置),开发者能够更加专注于业务逻辑。 1.2.、特性 快速创建独立 Spring 应用 SSM:导包、写配置、启动运行 直接嵌入Tomcat、Jetty or Undertow(无需部署 war 包)【Servlet容器】 linux java tomcat mysql: war 放到 tomcat 的 webapps下 jar: java环境; java -jar 重点:提供可选的starter,简化应用整合 场景启动器(starter):web、json、邮件、oss(对象存储)、异步、定时任务、缓存... 导包一堆,控制好版本。 为每一种场景准备了一个依赖; web-starter。mybatis-starter 重点:按需自动配置 Spring 以及 第三方库 如果这些场景我要使用(生效)。这个场景的所有配置都会自动配置好。 约定大于配置:每个场景都有很多默认配置。 自定义:配置文件中修改几项就可以 提供生产级特性:如 监控指标、健康检查、外部化配置等 监控指标、健康检查(k8s)、外部化配置 无代码生成、无xml 1.3、四大核心 自动配置、起步依赖、Actuator、命令行界面 1.4、特点 简化开发,简化配置,简化整合,简化部署,简化监控,简化运维。 二、快速入门 2.1、开发流程 2.1.1、创建项目 maven项目 <!-- 所有springboot项目都必须继承自 spring-boot-starter-parent --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.0.5</version> </parent> 2.1.2、导入场景 场景启动器 <dependencies> <!-- web开发的场景启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> 2.1.3、主程序 @SpringBootApplication //这是一个SpringBoot应用 public class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class,args); } } 2.1.4、业务 @RestController public class HelloController { @GetMapping("/hello") public String hello(){ return "Hello,Spring Boot 3!"; } } 2.1.5、测试 默认启动访问:localhost:8080 三、Spring Initializr 创建向导 一键创建好整个项目结构 3.1、依赖管理机制 3.1.1、为什么导入starter-web所有相关依赖都导入进来? 开发什么场景,导入什么场景启动器。 maven依赖传递原则。A-B-C: A就拥有B和C 导入 场景启动器。 场景启动器 自动把这个场景的所有核心依赖全部导入进来 3.1.2、为什么版本号不用写? 每个boot项目都有一个父项目spring-boot-starter-parent parent的父项目是spring-boot-dependencies 父项目 版本仲裁中心,把所有常见的jar的依赖版本都声明好了。 比如:mysql-connector-j 3.1.3、自定义版本号 利用maven的就近原则 直接在当前项目properties标签中声明父项目用的版本属性的key 直接在导入依赖的时候声明版本 3.1.4、第三方的jar包 boot父项目没有管理的需要自行声明好 <!-- https://mvnrepository.com/artifact/com.alibaba/druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.16</version> </dependency> 四、自动配置机制 4.1、 初步理解 自动配置的Tomcat、SpringMVC等 导入场景,容器中就会自动配置好这个场景的核心组件。 以前:DispatcherServlet、ViewResolver、CharacterEncodingFilter…… 现在:自动配置好的这些组件 验证:容器中有了什么组件,就具有什么功能 public static void main(String[] args) { //java10: 局部变量类型的自动推断 var ioc = SpringApplication.run(MainApplication.class, args); //1、获取容器中所有组件的名字 String[] names = ioc.getBeanDefinitionNames(); //2、挨个遍历: // dispatcherServlet、beanNameViewResolver、characterEncodingFilter、multipartResolver // SpringBoot把以前配置的核心组件现在都给我们自动配置好了。 for (String name : names) { System.out.println(name); } } 默认的包扫描规则 @SpringBootApplication 标注的类就是主程序类 SpringBoot只会扫描主程序所在的包及其下面的子包,自动的component-scan功能 自定义扫描路径 @SpringBootApplication(scanBasePackages = "com.atguigu") @ComponentScan("com.atguigu") 直接指定扫描的路径 配置默认值 配置文件的所有配置项是和某个类的对象值进行一一绑定的。 绑定了配置文件中每一项值的类: 属性类。 比如: ServerProperties绑定了所有Tomcat服务器有关的配置 ....参照官方文档:或者参照 绑定的 属性类。 MultipartProperties绑定了所有文件上传相关的配置 按需加载自动配置 导入场景spring-boot-starter-web 场景启动器除了会导入相关功能依赖,导入一个spring-boot-starter,是所有starter的starter,基础核心starter spring-boot-starter导入了一个包 spring-boot-autoconfigure。包里面都是各种场景的AutoConfiguration自动配置类 虽然全场景的自动配置都在 spring-boot-autoconfigure这个包,但是不是全都开启的。 导入哪个场景就开启哪个自动配置 总结: 导入场景启动器、触发 spring-boot-autoconfigure这个包的自动配置生效、容器中就会具有相关场景的功能 4.2、 完整流程 自动配置流程细节梳理: 1、导入starter-web:导入了web开发场景 1、场景启动器导入了相关场景的所有依赖:starter-json、starter-tomcat、springmvc 2、每个场景启动器都引入了一个spring-boot-starter,核心场景启动器。 3、核心场景启动器引入了spring-boot-autoconfigure包。 4、spring-boot-autoconfigure里面囊括了所有场景的所有配置。 5、只要这个包下的所有类都能生效,那么相当于SpringBoot官方写好的整合功能就生效了。 6、SpringBoot默认却扫描不到 spring-boot-autoconfigure下写好的所有配置类。(这些配置类给我们做了整合操作),默认只扫描主程序所在的包。 2、主程序:@SpringBootApplication 1、@SpringBootApplication由三个注解组成@SpringBootConfiguration、@EnableAutoConfiguratio、@ComponentScan 2、SpringBoot默认只能扫描自己主程序所在的包及其下面的子包,扫描不到 spring-boot-autoconfigure包中官方写好的配置类 3、@EnableAutoConfiguration:SpringBoot 开启自动配置的核心。 1. 是由@Import(AutoConfigurationImportSelector.class)提供功能:批量给容器中导入组件。 2. SpringBoot启动会默认加载 142个配置类。 3. 这142个配置类来自于spring-boot-autoconfigure下 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件指定的 项目启动的时候利用 @Import 批量导入组件机制把 autoconfigure 包下的142 xxxxAutoConfiguration类导入进来(自动配置类) 虽然导入了142个自动配置类 4、按需生效: 并不是这142个自动配置类都能生效 每一个自动配置类,都有条件注解@ConditionalOnxxx,只有条件成立,才能生效 3、xxxxAutoConfiguration自动配置类 1、给容器中使用@Bean 放一堆组件。 2、每个自动配置类都可能有这个注解@EnableConfigurationProperties(ServerProperties.class),用来把配置文件中配的指定前缀的属性值封装到 xxxProperties属性类中 3、以Tomcat为例:把服务器的所有配置都是以server开头的。配置都封装到了属性类中。 4、给容器中放的所有组件的一些核心参数,都来自于xxxProperties。xxxProperties都是和配置文件绑定。 只需要改配置文件的值,核心组件的底层参数都能修改 ———————————————— 原文链接:https://blog.csdn.net/nine06/article/details/136487114
-
Spring Boot是Spring生态系统中的重要组成部分,它极大地简化了Spring应用的开发和配置。本文将详细介绍Spring Boot的核心概念、关键特性及其在实际开发中的应用,帮助读者全面掌握Spring Boot的使用。 1. Spring Boot简介 1.1 什么是Spring Boot? Spring Boot是由Pivotal团队开发的基于Spring框架的项目,旨在简化新Spring应用的初始搭建及开发过程。通过提供一系列默认配置和自动化功能,Spring Boot可以大幅减少配置文件的数量和复杂度,使开发者能够专注于业务逻辑的实现。 1.2 Spring Boot的历史背景 Spring Boot最早于2014年发布,其设计初衷是为了应对复杂的企业级应用开发中频繁出现的配置冗余和重复代码问题。通过Spring Boot,开发者可以更快地启动一个新项目,并迅速进入实际开发阶段。 1.3 Spring Boot的核心特点 自动配置:Spring Boot自动配置机制能根据类路径中的依赖和环境,自动配置Spring应用程序。 独立运行:Spring Boot应用可以打包成JAR文件并独立运行,不依赖外部的应用服务器。 生产就绪:内置的监控、健康检查及外部配置功能,使应用能够在生产环境中平稳运行。 简化的依赖管理:通过Spring Boot Starter简化依赖管理和版本控制。 2. Spring Boot的核心概念 2.1 自动配置 自动配置是Spring Boot的核心特性之一。它通过@EnableAutoConfiguration注解实现,根据类路径中的依赖自动配置合适的Spring组件。 2.1.1 自动配置原理 Spring Boot的自动配置通过扫描META-INF/spring.factories文件,加载其中定义的自动配置类。每个自动配置类都会根据一定的条件(如类路径中是否存在特定的类或Bean)来决定是否生效。 2.1.2 自定义配置 虽然自动配置为开发者提供了极大的便利,但有时需要自定义配置以满足特定需求。可以通过以下几种方式进行自定义配置: 配置属性:在application.properties或application.yml文件中配置属性。 配置类:创建配置类并使用@Configuration注解。 排除自动配置:通过@SpringBootApplication(exclude = ...)注解排除特定的自动配置类。 2.2 Spring Boot Starter Spring Boot Starter是Spring Boot提供的依赖管理机制,通过预定义的一组依赖,简化项目中的依赖管理。例如,spring-boot-starter-web包含了开发Web应用所需的所有基本依赖。 2.3 Spring Boot CLI Spring Boot CLI(命令行界面)是一个用于快速创建、运行和测试Spring Boot应用的工具。通过Spring Boot CLI,开发者可以使用Groovy脚本快速搭建Spring Boot应用。 3. Spring Boot的主要功能模块 3.1 Web开发 Spring Boot通过spring-boot-starter-web提供了简便的Web开发支持。这个Starter包括Spring MVC、Jackson和Tomcat(默认嵌入式容器)。 3.1.1 Spring MVC Spring MVC是Spring框架的核心Web模块,支持创建基于注解的Web应用。通过Spring Boot,开发者可以轻松配置和使用Spring MVC。 示例: @RestController public class HelloController { @GetMapping("/hello") public String hello() { return "Hello, Spring Boot!"; } } 3.1.2 嵌入式服务器 Spring Boot默认使用Tomcat作为嵌入式服务器,但也支持Jetty和Undertow。嵌入式服务器使应用可以打包成JAR文件,并通过简单的命令运行: java -jar myapp.jar 3.2 数据访问 Spring Boot提供了一整套便捷的数据访问解决方案,包括Spring Data JPA、JDBC和Redis等。 3.2.1 Spring Data JPA Spring Data JPA通过spring-boot-starter-data-jpa简化了JPA的使用。只需简单配置即可连接数据库并进行CRUD操作。 示例: @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; // getters and setters } public interface UserRepository extends JpaRepository<User, Long> { } 3.2.2 数据库配置 在application.properties中配置数据库连接信息: spring.datasource.url=jdbc:mysql://localhost:3306/mydb spring.datasource.username=root spring.datasource.password=secret spring.jpa.hibernate.ddl-auto=update 3.3 安全管理 Spring Boot通过spring-boot-starter-security提供了Spring Security的默认配置,使应用能够轻松实现认证和授权功能。 3.3.1 基本安全配置 默认情况下,Spring Security会保护所有的HTTP端点,需要用户进行身份验证。可以通过自定义配置类来调整安全设置: @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/public/**").permitAll() .anyRequest().authenticated() .and() .formLogin().and() .httpBasic(); } } 3.4 测试支持 Spring Boot提供了强大的测试支持,包括单元测试和集成测试工具。 3.4.1 单元测试 使用@SpringBootTest注解,可以方便地加载Spring应用上下文进行测试: @RunWith(SpringRunner.class) @SpringBootTest public class MyApplicationTests { @Autowired private MockMvc mockMvc; @Test public void testHelloEndpoint() throws Exception { mockMvc.perform(get("/hello")) .andExpect(status().isOk()) .andExpect(content().string("Hello, Spring Boot!")); } } 3.4.2 集成测试 Spring Boot集成测试可以测试应用的整个运行环境,包括数据库连接和Web服务器: @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class MyApplicationTests { @LocalServerPort private int port; @Test public void testHomePage() throws Exception { URL url = new URL("http://localhost:" + port + "/"); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); assertEquals(200, connection.getResponseCode()); } } 4. Spring Boot实战案例 4.1 创建一个简单的RESTful API 4.1.1 项目结构 src └── main ├── java │ └── com.example.demo │ ├── DemoApplication.java │ ├── controller │ │ └── UserController.java │ ├── model │ │ └── User.java │ └── repository │ └── UserRepository.java └── resources └── application.properties 4.1.2 代码实现 DemoApplication.java @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } } User.java @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String email; // getters and setters } UserRepository.java public interface UserRepository extends JpaRepository<User, Long> { } UserController.java @RestController @RequestMapping("/users") public class UserController { @Autowired private UserRepository userRepository; @GetMapping public List<User> getAllUsers() { return userRepository.findAll(); } @PostMapping public User createUser(@RequestBody User user) { return userRepository.save(user); } @GetMapping("/{id}") public User getUserById(@PathVariable Long id) { return userRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("User not found")); } @PutMapping("/{id}") public User updateUser(@PathVariable Long id, @RequestBody User userDetails) { User user = userRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("User not found")); user.setName(userDetails.getName()); user.setEmail(userDetails.getEmail()); return userRepository.save(user); } @DeleteMapping("/{id}") public ResponseEntity<?> deleteUser(@PathVariable Long id) { User user = userRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("User not found")); userRepository.delete(user); return ResponseEntity.ok().build(); } } 4.1.3 配置文件 application.properties spring.datasource.url=jdbc:mysql://localhost:3306/demo spring.datasource.username=root spring.datasource.password=secret spring.jpa.hibernate.ddl-auto=update 5. 总结 Spring Boot通过提供自动配置、简化依赖管理和独立运行等特性,大大提高了开发效率,使得构建和部署Spring应用变得更加简单。本文介绍了Spring Boot的核心概念和主要功能模块,并通过一个简单的RESTful API示例展示了Spring Boot的实际应用。掌握Spring Boot的使用,不仅可以提升开发效率,还能更好地应对复杂的企业级应用开发需求。 Spring Boot的生态系统仍在不断发展和完善,未来的版本将引入更多新特性和改进。通过不断学习和实践,开发者可以充分利用Spring Boot的优势,构建高质量的Java应用程序。 ———————————————— 原文链接:https://blog.csdn.net/Easonmax/article/details/139358071
-
本文介绍了Spring Boot的发展历程、核心概念及其与Spring的关系。Spring Boot简化了Spring应用程序的开发,通过自动配置和starter依赖,解决了Spring配置复杂的问题。文章详细阐述了Spring的历史,Spring Boot的诞生背景,以及它如何解决Spring的配置难题,成为微服务时代的热门框架。此外,还探讨了Spring Boot与Spring MVC、微服务和Spring Cloud的关系,以及使用Spring Boot的八大理由。 Spring Boot是一个全新的框架,是用来简化Spring应用的初始搭建及开发过程的,可以使用特定的方式来进行配置,可使得开发人员不在需要定义样板化的配置,所以,Spring boot能够大大简化开发模式,学习spring boot可以将你想集成的常用框架,都有对应的组件支持。 Springboot如何系统学习? 1、理论联系实践 在很多时候,我们接触到一个新的技术的时候,最开始肯定是被这些技术涉及到的术语、词汇所困扰,不明白这些技术术语词汇的定义、概念、含义,没有这些做根基,就很难做到掌握和学习这个技术,并达到融汇贯通的程度。所以学习SpringBoot,首先就要从宏观的层面上,去了解这个技术它的背景知识、运用场景、发展渊源,演进历史等。 2、多访问官方网站了解官方定义和解读 建议访问spring官网,获取最权威的介绍和定义。 3、全面系统的从基础知识入手,包括但不仅限于如下知识点: 框架原理介绍 框架环境搭建 快速入门 创建Bean的方式及实现原理 Bean种类 Bean生命周期 Bean的作用域 Bean的注值方式 SpEL 整合Junit测试 Web项目集成Spring 注解装配Bean AOP思想、原理解剖 传统方式实现AOP开发 AspectJ介绍及实现AOP开发 使用Spring Boot有什么好处? 其实就是简单、快速、方便!平时如果我们需要搭建一个Spring Web项目的时候需要怎么做呢? 1)配置web.xml,加载Spring和Spring mvc 2)配置数据库连接、配置Spring事务 3)配置加载配置文件的读取,开启注解 4)配置日志文件 配置完成之后部署Tomcat调试 现在非常流行微服务,如果我这个项目仅仅只是需要发送一个邮件,如果我的项目仅仅是生产一个积分;我都需要这样折腾一遍! 但是如果使用Spring Boot呢? 很简单,我仅仅只需要非常少的几个配置就可以迅速方便的搭建起来一套Web项目或者是构建一个微服务! SpringBoot所具备的特征 (1)可以创建独立的Spring应用程序,并且基于其Maven或Gradle插件,可以创建可执行的JARs和WARs; (2)内嵌Tomcat或Jetty等Servlet容器; (3)提供自动配置的“starter”项目对象模型(POMS)以简化Maven配置; (4)尽可能自动配置Spring容器; (5)提供准备好的特性,如指标、健康检查和外部化配置; (6)绝对没有代码生成,不需要XML配置。 Spring Boot官方提供了很多Starter组件,涉及Web、模板引擎、SQL、NoSQL、缓存、验证、日志、测试、内嵌容器,还提供了事务、消息、安全、监控、大数据等支持。前面模块会在本书中一一介绍,后面这些模块本书不会涉及,如需自行请参看Spring Boot官方文档。 每个模块会有多种技术实现选型支持,来实现各种复杂的业务需求: Web:Spring Web、Spring WebFlux等 模板引擎:Thymeleaf、FreeMarker、Groovy、Mustache等 SQL:MySQL、H2等 NoSQL:Redis、MongoDB、Cassandra、Elasticsearch等 验证框架:Hibernate Validator、Spring Validator等 日志框架:Log4j2、Logback等 测试:JUnit、Spring Boot Test、AssertJ、Mockito等 内嵌容器:Tomcat、Jetty、Undertow等 另外,Spring WebFlux框架目前支持Servlet 3.1以上的Servlet容器和Netty,各种模块组成了Spring Boot 2.x的工作常用技术栈,如图1-1所示。 动力节点的 SpringBoot入门教程由浅入深,手把手带你学习Spring Boot,体验Spring Boot的极速开发过程,内容丰富,涵盖了SpringBoot开发的方方面面,并且同步更新到Spring 2.x版本。 ———————————————— 原文链接:https://blog.csdn.net/Javanewspaper/article/details/121422135
-
Spring Framework 是跨平台 Java/Spring 应用程序开发框架,也是 J2EE(Java 2 Platform, Enterprise Edition) 轻量级框架,其 Spring 平台为 Java 开发者提供了全面的基础设施支持。 Spring 许多基础组件的代码是轻量级,但其配置依旧是重量级的。它是怎么解决了呢?当然是 Spring Boot,Spring Boot 提供了新的编程模式,让开发 Spring 应用变得更加简单方便。本书将会由各个最佳实践案例驱动,涉及 Spring Boot 开发相关方面。下面先了解下 Spring Boot 框架。1.1 Spring Boot 是什么Spring Boot (Boot 顾名思义,引导的意思)框架是简化 Spring 应用从搭建到开发的过程。应用开箱即用,只要通过一个指令,包括命令行 java -jar 、SpringApplication 应用启动类 、 Spring Boot Maven 插件等,就可以启动应用了。另外,Spring Boot 强调只需要很少的配置文件,所以在开发生产级 Spring 应用中,让开发变得高效和简易。1.1.1 Spring Boot 2.x 特性那么 Spring Boot 2.x 具有哪些生产的特性呢?常用的特性如下:SpringApplication 应用类自动配置外化配置内嵌容器Starter 组件还有对日志、Web、消息、测试及扩展等支持。SpringApplicationSpringApplication 是 Spring Boot 应用启动类,在 main() 方法中调用 SpringApplication.run() 静态方法,即可运行一个 Spring Boot 应用。简单使用代码片段如下:public static void main(String[] args) { SpringApplication.run(QuickStartApplication.class, args); }AI助手Spring Boot 运行的应用是独立的一个 Jar 应用,实际上在运行时启动了应用内部内嵌容器,容器初始化 Spring 环境及其组件。也可以使用 Spring Boot 开发传统的应用,只要通过 Spring Boot Maven 插件将 Jar 应用转换成 War 应用即可。自动配置Spring Boot 在不需要任何配置情况下,就直接可以运行一个应用。实际上,Spring Boot 框架的 spring-boot-autoconfigure 依赖做了很多默认的配置项,即应用默认值。这种模式叫做 “自动配置”。Spring Boot 自动配置会根据添加的依赖,自动加载依赖相关的配置属性。例如,默认用的内嵌式容器是 Tomcat 并端口设置为 8080。外化配置Spring Boot 简化了配置,基本在 application.properties 文件配置常用的应用属性。Spring Boot 可以将配置外部化,这种模式叫做 “外化配置”。将配置从代码中分离外置,最明显的作用是只有简单地修改下外化配置文件,就可以在不同环境中,可以运行相同的应用代码。配置相关的会在第 2 章进行实践介绍。内嵌容器Spring Boot 启动应用,默认情况下是自动启动了内嵌容器 Tomcat,并且自动设置了端口为 8080。另外还提供了对 Jetty、Undertow 等容器的支持。开发者自行在添加对应的容器 Starter 组件,即可配置对应内嵌容器实例。Starter 组件Starter 组件,其开箱即用,是 Spring Boot 重要的组成部分。实际上,Starter 组件是一组可以被加载在应用中的 Maven 依赖项,只需要对应在 Maven 配置中添加依赖配置,即可开启对应依赖使用。例如,添加 spring-boot-starter-web 依赖,就可用于构建 RESTful Web 服务,其包含了 Spring MVC 和 Tomcat 内嵌容器等。其实,开发中很多功能是通过添加 Starter 组件的方式来进行实现。那么,Spring Boot 2.x 常用的 Starter 组件有哪些呢?Spring Boot 2.x Starter 组件Spring Boot 官方提供了很多 Starter 组件,涉及 Web、模板引擎、SQL 、NoSQL、缓存、验证、日志、测试、内嵌容器等,还提供了事务、消息、安全、监控、大数据等支持。原文链接:https://blog.csdn.net/Majker/article/details/84728820
上滑加载中
推荐直播
-
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步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签