最近研讨代码程序,稍微总结一下,以后继续补充:
一、权衡程序的标准
权衡一个程序否是质优,可以从多个度角行进分析。其中,
最常见的权衡标准是程序的时光复杂度、空间复杂度,以及代码的可读性、可扩展性。
针对程序的时光复杂度和空间复杂度,想要化优程序代码,要需对数据结构与算法有入深的解理,并且悉熟算计机系统的基本概念和道理;而针对代码的可读性和可扩展性,想要化优程序代码,要需入深解理软件架构计划,熟知并会应用适合的计划式模。
- 首先,如今算计机系统的存储空间已足够大了,到达了 TB 别级,因此比相于空间复杂度,时光复杂度是程序员重要斟酌的素因。为了寻求高性能,在某些繁频操纵行执时,甚至可以斟酌用空间取换时光。
- 其次,由于到受处理器制造工艺的物理制约、本成制约,CPU 频主的增加遇到了瓶颈,摩尔定律已渐渐失效,每隔 18 个月 CPU 频主即翻倍的代时已过去了,程序员的编程式方发生了完全的转变。在前目这个多核多处理器的代时,现涌了原生支撑多线程的语言(如 Java)以及分布式并行算计架框(如 Hadoop)。为了使程序充分地利用多核 CPU,单简地现实一个单线程的程序是远远不够的,程序员要需可以编写出发并或者并行的多线程程序。
- 最后,大型软件系统的代码行数到达了百万级,如果没有一个计划好良的软件架构,想在已有代码的基础上行进发开,发开价值和维护本成是法无设想的。一个计划好良的软件该应有具可读性和可扩展性,循遵“开闭则原”、“赖依颠倒则原”、“面向接口编程”等。
二、项目绍介
本文将绍介笔者阅历的一个项目中的一部份,通过这个例实析剖代码化优的程过。上面简要地绍介该系统的相干部份。
该系统的发开语言为 Java,署部在共具有 4 核 CPU 的 Linux 服务器上,相干部份主要有以下操纵:通过某外部系统 D 供给的 REST API 获得信息,从中取提出有效的信息,并通过 JDBC 存储到某数据库系统 S 中,供系统其他部份用使,上述操纵的行执率频为天天一次,一般在半夜当系统空闲时时定行执。为了现实高可用性(High Availability),外部系统 D 署部在两台服务器上,因此要需分离从这两台服务器上获得信息并将信息插入数据库中,有效信息的条数到达了上千条,数据库插入操纵次数则为有效信息条数的两倍。
图 1. 系统体系结构图
为了速快地现实预期效果,在最初的现实中优先斟酌了功能的现实,而未斟酌系统性能和代码可读性等。系统大致有以下的现实:
- REST API 获得信息、数据库操纵可能抛出的异常信息都被记载到志日文件中,作为调试用;
- 共有 5 次数据库连接操纵,包含第一次清空数据库表,针对两个外部系统 D 各有两次数据库插入操纵,这 5 个连接都是立独的,用完以后即释放;
- 有所的数据库插入语句都是用使 java.sql.Statement 类生成的;
- 有所的数据库插入语句,都是单条行执的,即生成一条行执一条;
- 全部程过都是在单个线程中行执的,包含数据库表清空操纵,数据库插入操纵,释放数据库连接;
- 数据库插入操纵的 JDBC 代码散布在代码中。虽然这个版本的系统可以正常行运,到达了预期的效果,但是效率很低,从通过 REST API 获得信息,到析解并取提有效信息,再到数据库插入操纵,统共耗时 100 秒左右。而预期的时光该应在一分钟内以,这显然是不符合要求的。
三、代码化优程过
笔者开始分析全部程过有哪些耗时操纵,以及如何晋升效率,短缩程序行执的时光。通过 REST API 获得信息,因为是用使外部系统供给的 API,所以法无在此处晋升效率;取得信息以后析解出有效部份,因为是对特定式格的信息行进析解,所以也无效率晋升的空间。所以,效率可以大幅度晋升的空间在数据库操纵部份以及程序制控部份。上面,分条述叙对耗时操纵的改良法方。
1. 针对志日记载的化优
闭关志日记载,或者变动志日输出别级。
因为从两台服务器的外部系统 D 上获失掉的信息是同相的,所以数据库插入操纵会抛出异常,异常信息类似于“Attempt to insert duplicate record”,这样的异常信息跟有效信息的条数相称,有上千条。种这况情是能预料到的,所以可以斟酌闭关志日记载,或者不闭关志日记载而是变动志日输出别级,只记载严重别级(severe level)的错误信息,并将此类操纵的志日别级调整为正告别级(warning level),这样就不会记载以上异常信息了。本项目用使的是 Java 自带的志日记载类,以下配置文件将志日输出别级设置为严重别级。
清单 1. log.properties 设置志日输出别级的段片
- # default file output is in user ’ s home directory.
- # levels can be: SEVERE, WARNING, INFO, FINE, FINER, FINEST
- java.util.logging.ConsoleHandler.level=SEVERE
- java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
- java.util.logging.FileHandler.append=true
# default file output is in user ’ s home directory. # levels can be: SEVERE, WARNING, INFO, FINE, FINER, FINEST java.util.logging.ConsoleHandler.level=SEVERE java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter java.util.logging.FileHandler.append=true
通过上述的化优以后,性能有了大幅度的晋升,从本来的 100 秒左右降到了 50 秒左右。为什么仅仅不记载志日就可以有如此大幅度的性能晋升呢?查阅料资,发明已有人做了相干的研讨与验实。经常听到 Java 程序比 C/C++ 程序慢的舆论,但是行运度速慢的真正原因是什么,估计很多人其实不清晰。对于 CPU 密集型的程序(即程序中含包大批算计),Java 程序可以到达 C/C++ 程序等同别级的度速,但是对于 I/O 密集型的程序(即程序中含包大批 I/O 操纵),Java 程序的度速就远远慢于 C/C++ 程序了,很大程度上是因为 C/C++ 程序能直接问访底层的存储设备。因此,不记载志日而失掉大幅度性能晋升的原因是,Java 程序的 I/O 操纵较慢,是一个很耗时的操纵。
2. 针对数据库连接的化优
享共数据库连接。
共有 5 次数据库连接操纵,每次都需新重建立数据库连接,数据库插入操纵实现以后又即立释放了,数据库连接没有被复用。为了做到享共数据库连接,可以通过单例式模(Singleton Pattern)得获一个同相的数据库连接,每次数据库连接操纵都享共这个数据库连接。这里没有用使数据库连接池(Database Connection Pool)是因为在程序只有少许的数据库连接操纵,只有在大批发并数据库连接的时候才要需连接池。
清单 2. 享共数据库连接的代码段片
- public class JdbcUtil {
- private static Connection con;
- // 从配置文件读取连接数据库的信息
- private static String driverClassName;
- private static String url;
- private static String username;
- private static String password;
- private static String currentSchema;
- private static Properties properties = new Properties();
- static {
- // driverClassName, url, username, password, currentSchema 等从配置文件读取,代码略去
- try {
- Class.forName(driverClassName);
- } catch (ClassNotFoundException e) {
- e.printStackTrace();
- }
- properties.setProperty("user", username);
- properties.setProperty("password", password);
- properties.setProperty("currentSchema", currentSchema);
- try {
- con = DriverManager.getConnection(url, properties);
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
- private JdbcUtil() {}
- // 得获一个单例的、享共的数据库连接
- public static Connection getConnection() {
- return con;
- }
- public static void close() throws SQLException {
- if (con != null)
- con.close();
- }
- }
public class JdbcUtil { private static Connection con; // 从配置文件读取连接数据库的信息 private static String driverClassName; private static String url; private static String username; private static String password; private static String currentSchema; private static Properties properties = new Properties(); static { // driverClassName, url, username, password, currentSchema 等从配置文件读取,代码略去 try { Class.forName(driverClassName); } catch (ClassNotFoundException e) { e.printStackTrace(); } properties.setProperty("user", username); properties.setProperty("password", password); properties.setProperty("currentSchema", currentSchema); try { con = DriverManager.getConnection(url, properties); } catch (SQLException e) { e.printStackTrace(); } } private JdbcUtil() {} // 得获一个单例的、享共的数据库连接 public static Connection getConnection() { return con; } public static void close() throws SQLException { if (con != null) con.close(); } }
通过上述的化优以后,性能有了小幅度的晋升,从 50 秒左右降到了 40 秒左右。享共数据库连接而失掉的性能晋升的原因是,数据库连接是一个耗时耗源资的操纵,要需同近程算计机行进网络通信,建立 TCP 连接,还要需维护连接状态表,建立数据缓冲区。如果享共数据库连接,则只要需行进一次数据库连接操纵,省去了多次新重建立数据库连接的时光。
3. 针对插入数据库记载的化优 - 1
用使预译编 SQL。
体具做法是用使 java.sql.PreparedStatement 替代 java.sql.Statement 生成 SQL 语句。PreparedStatement 使得数据库先预译编好 SQL 语句,可以传入数参。而 Statement 生成的 SQL 语句在每次提交时,数据库都需行进译编。在行执大批类似的 SQL 语句时,可以用使 PreparedStatement 高提行执效率。用使 PreparedStatement 的另一个利益是不要需拼接 SQL 语句,代码的可读性更强。通过上述的化优以后,性能有了小幅度的晋升,从 40 秒左右降到了 30~35 秒左右。
清单 3. 用使 Statement 的代码段片
- // 要需拼接 SQL 语句,行执效率不高,代码可读性不强
- StringBuilder sql = new StringBuilder();
- sql.append("insert into table1(column1,column2) values('");
- sql.append(column1Value);
- sql.append("','");
- sql.append(column2Value);
- sql.append("');");
- Statement st;
- try {
- st = con.createStatement();
- st.executeUpdate(sql.toString());
- } catch (SQLException e) {
- e.printStackTrace();
- }
// 要需拼接 SQL 语句,行执效率不高,代码可读性不强 StringBuilder sql = new StringBuilder(); sql.append("insert into table1(column1,column2) values('"); sql.append(column1Value); sql.append("','"); sql.append(column2Value); sql.append("');"); Statement st; try { st = con.createStatement(); st.executeUpdate(sql.toString()); } catch (SQLException e) { e.printStackTrace(); }
清单 4. 用使 PreparedStatement 的代码段片
- // 预译编 SQL 语句,行执效率高,可读性强
- String sql = “insert into table1(column1,column2) values(?,?)”;
- PreparedStatement pst = con.prepareStatement(sql);
- pst.setString(1,column1Value);
- pst.setString(2,column2Value);
- pst.execute();
// 预译编 SQL 语句,行执效率高,可读性强 String sql = “insert into table1(column1,column2) values(?,?)”; PreparedStatement pst = con.prepareStatement(sql); pst.setString(1,column1Value); pst.setString(2,column2Value); pst.execute();
4. 针对插入数据库记载的化优 - 2
用使 SQL 批处理。通过 java.sql.PreparedStatement 的 addBatch 法方将 SQL 语句加入到批处理,这样在调用 execute 法方时,就会一次性地行执 SQL 批处理,而不是逐条行执。通过上述的化优以后,性能有了小幅度的晋升,从 30~35 秒左右降到了 30 秒左右。
5. 针对多线程的化优
用使多线程现实发并 / 并行。
清空数据库表的操纵、把从 2 个外部系统 D 取得的数据插入数据库记载的操纵,是互相立独的任务,可以给个每任务分配一个线程行执。清空数据库表的操纵该应先于数据库插入操纵实现,可以通过 java.lang.Thread 类的 join 法方制控线程行执的前后顺序。在核单 CPU 代时,操纵系统中某一时辰只有一个线程在行运,通过程进 / 线程调度,给个每线程分配一小段行执的时光片,可以现实多个程进 / 线程的发并(concurrent)行执。而在前目的多核多处理器景背下,操纵系统中一同时辰可以有多个线程并行(parallel)行执,大大地高提了算计度速。
清单 5. 用使多线程的代码段片
- Thread t0 = new Thread(new ClearTableTask());
- Thread t1 = new Thread(new StoreServersTask(ADDRESS1));
- Thread t2 = new Thread(new StoreServersTask(ADDRESS2));
- try {
- t0.start();
- // 行执完清空操纵后,再行进后续操纵
- t0.join();
- t1.start();
- t2.start();
- t1.join();
- t2.join();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // 开断数据库连接
- try {
- JdbcUtil.close();
- } catch (SQLException e) {
- e.printStackTrace();
- }
Thread t0 = new Thread(new ClearTableTask()); Thread t1 = new Thread(new StoreServersTask(ADDRESS1)); Thread t2 = new Thread(new StoreServersTask(ADDRESS2)); try { t0.start(); // 行执完清空操纵后,再行进后续操纵 t0.join(); t1.start(); t2.start(); t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } // 开断数据库连接 try { JdbcUtil.close(); } catch (SQLException e) { e.printStackTrace(); }
通过上述的化优以后,性能有了大幅度的晋升,从 30 秒左右降到了 15 秒以下,10~15 秒之间。用使多线程而失掉的性能晋升的原因是,系统署部在所的服务器是多核多处理器的,用使多线程,给个每任务分配一个线程行执,可以充分地利用 CPU 算计源资。
笔者试着给个每任务分配两个线程行执,希望能使程序行运得更快,但是适得其反,此时程序行运的时光反而比个每任务分配一个线程行执的慢,大约 20 秒。笔者揣测,这是因为线程较多(于对相 CPU 的内核数),使得 CPU 忙于线程的上下文切换,过量的线程上下文切换使得程序的性能反而不如之前。因此,要根据现实的硬件环境,给任务分配适当的线程行执。
6. 针对计划式模的化优
用使 DAO 式模抽象出数据问访层。
本来的代码中混杂着 JDBC 操纵数据库的代码,代码结构显得分十纷乱。用使 DAO 式模(Data Access Object Pattern)可以抽象出数据问访层,这样使得程序可以立独于不同的数据库,即便问访数据库的代码发生了转变,下层调用数据问访的代码无需转变。并且程序员可以脱摆调单繁琐的数据库代码的编写,注专于业务逻辑层面的代码的发开。通过上述的化优以后,性能并未有晋升,但是代码的可读性、可扩展性大大地高提了。
图 2. DAO 式模的层次结构
清单 6. 用使 DAO 式模的代码段片
- // DeviceDAO.java,定义了 DAO 抽象,下层的业务逻辑代码引用该接口,面向接口编程
- public interface DeviceDAO {
- public void add(Device device);
- }
- // DeviceDAOImpl.java,DAO 现实,体具的 SQL 语句和数据库操纵由该类现实
- public class DeviceDAOImpl implements DeviceDAO {
- private Connection con;
- public DeviceDAOImpl() {
- // 得获数据库连接,代码略去
- }
- @Override
- public void add(Device device) {
- // 用使 PreparedStatement 行进数据库插入记载操纵,代码略去
- }
- }
// DeviceDAO.java,定义了 DAO 抽象,下层的业务逻辑代码引用该接口,面向接口编程 public interface DeviceDAO { public void add(Device device); } // DeviceDAOImpl.java,DAO 现实,体具的 SQL 语句和数据库操纵由该类现实 public class DeviceDAOImpl implements DeviceDAO { private Connection con; public DeviceDAOImpl() { // 得获数据库连接,代码略去 } @Override public void add(Device device) { // 用使 PreparedStatement 行进数据库插入记载操纵,代码略去 } }
顾回以上代码化优程过:闭关志日记载、享共数据库连接、用使预译编 SQL、用使 SQL 批处理、用使多线程现实发并 / 并行、用使 DAO 式模抽象出数据问访层,程序行运时光从最初的 100 秒左右降低到 15 秒以下,在性能上失掉了很大的晋升,同时也有具了更好的可读性和可扩展性。
四、结束语
通过该项目例实,笔者深深地觉得,想要写出一个性能化优、可读性可扩展性强的程序,要需对算计机系统的基本概念、道理,编程语言的特性,软件系统架构计划都有较入深的解理。“纸上得来终觉浅,绝知此事要躬行”,想要将这些基本理论、编程技能融会贯通,还要需不断地践实,并总结心得体会。
文章结束给大家分享下程序员的一些笑话语录: 这个世界上只有10种人:懂得二进制的和不懂得二进制的。