• [技术干货] Java 代码性能优化
    1.尽量避免过多过常地创建Java对象尽量避免在经常调用的方法,循环中new对象,由于系统不仅要花费时间来创建对象,而且还要花时间对这些对象进行垃圾回收和处理,在我们可以控制的范围内,最大限度地重用对象,最好能用基本的数据类型或数组来替代对象。2. 尽量使用基本数据类型代替对象 String str = "hello";上面这种方式会创建一个“hello”字符串,而且JVM的字符缓存池还会缓存这个字符串;String str = new String("hello");此时程序除创建字符串外,str所引用的String对象底层还包含一个char[]数组,这个char[]数组依次存放了h,e,l,l,o3.尽量合理的创建HashMap当你要创建一个比较大的hashMap时,充分利用这个构造函数public HashMap(int initialCapacity, float loadFactor);避免HashMap多次进行了hash重构,扩容是一件很耗费性能的事,在默认中initialCapacity只有16,而loadFactor是 0.75,需要多大的容量,你最好能准确的估计你所需要的最佳大小,同样的Hashtable,Vectors也是一样的道理。4.尽量减少对变量的重复计算如:for(int i=0;i<list.size();i++)应该改为:for(int i=0,len=list.size();i<len;i++)并且在循环中应该避免使用复杂的表达式,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快。5.尽量早释放无用对象的引用大部分时,方法局部引用变量所引用的对象会随着方法结束而变成垃圾,因此,大部分时候程序无需将局部,引用变量显式设为null。例如:Public void test(){ Object obj = new Object(); …… Obj=null; }上面这个就没必要了,随着方法test()的执行完成,程序中obj引用变量的作用域就结束了。但是如果是改成下面:Public void test(){ Object obj = new Object(); …… Obj=null; //执行耗时,耗内存操作;或调用耗时,耗内存的方法 …… }这时候就有必要将obj赋值为null,可以尽早的释放对Object对象的引用。6. 尽量缓存经常使用的对象尽可能将经常使用的对象进行缓存,可以使用数组,或HashMap的容器来进行缓存,但这种方式可能导致系统占用过多的缓存,性能下降,推荐可以使用一些第三方的开源工具,如EhCache,Oscache进行缓存,他们基本都实现了FIFO/FLU等缓存算法。7. 尽量重用对象特别是String对象的使用中,出现字符串连接情况时应使用StringBuffer代替,由于系统不仅要花时间生成对象,以后可能还需要花时间对这些对象进行垃圾回收和处理。因此生成过多的对象将会给程序的性能带来很大的影响。8. 过分的创建对象会消耗系统的大量内存,严重时,会导致内存泄漏,因此,保证过期的对象的及时回收具有重要意义。JVM的GC并非十分智能,因此建议在对象使用完毕后,手动设置成null。9. 不要在循环中使用Try/Catch语句,应把Try/Catch放在循环最外层Error是获取系统错误的类,或者说是虚拟机错误的类。不是所有的错误Exception都能获取到的,虚拟机报错Exception就获取不到,必须用Error获取。10. 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。————————————————                        原文链接:https://blog.csdn.net/qq_27346503/article/details/110144811
  • [技术干货] 35 个 Java 代码性能优化总结
    代码优化,一个很重要的课题。可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗?没用,但是,吃的小虾米一多之后,鲸鱼就被喂饱了。代码优化也是一样,如果项目着眼于尽快无BUG上线,那么此时可以抓大放小,代码的细节可以不精打细磨;但是如果有足够的时间开发、维护代码,这时候就必须考虑每个可以优化的细节了,一个一个细小的优化点累积起来,对于代码的运行效率绝对是有提升的。  代码优化的目标是:  1、减小代码的体积  2、提高代码运行的效率代码优化细节   1、尽量指定类、方法的final修饰符  带有final修饰符的类是不可派生的。在Java核心API中,有许多应用final的例子,例如java.lang.String,整个类都是final的。为类指定final修饰符可以让类不可以被继承,为方法指定final修饰符可以让方法不可以被重写。如果指定了一个类为final,则该类所有的方法都是final的。Java编译器会寻找机会内联所有的final方法,内联对于提升Java运行效率作用重大,具体参见Java运行期优化。此举能够使性能平均提高50%。   2、尽量重用对象  特别是String对象的使用,出现字符串连接时应该使用StringBuilder/StringBuffer代替。由于Java虚拟机不仅要花时间生成对象,以后可能还需要花时间对这些对象进行垃圾回收和处理,因此,生成过多的对象将会给程序的性能带来很大的影响。   3、尽可能使用局部变量  调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中速度较快,其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢。另外,栈中创建的变量,随着方法的运行结束,这些内容就没了,不需要额外的垃圾回收。   4、及时关闭流  Java编程过程中,进行数据库连接、I/O流操作时务必小心,在使用完毕后,及时关闭以释放资源。因为对这些大对象的操作会造成系统大的开销,稍有不慎,将会导致严重的后果。   5、尽量减少对变量的重复计算  明确一个概念,对方法的调用,即使方法中只有一句语句,也是有消耗的,包括创建栈帧、调用方法时保护现场、调用方法完毕时恢复现场等。所以例如下面的操作:for (int i = 0; i < list.size(); i++){...}  建议替换为:for (int i = 0, int length = list.size(); i < length; i++){...}  这样,在list.size()很大的时候,就减少了很多的消耗   6、尽量采用懒加载的策略,即在需要的时候才创建  例如:String str = "aaa";if (i == 1){list.add(str);}  建议替换为:if (i == 1){String str = "aaa";list.add(str);}   7、慎用异常  异常对性能不利。抛出异常首先要创建一个新的对象,Throwable接口的构造函数调用名为fillInStackTrace()的本地同步方法,fillInStackTrace()方法检查堆栈,收集调用跟踪信息。只要有异常被抛出,Java虚拟机就必须调整调用堆栈,因为在处理过程中创建了一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。  8、不要在循环中使用try…catch…,应该把其放在最外层  除非不得已。如果毫无理由地这么写了,只要你的领导资深一点、有强迫症一点,八成就要骂你为什么写出这种垃圾代码来了  9、如果能估计到待添加的内容长度,为底层以数组方式实现的集合、工具类指定初始长度  比如ArrayList、LinkedLlist、StringBuilder、StringBuffer、HashMap、HashSet等等,以StringBuilder为例:  (1)StringBuilder()      // 默认分配16个字符的空间  (2)StringBuilder(int size)  // 默认分配size个字符的空间  (3)StringBuilder(String str) // 默认分配16个字符+str.length()个字符空间  可以通过类(这里指的不仅仅是上面的StringBuilder)的来设定它的初始化容量,这样可以明显地提升性能。比如StringBuilder吧,length表示当前的StringBuilder能保持的字符数量。因为当StringBuilder达到最大容量的时候,它会将自身容量增加到当前的2倍再加2,无论何时只要StringBuilder达到它的最大容量,它就不得不创建一个新的字符数组然后将旧的字符数组内容拷贝到新字符数组中—-这是十分耗费性能的一个操作。试想,如果能预估到字符数组中大概要存放5000个字符而不指定长度,最接近5000的2次幂是4096,每次扩容加的2不管,那么:  (1)在4096 的基础上,再申请8194个大小的字符数组,加起来相当于一次申请了12290个大小的字符数组,如果一开始能指定5000个大小的字符数组,就节省了一倍以上的空间  (2)把原来的4096个字符拷贝到新的的字符数组中去  这样,既浪费内存空间又降低代码运行效率。所以,给底层以数组实现的集合、工具类设置一个合理的初始化容量是错不了的,这会带来立竿见影的效果。但是,注意,像HashMap这种是以数组+链表实现的集合,别把初始大小和你估计的大小设置得一样,因为一个table上只连接一个对象的可能性几乎为0。初始大小建议设置为2的N次幂,如果能估计到有2000个元素,设置成new HashMap(128)、new HashMap(256)都可以。   10、当复制大量数据时,使用System.arraycopy()命令   11、乘法和除法使用移位操作  例如:for (val = 0; val < 100000; val += 5){a = val * 8;b = val / 2;}  用移位操作可以极大地提高性能,因为在计算机底层,对位的操作是最方便、最快的,因此建议修改为:for (val = 0; val < 100000; val += 5){a = val << 3;b = val >> 1;}  移位操作虽然快,但是可能会使代码不太好理解,因此最好加上相应的注释。   12、循环内不要不断创建对象引用  例如:for (int i = 1; i <= count; i++){Object obj = new Object();}  这种做法会导致内存中有count份Object对象引用存在,count很大的话,就耗费内存了,建议为改为:Object obj = null;for (int i = 0; i <= count; i++) { obj = new Object(); }  这样的话,内存中只有一份Object对象引用,每次new Object()的时候,Object对象引用指向不同的Object罢了,但是内存中只有一份,这样就大大节省了内存空间了。  13、基于效率和类型检查的考虑,应该尽可能使用array,无法确定数组大小时才使用ArrayList  14、尽量使用HashMap、ArrayList、StringBuilder,除非线程安全需要,否则不推荐使用Hashtable、Vector、StringBuffer,后三者由于使用同步机制而导致了性能开销  15、不要将数组声明为public static final  因为这毫无意义,这样只是定义了引用为static final,数组的内容还是可以随意改变的,将数组声明为public更是一个安全漏洞,这意味着这个数组可以被外部类所改变  16、尽量在合适的场合使用单例  使用单例可以减轻加载的负担、缩短加载的时间、提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面:  (1)控制资源的使用,通过线程同步来控制资源的并发访问  (2)控制实例的产生,以达到节约资源的目的  (3)控制数据的共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信  17、尽量避免随意使用静态变量  要知道,当某个对象被定义为static的变量所引用,那么gc通常是不会回收这个对象所占有的堆内存的,如:public class A{ private static B b = new B();}  此时静态变量b的生命周期与A类相同,如果A类不被卸载,那么引用B指向的B对象会常驻内存,直到程序终止  18、及时清除不再需要的会话  为了清除不再活动的会话,许多应用服务器都有默认的会话超时时间,一般为30分钟。当应用服务器需要保存更多的会话时,如果内存不足,那么操作系统会把部分数据转移到磁盘,应用服务器也可能根据MRU(最近最频繁使用)算法把部分不活跃的会话转储到磁盘,甚至可能抛出内存不足的异常。如果会话要被转储到磁盘,那么必须要先被序列化,在大规模集群中,对对象进行序列化的代价是很昂贵的。因此,当会话不再需要时,应当及时调用HttpSession的invalidate()方法清除会话。  19、实现RandomAccess接口的集合比如ArrayList,应当使用最普通的for循环而不是foreach循环来遍历  这是JDK推荐给用户的。JDK API对于RandomAccess接口的解释是:实现RandomAccess接口用来表明其支持快速随机访问,此接口的主要目的是允许一般的算法更改其行为,从而将其应用到随机或连续访问列表时能提供良好的性能。实际经验表明,实现RandomAccess接口的类实例,假如是随机访问的,使用普通for循环效率将高于使用foreach循环;反过来,如果是顺序访问的,则使用Iterator会效率更高。可以使用类似如下的代码作判断:if (list instanceof RandomAccess){ for (int i = 0; i < list.size(); i++){}}else{Iterator<?> iterator = list.iterable(); while (iterator.hasNext()){iterator.next()}}  foreach循环的底层实现原理就是迭代器Iterator,参见Java语法糖1:可变长度参数以及foreach循环原理。所以后半句”反过来,如果是顺序访问的,则使用Iterator会效率更高”的意思就是顺序访问的那些类实例,使用foreach循环去遍历。  20、使用同步代码块替代同步方法  这点在多线程模块中的synchronized锁方法块一文中已经讲得很清楚了,除非能确定一整个方法都是需要进行同步的,否则尽量使用同步代码块,避免对那些不需要进行同步的代码也进行了同步,影响了代码执行效率。  21、将常量声明为static final,并以大写命名  这样在编译期间就可以把这些内容放入常量池中,避免运行期间计算生成常量的值。另外,将常量的名字以大写命名也可以方便区分出常量与变量  22、不要创建一些不使用的对象,不要导入一些不使用的类  这毫无意义,如果代码中出现”The value of the local variable i is not used”、”The import java.util is never used”,那么请删除这些无用的内容  23、程序运行过程中避免使用反射  关于,请参见反射。反射是Java提供给用户一个很强大的功能,功能强大往往意味着效率不高。不建议在程序运行过程中使用尤其是频繁使用反射机制,特别是Method的invoke方法,如果确实有必要,一种建议性的做法是将那些需要通过反射加载的类在项目启动的时候通过反射实例化出一个对象并放入内存—-用户只关心和对端交互的时候获取最快的响应速度,并不关心对端的项目启动花多久时间。  24、使用数据库连接池和线程池  这两个池都是用于重用对象的,前者可以避免频繁地打开和关闭连接,后者可以避免频繁地创建和销毁线程  25、使用带缓冲的输入输出流进行IO操作  带缓冲的输入输出流,即BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream,这可以极大地提升IO效率  26、顺序插入和随机访问比较多的场景使用ArrayList,元素删除和中间插入比较多的场景使用LinkedList  这个,理解ArrayList和LinkedList的原理就知道了  27、不要让public方法中有太多的形参  public方法即对外提供的方法,如果给这些方法太多形参的话主要有两点坏处:  1、违反了面向对象的编程思想,Java讲求一切都是对象,太多的形参,和面向对象的编程思想并不契合  2、参数太多势必导致方法调用的出错概率增加  至于这个”太多”指的是多少个,3、4个吧。比如我们用JDBC写一个insertStudentInfo方法,有10个学生信息字段要插如Student表中,可以把这10个参数封装在一个实体类中,作为insert方法的形参  28、字符串变量和字符串常量equals的时候将字符串常量写在前面  这是一个比较常见的小技巧了,如果有以下代码:String str = "123";if (str.equals("123")) {...}  建议修改为:String str = "123";if ("123".equals(str)){...}  这么做主要是可以避免空指针异常  29、请知道,在java中if (i == 1)和if (1 == i)是没有区别的,但从阅读习惯上讲,建议使用前者  平时有人问,”if (i == 1)”和”if (1== i)”有没有区别,这就要从C/C++讲起。  在C/C++中,”if (i == 1)”判断条件成立,是以0与非0为基准的,0表示false,非0表示true,如果有这么一段代码:int i = 2;if (i == 1){...}else{...}  C/C++判断”i==1″不成立,所以以0表示,即false。但是如果:int i = 2;if (i = 1) { ... }else{ ... }  万一程序员一个不小心,把”if (i == 1)”写成”if (i = 1)”,这样就有问题了。在if之内将i赋值为1,if判断里面的内容非0,返回的就是true了,但是明明i为2,比较的值是1,应该返回的false。这种情况在C/C++的开发中是很可能发生的并且会导致一些难以理解的错误产生,所以,为了避免开发者在if语句中不正确的赋值操作,建议将if语句写为:int i = 2;if (1 == i) { ... }else{ ... }  这样,即使开发者不小心写成了”1 = i”,C/C++编译器也可以第一时间检查出来,因为我们可以对一个变量赋值i为1,但是不能对一个常量赋值1为i。  但是,在Java中,C/C++这种”if (i = 1)”的语法是不可能出现的,因为一旦写了这种语法,Java就会编译报错”Type mismatch: cannot convert from int to boolean”。但是,尽管Java的”if (i == 1)”和”if (1 == i)”在语义上没有任何区别,但是从阅读习惯上讲,建议使用前者会更好些。  30、不要对数组使用toString()方法  看一下对数组使用toString()打印出来的是什么:public static void main(String[] args){ int[] is = new int[]{1, 2, 3};System.out.println(is.toString());}  结果是:[I@18a992f  本意是想打印出数组内容,却有可能因为数组引用is为空而导致空指针异常。不过虽然对数组toString()没有意义,但是对集合toString()是可以打印出集合里面的内容的,因为集合的父类AbstractCollections<E>重写了Object的toString()方法。  31、不要对超出范围的基本数据类型做向下强制转型  这绝不会得到想要的结果:public static void main(String[] args){ long l = 12345678901234L;int i = (int)l;System.out.println(i);}  我们可能期望得到其中的某几位,但是结果却是:  1942892530  解释一下。Java中long是8个字节64位的,所以12345678901234在计算机中的表示应该是:  0000 0000 0000 0000 0000 1011 0011 1010 0111 0011 1100 1110 0010 1111 1111 0010  一个int型数据是4个字节32位的,从低位取出上面这串二进制数据的前32位是:  0111 0011 1100 1110 0010 1111 1111 0010  这串二进制表示为十进制1942892530,所以就是我们上面的控制台上输出的内容。从这个例子上还能顺便得到两个结论:  1、整型默认的数据类型是int,long l = 12345678901234L,这个数字已经超出了int的范围了,所以最后有一个L,表示这是一个long型数。顺便,浮点型的默认类型是double,所以定义float的时候要写成”"float f = 3.5f”  2、接下来再写一句”int ii = l + i;”会报错,因为long + int是一个long,不能赋值给int  32、公用的集合类中不使用的数据一定要及时remove掉  如果一个集合类是公用的(也就是说不是方法里面的属性),那么这个集合里面的元素是不会自动释放的,因为始终有引用指向它们。所以,如果公用集合里面的某些数据不使用而不去remove掉它们,那么将会造成这个公用集合不断增大,使得系统有内存泄露的隐患。  33、把一个基本数据类型转为字符串,基本数据类型.toString()是最快的方式、String.valueOf(数据)次之、数据+”"最慢  把一个基本数据类型转为一般有三种方式,我有一个Integer型数据i,可以使用i.toString()、String.valueOf(i)、i+”"三种方式,三种方式的效率如何,看一个测试:public static void main(String[] args){ int loopTime = 50000;Integer i = 0; long startTime = System.currentTimeMillis(); for (int j = 0; j < loopTime; j++){String str = String.valueOf(i);}System.out.println("String.valueOf():" + (System.currentTimeMillis() - startTime) + "ms");startTime = System.currentTimeMillis(); for (int j = 0; j < loopTime; j++){String str = i.toString();}System.out.println("Integer.toString():" + (System.currentTimeMillis() - startTime) + "ms");startTime = System.currentTimeMillis(); for (int j = 0; j < loopTime; j++){String str = i + "";}System.out.println("i + \"\":" + (System.currentTimeMillis() - startTime) + "ms");}  运行结果为:String.valueOf():11ms Integer.toString():5ms i + "":25ms  所以以后遇到把一个基本数据类型转为String的时候,优先考虑使用toString()方法。至于为什么,很简单:  1、String.valueOf()方法底层调用了Integer.toString()方法,但是会在调用前做空判断  2、Integer.toString()方法就不说了,直接调用了  3、i + “”底层使用了StringBuilder实现,先用append方法拼接,再用toString()方法获取字符串  三者对比下来,明显是2最快、1次之、3最慢  34、使用最有效率的方式去遍历Map  遍历Map的方式有很多,通常场景下我们需要的是遍历Map中的Key和Value,那么推荐使用的、效率最高的方式是:public static void main(String[] args){HashMap<String, String> hm = new HashMap<String, String>();hm.put("111", "222");Set<Map.Entry<String, String>> entrySet = hm.entrySet();Iterator<Map.Entry<String, String>> iter = entrySet.iterator(); while (iter.hasNext()){Map.Entry<String, String> entry = iter.next();System.out.println(entry.getKey() + "\t" + entry.getValue());}}  如果你只是想遍历一下这个Map的key值,那用”Set<String> keySet = hm.keySet();”会比较合适一些  35、对资源的close()建议分开操作  意思是,比如我有这么一段代码:try{XXX.close();YYY.close();}catch (Exception e){...}  建议修改为:try{ XXX.close(); }catch (Exception e) { ... }try{ YYY.close(); }catch (Exception e) { ... }  虽然有些麻烦,却能避免资源泄露。我们想,如果没有修改过的代码,万一XXX.close()抛异常了,那么就进入了cath块中了,YYY.close()不会执行,YYY这块资源就不会回收了,一直占用着,这样的代码一多,是可能引起资源句柄泄露的。而改为下面的写法之后,就保证了无论如何XXX和YYY都会被close掉。 原文链接:https://blog.csdn.net/null_cy/article/details/50944627
  • [技术干货] Java代码优化:常用的9种优化策略,性能飙升不是梦
    在Java编程的世界里,追求代码的高效性就如同探索一片广袤无垠的未知领域。每一行代码的优化,都可能成为提升程序性能的关键。 本文将深入探讨几种有效的代码优化策略,旨在帮助开发者编写出更加高效的Java应用程序。一、选择合适的算法和数据结构算法的选择和优化是提升代码效率的首要步骤。一个简单的算法可能因为其时间复杂度较低而在处理大数据时显得力不从心。例如,快速排序通常比冒泡排序更高效,因为它的平均时间复杂度为O(n log n),而冒泡排序的时间复杂度为O(n^2)。通过优化算法的时间复杂度,我们可以显著提高程序处理数据的能力。在Java中,选择合适的数据结构同样至关重要。不同的数据结构适用于不同的场景,错误的选择可能会导致性能问题。例如,如果需要频繁地进行查找操作,哈希表是一个不错的选择;而对于有序数据的查找和修改操作,平衡树如红黑树则更为合适。使用正确的数据结构不仅能够提升算法的执行效率,还能节省内存消耗。二、充分利用缓存机制缓存重复使用的计算结果也是提升效率的一种方法。动态规划和记忆化递归是常用的优化技术。未经优化的递归算法可能时间复杂度非常高,例如O(2^n),但通过使用动态规划可以将时间复杂度降低到O(n)。 三、选择正确的类型的基本类型与包装类型的使用也是优化的一个方面。Java中的基本类型(如int、float等)直接存储在栈内存中,而包装类型(如Integer、Float等)则需要存储在堆内存中。因此,在使用循环时,使用基本类型可以显著提高代码的运行速度。然而,在某些情况下,递归算法可能比循环更高效,但这需要根据具体情况进行分析和测试。四、减少动态代码的使用动态代码生成在某些情况下可能会导致Java代码运行缓慢。这是因为动态生成的代码需要额外的编译时间,这可能会影响程序的整体性能。因此,在设计系统时,应尽量减少动态代码的使用,或者将其优化以减少对性能的影响。五、减少对象创建和垃圾回收频繁创建对象会导致垃圾回收器频繁工作,从而影响性能。尽量减少不必要的对象创建是提升性能的重要手段。尽量重用对象,使用池化技术(如连接池、线程池)可以显著提升性能。例如,使用StringBuilder代替字符串拼接,因为StringBuilder可以在内部维护一个可变的字符序列,而不需要每次拼接都创建新的字符串对象。通过这种方式,不仅减少了对象的创建次数,还提高了拼接字符串的效率。六、优化循环结构在循环中避免不必要的计算和方法调用是提升代码效率的另一个关键点。将循环中不变的计算提取到循环外部,可以减少每次迭代的计算量。例如:// 未优化的代码for (int i = 0; i < list.size(); i++) {    //循环内逻辑} // 优化后的代码int size = list.size();for (int i = 0; i < size; i++) {     //循环内逻辑}通过将list.size()提取到循环外,可以减少每次迭代的计算量,从而提高循环的执行效率。七、使用合适的异常处理机制异常处理是Java编程中不可避免的一部分,但过度使用异常处理会影响性能。尽量使用条件判断替代异常来处理错误情况,只有在必要时才抛出异常。例如:// 未优化的代码try {    performOperation(); } catch (SpecificException e) {    //  exception} // 优化后的代码if (condition) {    performOperation();} else {    //  exception  }通过这种方式,可以减少异常处理的开销,从而提高代码的执行效率。八、合理使用并发和多线程并发和多线程是提升性能的重要手段。Java提供了丰富的并发工具类,如java.util.concurrent包中的类。使用这些工具类可以有效地管理线程资源,提高程序的并行度。例如,使用ExecutorService来管理线程池,可以避免手动创建和管理线程的开销。ExecutorService executor = Executors.newFixedThreadPool(50); executor.submit(() -> {    // some task}); executor.shutdown();通过使用线程池,可以有效地复用线程资源,提高程序的执行效率。九、使用高效的I/O操作I/O操作通常是应用程序中的一个瓶颈点。优化I/O操作可以提高系统的整体性能。例如,使用缓冲流(BufferedInputStream、BufferedOutputStream)可以提高文件读写的效率。另外,尽量减少网络请求的次数和数据传输量也是提升性能的重要手段。BufferedReader reader = new BufferedReader(new FileReader("openfile.txt"));BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt")); String line; while ((line = reader.readLine()) != null) {    writer.write(line);    writer.newLine();} reader.close();writer.close();通过使用缓冲流,可以减少I/O操作的频率,提高文件读写的效率。总结优化Java代码是一项复杂且细致的工作,涉及多个方面。通过选择合适的算法和数据结构、减少对象创建和垃圾回收、优化循环结构和异常处理机制、合理使用并发和多线程以及优化I/O操作,可以显著提升Java程序的效率和性能。希望本文介绍的方法能够帮助开发者在实际应用中编写出更高效的Java代码————————————————                        原文链接:https://blog.csdn.net/m0_63998314/article/details/144847848
  • [技术干货] 11个值得掌握的Java代码性能优化技巧
    1. 前言在开发任何Java应用的时候,我们都会谈及optimization——优化的概念。作为一个开发者,我们应该尽可能地保证自己写的代码干净、没有缺陷,并且尽可能地考虑性能问题。为此,笔者总结了以下11个你肯定会用到的Java代码性能优化的技巧。编程学习资料点击免费领取2. 11个优化的技巧2.1 避免方法过长我们在定义一个方式的时候,应该考虑到一个方法不应该太长,它就应该是专门是来执行单一功能的。这样其实对维护和性能都有好处。一方面,从维护角度来说,适当长度的方法易读性更强,更容易理解;另一方面,在类的加载和方法调用的过程中,方法会被加载到内存中。如果一个方法太大,处理起来就需要消耗额外的内存和CPU周期。我们应该学会在恰当的逻辑点上将一个长方法拆开。2.2 避免多个if-else语句对于这个优化点,大家应该很熟悉了。但是实际在写代码的时候,还是if-else一撸到底。这样做的话,其实也会影响性能。因为JVM必须对条件进行比较。如果在for、while等循环语句中使用同样的条件,情况会变得更糟糕。如果我们的业务逻辑中有很多的条件,我们可以尝试着将这些条件分组并且返回一个布尔值,然后再将其用于if语句。另外,如果可能的话,我们可以考虑使用switch语句来代替多个if-else。switch语句比if-else有性能优势。 下面我们看一个例子:if (condition1) {     if (condition2) {         if (condition3 || condition4) { execute ..}         else { execute..}复制代码对比上面这段代码,合适的做法应该如下:boolean result = (condition1 && condition2) && (condition3  || condition4)复制代码2.3 避免使用iterator用Java5的foreach风格来写循环确实很方便很简洁,看起来就很酷!但是有的时候耍酷是要付出性能的代价的。例如:for (String str: strs) {    . . .}复制代码每次运行代码,如果strs是Iterable的,你将会创建一个新的Iterator对象。这样做会导致更多内存的消耗。如果你对性能有着极致的追求,那么还是建议你使用原始的写法:int size = strs.size();for (int i = 0; i < size; i++) {    String value = strs.get(i);    . . .}复制代码2.4 避免在集合中获取size在对任何集合进行迭代时,要事先得到集合的大小,而不是在迭代过程中得到它——这样避免多次调用size()方法。下面请看这个例子:List<String> eleList = getData();for (int i = 0; i < eleList.size(); i++) { execute code ..}复制代码对比上面这段代码,合适的做法应该如下:List<String> objList = getData();int size = objList.size();for (int i = 0; i < size; i++) { execute code ..} 复制代码2.5 避免使用+号拼接字符串从JDK5开始,Java编译器就做了优化,使用+号拼接字符串,编译器编译后实际就自动优化为使用StringBuilder。而且String是final类,用String创建的对象无法重复使用。因此,如果我们需要连续拼接,使用+号拼接字符串将导致创建多个String对象,从而会占用更多的堆内存。一般来说,当字符串不多的时候,+号与StringBuilder的拼接效率其实相差无几;但是如果涉及到单线程循环拼接的时候,我们最好还是使用StringBuilder以保证性能上的优化。下面请看一个例子:String str = "sample";    for (int i = 0; i < count; i++) {        str = str + "-" + i;    }复制代码更合适的做法如下:StringBuilder stringBuilder = new StringBuilder("sample");    for (int i = 0; i < count; i++) {        stringBuilder.append("-");        stringBuilder.append(i);    }复制代码2.6 尽可能使用基本类型因为基本类型存储在栈内存中,而对象存储在堆内存中。如果可以的话,我们应该尽可能使用基本类型而非对象,因为栈内存的访问速度比堆内存快。因此在某些情况下,定义一个变量或者数组,我们可以使用int而非Integer,double而非Double。2.7 避免使用BigDecimal类BigDecimal类提供了精确的小数值,过度使用这个对象会对性能造成影响,特别是当这个对象被用来在循环中计算某些数值时。BigDecimal在进行计算时要比long或double占用更多的内存。如果精度不受限制,或者我们确认计算值的范围不会超过long或double,我们可以避免使用BigDecimal,而使用long或double,并进行适当的转换。2.8 避免经常创建“代价昂贵”的对象有一些类在应用程序中承载着数据,这些对象的创建开销很大,我们应该避免多次创建。比如说,数据库连接对象,系统配置对象,或者是用户登录的会话对象。这些对象在创建的时候占用了大量资源,我们应该选择重用这些对象,而不是再次创建。对于这些"代价昂贵"的对象,我们尽可能使用单例模式来创建单一实例,并在需要的地方重用它。2.9 使用PreparedStatement而不是Statement现在应该比较少用JDBC API进行SQL查询了,但是我觉得还是有必要了解一下。对于参数化查询,PreparedStatement比Statement更有优势,因为PreparedStatement对象被编译一次并执行多次。Statement对象在每次被调用时都会被编译和执行。此外,PreparedStatement对象是安全的,可以避免SQL注入攻击。2.10 避免使用不必要的日志语句和不正确的日志级别这个建议应该是很普遍的,但是很多代码忽略了这一点。我们在创建调试信息的时候,应该先检查一下当前的日志级别。否则你可能会无意之间创建一条无用的日志信息。 请看例子:log.debug("User [" + userName + "] called method X with [" + i + "]");log.debug(String.format("User [%s] called method X with [%d]", userName, i));复制代码在这种情况下,我们需要执行所有必要的步骤去创建日志信息,而不知道程序到底会不会使用该日志信息。更进一步,如果说这种日志信息的创建涉及到更多资源的占用呢?所以最好是先检查一下当前的日志级别,请看:if(log.isDebugEnabled()) {    log.debug("User [" + userName + " ] called method X with [" + i + "] ");}复制代码2.11 选择SQL查询中的必要字段有时,我们需要写SQL来获取数据。此时我们应该避免选择所有数据库列,只选择我们需要的数据库列。选择太多的列会导致数据库查询执行的延迟,也会增加网络流量。请看示例:select * from books where book_id = 6;复制代码对此,我建议这么写:select book_title, book_desc, book_price from books where book_id = 6;复制代码3. 结语很多人认为性能优化是一个复杂的话题,需要大量的经验和知识,这在一定程度上是对的。我们开发一个应用程序并且期望获得尽可能好的性能并不是一件容易的事情。但是,即使你不是性能调优专家,也可以采取一些简单的方法来提高性能。————————————————                        原文链接:https://blog.csdn.net/m0_63171455/article/details/122680235
  • [技术干货] Java 中 10 大简单的性能优化
    Java 7 ForkJoinPool和 Java 8 的并行Stream有助于并行化东西,这在您将 Java 程序部署到多核处理器机器上时非常有用。与跨网络上的不同机器进行扩展相比,这种并行性的优势在于您几乎可以完全消除延迟效应,因为所有内核都可以访问相同的内存。但是不要被并行的效果所迷惑!记住以下两点:并行性会吞噬你的核心。这对于批处理非常有用,但对于异步服务器(例如 HTTP)来说则是一场噩梦。在过去的几十年里,我们使用单线程 servlet 模型是有充分理由的。因此,并行性仅在扩大规模时才有帮助。并行性对算法的Big O Notation没有影响。如果您的算法是O(n log n),并且您让该算法在c内核上运行,您仍然会有一个O(n log n / c)算法,因为c在您的算法复杂性中是一个微不足道的常数。您将节省挂钟时间,但不会降低复杂性!当然,提高性能的最佳方法是降低算法复杂度。杀手是实现O(1)或准O(1),当然,例如HashMap查找。但这并不总是可能的,更不用说容易了。如果你不能降低复杂性,如果你在真正重要的地方调整你的算法,如果你能找到正确的位置,你仍然可以获得很多性能。假设以下算法的可视化表示: 算法的整体复杂度是,或者如果我们要处理单个数量级。但是,在分析此代码时,您可能会发现一个有趣的场景:O(N3)O(N x O x P)在您的开发框中,左分支 ( N -> M -> Heavy operation) 是您可以在分析器中看到的唯一分支,因为 和 的值O在P您的开发示例数据中很小。然而,在生产中,正确的分支(N -> O -> P -> Easy operation或NOPE)确实造成了麻烦。您的运营团队可能已经使用AppDynamics或DynaTrace或一些类似软件解决了这个问题。如果没有生产数据,您可能会很快得出结论并优化“繁重操作”。你运送到生产环境,你的修复没有效果。除了以下事实之外,没有优化的黄金法则:设计良好的应用程序更容易优化过早的优化不会解决任何性能问题,反而会使您的应用程序设计得不那么好,从而使优化变得更加困难理论够了。让我们假设您找到了正确的分支是问题所在。很可能是一个非常简单的操作在生产中失败了,因为它被调用了很多次(如果N、O和P很大)。请在不可避免算法的叶节点出现问题的情况下阅读本文。这些优化不会帮助您扩展。他们将帮助您暂时节省客户的时间,将整体算法的困难改进推迟到以后!O(N3) 以下是 Java 中最简单的 10 个性能优化:1、使用StringBuilder这应该是几乎所有 Java 代码中的默认设置。尽量避免使用+操作符。当然,您可能会争辩说,它只是StringBuilder的语法糖,例如:String x = "a" + args.length + "b";翻译为:new java.lang.StringBuilder [16]dupldc <String "a"> [18]invokespecial java.lang.StringBuilder(java.lang.String) [20]aload_0 [args]arraylengthinvokevirtual java.lang.StringBuilder.append(int) : java.lang.StringBuilder [23]ldc <String "b"> [27]invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [29]invokevirtual java.lang.StringBuilder.toString() : java.lang.String [32]astore_1 [x]但是,如果稍后您需要使用可选部分修改您的字符串,会发生什么?String x = "a" + args.length + "b"; if (args.length == 1)    x = x + args[0];您现在将有第二个StringBuilder,这只是不必要地消耗您的堆内存,给您的 GC 施加压力。改为这样写:StringBuilder x = new StringBuilder("a");x.append(args.length);x.append("b"); if (args.length == 1);    x.append(args[0]);在上面的示例中,如果您使用显式StringBuilder实例,或者您依赖 Java 编译器为您创建隐式实例,这可能完全无关紧要。但请记住,我们在N.O.P.E.分支中。我们浪费在像 GC 或分配 aStringBuilder的默认容量这样愚蠢的事情上的每个 CPU 周期,我们都在浪费N x O x P时间。根据经验,始终使用 aStringBuilder而不是+运算符。如果可以的话,如果你的构建更复杂,请保留StringBuilder多个方法的引用。只有一个StringBuilder“遍历”你的整个 SQL AST(抽象语法树) 对于大声喊叫,如果您仍然有StringBuffer参考资料,请将它们替换为StringBuilder. 您几乎不需要同步正在创建的字符串。2、避免正则表达式正则表达式相对便宜和方便。但是,如果您在 N.O.P.E. 分支中,那么它们就是您能做的最糟糕的事情。如果您绝对必须在计算密集型代码部分使用正则表达式,至少缓存Pattern引用而不是一直重新编译它:static final Pattern HEAVY_REGEX =    Pattern.compile("(((X)*Y)*Z)*");但是如果你的正则表达式真的很傻String[] parts = ipAddress.split("\.");那么你真的最好求助于普通char[]或基于索引的操作。例如,这个完全不可读的循环做同样的事情:int length = ipAddress.length();int offset = 0;int part = 0;for (int i = 0; i < length; i++) {    if (i == length - 1 ||            ipAddress.charAt(i + 1) == '.') {        parts[part] =            ipAddress.substring(offset, i + 1);        part++;        offset = i + 2;    }}这也说明了为什么你不应该做任何过早的优化。与split()版本相比,这是不可维护的。挑战:读者中聪明的人可能会发现更快的算法。外卖 正则表达式很有用,但它们是有代价的。如果您深陷于N.O.P.E.分支中,则必须不惜一切代价避免使用正则表达式。请注意各种使用正则表达式的 JDK 字符串方法,例如String.replaceAll(), 或String.split(). 请改用Apache Commons Lang之类的流行库来进行字符串操作。3、不要使用iterator()现在,此建议实际上不适用于一般用例,而仅适用于N.O.P.E.分支的深层。尽管如此,你应该考虑一下。编写 Java-5 风格的 foreach 循环很方便。您可以完全忘记循环内部,并编写:for (String value : strings) {    // Do something useful here}但是,每次遇到此循环时,如果strings是一个Iterable,您将创建一个新Iterator实例。如果您使用的是ArrayList,这将ints在您的堆上分配一个 3 的对象:private class Itr implements Iterator<E> {    int cursor;    int lastRet = -1;    int expectedModCount = modCount;    // ...相反,您可以编写以下等效循环并仅“浪费”堆栈上的单个int值,这非常便宜:int size = strings.size();for (int i = 0; i < size; i++) {    String value : strings.get(i);    // Do something useful here}……或者,如果您的列表没有真正改变,您甚至可以对它的数组版本进行操作:for (String value : stringArray) {    // Do something useful here}从可写性和可读性的角度来看,以及从 API 设计的角度来看,迭代器、Iterable 和 foreach 循环都非常有用。但是,它们会在每次迭代时在堆上创建一个小的新实例。如果你多次运行这个迭代,你要确保避免创建这个无用的实例,而是编写基于索引的迭代。4、不要调用那个方法有些方法简单昂贵。在我们的N.O.P.E.分支示例中,我们在叶子中没有这样的方法,但您可能有一个。让我们假设您的 JDBC 驱动程序需要经历令人难以置信的麻烦来计算ResultSet.wasNull(). 您自己开发的 SQL 框架代码可能如下所示:if (type == Integer.class) {    result = (T) wasNull(rs,        Integer.valueOf(rs.getInt(index)));}// And then...static final <T> T wasNull(ResultSet rs, T value)throws SQLException {    return rs.wasNull() ? null : value;}ResultSet.wasNull() 现在,每次您int从结果集中获得一个时, 都会调用此逻辑。但getInt()合同上写着:返回:列值;如果值为 SQL NULL,则返回值为 0因此,对上述内容的一个简单但可能是巨大的改进将是:static final <T extends Number> T wasNull(    ResultSet rs, T value)throws SQLException {    return (value == null ||           (value.intValue() == 0 && rs.wasNull()))        ? null : value;}所以,这很简单:要点 不要在算法“叶节点”中调用昂贵的方法,而是缓存调用,或者在方法合约允许的情况下避免调用。5、使用原语和堆栈上面的例子,它使用了很多泛型,因此被迫使用包装器类型byte, short, int, 和long– 至少在泛型在 Java 10 和项目 Valhalla 中专用之前。但是你的代码中可能没有这个约束,所以你应该采取一切措施来替换:// Goes to the heapInteger i = 817598;这样:// Stays on the stackint i = 817598;使用数组时情况会变得更糟:// Three heap objects!Integer[] i = { 1337, 424242 };这样:// One heap object.int[] i = { 1337, 424242 };当您深入到N.O.P.E.分支时,您应该非常小心使用包装器类型。很有可能你会给你的 GC 造成很大的压力,它必须一直在清理你的烂摊子。一个特别有用的优化可能是使用一些原始类型并创建它的大型一维数组,以及几个分隔符变量来指示您的编码对象在数组上的确切位置。trove4jint[]是一个优秀的原始集合库,它比你的平均水平要复杂一些,它与 LGPL 一起提供。例外 此规则有一个例外:和booleanbyte很少有足够的值完全被 JDK 缓存。你可以写:Boolean a1 = true; // ... syntax sugar for:Boolean a2 = Boolean.valueOf(true); Byte b1 = (byte) 123; // ... syntax sugar for:Byte b2 = Byte.valueOf((byte) 123);对于其他整数基本类型的低值也是如此,包括char, short, int, long。但仅当您自动装箱或调用TheType.valueOf()时,才不会在调用构造函数时!永远不要在包装类型上调用构造函数,除非你真的想要一个新实例这个事实也可以帮助你为你的同事写一个复杂的愚人节笑话 堆 外 当然,你可能还想尝试堆外库,尽管它们更多的是战略决策,而不是本地优化。6、避免递归像 Scala 这样的现代函数式编程语言鼓励使用递归,因为它们提供了将尾递归算法优化回迭代算法的方法。如果你的语言支持这样的优化,你可能没问题。但即便如此,算法的最轻微变化也可能会产生一个分支,阻止你的递归是尾递归的。希望编译器会检测到这一点!否则,您可能会浪费大量堆栈帧,而这些堆栈帧可能仅使用几个局部变量就可以实现。当你深入N.O.P.E.分支时,总是更喜欢迭代而不是递归7、使用 entrySet()当您想要遍历 aMap并且需要键和值时,您必须有充分的理由编写以下内容:for (K key : map.keySet()) {    V value : map.get(key);}而不是以下内容:for (Entry<K, V> entry : map.entrySet()) {    K key = entry.getKey();    V value = entry.getValue();}当你在N.O.P.E.分支时,无论如何你都应该警惕地图,因为大量的O(1)地图访问操作仍然是大量的操作。而且访问也不是免费的。但至少,如果您不能没有地图,请使用它entrySet()来迭代它们!无论如何,该Map.Entry实例都在那里,您只需要访问它。entrySet()在 map 迭代过程中同时需要键和值时 始终使用。8、使用 EnumSet 或 EnumMap在某些情况下,映射中可能的键的数量是预先知道的——例如在使用配置映射时。如果该数字相对较小,您应该真正考虑使用EnumSetor EnumMap,而不是常规HashSetor HashMap。这很容易通过查看来解释EnumMap.put():private transient Object[] vals; public V put(K key, V value) {    // ...    int index = key.ordinal();    vals[index] = maskNull(value);    // ...}这个实现的本质是,我们有一个索引值数组,而不是一个哈希表。当插入一个新值时,为了查找映射条目,我们所要做的就是向enum查询它的常数序数,该常数序数是由Java编译器在每个enum类型上生成的。如果这是一个全局配置映射(即只有一个实例),增加的访问速度将帮助EnumMap大大超过HashMap,它可能使用更少的堆内存,但必须在每个键上运行hashCode()和equals()。Enum和EnumMap是非常亲密的朋友。当您使用类似于枚举的结构作为键时,请实际考虑将这些结构作为枚举,并在EnumMap中使用它们作为键。9、优化你的 hashCode() 和 equals() 方法如果不能使用EnumMap,至少优化hashCode()和equals()方法。一个好的hashCode()方法是必要的,因为它将防止进一步调用开销大得多的equals(),因为它将为每个实例集生成更多不同的散列桶。在每个类层次结构中,都可能有流行的和简单的对象。hashCode()最简单、最快的实现是这样的:// AbstractTable, a common Table base implementation: @Overridepublic int hashCode() {     // [#1938] This is a much more efficient hashCode()    // implementation compared to that of standard    // QueryParts    return name.hashCode();}name表名在哪里。我们甚至不考虑表的模式或任何其他属性,因为表名通常在数据库中足够不同。此外,它name是一个字符串,所以它里面已经有一个缓存hashCode()值。注释很重要,因为AbstractTableextends是任何AST(抽象语法树)元素AbstractQueryPart的通用基础实现。通用 AST 元素没有任何属性,因此它不能对优化实现做出任何假设。因此,被覆盖的方法如下所示:hashCode()// AbstractQueryPart, a common AST element// base implementation: @Overridepublic int hashCode() {    // This is a working default implementation.    // It should be overridden by concrete subclasses,    // to improve performance    return create().renderInlined(this).hashCode();}换句话说,必须触发整个 SQL 渲染工作流来计算一个普通 AST 元素的哈希码。事情变得更有趣equals()// AbstractTable, a common Table base implementation: @Overridepublic boolean equals(Object that) {    if (this == that) {        return true;    }     // [#2144] Non-equality can be decided early,    // without executing the rather expensive    // implementation of AbstractQueryPart.equals()    if (that instanceof AbstractTable) {        if (StringUtils.equals(name,            (((AbstractTable<?>) that).name))) {            return super.equals(that);        }         return false;    }     return false;}第一件事:总是(不仅在NOPE 分支中)提前中止每个equals()方法,如果:-   this == argument-   this "incompatible type" argument- 请注意,后一个条件包括argument == null, 如果您instanceof用于检查兼容类型。我们之前在10 Subtle Best Practices when Coding Java中对此进行了博文。现在,在明显情况下尽早中止比较之后,您可能还希望在可以做出部分决定时尽早中止比较。例如,约定Table.equals()是两个表被认为是相等的,它们必须具有相同的名称,而不管具体的实现类型如何。例如,这两项不可能相等:-   com.example.generated.Tables.MY_TABLE-   DSL.tableByName("MY_OTHER_TABLE")如果argument 不能等于this,并且我们可以轻松地检查它,那么让我们这样做并在检查失败时中止。如果检查成功,我们仍然可以从super. 鉴于宇宙中的大多数对象都不相等,我们将通过快捷方式节省大量 CPU 时间。10、在集合中思考,而不是在单个元素最后但并非最不重要的一点是,有一件事与 Java 无关,但适用于任何语言。此外,我们将离开N.O.P.E.分支,因为此建议可能只会帮助您从 迁移到,或类似的东西。不幸的是,许多程序员从简单的本地算法的角度来思考。他们正在逐步解决问题,一个分支一个分支,一个循环一个循环,一个方法一个方法。这就是命令式和/或函数式编程风格。虽然在从纯命令式到面向对象(仍然是命令式)再到函数式编程时,对“更大的图景”进行建模变得越来越容易,但所有这些风格都缺乏只有 SQL 和 R 以及类似语言才有的东西:声明式编程。在 SQL 中(我们喜欢它,因为这是O(N3)O(n log n)) 你可以声明你想从你的数据库中得到的结果,而不会对算法产生任何影响。然后,数据库可以考虑所有可用的元数据(例如约束、键、索引等),以找出可能的最佳算法。从理论上讲,这从一开始就是SQL 和关系演算背后的主要思想。使用集合的主要优点是您的算法将变得更加简洁。而不是:// Pre-Java 8Set result = new HashSet();for (Object candidate : someSet)if (someOtherSet.contains(candidate))result.add(candidate);// Even Java 8 doesn’t really helpsomeSet.stream().filter(someOtherSet::contains).collect(Collectors.toSet()); 有些人可能会争辩说,函数式编程和 Java 8 将帮助您编写更简单、更简洁的算法。这不一定是真的。您可以将命令式 Java-7 循环转换为功能性 Java-8 Stream 集合,但您仍在编写相同的算法。编写类似 SQL 的表达式是不同的。SomeSet 相交 SomeOtherSet可以通过实现引擎以 1000 种方式实现。EnumSet正如我们今天所了解的,在运行操作之前将这两个集合自动转换为明智的做法INTERSECT。也许我们可以在INTERSECT不进行低级调用的情况下将其并行化Stream.parallel()11、结论在本文中,我们讨论了在N.O.P.E.分支上进行的优化,即在高复杂度算法的深处。  -   每个查询仅在单个上生成StringBuilder-   我们的模板引擎实际上是解析字符,而不是使用正则表达式-   我们尽可能使用数组,尤其是在迭代侦听器时-   我们远离我们不必调用的 JDBC 方法-   等等…————————————————                        原文链接:https://blog.csdn.net/weixin_42261238/article/details/137353891
  • [技术干货] Java 性能优化技巧:让你的应用飞起来
    性能优化是每个开发者和架构师都必须关注的重要领域,尤其是在企业级应用和高并发系统中,性能的好坏直接影响到系统的稳定性、用户体验以及资源消耗。Java 是一种高效、稳定的编程语言,但要确保应用程序在高负载环境下保持优秀的性能,仍然需要对代码和系统进行针对性的优化。本文将深入探讨一些常见的 Java 性能优化技巧,并通过示例代码帮助开发者提升应用程序的执行效率,降低资源消耗。目录性能优化的基本原则内存管理与优化CPU 优化技巧I/O 性能优化多线程与并发优化JVM 性能调优数据库性能优化总结1. 性能优化的基本原则性能优化通常需要平衡多个方面:时间、空间、可维护性和开发成本。以下是一些优化的基本原则:避免过早优化:在代码的初期阶段,应该专注于功能实现,避免过早进行性能优化。只有在性能瓶颈出现时,才应考虑进行优化。局部优化与全局优化:优化时要注意区分局部优化与全局优化,局部优化只会影响某一部分功能,而全局优化会影响系统的整体性能。定期性能分析与监控:通过工具进行性能分析,找到性能瓶颈所在,而不是依靠猜测。2. 内存管理与优化内存管理是 Java 性能优化中不可忽视的一部分。Java 通过垃圾回收机制来自动管理内存,但这并不意味着我们完全不需要关心内存管理。以下是一些优化技巧:2.1 避免频繁的垃圾回收尽量减少对象的创建:频繁的对象创建和销毁会导致垃圾回收的频繁触发,从而影响性能。尽量使用对象池来复用对象。// 使用对象池复用对象public class ObjectPool {    private List<SomeObject> pool = new ArrayList<>();    public SomeObject getObject() {        if (pool.isEmpty()) {            return new SomeObject();        }        return pool.remove(pool.size() - 1);    }    public void releaseObject(SomeObject object) {        pool.add(object);    }}避免使用过多的临时对象:特别是在循环中,避免每次迭代都创建临时对象。// 不推荐的做法for (int i = 0; i < 100000; i++) {    String temp = "Item " + i; // 每次都创建新的对象}// 优化后的做法StringBuilder builder = new StringBuilder();for (int i = 0; i < 100000; i++) {    builder.append("Item ").append(i); // 使用 StringBuilder 避免频繁创建新对象}2.2 内存泄漏检测内存泄漏会导致应用程序的内存占用不断增加,最终可能导致系统崩溃。常用的内存泄漏检查工具有:VisualVM:用于监控 JVM 的内存使用情况,并且可以生成堆转储文件来查看对象的引用情况。Eclipse MAT(Memory Analyzer Tool):帮助分析堆转储文件,找出潜在的内存泄漏问题。3. CPU 优化技巧CPU 的使用效率直接影响到应用程序的响应速度和吞吐量。以下是一些常见的 CPU 优化技巧:3.1 避免过多的线程上下文切换减少线程数量:线程创建和销毁会消耗 CPU 资源。避免线程的过度创建,使用线程池进行线程的管理和复用。// 使用线程池管理线程ExecutorService executorService = Executors.newFixedThreadPool(10); // 创建固定大小的线程池for (int i = 0; i < 1000; i++) {    executorService.submit(() -> {        // 执行任务    });}3.2 优化算法和数据结构选择合适的算法:算法的时间复杂度直接影响程序的运行速度。对于计算密集型的操作,尽量选择效率更高的算法。选择合适的数据结构:不同的数据结构有不同的访问效率。例如,查询频繁的操作应选择 HashMap,而对于有序的数据集合,则优先选择 TreeMap 或 TreeSet。// 使用 HashMap 提高查找效率Map<String, String> map = new HashMap<>();map.put("key", "value");String result = map.get("key"); // 查找操作效率高4. I/O 性能优化I/O 操作是影响 Java 应用性能的另一个重要因素。优化 I/O 性能可以显著提升程序的响应速度。常见的优化方法包括:4.1 使用缓冲流缓冲输入输出流(BufferedReader、BufferedWriter 等)能显著提高 I/O 操作的效率,尤其是在大量读取或写入数据时。缓冲区的存在可以减少每次 I/O 操作时的磁盘访问。// 使用缓冲输入流try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {    String line;    while ((line = reader.readLine()) != null) {        System.out.println(line);    }}4.2 批量处理数据批量读取和写入:对于需要大量读取或写入数据的操作,尽量批量处理数据,减少 I/O 操作的次数。// 批量读取文件try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {    String[] lines = new String[1000];    int index = 0;    while ((lines[index] = reader.readLine()) != null) {        index++;        if (index == 1000) {            // 批量处理数据            processBatch(lines);            index = 0;        }    }    if (index > 0) {        processBatch(lines);    }}5. 多线程与并发优化Java 的多线程和并发编程是提升应用程序性能的关键。以下是一些常见的优化技巧:5.1 线程池的使用使用线程池:线程池能有效管理和复用线程,避免线程的频繁创建和销毁。通过配置合适的线程池大小,可以优化 CPU 和内存的使用。// 创建线程池ExecutorService executorService = Executors.newFixedThreadPool(10); // 创建固定大小的线程池for (int i = 0; i < 1000; i++) {    executorService.submit(() -> {        // 执行任务    });}5.2 锁优化减少锁的粒度:锁的粒度越小,竞争就越少,性能也就越高。避免对整个方法或类进行加锁,而是只对真正需要加锁的部分加锁。// 锁粒度优化public class Counter {    private int count = 0;    public synchronized void increment() {        count++;    }    // 优化后的锁粒度    public void incrementOptimized() {        synchronized (this) {            count++;        }    }}6. JVM 性能调优JVM 是 Java 应用程序运行的核心,优化 JVM 的配置可以有效提升程序的性能。6.1 调整堆内存大小堆内存的调整:可以通过调整 JVM 启动参数来设置堆内存的初始大小和最大大小,避免频繁的垃圾回收。# 设置初始堆内存和最大堆内存-Xms512m -Xmx1024m6.2 调整垃圾回收器选择合适的垃圾回收器:根据应用场景选择适合的垃圾回收器。例如,对于低延迟的应用,G1 垃圾回收器是一个不错的选择;对于高吞吐量的应用,Parallel GC 更适合。# 使用 G1 垃圾回收器-XX:+UseG1GC7. 数据库性能优化数据库是应用性能的瓶颈之一,优化数据库的操作可以大幅提高系统的响应速度。7.1 索引优化创建适当的索引:通过合理的索引设计,可以显著提升查询效率,避免全表扫描。-- 创建索引以提高查询性能CREATE INDEX idx_name ON table_name(column_name);7.2 批量操作批量插入:对于大量数据的插入操作,使用批量插入可以大大减少数据库的负载。// 批量插入数据String sql = "INSERT INTO table_name (column1, column2) VALUES (?, ?)";try (PreparedStatement stmt = connection.prepareStatement(sql)) {    for (int i = 0; i < 1000; i++) {        stmt.setString(1, "value1");        stmt.setString(2, "value2");        stmt.addBatch();    }    stmt.executeBatch();}8. 总结Java 性能优化是一个持续的过程,需要根据具体的应用场景和性能瓶颈来选择合适的优化策略。通过合理的内存管理、算法优化、多线程处理、JVM 调优等方法,我们可以显著提升应用的性能和稳定性。性能优化是一个复杂而细致的过程,但通过不断的实践和学习,你可以使你的 Java 应用在面对高并发、大流量等场景时依然保持高效、稳定。————————————————原文链接:https://blog.csdn.net/mmc123125/article/details/143667289
  • [技术干货] Java性能优化的50个细节
    在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 应用程序,随着数据量和并发用户的增加,如何保证系统的高效运行,减少延迟和资源消耗,是确保系统可持续性和用户体验的关键。本文将探讨几个常见的 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
  • [技术干货] Java代码性能优化有哪些
    接口性能优化对于从事后端开发的同学来说,肯定再熟悉不过了,因为它是一个跟开发语言无关的公共问题。该问题说简单也简单,说复杂也复杂。有时候,只需加个索引就能解决问题。有时候,需要做代码重构。有时候,需要增加缓存。有时候,需要引入一些中间件,比如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
  • [技术干货] java高并发实战
    由于之前看的容易忘记,因此特记录下来,以便学习总结与更好理解,该系列博文也是第一次记录,所有有好多不完善之处请见谅与留言指出,如果有幸大家看到该博文,希望报以参考目的看浏览,如有错误之处,谢谢大家指出与留言。这里只是讲解下锁优化思路以及方法的总结,具体技术深究以后慢慢补充一、锁优化的思路和方法锁优化是指:在多线程的并发中当用到锁时,尽可能让性能有所提高。一般并发中用到锁,就是阻塞的并发,前面讲到一般并发级别分为阻塞的和非阻塞的(非阻塞的包含:无障碍的,无等待的,无锁的等等),一旦用到锁,就是阻塞的,也就是一般最糟糕的并发,因此锁优化就是在堵塞的情况下去提高性能;所以所锁的优化就是让性能尽可能提高,不管怎么提高,堵塞的也没有无锁的并发底。让锁定的障碍降到最低,锁优化并不是说就能解决锁堵塞造成的性能问题。这是做不到的。方法如下: 减少锁持有时间 减小锁粒度 锁分离 锁粗化 锁消除二、减少锁持有时间举例: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中的锁机制。一、同步锁性能分析同步锁在多线程编程中是保证线程安全的重要工具,其性能开销一直是不可忽视的存在。(一)性能验证说明为了直观说明我们可以直接先准备两个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
  • [问题求助] CodeArts Ide SmartAssist启动失败
    CodeArts Ide  当同一个窗口打开多个工程模块的时候,SmartAssist启动失败。
  • [问题求助] codearts Ide 同一目录下多模块,Java智能助手无法识别。
    codearts Ide 同一目录下多模块,Java智能助手无法识别。无法设置maven环境和jdk
  • [技术干货] SSL/TLS(弱加密)抓包解密办法
    SSL/TLS(弱加密)抓包解密办法
  • [专题汇总] 2024的尾声,12月份干货合集。
     大家好,首先祝大家新年快乐,身体健康,代码没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 
总条数:737 到第
上滑加载中