Shared posts

31 Jan 02:09

Article: Big Data Processing with Apache Spark – Part 1: Introduction

by Srini Penchikala

What is Spark

Apache Spark is an open source big data processing framework built around speed, ease of use, and sophisticated analytics. It was originally developed in 2009 in UC Berkeley’s AMPLab, and open sourced in 2010 as an Apache project.

Spark has several advantages compared to other big data and MapReduce technologies like Hadoop and Storm.

First of all, Spark gives us a comprehensive, unified framework to manage big data processing requirements with a variety of data sets that are diverse in nature (text data, graph data etc) as well as the source of data (batch v. real-time streaming data).

Spark enables applications in Hadoop clusters to run up to 100 times faster in memory and 10 times faster even when running on disk.

Spark lets you quickly write applications in Java, Scala, or Python. It comes with a built-in set of over 80 high-level operators. And you can use it interactively to query data within the shell.

In addition to Map and Reduce operations, it supports SQL queries, streaming data, machine learning and graph data processing. Developers can use these capabilities stand-alone or combine them to run in a single data pipeline use case.

In this first installment of Apache Spark article series, we'll look at what Spark is, how it compares with a typical MapReduce solution and how it provides a complete suite of tools for big data processing.

Hadoop and Spark

Hadoop as a big data processing technology has been around for 10 years and has proven to be the solution of choice for processing large data sets. MapReduce is a great solution for one-pass computations, but not very efficient for use cases that require multi-pass computations and algorithms. Each step in the data processing workflow has one Map phase and one Reduce phase and you'll need to convert any use case into MapReduce pattern to leverage this solution.

The Job output data between each step has to be stored in the distributed file system before the next step can begin. Hence, this approach tends to be slow due to replication & disk storage. Also, Hadoop solutions typically include clusters that are hard to set up and manage. It also requires the integration of several tools for different big data use cases (like Mahout for Machine Learning and Storm for streaming data processing).

If you wanted to do something complicated, you would have to string together a series of MapReduce jobs and execute them in sequence. Each of those jobs was high-latency, and none could start until the previous job had finished completely.

Spark allows programmers to develop complex, multi-step data pipelines using directed acyclic graph (DAG) pattern. It also supports in-memory data sharing across DAGs, so that different jobs can work with the same data.

Spark runs on top of existing Hadoop Distributed File System (HDFS) infrastructure to provide enhanced and additional functionality. It provides support for deploying Spark applications in an existing Hadoop v1 cluster (with SIMR – Spark-Inside-MapReduce) or Hadoop v2 YARN cluster or even Apache Mesos.

We should look at Spark as an alternative to Hadoop MapReduce rather than a replacement to Hadoop. It’s not intended to replace Hadoop but to provide a comprehensive and unified solution to manage different big data use cases and requirements.

Spark Features

Spark takes MapReduce to the next level with less expensive shuffles in the data processing. With capabilities like in-memory data storage and near real-time processing, the performance can be several times faster than other big data technologies.

Spark also supports lazy evaluation of big data queries, which helps with optimization of the steps in data processing workflows. It provides a higher level API to improve developer productivity and a consistent architect model for big data solutions.

Spark holds intermediate results in memory rather than writing them to disk which is very useful especially when you need to work on the same dataset multiple times. It’s designed to be an execution engine that works both in-memory and on-disk. Spark operators perform external operations when data does not fit in memory. Spark can be used for processing datasets that larger than the aggregate memory in a cluster.

Spark will attempt to store as much as data in memory and then will spill to disk. It can store part of a data set in memory and the remaining data on the disk. You have to look at your data and use cases to assess the memory requirements. With this in-memory data storage, Spark comes with performance advantage.

Other Spark features include:

  • Supports more than just Map and Reduce functions.
  • Optimizes arbitrary operator graphs.
  • Lazy evaluation of big data queries which helps with the optimization of the overall data processing workflow.
  • Provides concise and consistent APIs in Scala, Java and Python.
  • Offers interactive shell for Scala and Python. This is not available in Java yet.

Spark is written in Scala Programming Language and runs on Java Virtual Machine (JVM) environment. It currently supports the following languages for developing applications using Spark:

  • Scala
  • Java
  • Python
  • Clojure
  • R

Spark Ecosystem

Other than Spark Core API, there are additional libraries that are part of the Spark ecosystem and provide additional capabilities in Big Data analytics and Machine Learning areas.

These libraries include:

  • Spark Streaming:
    • Spark Streaming can be used for processing the real-time streaming data. This is based on micro batch style of computing and processing. It uses the DStream which is basically a series of RDDs, to process the real-time data.
  • Spark SQL:
    • Spark SQL provides the capability to expose the Spark datasets over JDBC API and allow running the SQL like queries on Spark data using traditional BI and visualization tools. Spark SQL allows the users to ETL their data from different formats it’s currently in (like JSON, Parquet, a Database), transform it, and expose it for ad-hoc querying.
  • Spark MLlib:
    • MLlib is Spark’s scalable machine learning library consisting of common learning algorithms and utilities, including classification, regression, clustering, collaborative filtering, dimensionality reduction, as well as underlying optimization primitives.
  • Spark GraphX:
    • GraphX is the new (alpha) Spark API for graphs and graph-parallel computation. At a high level, GraphX extends the Spark RDD by introducing the Resilient Distributed Property Graph: a directed multi-graph with properties attached to each vertex and edge. To support graph computation, GraphX exposes a set of fundamental operators (e.g., subgraph, joinVertices, and aggregateMessages) as well as an optimized variant of the Pregel API. In addition, GraphX includes a growing collection of graph algorithms and builders to simplify graph analytics tasks.

Outside of these libraries, there are others like BlinkDB and Tachyon.

BlinkDB is an approximate query engine and can be used for running interactive SQL queries on large volumes of data. It allows users to trade-off query accuracy for response time. It works on large data sets by running queries on data samples and presenting results annotated with meaningful error bars.

Tachyon is a memory-centric distributed file system enabling reliable file sharing at memory-speed across cluster frameworks, such as Spark and MapReduce. It caches working set files in memory, thereby avoiding going to disk to load datasets that are frequently read. This enables different jobs/queries and frameworks to access cached files at memory speed.

And there are also integration adapters with other products like Cassandra (Spark Cassandra Connector) and R (SparkR). With Cassandra Connector, you can use Spark to access data stored in a Cassandra database and perform data analytics on that data.

Following diagram (Figure 1) shows how these different libraries in Spark ecosystem are related to each other.

Figure 1. Spark Framework Libraries

We'll explore these libraries in future articles in this series.

Spark Architecture

Spark Architecture includes following three main components:

  • Data Storage
  • API
  • Management Framework

Let’s look at each of these components in more detail.

Data Storage:

Spark uses HDFS file system for data storage purposes. It works with any Hadoop compatible data source including HDFS, HBase, Cassandra, etc.

API:

The API provides the application developers to create Spark based applications using a standard API interface. Spark provides API for Scala, Java, and Python programming languages.

Following are the website links for the Spark API for each of these languages.

Resource Management:

Spark can be deployed as a Stand-alone server or it can be on a distributed computing framework like Mesos or YARN.

Figure 2 below shows these components of Spark architecture model.

Figure 2. Spark Architecture

Resilient Distributed Datasets

Resilient Distributed Dataset (based on Matei’s research paper) or RDD is the core concept in Spark framework. Think about RDD as a table in a database. It can hold any type of data. Spark stores data in RDD on different partitions.

They help with rearranging the computations and optimizing the data processing.

They are also fault tolerance because an RDD know how to recreate and recompute the datasets.

RDDs are immutable. You can modify an RDD with a transformation but the transformation returns you a new RDD whereas the original RDD remains the same.

RDD supports two types of operations:

  • Transformation
  • Action

Transformation: Transformations don't return a single value, they return a new RDD. Nothing gets evaluated when you call a Transformation function, it just takes an RDD and return a new RDD.

Some of the Transformation functions are map, filter, flatMap, groupByKey, reduceByKey, aggregateByKey, pipe, and coalesce.

Action: Action operation evaluates and returns a new value. When an Action function is called on a RDD object, all the data processing queries are computed at that time and the result value is returned.

Some of the Action operations are reduce, collect, count, first, take, countByKey, and foreach.

How to Install Spark

There are few different to install and use Spark. You can install it on your machine as a stand-alone framework or use one of Spark Virtual Machine (VM) images available from vendors like Cloudera, HortonWorks, or MapR. Or you can also use Spark installed and configured in the cloud (like Databricks Cloud).

In this article, we’ll install Spark as a stand-alone framework and launch it locally. Spark 1.2.0 version was released recently. We’ll use this version for sample application code demonstration.

How to Run Spark

When you install Spark on the local machine or use a Cloud based installation, there are few different modes you can connect to Spark engine.

The following table shows the Master URL parameter for the different modes of running Spark.

How to Interact with Spark

Once Spark is up and running, you can connect to it using the Spark shell for interactive data analysis. Spark Shell is available in both Scala and Python languages. Java doesn’t support an interactive shell yet, so this feature is currently not available in Java.

You use the commands spark-shell.cmd and pyspark.cmd to run Spark Shell using Scala and Python respectively.

Spark Web Console

When Spark is running in any mode, you can view the Spark job results and other statistics by accessing Spark Web Console via the following URL:

http://localhost:4040

Spark Console is shown in Figure 3 below with tabs for Stages, Storage, Environment, and Executors.

(Click on the image to enlarge it)

Figure 3. Spark Web Console

Shared Variables

Spark provides two types of shared variables to make it efficient to run the Spark programs in a cluster. These are Broadcast Variables and Accumulators.

Broadcast Variables: Broadcast variables allow to keep read-only variable cached on each machine instead of sending a copy of it with tasks. They can be used to give the nodes in the cluster copies of large input datasets more efficiently.

Following code snippet shows how to use the broadcast variables.

//
// Broadcast Variables
//
val broadcastVar = sc.broadcast(Array(1, 2, 3))
broadcastVar.value

Accumulators: Accumulators are only added using an associative operation and can therefore be efficiently supported in parallel. They can be used to implement counters (as in MapReduce) or sums. Tasks running on the cluster can add to an accumulator variable using the add method. However, they cannot read its value. Only the driver program can read the accumulator's value.

The code snippet below shows how to use Accumulator shared variable:

//
// Accumulators
//

val accum = sc.accumulator(0, "My Accumulator")

sc.parallelize(Array(1, 2, 3, 4)).foreach(x => accum += x)

accum.value

Sample Spark Application

The sample application I cover in this article is a simple Word Count application. This is the same example one would cover when they are learning Big Data processing with Hadoop. We’ll perform some data analytics queries on a text file. The text file and the data set in this example are small, but same Spark queries can be used for large size data sets, without any modifications in the code.

To keep the discussion simple, we’ll use the Spark Scala Shell.

First, let’s look at how to install Spark on your local machine.

Pre-Requisites:

  • You will need Java Development Kit (JDK) installed for Spark to work locally. This is covered in Step 1 below.
  • You will also need to install Spark software on your laptop. The instructions on how to do this are covered in the Step 2 below.

Note: These instructions are for Windows environment. If you are using a different operating system environment, you'll need to modify the system variables and directory paths to match your environment.

I. INSTALL JDK:

1) Download JDK from Oracle website. JDK version 1.7 is recommended.

Install JDK in a directory name without spaces. For Windows users, install JDK in a folder like c:\dev, not in "c:\Program Files". "Program Files" directory has a space in the name and this causes problems when software is installed in this folder.

NOTE: DO NOT INSTALL JDK or Spark Software (described in Step 2) in "c:\Program Files" directory.

2) After installing JDK, verify it was installed correctly by navigating to "bin" folder under JDK 1.7 directory and typing the following command:

java -version

If JDK is installed correctly, the above command would display the Java version.

II. INSTALL SPARK SOFTWARE:

Download the latest Spark version from Spark website. Latest version at the time of publication of this article is Spark 1.2. You can choose a specific Spark installation depending on the Hadoop version. I downloaded Spark for Hadoop 2.4 or later, and the file name is spark-1.2.0-bin-hadoop2.4.tgz.

Unzip the installation file to a local directory (For example, c:\dev).

To verify Spark installation, navigate to spark directory and launch Spark Shell using the following commands. This is for Windows. If you are using Linux or Mac OS, please edit the commands to work on your OS.

c:
cd c:\dev\spark-1.2.0-bin-hadoop2.4
bin\spark-shell

If Spark was installed correctly, you should the see the following messages in the output on the console.

….
15/01/17 23:17:46 INFO HttpServer: Starting HTTP Server
15/01/17 23:17:46 INFO Utils: Successfully started service 'HTTP class server' on port 58132.
Welcome to
      ____              __
     / __/__  ___ _____/ /__
    _\ \/ _ \/ _ `/ __/  '_/
   /___/ .__/\_,_/_/ /_/\_\   version 1.2.0
      /_/

Using Scala version 2.10.4 (Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0_71)
Type in expressions to have them evaluated.
Type :help for more information.
….
15/01/17 23:17:53 INFO BlockManagerMaster: Registered BlockManager
15/01/17 23:17:53 INFO SparkILoop: Created spark context..
Spark context available as sc.

You can type the following commands to check if Spark Shell is working correctly.

sc.version

(or)

sc.appName

After this step, you can exit the Spark Shell window by typing the following command:

:quit

To launch Spark Python Shell, you need to have Python installed on your machine. You can download and install Anaconda which is a free Python distribution and includes several popular Python packages for science, math, engineering, and data analysis.

Then you can run the following commands:

c:
cd c:\dev\spark-1.2.0-bin-hadoop2.4
bin\pyspark

Sample Spark Application

Once you have Spark installed and have it up and running, you can run the data analytics queries using Spark API.

These are simple commands to read the data from a text file and process it. We’ll look at advanced use cases of using Spark framework in the future articles in this series.

First, let’s use Spark API to run the popular Word Count example. Open a new Spark Scala Shell if you don’t already have it running. Here are the commands for this example.

import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._
 
val txtFile = "README.md"
val txtData = sc.textFile(txtFile)
txtData.cache()

We call the cache function to store the RDD created in the above step in the cache, so Spark doesn’t have to compute it every time we use it for further data queries. Note that cache() is a lazy operation. Spark doesn’t immediately store the data in memory when we call cache. It actually takes place when an action is called on an RDD.

Now, we can call the count function to see how many lines are there in the text file.

txtData.count()

Now, we can run the following commands to perform the word count. The count shows up next to each word in the text file.

val wcData = txtData.flatMap(l => l.split(" ")).map(word => (word, 1)).reduceByKey(_ + _)

wcData.collect().foreach(println)

If you want to look at more code examples of using Spark Core API, checkout Spark documentation on their website.

What's Next

In the future articles of this series, we'll learn more about other parts of Spark ecosytem starting with Spark SQL. Later, we'll look at Spark Streaming, Spark MLlib, and Spark GraphX. We'll also look at the upcoming frameworks like Tachyon and BlinkDB.

Conclusions

In this article, we looked at how Apache Spark framework helps with big data processing and analytics with its standard API. We also looked at how Spark compares with traditional MapReduce implementation like Apache Hadoop. Spark is based on the same HDFS file storage system as Hadoop, so you can use Spark and MapReduce together if you already have significant investment and infrastructure setup with Hadoop.

You can also combine the Spark processing with Spark SQL, Machine Learning and Spark Streaming as we’ll see in a future article.

With several integrations and adapters on Spark, you can combine other technologies with Spark. An example of this is to use Spark, Kafka, and Apache Cassandra together where Kafka can be used for the streaming data coming in, Spark to do the computation, and finally Cassandra NoSQL database to store the computation result data.

But keep in mind, Spark is a less mature ecosystem and needs further improvements in areas like security and integration with BI tools.

References

About the Author

Srini Penchikala currently works as Software Architect at a financial services organization in Austin, Texas. He has over 20 years of experience in software architecture, design and development. Srini is currently authoring a book on NoSQL Database Patterns topic. He is also the co-author of "Spring Roo in Action" book (http://www.manning.com/SpringRooinAction) from Manning Publications. He has presented at conferences like JavaOne, SEI Architecture Technology Conference (SATURN), IT Architect Conference (ITARC), No Fluff Just Stuff, NoSQL Now and Project World Conference. Srini also published several articles on software architecture, security and risk management, and NoSQL databases on websites like InfoQ, The ServerSide, OReilly Network (ONJava), DevX Java, java.net and JavaWorld. He is a Lead Editor for NoSQL Databases community at InfoQ.

28 Jan 11:43

不正当使用HashMap导致cpu 100%的问题追究

by importnewzz

因最近hashmap误用引起的死循环又发生了一些案例,左耳朵浩子写了一篇blog 疫苗:Java HashMap的死循环,看了一下,大家的分析如出一辙。这篇blog也是好几年前写的了,之前在平台技术部的博客上贴过,随着组织结构的调整,那个博客可能不再维护,把这篇文章在这儿也保存一下。

李鹏同学在blog里写了篇关于HashMap死锁模拟的文章: http://blog.csdn.net/madding/archive/2010/08/25/5838477.aspx 做个纠正,那个不是死锁问题,而是死循环。

这个问题,我们以前讨论过。 校长之前的博客和淘宝的毕玄的《分布式Java应用:基础与实践》一书中都提到过 velocity导致cpu 100% 的bug,起因是HashMap的使用不当所致。

在之前的邮件列表里,校长提出过这个问题,当时我没仔细看,不清楚这个问题究竟是对 HashMap的误用,还是HashMap的潜在问题, 当时感觉不太可能是HashMap自身的问题,否则问题大了。应该是属于在并发的场景下错误的 使用了HashMap。昨天看了李鹏的blog后,觉得这个事情还是应该搞清楚一下;虽然我推测是链表形成闭环,但 没有去证明过。从网上找了一下: http://blog.csdn.net/autoinspired/archive/2008/07/16/2662290.aspx 里面也有提到:

产生这个死循环的根源在于对一个未保护的共享变量 — 一个”HashMap”数据结构的操作。当在 所有操作的方法上加了”synchronized”后,一切恢复了正常。检查”HashMap”(Java SE 5.0)的源 码,我们发现有潜在的破坏其内部结构最终造成死循环的可能。在下面的代码中,如果我们使得 HashMap中的entries进入循环,那 么”e.next()”永远都不会为null。

不仅get()方法会这样,put()以及其他对外暴露的方法都会有这个风险,这算jvm的bug吗?应该说不是的,这个现象很早以前就报告出来了(详细见: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6423457)。Sun的工程师并不认为这 是bug,而是建议在这样的场景下应用”ConcurrentHashMap”,在构建可扩展的系统时应将这点 纳入规范中。

这篇翻译提到了对HashMap的误用,但它没有点破HashMap内部结构在什么样误用情况下怎么被 破坏的;我想要一个有力的场景来弄清楚。再从李鹏的blog来看,用了2个线程来put就模拟出来了,最后堆栈是在 transfer 方法上(该方法是数据扩容时将数据从旧容器转移到新容器)。 仔细分析了一下里面的代码,基本得出了原因,证明了我之前的推测。

假设扩容时的一个场景如下(右边的容器是一个长度 2 倍于当前容器的数组) 单线程情况。

我们分析数据转移的过程,主要是链表的转移。

执行过一次后的状态:

最终的结果:

两个线程并发情况下,扩容时可能会创建出 2 个新数组容器

顺利的话,最终转移完可能是这样的结果

但并发情况下,出现死循环的可能场景是什么呢? 还要详细的分析一下代码,下面的代码中重点在 do/while 循环结构中(完成链 表的转移)。

// 扩容操作,从一个数组转移到另一个数组
void transfer(Entry[] newTable) { 
    Entry[] src = table;
    int newCapacity = newTable.length; 
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j]; 
        if (e != null) {
            src[j] = null; 
            do {
                Entry<K,V> next = e.next; //假设第一个线程执行到这里 
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null); // 可能导致死循环
        }
    }
}

2 个线程并发情况下, 当线程 1 执行到上面第 9 行时,而线程 2 已经完成了一 轮 do/while 操作,那么它的状态如下图:
(上面的数组时线程 1 的,已经完成了链表数据的转移;下面的是线程 2 的,它 即将开始进行对链表数据的转移,此时它记录 E1 和 E2 的首位已经被线程 1 翻 转了)

后续的步骤如下:

1) 插入 E1 节点,E1 节点的 next 指向新容器索引位置上的值(null 或 entry)

2) 插入 E2 节点,E2 的 next 指向当前索引位置上的引用值 E1

3)因为 next 不为 null,链表继续移动,此时 2 节点之间形成了闭环。造成了 死循环。

上面只是一种情况,造成单线程死循环,双核 cpu 的话占用率是 50%,还有导致 100%的情况,应该也都是链表的闭环所致。

最终,这并不是 HashMap 的问题,是使用场景的不当,在并发情况下选择非线程 安全的容器是没有保障的。

相关阅读:深入剖析ConcurrentHashMap(1)深入剖析ConcurrentHashMap(2)

相关文章

28 Jan 06:47

.NET API审查第二部分

by Jonathan Allen

本文是对于1月14日所进行的.NET API审查会议的分析的第二部分。这篇报告中涵盖了HashSet、RegEx、Process.Start、Immutable collections和BitVector32等API的相关讨论。

[视频] GitHub Issue#382:为HashSet<T>添加一个新的构造函数,可以在其中指定数据结构的初始大小

这一条意见毫无争议地通过了,只是让人有些疑惑的是,为什么原始版本中竟然会不支持这一特性。

[视频] GitHub Issue#304:正则功能应当提供一个校验方法

目前,RegEx解析器采取了一种快速失败的方法。这就意味着,如果在某个正则表达式字符串中存在多个错误,解析器只会报告第一个错误的存在,并且会立即中断解析过程。

这一建议的关键就在于希望获取更多的信息。在理想的情况下,解析器应当返回一个错误集合,以显示正则表达式中的所有问题。但这就需要解析器尝试从错误中进行恢复,继续解析之后的表达式内容,并寻找其它错误。

对此建议的一个顾虑在于,如果接受了这一建议。接下来就会有人提出进一步的要求,例如自定义高亮、代码自动完成等等,以便在Roslyn中进行使用。而这种要求已经远远超出API的范围了,因此评审团队担心,一旦接受了这一建议,迟早都会需要一个新的API。

结论:分别讨论两组建议。

一个建议是在RegEx在解析过程中出错时,对RegEx抛出的异常信息加以改善。这里只会包含一些基本信息,例如第一个错误的具体位置,前提是这种方式不会让代码产生太多重写要求。

第二个建议是,是否应该添加对应的TryParse方法。

RegEx API并不会扩展到这种可以支持IDE的程度。

[视频] GitHub Issue#311:重新引入Console.CancelKeyPress方法

这一条建议本身非常简单,就是重新引入这个存在于Windows桌面版的.NET中的方法,并将其扩展至跨平台版本的.NET中。不考虑兼容性问题的话,那就是简单地从原始版本中进行拷贝粘贴而已。

[视频] GitHub Issue#306:为Process.Start方法增加一个选项,以改变句柄的继承

表面上看,这个问题十分简单。在Windows中,句柄可以在子进程中继承,这可能会导致预料之外的问题,例如Notepad可能会持续保持一个打开的TCP socket,即使在父应用尝试关闭它之后。这个新的参数则能够让用户选择不继承句柄。

问题在于,这种处理在Linux环境下毫无意义。在Windows下创建子进程会使用一个全局的配置,而Linux则允许在创建句柄时指定每个句柄是否能够被继承。

一种建议是为句柄继承提供一个三态的属性:操作系统默认、继承句柄,以及不继承句柄。

另一种建议是创建一个基础版本的ProcessStartInfo方法,并且只提供跨平台的选项。然后通过创建子类的方法实现Linux、Win32和Windows Shell Execute的特定功能。

结论:提供一个新的属性,其类型为现有的HandleInheritability枚举,其中包含两个状态值。

[视频] GitHub Issue#318:对ImmutableInterlocked.ApplyChange API的建议

要确保对一个可变集合的变更是线程安全的,你必须获得对该集合的一个引用,创建一个替代的列表,并尝试调用Interlocked.CompareExchange来决定是否能够成功地替换原始列表。如果另一个线程在同一时间要覆盖该集合,该方法会返回false,你必须重新启动该过程以再次尝试。

ImmutableInterlocked能够简化一些基础操作的过程,例如添加或移除集合的项。你仍然需要在循环中使用它,但可以避免对本地变量的处理。

下一个建议是增加一个相应的泛型实现,其中可以接受一个lambda表达式进行某些操作。从技术上来说,这个泛型实现会使其它实现方式都变成多余的了,但既然那些实现已经发布,也不可能删除它们了。

结论:决定添加这个新方法。

有一个问题是,是否要将那些多余的方法标记为过期。

结论:由于那些多余的方法依然可以正常工作,因此不应该标记为过期。

[视频] GitHub Issue#373:BitVector32应当实现IEquatable<T>接口

BitVector32能够以一个比特和小整数数组的形式表现一个整数值。该类型存在于System.Collections.Specialiazed命名空间中,这个命名空间通常被基础类库团队当作遗留代码,没人敢去动其中的内容。

与之相关的一个问题是BitArray类,如果不把BitVector32当作遗留代码处理,那么它就应当和BitArray中的功能相一致。

对于该提议尚未得出任何结论,稍后将会再次对此展开讨论。

查看英文原文:.NET API Review Part 2

28 Jan 02:11

要你命三千:老代码中的那些坑

by scsecrystal

最近在给以前的老项目维护,说起来工作很简单,一个字:改Bug。这看起来平淡无常的工作,实际上凶险无比,藏坑无数。时至今日,感觉整个人都得到了升华。在睡觉前抽空写篇博客,和各位分享一下踩坑经历,一起品味其中的种种酸苦辣 (没甜)。

为保证个码隐私,文中代码均为化名,还望谅解。如有雷同,纯属巧合 (可以通过git blame 查看是谁写的)。

第一回:变量命名没点数,有时写着还手误

如果要折磨一个强迫症,最好的方法就是用各种恶心的变量名恶心死他。

什么?你说首字母要大写?

@property (nonatomic, assign) PERSONTYPE personType;

什么?你说单词里面要小写?

typedef enum tagPersonType
{
    person_type = 1,
    group_type,
} PERSONTYPE;

什么?你说要用英文单词命名?

- (void)uploadSeccess:(MessageEntity *)message;

什么?你说类前面要加前缀避免冲突?

@interface PMWLogger : NSObject
...
@interface PMTool : NSObject
...
@interface MainControler : NSObject

什么?你说文件要按照目录存放?

- Classes
    - MainControllers
        - MyController
        - Controllers
        - SettingControllers
        - ChatModel.h
        - ChatModel.m
        - SettingControllers (不是手误)
    - Chatting
    - SearchView.h
    - SearchView.m
    - Voice
    - AgentModels
- Public 
    - Common
    - PublicDef.h
    - PublicDef.m

什么?听说OC可以用宏定义?

#define STRHASSBUSTR(str,subStr) ...

各位看官,这,能忍?

正所谓:

命名拼写看心情,文件目录不分明。
随机掺杂宏定义,鸡不安也犬不宁。

第二回:界面全靠神奇数,保准看到就发怵

前阵子在做 iPhone4 和 iPhone6 以及 iPhone6 Plus 的适配工作。

由于历史原因没有用 AutoLayout ,也由于历史原因老代码的布局全是用数字一个一个写死的。这就给适配带来了莫大的困难。

随便拣点代码给大家欣赏欣赏:

UILabel *infoLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 241, 320, 28)];

0 这种数字还好说,241 就完全让人摸不着头脑,至于 320 这个改成屏幕宽度倒也就还好,但是 28 这种神奇数字又是什么呢?

这种代码就是冲着干死队友的不偿命的态度去的。虽然写起来容易,但是维护困难,可读性极差,尤其是有多个控件布局的时候,依赖关系不明显,如果调整布局需要挨个重新计算并设置值,维护起来的酸爽,谁用谁知道。

要说神奇数字,集大成者莫过于此:

CGRect rect = CGRectMake(12.2+(page-1)*320+42.5*(i%7),((totalRows-1)%3)*55+2,42.5,42.5);

那天早上看到这代码差点就抱着键盘委屈的哭了出来。

正所谓:

界面写法各不同,歪门邪道千万种。
有朝一日被辞了,你的代码我不懂。

第三回:私有公有混一处,代理委托亦糊涂

在聊天的时候有这样一个数据类:

@interface HBTalkData : NSObject
{
    UIImage *_firstImage;
    NSArray *_imageArry;
    id _contents;
}
@property (nonatomic, assign) NSInteger messageId;
@property (nonatomic, strong) id contents;
@property (nonatomic, assign) NSTimeInterval timeInterval;
@property (nonatomic) BOOL fromSelf;
@property (nonatomic) BOOL isGroup;
@property (nonatomic, assign) HBTalkDataStatus talkDataStatus;
@property (nonatomic) HBTalkDataContentType contentType;
@property (nonatomic, strong) PersonInfo *personInfo;
@property (nonatomic, strong) UserInfo *cardUser;
@property (nonatomic, assign) CallType callType;
@property (nonatomic, strong) NSString *duartion;
@property (nonatomic, strong) NSString *mPhoneNumber;
@property (nonatomic, strong) NSString *imageList;
@property (nonatomic, strong) NSString *msgDesc;
@property (nonatomic, readonly) UIImage *firstImage;
@property (nonatomic, readonly) NSArray *imageArry;
@property (nonatomic, assign) float     cellHeight;
@property (nonatomic, assign) CGSize    textSize;
@property (nonatomic) NSTimeInterval voiceDuration;
@property (nonatomic) CGFloat dataSize;
@property (nonatomic) NSUInteger bubbleCount;
@property (nonatomic, copy) NSString *chatUserName;
@property (nonatomic, strong) MessageEntity *originalMessage;
@property (nonatomic, strong) HBTalkDataRegisterInfo *registerInfo;

-(void)reset;
- (NSString *)bubbleDescription;
...
@end

纤弱的头文件里塞满了各种属性定义和方法定义,仿佛可以听到头文件的不满和娇喘。

给大家出个题:看下下面的内容,猜一下这个类的文件名是什么:

... // 此处省略20行

@interface PersonInfo : NSObject
... // 此处省略20行
@property (nonatomic, assign)BOOL     isGrey;
@property (nonatomic, assign)BOOL     isBlack;
@property (nonatomic, assign)BOOL     isTop;
@property (nonatomic, assign)BOOL     isStar;

- (BOOL)isStranger;
- (BOOL)isIndividual;
- (BOOL)isDuDuSecretary;

@end

@interface UserInfo : PersonInfo
... // 此处再省20行
@property (nonatomic, assign)BOOL     mobileVerified;
@property (nonatomic, strong)NSString *countryCode;
@property (nonatomic, readonly)NSString *dialogName;
@end

@interface GroupInfo : PersonInfo
... // 此处又省20行
@property (nonatomic, strong)NSString *creater;
@property (nonatomic, assign)int      memberCount;
@property (nonatomic, strong)NSString *members;
@end

嗯然后这个文件叫做 UserInfo.h ,头文件将近100行。大兄弟,我读书少,你不要骗我。把三个类塞在一个文件里这种行为,除了难为队友,实在是没看出来有什么其他动机可言。

正所谓:

头文件里地方小,塞到一处并不好。
外部对象都知道,安全问题可不小。

第四回:消息通知满天飞,委托方法一大堆

我一直在想,到底是什么,让这个项目的开发人员对 NSNotificationCenter 如此痴迷,痴迷的令人陶醉。

在通过 Model 调用业务逻辑的时候,它这样发了一条命令:

// 喂,LOGIN_MODEL,帮我查下有没有更新
[LOGIN_MODEL versionCheckFromAbout:YES];

这个业务是用 GCD 开了新线程来做的,在后台检查有没有更新,如果有更新那么版本号后面会加个感叹号。那么问题来了:你咋告诉我你检查的结果是有更新还是没更新呐?难道要写个委托?然后定义个方法?然后更新的时候指认委托?然后有了结果再告诉委托?听起来就很烦躁嘛那干脆就用通知好了:

if (self.versionStatus != VersionStatusNormal) {
    [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFY_HAS_NEW_VERSION object:nil];
}

然后在需要做处理的类里面添加 Observer 就可以了:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(myIconShouldChange) name:NOTIFY_HAS_NEW_VERSION object:nil];

哈哈哈哈搞定了。

哈哈哈哈你个头啊!整个项目里类似于这种的通知就有十来个,这还是有宏定义的,好追杀一点。对于那些没有宏定义的,随手一写复制粘贴的,不知道还要填坑多少。

通知虽好,但也不要贪杯啊。

看起来轻松,只是 post 了一下就搞定了,但是在 Debug 的时候有点麻烦。尤其是如果有多个 Observer ,改动的时候牵一发而动全身。如果真的是有这样使用的必要倒也罢了,但是本来一个 block 或者 delegate 就能简单清晰的解决,现在却被搞得这么繁重,实在是没有必要。

而且 NSNotificationCenter 的代码基本是一种变相的复制粘贴,十分的不工整。这是个人恩怨了,撇开不提。

NSNotificationCenter 这种只是不痛不痒的小问题,仅仅是逻辑不够优雅,关系不够清晰罢了。但是如果委托使用不当那是恶心的不行。看下这个聊天页面:

@interface ChattingViewController () <UITableViewDataSource, UITableViewDelegate, UITextViewDelegate, ChattingActionsPanelDelegate, ChatModelDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate, HBTalkTableViewCellDelegate, EGORefreshTableHeaderDelegate, XTImagePickerControllerDelegate, ChattingInputPanelDelegate, VoiceRecordingButtonTrashBinViewContainer, ChattingUserDetailPanelDelegate, VoiceRecordingButtonDelegate>

这是一个 真实的故事 。整个类将近3000行,有2000多行是委托里定义的方法,你能信?

在这三千行代码里漫步,万事都要小心。因为你不知道 callIn 这种方法到底是定义的私有方法,还是在委托里定义的方法。#pragma mark 自然也是看心情加的,说不定加错了你也不要当真。

有时候委托都删了不见影子了,但是委托里的各种方法还留在以前的类里。

没人敢动。

How to play.

正所谓:

异步回调用通知,委托多的令人痴。
反正老子看不懂,不写代码光写诗。

第五回:第三方库无出处,随手改动无备注

相信做 iOS 的都知道 AFNetworking 这个网络库,在我们的项目里 AFNetworking 分两种,一个是别人家的 AFNetworking,一个是咱们的 AFNetworking。对奏是这么任性。在一个300行的头文件里,在99行这样低调的位置里,静静的插上了自己的方法,还在上面认认真真的写上了准确的注释:

/*扩展*/
-(void)setDDCImageWithURL:(NSURL *)url
         placeholderImage:(UIImage *)placeholderImage
                  success:(void (^)(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image))success
                  failure:(void (^)(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error))failure;

扩展个头啊!你加在人家的头文件里你说你是扩展,谁信?

这种改动遍地都是,特点是极其低调,难以察觉,甚至 TTTAttributedLabel 这种 UI 库也不能避免:改了 init 为了统一字体和颜色。。。

你说这代码,谁敢改?

我还曾经单纯的想给项目加上 Cocoapods 更新一下第三方库,现在想想,Naive。等以后写到新的独立模块的时候再说吧。

正所谓:

项目勤用三方库,随意穿插改无数。
即使类库有更新,试问代码谁维护。

第六回:单个对象多职责,悲伤逆向流成河

在聊天模块有这样一个类:ChatModel ,简直就是个多面手。

上能和服务器聊天,上传聊天消息同步聊天记录:

- (void)reSendMessages;
- (void)receiveSecretaryMessage:(MessageEntity *)msgEntity;
- (void)deleteMessagesByUserInfo:(UserInfo *)user;
- (void)setAudioMessageBePlayed:(AudioMessageEntity *)audioMessage;
- (void)sendBubbleReplyWithCallMessage:(CallMessageEntity *)callMessage;
- (int)saveMessage:(MessageEntity *)message;

下能做本地缓存管理,增删改查样样精通:

- (void)saveCacheMsg:(NSString *)msg UserMd5:(NSString *)md5;
- (NSString *)loadCacheMsgWithMd5:(NSString *)md5;
- (void)clearCacheMsgWithMd5:(NSString *)md5;

至于什么弹窗提醒,上传进度,完成提示,亦是轻松拿下。

以至于你改着改着不知不觉都会走到这里,因为它处理了太多太多的业务逻辑,每次 DEBUG 追杀断点回到这里,都像是一场久别重逢时的相遇,似曾相识。

正所谓:

一人做事一人当,切忌都往类里装。
开发人员干的爽,维护人员很受伤。

第七回:产品突增新功能,一行代码变大神

有时候需求来也匆匆去也匆匆,让人猝不及防。比如一个简单的登录逻辑:

@interface LoginModel ()
@property (nonatomic, strong)NSString *tcpURL;
@property (nonatomic, strong)UserInfo *offlineCallUser;
@property (nonatomic, assign)VersionStatus versionStatus;
@end

突然发现需要在版本更新的时候多个 API 检查,简单,加个 BOOL ,需要的时候设置成 YES 就行:

@property (nonatomic, assign)BOOL isShowVersionUpdate;

但是这个功能在 About 页面又有点改动,简单,再加个 BOOL 就行:

@property (nonatomic, assign)BOOL checkVersionFromAbout;

然后如果已经显示了注册页面又要少一些请求,行,那再加个 BOOL 值:

@property (nonatomic, assign)BOOL isRegisterShow;

得了,这代码只有你能懂了:

@interface LoginModel ()
@property (nonatomic, strong)NSString *tcpURL;
@property (nonatomic, strong)UserInfo *offlineCallUser;
@property (nonatomic, assign)VersionStatus versionStatus;
@property (nonatomic, assign)BOOL isShowVersionUpdate;
@property (nonatomic, assign)BOOL checkVersionFromAbout;
@property (nonatomic, assign)BOOL isRegisterShow;
@end

想象一下实现方法里各种对 BOOL 标记的特殊处理,想象一下 N 个 if 嵌套的壮观场景。

心塞。

正所谓:

凡是都要听产品,各种业务催的紧。
天塌下来也别怕,逻辑清晰自然挺。

第八回:来了任务有委托,多写一行都嫌多

所谓悲哀就是,当程序员发现一个 delegate 就能访问上级的对象,于是便把各种需要通知上级的事情都放在了委托方法里,尽管这些事情与委托本身无关,但是为了实现功能已经不在意这些所谓的设计与美观。

一个简单的 @optional ,甚至可以用同一个 @protocol 获取到各种不同的上级对象,只需要每次调用的时候加个respondsToSelector 就行了。写上十几个可选方法,取一个通俗的委托名,比如 MyDelegate ,然后如果你持有了我但是我还想调用你的方法, so easy ,把你的方法扔到 MyDelegate 即可。

此时的代码便已经不再是一件艺术品,而只是一个平凡普通、毫无生机的花瓶了。

小结

原本还是挺欢快的吐槽,突然就不想写了。

看着以前的人写的代码,不禁有些凄凉。

在项目里用尽了各种低级下流的手段,只为了实现自己的业务。

这是对艺术的侮辱。

要你命三千:老代码中的那些坑,首发于博客 - 伯乐在线

28 Jan 01:56

几周内搞定Java的10个方法

by 孙 波翔

不要将Java与JavaScript弄混了,Java的目标是“一次编译,到处调试”(呃,不对,是“到处运行”)。简单来说,就是Java程序可以直接在任何设备上运行。

Java语言是什么?

不管我们是否意识到,实际上我们基本每天都在与Java打交道。在浏览网页时,可能会弹出一个提示,要求必须安装Java才能继续浏览。这种情况一般发生在使用flash或者是通过某种方式将flash组件集成到核心系统的站点。

Java并不是那种通常在新电脑上直接下载下来就能用的程序。我不能确定有没有操作系统将Java作为默认的可执行程序。虽然历史上Java在安全方面有过问题,但这也让Java变得更加成熟。

从笔记本到数据中心、从游戏机到科学家的超级计算机、从手机到互联网,Java无处不在!

  • 97%的企业桌面运行Java
  • 美国89%的计算机运行着Java
  • 世界共有9百万Java开发者
  • Java是开发者的首选
  • 头号开发平台
  • 三十亿部手机运行着Java
  • 所有的蓝光播放器中都含有Java
  • 有五十亿Java Cards在使用中
  • 1.25亿的TV设备运行着Java
  • 5个最大的OEM商都安装Java ME

从上述的统计中可以看到,Java语言非常受追捧而且Java的市场也很大。

Java开发者的薪酬

我想简要讨论下Java的薪酬,即全职的Java开发者在合理的时间内能赚多少钱。

该表是全美国范围的薪酬统计报告,大致在8万5千美元上下。由于我们能够免费学习Java,所以这个工资是相当高的。下面就来介绍能在短短几周内免费学习Java的资源。

Java入门

前一阵子我发布了这门在线课程,并收到了许多反馈。大部分人都说课程中介绍的方式学起Java太费时了,他们想在几周内就搞定Java。我同意这个观点,因此在这里我添加一些你应该了解的额外课程。

这些课程有些需要付费,但通过这些课程可以接触到一些独立的社区,会发现有35000名学生在学习这门课程。课程的内容超过10个小时,100多篇讲义,还有数百个针对疑难问题的讨论。

没有比这些课程能更好的学习Java了,学习Java最好的途径是在实践中成长为专家,但这样很难完成。另外,我很乐意在课程中回答问题。

Java基础

学习哪一门语言并不重要,任何一门语言都是要从基础学期,Java也不例外。值得庆幸的是, Oracle官方(拥有Java的公司 )网站有一个大量关于Java的介绍,解释Java是什么,展示Java是如何工作的基本知识。

读者还会发现还有许多可供进一步阅读的资源。但最重要的是,这些页面会帮你获得开始学习Java所需的所有工具和相关内容。

Java编程入门

这是另一个非常优秀的资源,不仅仅是因为这份资源由世界顶尖的大学出版。而是因为你肯定会发现这门课的质量非常高。在通过这门课学习Java的过程中会迫使你阅读大量的资料。

这门课程广受好评,虽然刚开始学习可能有点偏理论,但依然被认为是最适合初学者学习的Java资料。课程中有完整的图片、例子、代码和文档。

在线学习Java

近年来,交互式教程越来越多。所以我觉得有必要介绍一些网上的交互式课程。说实话,我们不会完整学完整个交互式课程,但我建议在用书本学习之前,先接触下这些交互式课程。

另外,交互式课程不用打开IDE就能载入并测试一些代码。

笨方法学Java

这是我推荐的唯一一本需要付费购买的书。因为这本书非常有用,很适合初学者。所以我认为还是值得买一本的,20刀。(译注:貌似没有中文版)。

读者可以在网上免费阅读前16章,链接在此。我想许多人都会喜欢上这本书的,该书的组织非常合理,有合适的练习题。不能要求再高了。

自己动手学Java

这个网站的作者与前面那本书的作者是同一个人,那本书的灵感即来自该网站。刚开始的时候会发现许多不需要大量编程技能就能解决的问题,但随着学习的深入,问题会越来越难。

我一直坚信,学习某个东西的最好途径就是先去做一遍,然后测试一下,再重新过一遍。所以选取上面列出的任何一个教程或书籍开始学习吧。

零基础学Java

有些人喜欢通过视频学习。在很忙的时候,视频可以随时重放,掌握其中关键的知识。

Udemy上的课程是免费的(但需要注册一个帐号才能观看),这门Java课程已经有超过10万人注册学习,是该网站上最火的课程之一。讲解者是John Purcell,他是拥有多年Java开发经验的软件工程师。

这门课有20多小时的内容,超过70篇讲义。加入Udemy,就能获得一个非常庞大的支持社区(所有学习该课程的学生都在这个社区里),Udemy还有内置的支持系统,可以提问。

CodingBat

不要让设计欺骗你,这会让你分心。 CodingBat(之前的JavaBat)是在浏览器中免费交互式学习Java的最佳途径之一。这是第二个推荐的交互式学习网站,我认为它比前一个网站更好。

与Codecademy不同,CodingBat中对所有内容都是从入门到深入,手把手的指导。CodingBat更多的是让你做你知道的事情,并安排你有能力进一步学习的内容。不要误认为这是没组织好,其中每节有大量的教程,学习起来会很快。

Java(初学者)编程教程

(译者注:需要自备梯子)

YouTube上的The New Boston用户发布了许多编程语言的视频教程,其中也包含Java,这些视频是已知最全面的介绍。里面有80部手把手教你学Java的教程,但可能有点旧了(视频使用的是Java 7)。

不要因为我说有点旧就放弃这个教程。对于那些下定决心学习Java的读者来说,这个资源依然非常有用。另外, 与Udemy不同,观看这个视频不需要注册。视频中的留言也非常有用。

Java面向对象编程

这是另一门大学课程,总计耗时6周。非常适合初学者,所有操作都可以在浏览器中完成。这门课程还有一个非常好的教程,介绍如何安装Java和相关工具。在课程中会学到计算机编程基础、算法,以及使用Java进行面向对象开发。

这门课程还有第二部分,同样为期六周。所以总计需要12-13周的时间来学习。完成课程后,应该学会如何构建自己的应用,如何以Java开发者的思维进行思考。这样就可以更进一步,更加深入的探索语Java言。

Java编程练习

最后给出一个用于练习编程的网站,该网站专注于Java,每个练习题都有解答。实际练习是非常重要的,特别是像Java这样的语言,这种语言并不能在一开始就熟练掌握。

这里总共有30个练习,如果遇到了难题,可以过一段时间再来看看,也许到时候就能找到解答方法。

学习Java的10种方法

这篇文章可能比不上我之前介绍的学习Python那篇文章。我认为这是因为Java并不是易学的编程语言,需要高度关注其中的细节。Java在今年的流程编程语言当中,背后有庞大的社区。

还有一个建议,就是多去类似StackOverflow和Reddit这样的站点看看。从中可以找到常见问题的答案、通过提问学习到更多的知识。不问问题,仅仅意味着目前不需要解决方案,但并非会一直如此。

我希望这些资源、书籍、和网站能帮到你。无论你是否会成为Java资深开发者,将这些资源转给你周围希望加入开发社区的人都是不错的选择。祝你好运!

相关文章

28 Jan 00:43

Apache Flink发布0.8.0,公布2015年规划

by Mikio Braun

Apache Flink发布了该项目的0.8.0版本。除了常规的性能、兼容性和稳定性方面的改进之外,这一版本增加了一个流式数据的Scala API,填补目前为止仍然缺乏的流式数据处理的能力。在大概9个月之前加入孵化器后,Apache Flink最近被升级为顶级的Apache项目。

Apache Flink是一个与Apache Spark有着相似目标的开源项目。它运行在Hadoop stack之上,通过提供比原有Hadoop系统的map和reduce操作更加强大的数据操作能力,让开发者可以更容易地编写可扩展的数据处理系统。

Kostas Tzoumas最近发布了一个关于Flink介绍和Flink2015年计划概览的演示文稿,他是Flink的代码提交者和一个位于柏林的Apache Flink相关的初创企业data Artisans的合伙人。

Flink的关键点之一,是它使用了一种类似于SQL数据库查询优化的方法,这也是它与当前版本的Apache Spark的主要区别。它可以将全局优化方案应用于某个查询之上以获得更佳的性能。例如,Flink可以重新排序操作以提升性能,或者根据相关数据集的属性,选择不同的实现方式来执行给定的操作符。

正如演示文稿中所提到的,这让Flink可以以流水线的方式执行一个操作序列,而Spark则需要一个接一个地执行不同的步骤。

Flink还提供类似迭代器的操作符以获得更多全局优化方面的潜能。利用这种操作符,Flink允许用户将迭代公式化,作为查询语句的一部分,而不需要在for循环中执行查询语句。

2015年规划的特性包括更好的内存管理和容错机制,交互使用的支持,统一的批处理和流处理,机器学习库Mahout的集成以及其他若干改进。

Flink最初源自于正在进行中的研究性项目Stratosphere的一部分。Flink还是柏林大数据中心的主平台,该数据中心是由德国政府资助的研究性项目,其目的是将机器学习研究者和可扩展的数据处理研究者汇集一处。

查看英文原文:Apache Flink 0.8.0 Released, Roadmap for 2015 Published

27 Jan 00:59

改进Linux网络性能

by WinterIsComing
RedHat的Jesper Brouer在LCA 2015会议上说,100Gb网络适配器即将到来。适配器的全线速运行对Linux内核是一大挑战。Brouer解释说,随着网络适配器越来越快,内核处理每个数据包的时间越来越短。以10Gb适配器为例,两个1538字节的数据包之间的时间间隔为 1230ns, 40Gb网络则将时间间隔进一步缩短到307ns,100Gb则再次减少到120ns,也就是说内核每秒需要处理高达815万个数据包,它没有多少时间弄清楚如何处理每个数据包。能发送的最小容量以太网帧是84字节,对10Gb网络而言,最小数据包的时间间隔是 67.2ns。Brouer称,对一个3GHz CPU,每个数据包处理只可用200 CPU周期,并不多。内核在处理这种网络密集工作负荷上做的并不好。






26 Jan 07:13

10个重要的Linux ps命令实战

by zzr0427

Linux作为Unix的衍生操作系统,Linux内建有查看当前进程的工具ps。这个工具能在命令行中使用。

PS 命令是什么

查看它的man手册可以看到,ps命令能够给出当前系统中进程的快照。它能捕获系统在某一事件的进程状态。如果你想不断更新查看的这个状态,可以使用top命令。

ps命令支持三种使用的语法格式

  1. UNIX 风格,选项可以组合在一起,并且选项前必须有“-”连字符
  2. BSD 风格,选项可以组合在一起,但是选项前不能有“-”连字符
  3. GNU 风格的长选项,选项前有两个“-”连字符

我们能够混用这几种风格,但是可能会发生冲突。本文使用 UNIX 风格的ps命令。这里有在日常生活中使用较多的ps命令的例子。

1. 不加参数执行ps命令

这是一个基本的 ps 使用。在控制台中执行这个命令并查看结果。

不加选项执行ps命令

结果默认会显示4列信息。

  • PID: 运行着的命令(CMD)的进程编号
  • TTY: 命令所运行的位置(终端)
  • TIME: 运行着的该命令所占用的CPU处理时间
  • CMD: 该进程所运行的命令

这些信息在显示时未排序。

2. 显示所有当前进程

使用 -a 参数。-a 代表 all。同时加上x参数会显示没有控制终端的进程。

$ ps -ax

这个命令的结果或许会很长。为了便于查看,可以结合less命令和管道来使用。

$ ps -ax | less

ps all 信息

3. 根据用户过滤进程

在需要查看特定用户进程的情况下,我们可以使用 -u 参数。比如我们要查看用户’pungki’的进程,可以通过下面的命令:

$ ps -u pungki

通过用户过滤

4. 通过cpu和内存使用来过滤进程

也许你希望把结果按照 CPU 或者内存用量来筛选,这样你就找到哪个进程占用了你的资源。要做到这一点,我们可以使用 aux 参数,来显示全面的信息:

$ ps -aux | less

显示全面信息

当结果很长时,我们可以使用管道和less命令来筛选。

默认的结果集是未排好序的。可以通过 –sort命令来排序。

根据 CPU 使用来升序排序

$ ps -aux --sort -pcpu | less

根据cpu使用排序

根据 内存使用 来升序排序

$ ps -aux --sort -pmem | less

根据内存使用来排序

我们也可以将它们合并到一个命令,并通过管道显示前10个结果:

$ ps -aux --sort -pcpu,+pmem | head -n 10

5. 通过进程名和PID过滤

使用 -C 参数,后面跟你要找的进程的名字。比如想显示一个名为getty的进程的信息,就可以使用下面的命令:

$ ps -C getty

通过进程名和PID过滤

如果想要看到更多的细节,我们可以使用-f参数来查看格式化的信息列表:

$ ps -f -C getty

通过进程名和PID过滤

6. 根据线程来过滤进程

如果我们想知道特定进程的线程,可以使用-L 参数,后面加上特定的PID。

$ ps -L 1213

根据线程来过滤进程

7. 树形显示进程

有时候我们希望以树形结构显示进程,可以使用 -axjf 参数。

$ps -axjf

树形显示进程

或者可以使用另一个命令。

$ pstree

树形显示进程

8. 显示安全信息

如果想要查看现在有谁登入了你的服务器。可以使用ps命令加上相关参数:

$ ps -eo pid,user,args

参数 -e 显示所有进程信息,-o 参数控制输出。Pid,User 和 Args参数显示PID,运行应用的用户该应用

显示安全信息

能够与-e 参数 一起使用的关键字是args, cmd, comm, command, fname, ucmd, ucomm, lstart, bsdstart 和 start

9. 格式化输出root用户(真实的或有效的UID)创建的进程

系统管理员想要查看由root用户运行的进程和这个进程的其他相关信息时,可以通过下面的命令:

$ ps -U root -u root u

-U 参数按真实用户ID(RUID)筛选进程,它会从用户列表中选择真实用户名或 ID。真实用户即实际创建该进程的用户。

-u 参数用来筛选有效用户ID(EUID)。

最后的u参数用来决定以针对用户的格式输出,由User, PID, %CPU, %MEM, VSZ, RSS, TTY, STAT, START, TIME 和 COMMAND这几列组成。

这里有上面的命令的输出结果:

show real and effective User ID

10. 使用PS实时监控进程状态

ps 命令会显示你系统当前的进程状态,但是这个结果是静态的。

当有一种情况,我们需要像上面第四点中提到的通过CPU和内存的使用率来筛选进程,并且我们希望结果能够每秒刷新一次。为此,我们可以将ps命令和watch命令结合起来

$ watch -n 1 ‘ps -aux --sort -pmem, -pcpu’

组合 ps 和 watch

如果输出太长,我们也可以限制它,比如前20条,我们可以使用head命令来做到。

$ watch -n 1 ‘ps -aux --sort -pmem, -pcpu | head 20’

组合 ps 和 watch

这里的动态查看并不像top或者htop命令一样。但是使用ps的好处是你能够定义显示的字段,你能够选择你想查看的字段。

举个例子,如果你只需要看名为’pungki’用户的信息,你可以使用下面的命令:

$ watch -n 1 ‘ps -aux -U pungki u --sort -pmem, -pcpu | head 20’

组合 ps 和 watch

结论

你也许每天都会使用ps命令来监控你的Linux系统。但是事实上,你可以通过ps命令的参数来生成各种你需要的报表。

ps命令的另一个优势是ps是各种 Linux系统都默认安装的,因此你只要用就行了。

不要忘了通过 man ps来查看更多的参数。(LCTT 译注:由于 ps 命令古老而重要,所以它在不同的 UNIX、BSD、Linux 等系统中的参数不尽相同,因此如果你用的不是 Linux 系统,请查阅你的文档了解具体可用的参数。)

10个重要的Linux ps命令实战,首发于博客 - 伯乐在线

26 Jan 00:58

Proxygen:Facebook开源的 C++ HTTP 框架

by sunbiaobiao

我们在这里激动的宣布开源我们的 Proxygen,一个 C++ HTTP库的集合,连同一个简单易用的HTTP 服务器。除了HTTP/1.1之外,Proxygen还支持SPDY 3 和 SPDY/3.1,目前正在添加对HTTP/2的支持。

Proxygen 的设计初衷并不是为了代替Apache 和nginx, 后两者都注重于使用C 语言提供一个极具弹性的HTTP 服务器,提供高的性能,但是配置工作非常复杂; 而我们则更关注构建一个高性能的C++ HTTP框架,该框架无论在服务器代码端还是客户代码端都配备了合理的预设值(默认不需要进行复杂配置),使其可以轻松嵌入到Facebook提供Web服务的现有应用中。 我们想帮助更多人构建和部署高性能的C++  HTTP服务器,我们相信Proxygen是一个很棒的框架。

背景

Proxygen一开始是一个可自定义配置,高性能的的HTTP(S)反向代理和负载平衡产品, 我们最开始是打算把Proxygen打造为一个软件库用来代理,这也是名字的由来, 但是 Proxygen 慢慢演变,看到已经有很多相同功能的软件已经存在(Apache Nginx HAProxy varnish 等)我们走了其他的方向。

为什么我们要打造自己的HTTP 技术堆栈

  • 能够与他们现有的基础设施和工具集成(Thrift、ODS),
  • 代码重用 , 创建一个可以供不同的内部项目(Haystack、HHVM和负载均衡器等)使用的事件驱动库,现在我们有很多内部的系统都是建立在Proxygen代码之上,包括  Haystack HHVM 我们的负载平衡器, 还有一些移动基础架构,Proxygen 提供了一个支持HTTP/2协议的平台
  • 扩展,我们尝试着使用其他已经存在的技术堆栈,但是它们已经存在很长时间了,一个已经积重难返的HTTP基础架构已经不能跟上我们的扩展迭代需求了
  •   一些既有的HTTP服务器还缺少很多特性,如:SPDY、WebSockets、HTTP/1.1 (keep-alive)、TLS false start和特定的负载调度算法

Proxygen项目发起于2011年,发起者一群对HTTP在Facebook的使用充满激情的工程师,从那时起,该项目就一直被一个三四个人的小组维护。在这个小组之外,还有很多内部的代码贡献者也加入到这个项目,自从项目开始以来,主要的里程碑包括:

  • 2011 –  Proxygen 开发部门开始谈论产品走的流量
  • 2012 –  加入了 SPDY/2的支持, 开始内部公开测试
  • 2013 – SPDY/3  开发测试迭代
  • 2014 –  完成  SPDY/3.1 rollout, 开始HTTP/2 的 开发

还有其他的里程碑可以参考这里 code

在该项目演进并在生产环境中测试了数年之后,我们到了一个可以将它开源的时间了

架构

Proxygen用到了下列概念:transaction(事务)、session(会话)、codec(编解码器)和handler(处理器)。事务表示的是在客户端和服务器之间交换的请求-响应对。这类相关的事务所组成的序列就是一个会话。编解码器负责将来自线路上的字节解析为对象,并将其与事务关联起来。消息最终传递给处理器进行真正地处理。  这个设置允许我们不用重写代码而支持新的 协议像  SPDY 和 HTTP/2

Proxygen 很是依赖最新的C++特新, 并且依赖 Thrift 和Folly 底层网络库和数据抽象, 我们使用了很多高级语法避免对大的对象像 body 缓存,和 header representations 进行多余拷贝,同时避免典型的一些坑像内存泄露, 另外,使用了 非阻塞IO和linux epoll技术,我们能够创建一个高效的服务器,

HTTP 服务器

如果你需要一个快速上手和事件驱动的服务器,那么我们的服务框架是一个很好的选择,只要一些很少的配置,你就可以使用了,参考这个demo:  echo server example ,  我们进行了在 32 logical core Intel(R) Xeon(R) CPU E5-2670 @ 2.60GHz with 16 GiB of RAM,* *从一个到八个线程做了基准测试, 我们的测试结果:

    # Load test client parameters:
    # Twice as many worker threads as server
    # 400 connections open simultaneously
    # 100 requests per connection
    # 60 second test
    # Results reported are the average of 10 runs for each test
    # simple GETs, 245 bytes of request headers, 600 byte response (in memory)
    # SPDY/3.1 allows 10 concurrent requests per connection

虽然 echo 服务器相对于真实的web 服务器来说很简单, 但是基准测试可以反映出解析一下二进制协议像SPDY 和HTTP/2 的效率指标,

影响

Proxygen 允许我们快速开发我们需要的特性并推出产品, 我们可以很快的看到结果, 举个例子,我们对升级 HPACK 编码压缩很是感兴趣, 但是我们还不知到有多少HTTP/2 客户端已经部署了, 并且HTTP/2 自己也在大幅度的开发中,  Proxygen允许我们自己实现HPACK,在SPDY上面使用,我们把它部署在移动客户端,和我们的服务器上进行模拟, 这个中真实的部署允许我们快速的理解性能和数据处理效率,其他的例子, 内部配置系统正一点一点变的更好,Proxygen 做的最积极的影响是给我们带来了快速迭代我们的HTTP 基础设施的能力

开源

Proxygen 现在正在快速的迭代中, 如果你对高性能网络代码,和现代C++感兴趣,我们欢迎你提交 pull requests

我们欢迎并拥抱开源,一直寻求机会分享我们的经验开源我们的软件。目前流量小组已经开源了 Thrift 和 Proxygen,  两个Facebook重要的基础网络架构组建。我们希望这些组件,能够构建其他的系统,并也希望这些新系统有一天也能开源出来

Proxygen:Facebook开源的 C++ HTTP 框架,首发于博客 - 伯乐在线

26 Jan 00:54

提升Java的锁性能

by importnewzz

几个月前我们介绍了如何通过Plumbr进行线程锁检测,随后便收到了很多类似的问题,“Hi,文章写得不错,现在我终于知道是什么引发的性能问题了,但是现在我该怎么做?”

为了在我们的产品中集成这个解决方案,我们付出了许多努力,不过在本文中,我想给大家分享几个常用的优化技巧,而不一定非要使用我们这款锁检测的工具。包括分拆锁,并发数据结构,保护数据而非代码,以及缩小锁的作用域。

锁无罪,竞争其罪

如果你在多线程代码中碰到了性能问题,你肯定会先抱怨锁。毕竟,从“常识”来讲,锁的性能是很差的,并且还限制了程序的可伸缩性。如果你怀揣着这样的想法去优化代码并删除锁的话,最后你肯定会引入一些难缠的并发BUG。

因此分清楚竞争锁与无竞争锁的区别是很有必要的。如果一个线程尝试进入另一个线程正在执行的同步块或者方法时,便会出现锁竞争。第二个线程就必须等待前一个线程执行完这个同步块并释放掉监视器(monitor)。如果只有一个线程在执行这段同步的代码,这个锁就是无竞争的。

事实上,JVM中的同步已经针对这种无竞争的情况进行了优化,对于绝大多数应用而言,无竞争的锁几乎是没有任何额外的开销的。因此,出了性能问题不能光怪锁,你得怪竞争锁。在明确了这点以后 ,我们来看下如何能减少锁的竞争或者竞争的时间。

保护数据而非代码

实现线程安全最快的方法就是直接将整个方法上锁。比如说下面的这个例子,这是在线扑克游戏服务端的一个简单的实现:

class GameServer {
  public Map<<String, List<Player>> tables = new HashMap<String, List<Player>>();

  public synchronized void join(Player player, Table table) {
    if (player.getAccountBalance() > table.getLimit()) {
      List<Player> tablePlayers = tables.get(table.getId());
      if (tablePlayers.size() < 9) {
        tablePlayers.add(player);
      }
    }
  }
  public synchronized void leave(Player player, Table table) {/*body skipped for brevity*/}
  public synchronized void createTable() {/*body skipped for brevity*/}
  public synchronized void destroyTable(Table table) {/*body skipped for brevity*/}
}

作者的想法是好的——就是当新的玩家加入的时候,必须得保证桌上的玩家的数量不能超过9个。

不过这个上锁的方案更适合加到牌桌上,而不是玩家进入的时候——即便是在一个流量一般的扑克网站上,这样的系统也肯定会由于线程等待锁释放而频繁地触发竞争事件。被锁住的代码块包含了帐户余额以及牌桌上限的检查,这里面很可能会包括一些很昂贵的操作,这样不仅会容易触发竞争并且使得竞争的时间变长。

解决问题的第一步就是要确保你保护的是数据,而不是代码,先将同步从方法声明移到方法体里。在上面这个简短的例子中,刚开始好像能修改的地方并不多。不过我们考虑的是整个GameServer类,而不只限于这个join()方法:

class GameServer {
  public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();

  public void join(Player player, Table table) {
    synchronized (tables) {
      if (player.getAccountBalance() > table.getLimit()) {
        List<Player> tablePlayers = tables.get(table.getId());
        if (tablePlayers.size() < 9) {
          tablePlayers.add(player);
        }
      }
    }
  }
  public void leave(Player player, Table table) {/* body skipped for brevity */}
  public void createTable() {/* body skipped for brevity */}
  public void destroyTable(Table table) {/* body skipped for brevity */}
}

这看似一个很小的改动,却会影响到整个类的行为。当玩家加入牌桌 时,前面那个同步的方法会锁在GameServer的this实例上,并与同时想离开牌桌(leave)的玩家产生竞争行为。而将锁从方法签名移到方法内部以后,则将上锁的时机往后推迟了,一定程度上减小了竞争的可能性。

缩小锁的作用域

现在我们已经确保保护的是数据而不是代码了,我们得再确认锁住的部分都是必要的——比如说,代码可以重写成这样 :

public class GameServer {
  public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();

  public void join(Player player, Table table) {
    if (player.getAccountBalance() > table.getLimit()) {
      synchronized (tables) {
        List<Player> tablePlayers = tables.get(table.getId());
        if (tablePlayers.size() < 9) {
          tablePlayers.add(player);
        }
      }
    }
  }
  //other methods skipped for brevity
}

现在检查玩家余额的这个耗时操作就在锁作用域外边了。注意到了吧,锁的引入其实只是为了保护玩家数量不超过桌子的容量而已,检查帐户余额这个事情并不在要保护的范围之内。

分拆锁

再看下上面这段代码,你会注意到整个数据结构都被同一个锁保护起来了。考虑到这个数据结构中可能会存有上千张牌桌,出现竞争的概率还是非常高的,因此保护每张牌桌不超出容量的工作最好能分别来进行。

对于这个例子而言,为每张桌子分配一个独立的锁并非难事,代码如下:

public class GameServer {
  public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();

  public void join(Player player, Table table) {
    if (player.getAccountBalance() > table.getLimit()) {
      List<Player> tablePlayers = tables.get(table.getId());
      synchronized (tablePlayers) {
        if (tablePlayers.size() < 9) {
          tablePlayers.add(player);
        }
      }
    }
  }
  //other methods skipped for brevity
}

现在我们把对所有桌子同步的操作变成了只对同一张桌子进行同步,因此出现锁竞争的概率就大大减小了。如果说桌子中有100张桌子的话,那么现在出现竞争的概率就小了100倍。

使用并发的数据结构

另一个可以改进的地方就是弃用传统的单线程的数据结构,改为使用专门为并发所设计的数据结构。比如说,可以用ConcurrentHashMap来存储所有的扑克桌,这样代码就会变成这样:

public class GameServer {
  public Map<String, List<Player>> tables = new ConcurrentHashMap<String, List<Player>>();

  public synchronized void join(Player player, Table table) {/*Method body skipped for brevity*/}
  public synchronized void leave(Player player, Table table) {/*Method body skipped for brevity*/}

  public synchronized void createTable() {
    Table table = new Table();
    tables.put(table.getId(), table);
  }

  public synchronized void destroyTable(Table table) {
    tables.remove(table.getId());
  }
}

join()和leave()方法的同步操作变得更简单了,因为我们现在不用再对tables进行加锁了,这都多亏了ConcurrentHashMap。然而,我们还是要保证每个tablePlayers的一致性。因此这个地方ConcurrentHashMap帮不上什么忙。同时我们还得在createTable()与destroyTable()方法中创建新的桌子以及销毁桌子,这对ConcurrentHashMap而言本身就是并发的,因此你可以并行地增加或者减少桌子的数量。

其它的技巧及方法

  • 降低锁的可见性。在上述例子中,锁是声明为public的,因此可以被别人所访问到,你所精心设计的监视器可能会被别人锁住,从而功亏一篑。
  • 看一下java.util.concurrent.locks包下面有哪些锁策略对你是有帮助的。
  • 使用原子操作。上面这个例子中的简单的计数器其实并不需要进行加锁。将计数的Integer换成AtomicInteger对这个场景来说就绰绰有余了。

希望本文对你解决锁竞争的问题能有所帮助,不管你有没有使用我们的Plumbr所提供的自动化锁检测方案,还是自己手动从线程dump信息中提取信息。

相关文章

24 Jan 03:20

376 页的全球互联网数据信息图,你想要的都在这里

by 欧狄

cover

近日,We Are Social 公司发布了一份囊括了全球互联网、移动互联网、社交媒体和电子商务统计数据的 376 页信息图报告,这份信息图报告根据 InternetLiveStats, InternetWorldStats, Wikipedia, GlobalWebIndex, Facebook, GSMA Intelligence 等多家调研公司或互联网公司发布的超过 240 个国家和地区的数据制作而成。

除了全球范围的概述外,该信息图报告还对以下 30 个世界最大的经济体进行详细描述。下文选取了信息图报告中的部分重要内容进行数据解读,而中国大陆的详细报告将在文尾附上。

Slide003

30

概述

Slide006

Slide007

We Are Social 公司指出,2014 年是互联网和移动互联网快速发展的一年,以下里程碑式的数字值得我们关注:

  • 全球活跃社交用户于 2014 年 8 月突破了 20 亿人;
  • 全球独立移动设备用户渗透率于 2014 年 9 月超过了总人口的 50%;
  • 全球活跃互联网用户在 2014 年 11 月 突破了 30 亿人;
  • 全球接入互联网的活跃移动设备于 2014 年 12 月超过了 36 亿台,这个数字相当于全球人口总数的一半。

互联网

Slide015

Slide017

2015 年 1 月,全球活跃互联网用户渗透率是总人口数的 42%,去年同期这个数字是 35 %。而在地域分配上,发达国家或地区使用互联网的人数比例普遍较高,互联网普及度基本和国家或地区的经济水平成正比。

比较极端的是,百慕大、巴林和冰岛的互联网用户数几乎等同于该国家或地区的人口总数,而朝鲜和南苏丹能使用上互联网的人数不及其总人口数的 0.1%。

Slide021

韩国荣登平均网速榜首,他们的国民普遍可以享受平均网速超过 25 Mbps 的上网快感,大大抛离其他国家和地区。而中国香港、日本、新加坡和美国紧随其后。中国大陆的平均网速只有 3.8 Mbps,低于全球平均网速 4.5 Mbps,位于榜单中下游。

Slide0183

全球平均每天使用网络时长是 4.4 小时,菲律宾人民最喜欢上网,平均每天花费超过 6 小时在网络上。泰国、越南、印度尼西亚和马来西亚人民同样不容小觑,平均每天花费在互联网上的时间都超过了 5 个小时,看来南亚人民对互联网挺依赖的。

移动互联网

Slide034

Slide040

2015 年 1 月,全球共有超过 36 亿的独立移动设备用户,同比增长 5 %。接入互联网的移动设备总数超过 70 亿台,而这 70 亿多台移动设备中活跃的约为 36 亿台。智能手机占接入互联网移动设备总数的 38%。

Slide046

Slide043

移动网速方面,发达国家的 3G&4G 普及率较高,移动网速的快慢基本和国家或地区的经济水平成正比。

和平均网速一样,韩国的平均移动网速抛离了其他国家或地区一条街,达到了 18.2 Mbps,是第二位的新加坡的两倍。中国大陆则以 6.2 Mbps 的平均移动网速排在这个榜单的中上游。

Slide020

随着全球三分之一的网页都为移动端做了适配,越来越多人使用移动设备来访问网页。2015 年 1 月,全球来自移动端的网页访问流量占总访问流量的 33%,同比增长 39%。

042

而在全球使用移动设备进行网页访问的流量中,38.9% 来自苹果旗下的 Safari 浏览器,30.9% 来自 Android 平台的 Webkit,还有 30.2% 来自其他移动浏览器平台。

社交媒体

Slide024

上图的数据基于每个国家最受欢迎的社交平台的月活跃用户数,可以看到,月活跃社交用户数已经超过了 20 亿,同比增长了 12%。而移动端的月活跃社交用户数则超过了 16 亿,占世界总人口数的 23%。

Slide0291

全球社交媒体用户平均每天花费 2.4 个小时在社交媒体上,而阿根廷和菲律宾的社交媒体用户则比较活跃,每天都花费超过 4 小时。

028

拥有超过 13 亿月活跃用户的 Facebook 依然是全球最受欢迎的社交平台,QQ 和 QQ 空间紧随其后。We Are Social 公司的分析师指出,虽然 QQ 的月活跃用户数有 8 亿 2 千万之多,但很多用户拥有不止一个账号,因此最好不要将这个数字用做 QQ 在世界范围内的渗透率统计。

拥有超过 4 亿月活跃用户的微信排在这个榜单的第 6 位。

mobile social

Slide031

虽然东亚拥有最多的活跃移动社交用户——5 亿 6 千万,但从活跃移动社交用户占总人口比例来看,北美优势明显。

电子商务

Slide0221

几乎 2/3 的英国人在网上进行购物,德国、韩国、美国和澳大利亚网购人数都超过了其总人口数的 50%。南亚和东南亚电子商务普遍不发达,泰国、菲律宾和印度网购人数都少于其总人口数的 1/5。

Slide0451

韩国再次出现在第一位,37% 的韩国人使用手机进行网购。韩国甩开其他国家一大截、高达 18.2 Mbps 的平均移动网速为其移动电子商务的发展提供了良好的基础。

中国大陆则出现在榜单的第二位,回想起天猫“双十一”期间移动端成交额占总额比例达 42.6%,这个排位也就不觉得奇怪了。

中国大陆的详细报告:

Slide093
Slide094
Slide095
Slide096
Slide097
Slide098
Slide099
Slide100
Slide101
Slide102
Slide103

而在 2014 年 1 月份,We Are Soical 公司也发布一份相同调研内容的报告,点击这里可以查看。

 

以上信息图均来自 We Are Social

#欢迎关注爱范儿认证微信公众号:AppSolution(微信号:appsolution),发现新酷精华应用。



爱范儿 · Beats of Bits | 原文链接 · 查看评论 · 新浪微博 · 微信订阅 · 加入爱范社区!


24 Jan 01:04

What happens when DNS in China is poisoned and pointed at your server

23 Jan 01:36

Improving Linux networking performance

23 Jan 00:51

Java中不同的并发实现的性能比较

image

Fork/Join框架在不同配置下的表现如何?

正如即将上映的星球大战那样,Java 8的并行流也是毁誉参半。并行流(Parallel Stream)的语法糖就像预告片里的新型光剑一样令人兴奋不已。现在Java中实现并发编程存在多种方式,我们希望了解这么做所带来的性能提升及风险是什么。从经过260多次测试之后拿到的数据来看,还是增加了不少新的见解的,这里我们想和大家分享一下。

ExecutorService vs. Fork/Join框架 vs. 并行流

在很久很久以前,在一个遥远的星球上。。好吧,其实我只是想说,在10年前,Java的并发还只能通过第三方库来实现。然后Java 5到来了,并引入了java.util.concurrent包,上面带有深深的Doug Lea的烙印。ExecutorService为我们提供了一种简单的操作线程池的方式。当然了,java.util.concurrent包也在不断完善,Java 7中还引入了基于ExecutorService线程池实现的Fork/Join框架。对很多开发人员来说,Fork/Join框架仍然显得非常神秘,因此Java 8的stream提供了一种更为方便地使用它的方法。我们来看下这几种方式有什么不同之处。

我们来通过两个任务来进行测试,一个是CPU密集型的,一个是IO密集型的,同样的功能,分别在4种场景下进行测试。不同实现中线程的数量也是一个非常重要的因素,因此这个也是我们测试的目标之一。测试机器共有8个核,因此我们分别使用4,8,16,32个线程来进行测试。对每个任务而言,我们还会测试下单线程的版本,不过这个在图中并没有标出来,因为它的时间要长得多。如果想了解这些测试用例是如何运行的,你可以看一下最后的基础库一节。我们开始吧。

给一段580万行6GB大小的文本建立索引

在本次测试中我们生成了一个超大的文本文件,并通过相同的方法来建立索引。我们来看下结果如何:

image

单线程执行时间:176,267毫秒,大约3分钟。 注意,上图是从20000毫秒开始的。

1. 线程过少会浪费CPU,而过多则会增加负载

从图中第一个容易注意到的就是柱状图的形状——光从这4个数据就能大概了解到各个实现的表现是怎样的了。8个线程到16个线程这里有所倾斜,这是因为某些线程阻塞在了文件IO这里,因此增加线程能更好地使用CPU资源。而当加到32个线程时,由于增加了额外的开销,性能又开始会变差。

2. 并行流表现最佳。与直接使用Fork/Join相比要快1秒左右

并行流所提供的可不止是语法糖(这里指的并不是lambda表达式),而且它的性能也比Fork/Join框架以及ExecutorService要更好。索引完6GB大小的文件只需要24.33秒。请相信Java,它的性能也能做到很好。

3. 但是。。并行流的表现也是最糟糕的:唯独它是超过了30秒的

并行流为什么会影响性能,这里也给你上了一课。这在本来就运行着多线程应用的机器上是有可能的。由于可用的线程本身就很少了,直接使用Fork/Join框架要比使用并行流更好一些——两者的结果相差5秒,大约是18%的性能损耗。

4. 如果涉及到IO操作的话,不要使用默认的线程池大小

测试中使用默认线程池大小(默认值是机器的CPU核数,在这里是8)的并行流,跟使用16个线程相比要慢上2秒。也就是说使用默认的池大小则要慢了7%。这是由于阻塞的IO线程导致的。由于有很多线程处于等待状态,因此引入更多的线程能够更好地利用CPU资源,当其它线程在等待调度时不至于让它们闲着。

如果改变并行流的默认的Fork/Join池的大小?你可以通过一个JVM参数来修改公用的Fork/Join线程池的大小:

-Djava.util.concurrent.ForkJoinPool.common.parallelism=16

(默认情况下,所有的Fork/Join任务都会共用同一个线程池,线程的数量等于CPU的核数。好处就是当线程空闲下来时可以收来处理其它任务。)

或者,你还可以用下这个小技巧,用一个自定义的Fork/Join池来运行并行流。它会覆盖掉默认的公用的Fork/Join池并让你能够使用自己配置好的线程池。手段有点卑劣。测试中我们使用的是公用的线程池。

5. 单线程的性能跟最快的结果相比要慢7.25倍

并发能够提升7.25倍的性能,考虑到机器是8核的,也就是说接近是8倍的提升!还差的那点应该是消耗在线程的开销上了。不仅如此,即便是测试中表现最差的并行版本,也就是4个线程的并行流实现(30.23秒),也比单线程的版本(176.27秒)要快5.8倍。

如果不考虑IO的话呢?比如判断某个数是否是素数

对这次测试而言,我们将去除掉IO的部分,来测试下判断一个大整数是否是素数要花多长时间。这个数有多大?19位,1,530,692,068,127,007,263,换句话说,一百五十三万零六百九十二兆零六百八十一亿两千万七千二百六十三。好吧,让我透透气先。我们也没有做任何的优化,而是直接运算到它的平方根,为此我们还检查了所有的偶数,尽管这个大数并不能被2整除,这只是为了让运算的时间更久一些。先剧透一下:这的确是一个素数。每个实现运算的次数也都是一样的。

下面是测试的结果:

image

单线程执行时间:118,127毫秒,大约2分钟 注意,上图是从20000毫秒开始的

1. 8个线程与16个线程相差不大

和IO测试中不同,这里并没有IO调用,因此8个线程和16个线程的差别并不大,Fork/Join的版本例外。由于它的反常表现,我们还多运行了好几组测试以确保得到的结果是正确的,但事实表明,结果仍是一样。希望你能在下方的评论一栏说一下你对这个的看法。

2. 不同实现的最好结果都很接近

我们看到,不同的实现版本最快的结果都是一样的,大约是28秒左右。不管实现的方法如何,结果都大同小异。但这并不意味着使用哪种方法都一样。请看下面这点。

3. 并行流的线程处理开销要优于其它实现

这点非常有意思。在本次测试中,我们发现,并行流的16个线程的再次胜出。不止如此,在这次测试中,不管线程数是多少,并行流的表现都是最好的。

4. 单线程的版本比最快的结果要慢4.2倍

除此之外,在运行计算密集型任务时,并行版本的优势要比带有IO的测试要减少了2倍。由于这是个CPU密集型的测试,这个结果倒也说得过去,不像前面那个测试中那样,减少CPU的等待IO的时间能获得额外的收益。

结论

之前我也建议过大家读一下源码,了解下何时应该使用并行流,并且在Java中进行并发编程时,不要武断地下结论。最好的检验方式就是在演示环境中多跑跑类似的测试用例。需要特别注意的因素包括你所运行的硬件环境 (以及测试的硬件环境),还有应用程序的总线程数。包括公用Fork/Join的线程池以及团队中其它开发人员所写的代码中包含的线程。在你编写自己的并发逻辑前,最好先检查下上述这些情况,对你的应用程序有一个整体的了解。

基础库

我们是在EC2的c3.2xlarge实例上运行的本次测试,它有8个vCPU核以及15GB的内存。vCPU是因为这里用到了超线程技术,因此实际上只有4个物理核,但每个核模拟成了两个。对操作系统的调度器而言,认为我们一共有8个核。为了尽可能的公平,每个实现都运行了10遍,并选择了第2次到第9次的平均运行时间。也就是一共运行了260次!处理时长也非常重要。我们所选择的任务的运行时间都会超过20秒,因此时间差异能很容易看出来,而不太受外部因素的影响。

最后

原始的测试结果在这里,代码放在Github上。欢迎进行修改,并告诉我们你的测试结果。如果发现了什么我们这里没有讲到的有意思的新的见解或者现象,欢迎告诉我们,我们很希望能把它们追加到本文中。

原创文章转载请注明出处:Java中不同的并发实现的性能比较

英文原文链接

23 Jan 00:45

浅谈Java两种并发类型——计算密集型与IO密集型

by 菠萝大象
     摘要: 在Java并发编程方面,计算密集型与IO密集型是两个非常典型的例子,这次大象就来讲讲自己在这方面的内容,本篇比较基础,只适合刚入门的童鞋,请各种牛人不喜勿喷。  阅读全文

菠萝大象 2015-01-20 15:08 发表评论
22 Jan 00:36

How to get started with the LLVM C API

22 Jan 00:31

那些年我们一起追过的缓存写法(一)

by zzr0427

介绍

本篇主要说下楼主平常项目中缓存使用经验和遇到过的问题。

 

目录

一: 基本写法

二:缓存雪崩

1:全局锁,实例锁

 2:字符串锁

三:缓存穿透

四:再谈缓存雪崩

五:总结

 

一:基本写法

为了方便演示,我们用Runtime.Cache做缓存容器,并定义个简单操作类。如下:

public class CacheHelper
   {
       public static object Get(string cacheKey)
       {
           return HttpRuntime.Cache[cacheKey];
       }
       public static void Add(string cacheKey, object obj, int cacheMinute)
       {
           HttpRuntime.Cache.Insert(cacheKey, obj, null, DateTime.Now.AddMinutes(cacheMinute),
               Cache.NoSlidingExpiration, CacheItemPriority.Normal, null);
       }
   }

简单读取:

public object GetMemberSigninDays1()
    {
        const int cacheTime = 5;
        const string cacheKey = "mushroomsir";

        var cacheValue = CacheHelper.Get(cacheKey);
        if (cacheValue != null)
            return cacheValue;

        cacheValue = "395"; //这里一般是 sql查询数据。 例:395 签到天数
        CacheHelper.Add(cacheKey, cacheValue, cacheTime);
        return cacheValue;
    }

在项目中,有不少这样写法。这样写没有错,但在并发量上来后就会有问题。继续看

 

 二:缓存雪崩

缓存雪崩是由于缓存失效(过期),新缓存未到期间。

这个中间时间内,所有请求都去查询数据库,而对数据库CPU和内存造成巨大压力,前端连接数不够、查询阻塞。

这个中间时间并没有那么短,比如sql查询1秒,加上传输解析0.5秒。  就是说1.5秒内所有用户查询,都是直接查询数据库的。

这种情况下,我们想到最多的就是加锁排队了。

1:全局锁,实例锁

 public static object obj1 = new object();
        public object GetMemberSigninDays2()
        {
            const int cacheTime = 5;
            const string cacheKey = "mushroomsir";

            var cacheValue = CacheHelper.Get(cacheKey);

            if (cacheValue != null)
                return cacheValue;

            //lock (obj1)         //全局锁
            //{
            //    cacheValue = CacheHelper.Get(cacheKey);
            //    if (cacheValue != null)
            //        return cacheValue;
            //    cacheValue = "395"; //这里一般是 sql查询数据。 例:395 签到天数
            //    CacheHelper.Add(cacheKey, cacheValue, cacheTime);
            //}
            lock (this)
            {
                cacheValue = CacheHelper.Get(cacheKey);
                if (cacheValue != null)
                    return cacheValue;

                cacheValue = "395"; //这里一般是 sql查询数据。 例:395 签到天数
                CacheHelper.Add(cacheKey, cacheValue, cacheTime);
            }
            return cacheValue;
        }

 

第一种:lock (obj1)  是全局锁可以满足,但我们要为每个函数都声明一个obj,不然在A、B函数都锁obj1时,必然会让其中一个阻塞。

第二种:lock (this)  这个锁当前实例,对其他实例无效,这个锁就没什么效果了。使用单例模式的可以锁。

但在当前实例中:A函数锁当前实例,其他锁当前实例的函数读写,也被阻塞。  不可取

 

2:字符串锁

既然锁对象不行,利用字符串的特性,我们直接锁缓存key呢。来看下

 public object GetMemberSigninDays3()
        {
            const int cacheTime = 5;
            const string cacheKey = "mushroomsir";

            var cacheValue = CacheHelper.Get(cacheKey);
            if (cacheValue != null)
                return cacheValue;
            const string lockKey = cacheKey + "n(*≧▽≦*)n";

            //lock (cacheKey)
            //{
            //    cacheValue = CacheHelper.Get(cacheKey);
            //    if (cacheValue != null)
            //        return cacheValue;
            //    cacheValue = "395"; //这里一般是 sql查询数据。 例:395 签到天数
            //    CacheHelper.Add(cacheKey, cacheValue, cacheTime);
            //}
            lock (lockKey)
            {
                cacheValue = CacheHelper.Get(cacheKey);
                if (cacheValue != null)
                    return cacheValue;
                cacheValue = "395"; //这里一般是 sql查询数据。 例:395 签到天数
                CacheHelper.Add(cacheKey, cacheValue, cacheTime);
            }
            return cacheValue;
        }

第一种:lock (cacheName)  有问题,因为字符串也是共享的,会阻塞其他使用这个字符串的操作行为。    具体请看之前的博文 c#语言-多线程中的锁系统(一)

2015-01-04 13:36更新:因为字符串被公共语言运行库 (CLR)暂留,这意味着整个程序中任何给定字符串都只有一个实例。所以才会用第二种

第二种:lock (lockKey)  可以满足。其实目就是为了保证锁的粒度最小并且全局唯一性,只锁当前缓存的查询行为。

 

三:缓存穿透

举个简单例子:一般我们会缓存用户搜索结果。而数据库查询不到,是不会做缓存的。但如果频繁查这个关键字,就会每次都直查数据库了。

这样缓存就没意义了,这也是常提的缓存命中率问题。

  public object GetMemberSigninDays4()
        {
            const int cacheTime = 5;
            const string cacheKey = "mushroomsir";

            var cacheValue = CacheHelper.Get(cacheKey);
            if (cacheValue != null)
                return cacheValue;
            const string lockKey = cacheKey + "n(*≧▽≦*)n";

            lock (lockKey)
            {
                cacheValue = CacheHelper.Get(cacheKey);
                if (cacheValue != null)
                    return cacheValue;

                cacheValue = null; //数据库查询不到,为空。
                //if (cacheValue2 == null)
                //{
                //    return null;  //一般为空,不做缓存
                //}
                if (cacheValue == null)
                {
                    cacheValue = string.Empty; //如果发现为空,我设置个默认值,也缓存起来。
                }
                CacheHelper.Add(cacheKey, cacheValue, cacheTime);
            }
            return cacheValue;
        }

例子中我们把查询不到的结果,也给缓存起来了。这样就可以避免,查询为空时,引起缓存穿透了。

当然我们也可以单独设置个缓存区,进行第一层控制校验。 以便和正常缓存区分开了。

 

四:再谈缓存雪崩

额 不是用加锁排队方式就解决了吗?其实加锁排队只是为了减轻DB压力,并没有提高系统吞吐量。

在高并发下: 缓存重建期间,你是锁着的,1000个请求999个都在阻塞的。  用户体验不好,还浪费资源:阻塞的线程本可以处理后续请求的。

public object GetMemberSigninDays5()
        {
            const int cacheTime = 5;
            const string cacheKey = "mushroomsir";

            //缓存标记。
            const string cacheSign = cacheKey + "_Sign";
            var sign = CacheHelper.Get(cacheSign);

            //获取缓存值
            var cacheValue = CacheHelper.Get(cacheKey);
            if (sign != null)
                return cacheValue; //未过期,直接返回。

            lock (cacheSign)
            {
                sign = CacheHelper.Get(cacheSign);
                if (sign != null)
                    return cacheValue;

                CacheHelper.Add(cacheSign, "1", cacheTime);
                ThreadPool.QueueUserWorkItem((arg) =>
                {
                    cacheValue = "395"; //这里一般是 sql查询数据。 例:395 签到天数
                    CacheHelper.Add(cacheKey, cacheValue, cacheTime*2); //日期设缓存时间的2倍,用于脏读。
                });
            }
            return cacheValue;
        }

代码中,我们多用个缓存标记key,双检锁校验。它设置为正常时间,过期后通知另外的线程去更新缓存数据。

而实际的缓存由于设置了2倍的时间,仍然可以能用脏数据给前端展现。

这样就能提高不少系统吞吐量了。

 

五:总结

补充下: 这里说的阻塞其他函数指的是,高并发下锁同一对象。

实际使用中,缓存层封装往往要复杂的多。  关于更新缓存,可以单开一个线程去专门跑这些,图方便就扔线程池吧。

具体使用场景,可根据实际用户量来平衡。

那些年我们一起追过的缓存写法(一),首发于博客 - 伯乐在线

22 Jan 00:30

Java/JVM是如何构建的?看看OpenJDK吧!

by 范 忠瑞

简介&历史

正如有些人已经知道的那样,从Java7开始,OpenJDK就是Java的参考实现(Reference Implementation)。下图的时间线可以让你了解一下OpenJDK的历史。

OpenJDK历史(2006至今)

看看OpenJDK更详细的过去现在和将来[1]

如果你想了解从Oracle,Red Hat,etcetera等供应商那里下载得来的JDK或JRE库,那么可以告诉你,它们都是起源于OpenJDK。每个供应商在此基础上添加额外的部件。出于安全,专利或其他的考虑,这些额外的添加部分并不公开源码。

OpenJDK由什么组成?

OpenJDK由许多软件库组成,主要有corba,hotspot,jaxp,jaxws,jdk,langtools,以及nashorn。在OpenJDK8和OpenJDK9之间没有新的软件库加入,但有很多改变和结构调整,主要是因为Jigsaw——Java自身的模块化[2][3][4][5]。

代码的组成以及语言的分解构成(比例是估计的)

Java语言和平台是如何构建的?

Java通过引导一个旧版本的Java——例如,Java以其自身为构件建立。旧的组件被组合在一起创建一个新的组件,即成为下一阶段的结构单元。关于这种自展的一个很好的例子请参考 Scheme from Scratch或是 Wikipedia [7]

OpenJDK8使用JDK7编译和构建,类似地,OpenJDK9 则使用JDK8编译构建。理论上,OpenJDK8是可以使用从其自身创建的影像编译的,同理,OpenJDK9也能用OpenJDK9编译。使用一个叫做循环启动影像的进程——创建OpenJDK的JDK影像,使用同样的影像,OpenJDK再一次被编译。也可以用make命令实现OpenJDK的编译:

$ make bootcycle-images # Build images twice, second time with newly built JDK

make命令在OpenJDK8和OpenJDK9下都提供了很多设置选项,可以通过命名的方式建立独立的组件或模块。如下:

$ make [component-name] | [module-name]

甚至并行运行多个构建过程,如下:

$ make JOBS= # Run parallel make jobs

最后,用install选项安装上述已构建的组件,如下:

$ make install

一些被神话了的东西

具体来说,OpenJDK或是Hotspot都不完全是用C或C++写的,代码库中相当一部分代码是良好的OLE(对象连接与嵌入)Java(详细请看上文的组成图表)。所以对OpenJDK作出贡献并不要求你必须是核心硬件开发者。即使是底层的C/C++代码库也不是那么让人望而生畏。下面就是从HotSpot repo的vm/memory/universe.cpp中摘录出的一个代码片段——http://hg.openjdk.java.net/jdk6/jdk6/hotspot/raw-file/a541ca8fa0e3/src/share/vm/memory/universe.cpp [10]:

Universe::initialize_heap()

if (UseParallelGC) {
#ifndef SERIALGC
Universe::_collectedHeap = new ParallelScavengeHeap();
#else // SERIALGC
fatal("UseParallelGC not supported in this VM.");
#endif // SERIALGC

} else if (UseG1GC) {
#ifndef SERIALGC
G1CollectorPolicy* g1p = new G1CollectorPolicy();
G1CollectedHeap* g1h = new G1CollectedHeap(g1p);
Universe::_collectedHeap = g1h;
#else // SERIALGC
fatal("UseG1GC not supported in java kernel vm.");
#endif // SERIALGC

} else {
GenCollectorPolicy* gc_policy;

if (UseSerialGC) {
gc_policy = new MarkSweepPolicy();
} else if (UseConcMarkSweepGC) {
#ifndef SERIALGC
if (UseAdaptiveSizePolicy) {
gc_policy = new ASConcurrentMarkSweepPolicy();
} else {
gc_policy = new ConcurrentMarkSweepPolicy();
}
#else // SERIALGC
fatal("UseConcMarkSweepGC not supported in this VM.");
#endif // SERIALGC
} else { // default old generation
gc_policy = new MarkSweepPolicy();
}

Universe::_collectedHeap = new GenCollectedHeap(gc_policy);

(请注意上述代码片段可能已在发布在本文后的时间里有所变更了)

从上面的代码块中可以明显看出的是,我们的目的在于展示如何使用预编译符号创建HotSpot代码来支持某种类型的GC(GenCollector),比如Serial GC(串行GC)或者Parallel GC(并行GC)。在上述代码块中,在一种或多种GC转换器被触发之前就已经选择确定了GC策略的类型了,比如,当UseAdaptiveSizePolicy被激活后,才可以选择Asynchronous Concurrent Mark and Sweep策略。在Use Serial GC和Use Concurrent Mark Sweep GC二者仅选其一的情况下,被选中的GC策略就是Mark and Sweep策略。除了提供读起来像英文一样流畅的格式简洁的代码之外,又说了这么多,已经相当清楚了,再多说就很啰嗦了。

更多注释可以在Adopt OpenJDK Intermediate & Advance experiences [11] 文件夹的Deep Dive Hotspot stuff部分找到。

构建自己的JDK或JRE的步骤

早些时候我们提到JDK和JRE的影像——这些不再是只给Java世界中的大玩家们提供了,你和我都能很轻易的构建这样的影像。这过程中的步骤已经被简化了,想快速开始请参看 Adopt OpenJDK Getting Started Kit Adopt OpenJDK Intermediate & Advance experiences 文件。想要看更详细的版本请参看 Adopt OpenJDK home page。要从OpenJDK代码库中基本构建一个JDK影像,总结起来就是下面的几个命令:

(启动过程被简化了,忽略了一些命令,访问上述链接可以得到准确的操作步骤)

$ hg clone http://hg.openjdk.java.net/jdk8/jdk8 jdk8 (a)...OpenJDK8

或者

$ hg clone http://hg.openjdk.java.net/jdk9/jdk9 jdk9 (a)...OpenJDK9
$ ./get_sources.sh (b)
$ bash configure (c)
$ make clean images (d)

(设置的步骤和一些命令省略了,查看上面的链接可以查看更详细的步骤)

解释一下上述每个步骤的工作:

就像使用克隆版本库一样复制OpenJDK每次修改的版本。。。

一旦(a)已经完成,进入新创建的文件夹,执行get_sources.sh命令,等价于一次git fetch命令(抓取)或一次git pull命令(拉取远程仓库)。因为步骤(a)中只是降低基础文件而不是所有文件的成本。

这里运行一个脚本检查并创建编译与构建过程所需的配置

步骤(c)完成后,我们就算是从构建好的模块中完成了一个JDK和JRE影像文件的编译,构建和创建。

正如你看到的这样,这些步骤易如反掌,照做就可以建立一个自定义工具或者JDK/JRE影像[步骤a只需要执行一次]。

好处
• 促进Java语言&平台的发展和改进
• 了解Java语言和平台的内部构件
• 在达到以上两点的同时了解OS平台和其他技术
• 参与到F/OSS项目中
• 保持立于Java/JVM范畴的最新动态之上
• 提供有助于专业且还不能从其他如书籍,训练,工作实习,大学课程等来源获得的知识和经验
• 事业提升
• 个人发展(软技能及网络)

贡献

加入项目Adopt OpenJDK Betterrev,向我们提供这些项目中任何与java有关的反馈。可以从加入Adoption Discuss邮件列表和其他OpenJDK相关的邮件列表开始,这些可以让你了解OpenJDK相关的最新进展和变化。为你看到的任何项目建立分支库(通过github中的fork repo),然后提交自己的改变(通过github中发起一个Pull Request)。

感谢与支持

Adopt OpenJDK及其隶属项目由以下组织支持发展:JCPOpenjdk team),JUGs如London Java CommunitySouJava和巴西其他的JUGs,欧洲许多的JUGs比如BGJUG (Bulgarian JUG) [18], BeJUG (Belgium JUG)[19],Macedonian JUG [20],还有许多其他的比较小的JUGs。我们希望未来会有更多JUGs组织和个人参与进来。如果你或你的JUG希望参与请联系我们。

相关文章

22 Jan 00:29

Java开发者在某个重大发布后需要使用的15个工具

by 张 健

新发布的根本生存装备

不像玩僵尸毁灭的场景,也不像辩论大刀对抗猎枪,在Java的生产环境中问题是真实存在的,特别是在一个新的发布之后(有备无患嘛)。更进一步说,比起当时 将编码周期缩短至几周或是几天,甚至一天缩短多次,反而现在更容易陷入麻烦。为了避免这些麻烦,你需要完全理解新的代码会对你的系统产生什么影响。是否会 对原有的系统产生破坏?是否会让系统运行迟缓?怎么去解决这些可能出现的问题呢?下面介绍一些工具和架构来彻底破解这些问题。

现在开发生命周期非但不会缩短,每天不断膨胀的日志数据 甚至可以达到GB级别的量级。让我们说说在新发布之后的问题:如果你想及时响应,在没有合适的工具的情况下,想要处理多数据源多服务器的GB量级的数据, 几乎是不可能的。在这样的情况下,除开重型企业内部的工具Splunk和他的SaaS中的竞争对手,如Sumo Logic, Loggly等等。我们依然有提供类似功能的其他选择,因此我们对日志的管理做了一个深入的分析,你可以参照这里

#1  建立一个可靠的日志管理策略,能帮助你看到的不止是单独的日志文件,能让你在新发布之后快速做出相应。

我们最后发现,在新发布之后的一个有用的日志框架就是开源的ELK stack。因为它是开源免费的,所以被特别提到。

ELK stack包括 ElasticSearch,,Logstash 和Kibana

那么我们所说的 ELK到底是什么呢?它是一个混合体,包括用作搜索和性能分析的elasticsearch,用于日志收集的Logstash和用作前端展现的 Kibana。我们已经用过有一段时间了,依靠它和Redis分析我们的Java日志,它也有被用在开发和BI之中。现在,elasticsearch已经内置于Logstash,Kibana也是一个elasticsearch的产品,这样能让集成和安装更容易。

每当一个新的发布之后,前端会展现出我们对这个应用健康所关心的自定义指标。这些指标会实时更新,并且允许刚交付的代码上传到生产环境后马上就得到监测。

#2 搜索,可视化以及对多数据源日志的聚合,是决定你日志管理工具选择的第一要素。

#3 从一个开发者的角度,评估新的发布的影响也包括BI等方面。

可供选择的工具:

  1. 内置工具: Splunk
  2. SaaS:Sumo Logic
    3. SaaS:Loggly
    4. 开源:Graylog2
    5. 开源:Fluentd
    6. The ELK stack (开源): Elasticsearch + Logstash + Kibana

性能监控:

发 布周期被缩短,日志文件越来越大,但这并不是全部。随着用户请求的数量急剧增长,他们都希望达到性能的峰值。如果你不努力优化,简单的日志记录让你只能看 到这么多。这样说来,专用应用程序性能管理工具(APM)已不再被认为是奢侈品,它已经迅速成为一种标准。从本质上说,APM意味着时间需要多长时间来执 行不同的地区代码并完成汇报——要达到这个,可以通过代码检测,日志监控或是包括网络/硬件指标。考虑到后台以及用户设备,首先进入我脑中的两个APM工 具分别是New Relic(最近刚IPO)以及AppDynamics。

左边为AppDynamics,右边为New Relic的主面板

不 管是初创还是成熟的公司,这两个都能满足不同类型的开发者。但是两个都在走IPO,在经历巨大的增长后,产品线越来越模糊。这个选择不是很清晰,但是你不 能陷入误区,即On premise = AppDynamics。相反,这是一个独立的需求,它依赖于谁更适合你的站点(即它们提供的所有特性就是你实际上想要使用的)。看看我们最近发布的关于二者的分析报告,点击这里

我 们最近发布的另外两个工具是Ruxit(由Compuware开发)和DripStat(Chronon Systems开发),它们每一个都来自由New Relic开创的尝试自己解决SaaS监控市场的大公司。为了深入JVM硬核内部,jClarity和Plumbr也绝对值得一试。

#4:新的发布有可能影响你应用的性能使之运行变慢。APM工具可以提供你应用健康的总体情况。

可供选择的工具:

  1. AppDynamics
  2. New Relic

新的成员:

  1. jClarity
  2. Plumbr
  3. Ruxit
  4. Dripstat

产品调试

发 布周期短了,日志文件变大了,用户请求增多……允许犯错误的余地几乎不存在了。但当错误真的到来时,你需要能够马上解决掉。大规模的生产环境,每天可以从 成百上千个不同的代码处产生无数的错误。虽然有些错误时不重要的,但有些可能对你的应用产生致命的影响,并影响你所接触不到的终端用户。另外,为了鉴别和解决这些错误,你必须依赖于你的日志或是日志管理工具去查看错误的发生,更不用说如何修复它。

有了Takipi,你能够知道最有可能出问题且需要优化的地方,并且能得到怎么取解决每个问题的可行的信息。

为了关注新发布后的危机,Takipi解决了三个主要问题:

1.了解哪些错误最有可能影响你——在生产中发现100%的代码错误,包括JVM异常和记录的错误。使用智能过滤以减少噪音使之专注于最重要的错误。超过90%的Takipi用户报告说,在他们使用的第一天,至少在生产中找到了一个严重的bug。

2、在调试上花更少的时间和精力——Takipi再现了每个错误并显示出代码和导致它产生的变量,甚至可以跨服务器。这消除了手动复制错误的必要,节省了工程时间,显著降低时间。

3. 没有风险的发布——当新的版本中有错误,或是已经解决的错误又重现时,Takipi都会提醒你。

#5:运用Takipi你能很快地解决任何问题,以至于不让你在新的发布之后一无所知。

可选择的工具:

  1. Takipi

从这篇文章之后,Takipi的使用时间扩展到了两个月

100%发现生产中的错误

发现每个错误后面的参数

使大规模调试变得容易

报警和追踪

发 布周期,日志文件,用户请求,零错误……你怎么才能全部跟进呢?你可能认为这一类和其他的重叠了,可能你是对的,但是当所有的这些工具都有他们自己的流水 线时,你可能会意识到自己哪里错了——这将变得很混乱。特别是在各种意想不到的事情都可能发生的新发布后(也就是整个灾难降临)。

满足这个的事件管理工具之一的就是PagerDuty:它能从监控工具收集报警,创建时间表来协调你的团队,或是通过文本、邮件、短信或是推送通知,把每个报警发给特定的人。

#6:考虑使用一个事件管理系统来处理信息过载。

在这里我们真正喜欢使用的专业工具是Pingdom(也是和Pagerduty的结合)。它所做的很简单而且有用:即对你的站点的响应时间做24*7小时的追踪和告警。它能回答一个看起来微不足道,实则至关重要的问题:从世界各地检测来看,当前的站点可用吗?

系统如上图

另一个角度来解决信息过载的方法,是通过对日志分析来进行错误的跟踪:管理异常和日志错误的智能展现。从多个服务器聚合数据到一个地方,即使你的日志事件或是其他插件来自你的代码。为了更深入地错误追踪,点击这篇文章可以得到更多的信息。

#7 代码层的错误来源各种各样,在选用追踪工具时,应该给予特别的对待(在我们关注他们的时候就修复一些bug,哈哈)

可供选择的工具:

  1. PagerDuty
  2. Pingdom

总结

我们亲身经历,现代软件开发如何影响发布生命周期,放大如何评估新的快速部署的影响——在你部署之前,你应该完全了解最后更新的影响。从长远来看,任何工具都应该拥有这五个特点:

  1. 缩短发布周期
  2. 增加日志文件
  3. 增大用户请求
  4. 减小错误
  5. 信息的过载

最重要地是,思考一下现在你是怎么解决这些的,哪一个花了你更多的时间。很可能就有一个工具适合解决这个问题。

相关文章

22 Jan 00:28

MySQL 修改密码

by 草原上的骆驼
安装好Mysql后,首先要修改密码,别忘记启动mysql后再修改密码
1.使用mysqladmin
mysqladmin -u root password NEWPASSWORD
2.修改老密码
mysqladmin -u root -p'oldpassword' password newpass
3.验证修改的密码
mysql -u root -p'123456' db-name-here(可选择)
mysql -u root -p'123456' -e 'show databases;'
-e 执行后面的命令
4.使用SQL修改密码
一定要输入flush privileges后才可以生效。
1.mysql -u root -p
2.use mysql;
3.update user set password=PASSWORD("NEWPASSWORD") where User='username';
4. flush privileges;
5.quit
如果有问题,随时联系我们,bjwill

草原上的骆驼 2015-01-21 13:44 发表评论
21 Jan 13:11

Java线程的5个使用技巧

image

Java线程有哪些不太为人所知的技巧与用法?

萝卜白菜各有所爱。像我就喜欢Java。学无止境,这也是我喜欢它的一个原因。日常工作中你所用到的工具,通常都有些你从来没有了解过的东西,比方说某个方法或者是一些有趣的用法。比如说线程。没错,就是线程。或者确切说是Thread这个类。当我们在构建高可扩展性系统的时候,通常会面临各种各样的并发编程的问题,不过我们现在所要讲的可能会略有不同。

从本文中你将会看到线程提供的一些不太常用的方法及技术。不管你是初学者还是高级用户或者是Java专家,希望都能看一下哪些是你已经知道的,而哪些是刚了解的。如果你认为关于线程还有什么值得分享给大家的,希望能在下面积极回复。那我们就先开始吧。

初学者

1.线程名

程序中的每个线程都有一个名字,创建线程的时候会给它分配一个简单的Java字符串来作为线程名。默认的名字是"Thread-0", "Thread-1", "Thread-2"等等。现在有趣的事情来了——Thread提供了两种方式来设置线程名:

  1. 线程构造函数,下面是最简单的一个实现:
class SuchThread extends Thread {

    Public void run() {
        System.out.println ("Hi Mom! " + getName());
    }

}

SuchThread wow = new SuchThread("much-name");
  1. 线程名setter方法:
wow.setName(“Just another thread name”);

没错,线程名是可变的。因此我们可以在运行时修改它的名字,而不用在初始化的时候就指定好。name字段其实就是一个简单的字符串对象。也就是说它能达到2³¹-1个字符那么长(Integer.MAX_VALUE)。这足够用了。注意这个名字并不是一个唯一性的标识,因此不同的线程也可以拥有同样的线程名。还有一点就是,不要把null用作线程名,否则会抛出异常(当然了,"null"还是可以的)。

使用线程名来调试问题

既然可以设置线程名,那么如果遵循一定的命名规则的话,出了问题的时候排查起来就能更容易一些。“Thread-6″这样的名字看起来就太没心没肺了,肯定有比它更好的名字。在处理用户请求的时候,可以将事务ID追加到线程名后面,这样能显著减少你排查问题的时间。

“pool-1-thread-1″ #17 prio=5 os_prio=31 tid=0x00007f9d620c9800
nid=0x6d03 in Object.wait() [0x000000013ebcc000]

“pool-1-thread-1″,这也太严肃了吧。我们来看下这是什么情况,给它起一个好点的名字:

Thread.currentThread().setName(Context + TID + Params + current Time, ...);

现在我们再来运行下jstack,情况便豁然开朗了:

”Queue Processing Thread, MessageID: AB5CAD, type:
AnalyzeGraph, queue: ACTIVE_PROD, Transaction_ID: 5678956,
Start Time: 30/12/2014 17:37″ #17 prio=5 os_prio=31 tid=0x00007f9d620c9800
nid=0x6d03 in Object.wait() [0x000000013ebcc000]

如果我们能知道线程在做什么,这样当它出问题的时候,至少可以拿到事务ID来开始排查。你可以回溯这个问题,复现它,然后定位问题并搞定它。如果你想知道jstack有什么给力的用法,可以看下这篇文章

2. 线程优先级

线程还有一个有意思的属性就是它的优先级。线程的优先级介于1 (MINPRIORITY)到10 (MAXPRIORITY)之间,主线程默认是5(NORM_PRIORITY)。每个新线程都默认继承父线程的优先级,因此如果你没有设置过的话,所有线程的优先级都是5。这个是通常被忽视的属性,我们可以通过getPriority()与setPriority()方法来获取及修改它的值。线程的构造函数里是没有这个功能的。

什么地方会用到优先级?

当然并不是所有的线程都是平等的,有的线程需要立即引起CPU的重视,而有些线程则只是后台任务而已。优先级就是用来把这些告诉给操作系统的线程调度器的。在Takipi中,这是我们开发的一错误跟踪及排查的工具,负责处理用户异常的线程的优先级是MAX_PRIORITY,而那些只是在上报新的部署情况的线程,它们的优先级就要低一些。你可能会觉得优先级高的线程从JVM的线程调度器那得到的时间会多一些。但其实并都是这样的。

在操作系统层面,每一个新线程都会对应一个本地线程,你所设置的Java线程的优先级会被转化成本地线程的优先级,这个在各个平台上是不一样的。在Linux上,你可以打开“-XX:+UseThreadPriorities”选项来启用这项功能。正如前面所说的,线程优先级只是你所提供的一个建议。和Linux本地的优先级相比,Java线程的优先级并不能覆盖全所有的级别(Linux共有1到99个优先级,线程的优先级在是-20到20之间)。最大的好处就是你所设定的优先级能在每个线程获得的CPU时间上有所体现,不过完全依赖于线程优先级的做法是不推荐的。

image

进阶篇

3.线程本地存储

这个和前面提到的两个略有不同。ThreadLocal是在Thread类之外实现的一个功能(java.lang.ThreadLocal),但它会为每个线程分别存储一份唯一的数据。正如它的名字所说的,它为线程提供了本地存储,也就是说你所创建出来变量对每个线程实例来说都是唯一的。和线程名,线程优先级类似,你可以自定义出一些属性,就好像它们是存储在Thread线程内部一样,是不是觉得酷?不过先别高兴得太早了,有几句丑话得先说在前头。

创建ThreadLocal有两种推荐方式:要么是静态变量,要么是单例实例中的属性,这样可以是非静态的。注意,它的作用域是全局的,只不过对访问它的线程而言好像是本地的而已。在下面这个例子中,ThreadLocal里面存储了一个数据结构,这样我们可以很容易地访问到它:

public static class CriticalData
{
    public int transactionId;
    public int username;
}

public static final ThreadLocal<CriticalData> globalData =
    new ThreadLocal<CriticalData>();
 
 

一旦获取到了ThreadLocal对象,就可以通过 globalData.set()和globalData.get()方法来对它进行操作了。

全局变量?这不是什么好事

也尽然。ThreadLocal可以用来存储事务ID。如果代码中出现未捕获异常的时候它就相当有用了。最佳实践是设置一个UncaughtExceptionHandler,这个是Thread类本身就支持的,但是你得自己去实现一下这个接口。一旦执行到了UncaughtExceptionHandler里,就几乎没有任何线索能够知道到底发生了什么事情了。这会儿你能获取到的就只有Thread对象,之前导致异常发生的所有变量都无法再访问了,因为那些栈帧都已经被弹出了。一旦到了UncaughtExceptionHandler里,这个线程就只剩下最后一口气了,唯一能抓住的最后一根稻草就是ThreadLocal。

我们来试下这么做:

System.err.println("Transaction ID " + globalData.get().transactionId);

 

我们可以将一些与错误相关的有价值的上下文信息给存储到里面添。ThreadLocal还有一个更有创意的用法,就是用它来分配一块特定的内存,这样工作线程可以把它当作缓存来不停地使用。当然了,这有没有用得看你在CPU和内存之间是怎么权衡的了。没错,ThreadLocal需要注意的就是会造成内存空间的浪费。只要线程还活着,那么它就会一直存在,除非你主动释放否则它是不会被回收的。因此如果使用它的话你最好注意一下,尽量保持简单。

4. 用户线程及守护线程

我们再回到Thread类。程序中的每个线程都会有一个状态,要么是用户状态,要么是守护状态。换句话说,要么是前台线程要么是后台线程。主线程默认是用户线程,每个新线程都会从创建它的线程中继承线程状态。因此如果你把一个线程设置成守护线程,那么它所创建的所有线程都会被标记成守护线程。如果程序中的所有线程都是守护线程的话,那么这个进程便会终止。我们可以通过Boolean .setDaemon(true)和.isDaemon()方法来查看及设置线程状态。

什么时候会用到守护线程?

如果进程不必等到某个线程结束才能终止,那么这个线程就可以设置成守护线程。这省掉了正常关闭线程的那些麻烦事,可以立即将线程结束掉。换个角度来说,如果一个正在执行某个操作的线程必须要正确地关闭掉否则就会出现不好的后果的话,那么这个线程就应该是用户线程。通常都是些关键的事务,比方说,数据库录入或者更新,这些操作都是不能中断的。

专家级

5. 处理器亲和性(Processor Affinity)

这里要讲的会更靠近硬件,也就是说,当软件遇上了硬件。处理器亲和性使得你能够将线程或者进程绑定到特定的CPU核上。这意味着只要是某个特定的线程,它就肯定只会在某个特定的CPU核上执行。通常来讲如何绑定是由操作系统的线程调度器根据它自己的逻辑来决定的,它很可能会将我们前面提到的线程优先级也一并考虑进来。

这么做的好处在于CPU缓存。如果某个线程只会在某个核上运行,那么它的数据恰好在缓存里的概率就大大提高了。如果数据正好就在CPU缓存里,那么就没有必要重新再从内存里加载了。你所节省的这几毫秒时间就能用在刀刃上,在这段时间里代码可以马上开始执行,也就能更好地利用所分配给它的CPU时间。当然了,操作系统层面可能会存在某种优化,硬件架构当然也是个很重要的因素,但利用了处理器的亲和性至少能够减小线程切换CPU的机率。

由于这里掺杂着多种因素,处理器亲和性到底对吞吐量有多大的影响,最好还是通过测试的方式来进行证明。也许这个方法并不是总能显著地提升性能,但至少有一个好处就是吞吐量会相对稳定。亲和策略可以细化到非常细的粒度上,这取决于你具体想要什么。高频交易行业便是这一策略最能大显身手的场景之一。

处理器亲和性的测试

Java对处理器的亲和性并没有原生的支持,当然了,故事也还没有就此结束。在Linux上,我们可以通过taskset命令来设置进程的亲和性。假设我们现在有一个Java进程在运行,而我们希望将它绑定到某个特定的CPU上:

taskset -c 1 “java AboutToBePinned”

 

如果是一个已经在运行了的进程:

taskset -c 1 <PID>

 

要想深入到线程级别还得再加些代码才行。所幸的是,有一个开源库能完成这样的功能:Java-Thread-Affinity。这个库是由OpenHFT的Peter Lawrey开发的,实现这一功能最简单直接的方式应该就是使用这个库了。我们通过一个例子来快速看下如何绑定某个线程,关于该库的更多细节请参考它在Github上的文档:

AffinityLock al = AffinityLock.acquireLock();

 

这样就可以了。关于获取锁的一些更高级的选项——比如说根据不同的策略来选择CPU——在Github上都有详细的说明。

结论

本文我们介绍了关于线程的5点知识:线程名,线程本地存储,优先级,守护线程以及处理器亲和性。希望这能为你日常工作中所用到的内容打开一扇新的窗户,期待你们的反馈!还有什么有关线程处理的方法可以分享给大家的吗,请不吝赐教。

原创文章转载请注明出处:Java线程的5个使用技巧

英文原文链接

20 Jan 11:13

从机器学习谈起

by zzr0427

在本篇文章中,我将对机器学习做个概要的介绍。本文的目的是能让即便完全不了解机器学习的人也能了解机器学习,并且上手相关的实践。这篇文档也算是EasyPR开发的番外篇,从这里开始,必须对机器学习了解才能进一步介绍EasyPR的内核。当然,本文也面对一般读者,不会对阅读有相关的前提要求。

在进入正题前,我想读者心中可能会有一个疑惑:机器学习有什么重要性,以至于要阅读完这篇非常长的文章呢?

我并不直接回答这个问题前。相反,我想请大家看两张图,下图是图一:

 图1 机器学习界的执牛耳者与互联网界的大鳄的联姻

这幅图上上的三人是当今机器学习界的执牛耳者。中间的是Geoffrey Hinton, 加拿大多伦多大学的教授,如今被聘为“Google大脑”的负责人。右边的是Yann LeCun, 纽约大学教授,如今是Facebook人工智能实验室的主任。而左边的大家都很熟悉,Andrew Ng,中文名吴恩达,斯坦福大学副教授,如今也是“百度大脑”的负责人与百度首席科学家。这三位都是目前业界炙手可热的大牛,被互联网界大鳄求贤若渴的聘请,足见他们的重要性。而他们的研究方向,则全部都是机器学习的子类–深度学习。

下图是图二:

图2 语音助手产品

这幅图上描述的是什么?Windows Phone上的语音助手Cortana,名字来源于《光环》中士官长的助手。相比其他竞争对手,微软很迟才推出这个服务。Cortana背后的核心技术是什么,为什么它能够听懂人的语音?事实上,这个技术正是机器学习。机器学习是所有语音助手产品(包括Apple的siri与Google的Now)能够跟人交互的关键技术。

通过上面两图,我相信大家可以看出机器学习似乎是一个很重要的,有很多未知特性的技术。学习它似乎是一件有趣的任务。实际上,学习机器学习不仅可以帮助我们了解互联网界最新的趋势,同时也可以知道伴随我们的便利服务的实现技术。

机器学习是什么,为什么它能有这么大的魔力,这些问题正是本文要回答的。同时,本文叫做“从机器学习谈起”,因此会以漫谈的形式介绍跟机器学习相关的所有内容,包括学科(如数据挖掘、计算机视觉等),算法(神经网络,svm)等等。本文的主要目录如下:

1.一个故事说明什么是机器学习

2.机器学习的定义

3.机器学习的范围

4.机器学习的方法

5.机器学习的应用–大数据

6.机器学习的子类–深度学习

7.机器学习的父类–人工智能

8.机器学习的思考–计算机的潜意识

9.总结

10.后记

 

1.一个故事说明什么是机器学习

机器学习这个词是让人疑惑的,首先它是英文名称Machine Learning(简称ML)的直译,在计算界Machine一般指计算机。这个名字使用了拟人的手法,说明了这门技术是让机器“学习”的技术。但是计算机是死的,怎么可能像人类一样“学习”呢?

传统上如果我们想让计算机工作,我们给它一串指令,然后它遵照这个指令一步步执行下去。有因有果,非常明确。但这样的方式在机器学习中行不通。机器学习根本不接受你输入的指令,相反,它接受你输入的数据! 也就是说,机器学习是一种让计算机利用数据而不是指令来进行各种工作的方法。这听起来非常不可思议,但结果上却是非常可行的。“统计”思想将在你学习“机器学习”相关理念时无时无刻不伴随,相关而不是因果的概念将是支撑机器学习能够工作的核心概念。你会颠覆对你以前所有程序中建立的因果无处不在的根本理念。

下面我通过一个故事来简单地阐明什么是机器学习。这个故事比较适合用在知乎上作为一个概念的阐明。在这里,这个故事没有展开,但相关内容与核心是存在的。如果你想简单的了解一下什么是机器学习,那么看完这个故事就足够了。如果你想了解机器学习的更多知识以及与它关联紧密的当代技术,那么请你继续往下看,后面有更多的丰富的内容。

这个例子来源于我真实的生活经验,我在思考这个问题的时候突然发现它的过程可以被扩充化为一个完整的机器学习的过程,因此我决定使用这个例子作为所有介绍的开始。这个故事称为“等人问题”。

我相信大家都有跟别人相约,然后等人的经历。现实中不是每个人都那么守时的,于是当你碰到一些爱迟到的人,你的时间不可避免的要浪费。我就碰到过这样的一个例子。

对我的一个朋友小Y而言,他就不是那么守时,最常见的表现是他经常迟到。当有一次我跟他约好3点钟在某个麦当劳见面时,在我出门的那一刻我突然想到一个问题:我现在出发合适么?我会不会又到了地点后,花上30分钟去等他?我决定采取一个策略解决这个问题。

要想解决这个问题,有好几种方法。第一种方法是采用知识:我搜寻能够解决这个问题的知识。但很遗憾,没有人会把如何等人这个问题作为知识传授,因此我不可能找到已有的知识能够解决这个问题。第二种方法是问他人:我去询问他人获得解决这个问题的能力。但是同样的,这个问题没有人能够解答,因为可能没人碰上跟我一样的情况。第三种方法是准则法:我问自己的内心,我有否设立过什么准则去面对这个问题?例如,无论别人如何,我都会守时到达。但我不是个死板的人,我没有设立过这样的规则。

事实上,我相信有种方法比以上三种都合适。我把过往跟小Y相约的经历在脑海中重现一下,看看跟他相约的次数中,迟到占了多大的比例。而我利用这来预测他这次迟到的可能性。如果这个值超出了我心里的某个界限,那我选择等一会再出发。假设我跟小Y约过5次,他迟到的次数是1次,那么他按时到的比例为80%,我心中的阈值为70%,我认为这次小Y应该不会迟到,因此我按时出门。如果小Y在5次迟到的次数中占了4次,也就是他按时到达的比例为20%,由于这个值低于我的阈值,因此我选择推迟出门的时间。这个方法从它的利用层面来看,又称为经验法。在经验法的思考过程中,我事实上利用了以往所有相约的数据。因此也可以称之为依据数据做的判断。

依据数据所做的判断跟机器学习的思想根本上是一致的。

刚才的思考过程我只考虑“频次”这种属性。在真实的机器学习中,这可能都不算是一个应用。一般的机器学习模型至少考虑两个量:一个是因变量,也就是我们希望预测的结果,在这个例子里就是小Y迟到与否的判断。另一个是自变量,也就是用来预测小Y是否迟到的量。假设我把时间作为自变量,譬如我发现小Y所有迟到的日子基本都是星期五,而在非星期五情况下他基本不迟到。于是我可以建立一个模型,来模拟小Y迟到与否跟日子是否是星期五的概率。见下图:

 

图3 决策树模型

这样的图就是一个最简单的机器学习模型,称之为决策树。

当我们考虑的自变量只有一个时,情况较为简单。如果把我们的自变量再增加一个。例如小Y迟到的部分情况时是在他开车过来的时候(你可以理解为他开车水平较臭,或者路较堵)。于是我可以关联考虑这些信息。建立一个更复杂的模型,这个模型包含两个自变量与一个因变量。

再更复杂一点,小Y的迟到跟天气也有一定的原因,例如下雨的时候,这时候我需要考虑三个自变量。

如果我希望能够预测小Y迟到的具体时间,我可以把他每次迟到的时间跟雨量的大小以及前面考虑的自变量统一建立一个模型。于是我的模型可以预测值,例如他大概会迟到几分钟。这样可以帮助我更好的规划我出门的时间。在这样的情况下,决策树就无法很好地支撑了,因为决策树只能预测离散值。我们可以用节2所介绍的线型回归方法建立这个模型。

如果我把这些建立模型的过程交给电脑。比如把所有的自变量和因变量输入,然后让计算机帮我生成一个模型,同时让计算机根据我当前的情况,给出我是否需要迟出门,需要迟几分钟的建议。那么计算机执行这些辅助决策的过程就是机器学习的过程。

机器学习方法是计算机利用已有的数据(经验),得出了某种模型(迟到的规律),并利用此模型预测未来(是否迟到)的一种方法。

通过上面的分析,可以看出机器学习与人类思考的经验过程是类似的,不过它能考虑更多的情况,执行更加复杂的计算。事实上,机器学习的一个主要目的就是把人类思考归纳经验的过程转化为计算机通过对数据的处理计算得出模型的过程。经过计算机得出的模型能够以近似于人的方式解决很多灵活复杂的问题。

下面,我会开始对机器学习的正式介绍,包括定义、范围,方法、应用等等,都有所包含。

 

2.机器学习的定义

从广义上来说,机器学习是一种能够赋予机器学习的能力以此让它完成直接编程无法完成的功能的方法。但从实践的意义上来说,机器学习是一种通过利用数据,训练出模型,然后使用模型预测的一种方法。

让我们具体看一个例子。

图4 房价的例子

拿国民话题的房子来说。现在我手里有一栋房子需要售卖,我应该给它标上多大的价格?房子的面积是100平方米,价格是100万,120万,还是140万?

很显然,我希望获得房价与面积的某种规律。那么我该如何获得这个规律?用报纸上的房价平均数据么?还是参考别人面积相似的?无论哪种,似乎都并不是太靠谱。

我现在希望获得一个合理的,并且能够最大程度的反映面积与房价关系的规律。于是我调查了周边与我房型类似的一些房子,获得一组数据。这组数据中包含了大大小小房子的面积与价格,如果我能从这组数据中找出面积与价格的规律,那么我就可以得出房子的价格。

对规律的寻找很简单,拟合出一条直线,让它“穿过”所有的点,并且与各个点的距离尽可能的小。

通过这条直线,我获得了一个能够最佳反映房价与面积规律的规律。这条直线同时也是一个下式所表明的函数:

  房价 = 面积 * a + b

上述中的a、b都是直线的参数。获得这些参数以后,我就可以计算出房子的价格。

假设a = 0.75,b = 50,则房价 = 100 * 0.75 + 50 = 125万。这个结果与我前面所列的100万,120万,140万都不一样。由于这条直线综合考虑了大部分的情况,因此从“统计”意义上来说,这是一个最合理的预测。

在求解过程中透露出了两个信息:
1.房价模型是根据拟合的函数类型决定的。如果是直线,那么拟合出的就是直线方程。如果是其他类型的线,例如抛物线,那么拟合出的就是抛物线方程。机器学习有众多算法,一些强力算法可以拟合出复杂的非线性模型,用来反映一些不是直线所能表达的情况。
2.如果我的数据越多,我的模型就越能够考虑到越多的情况,由此对于新情况的预测效果可能就越好。这是机器学习界“数据为王”思想的一个体现。一般来说(不是绝对),数据越多,最后机器学习生成的模型预测的效果越好。

通过我拟合直线的过程,我们可以对机器学习过程做一个完整的回顾。首先,我们需要在计算机中存储历史的数据。接着,我们将这些 数据通过机器学习算法进行处理,这个过程在机器学习中叫做“训练”,处理的结果可以被我们用来对新的数据进行预测,这个结果一般称之为“模型”。对新数据 的预测过程在机器学习中叫做“预测”。“训练”与“预测”是机器学习的两个过程,“模型”则是过程的中间输出结果,“训练”产生“模型”,“模型”指导 “预测”。

让我们把机器学习的过程与人类对历史经验归纳的过程做个比对。

图5 机器学习与人类思考的类比

人类在成长、生活过程中积累了很多的历史与经验。人类定期地对这些经验进行“归纳”,获得了生活的“规律”。当人类遇到未知的问题或者需要对未来进行“推测”的时候,人类使用这些“规律”,对未知问题与未来进行“推测”,从而指导自己的生活和工作。

机器学习中的“训练”与“预测”过程可以对应到人类的“归纳”和“推测”过程。通过这样的对应,我们可以发现,机器学习的思想并不复杂,仅仅是对人类在生活中学习成长的一个模拟。由于机器学习不是基于编程形成的结果,因此它的处理过程不是因果的逻辑,而是通过归纳思想得出的相关性结论。

这也可以联想到人类为什么要学习历史,历史实际上是人类过往经验的总结。有句话说得很好,“历史往往不一样,但历史总是惊人的相似”。通过学习历史,我们从历史中归纳出人生与国家的规律,从而指导我们的下一步工作,这是具有莫大价值的。当代一些人忽视了历史的本来价值,而是把其作为一种宣扬功绩的手段,这其实是对历史真实价值的一种误用。

3.机器学习的范围

上文虽然说明了机器学习是什么,但是并没有给出机器学习的范围。

其实,机器学习跟模式识别,统计学习,数据挖掘,计算机视觉,语音识别,自然语言处理等领域有着很深的联系。

从范围上来说,机器学习跟模式识别,统计学习,数据挖掘是类似的,同时,机器学习与其他领域的处理技术的结合,形成了计算机视觉、语音识别、自然语言处理等交叉学科。因此,一般说数据挖掘时,可以等同于说机器学习。同时,我们平常所说的机器学习应用,应该是通用的,不仅仅局限在结构化数据,还有图像,音频等应用。

在这节对机器学习这些相关领域的介绍有助于我们理清机器学习的应用场景与研究范围,更好的理解后面的算法与应用层次。

下图是机器学习所牵扯的一些相关范围的学科与研究领域。


图6 机器学习与相关学科

模式识别
模式识别=机器学习。两者的主要区别在于前者是从工业界发展起来的概念,后者则主要源自计算机学科。在著名的《Pattern Recognition And Machine Learning》这本书中,Christopher M. Bishop在开头是这样说的“模式识别源自工业界,而机器学习来自于计算机学科。不过,它们中的活动可以被视为同一个领域的两个方面,同时在过去的10年间,它们都有了长足的发展”。

数据挖掘
数据挖掘=机器学习+数据库。这几年数据挖掘的概念实在是太耳熟能详。几乎等同于炒作。但凡说数据挖掘都会吹嘘数据挖掘如何如何,例如从数据中挖出金子,以及将废弃的数据转化为价值等等。但是,我尽管可能会挖出金子,但我也可能挖的是“石头”啊。这个说法的意思是,数据挖掘仅仅是一种思考方式,告诉我们应该尝试从数据中挖掘出知识,但不是每个数据都能挖掘出金子的,所以不要神话它。一个系统绝对不会因为上了一个数据挖掘模块就变得无所不能(这是IBM最喜欢吹嘘的),恰恰相反,一个拥有数据挖掘思维的人员才是关键,而且他还必须对数据有深刻的认识,这样才可能从数据中导出模式指引业务的改善。大部分数据挖掘中的算法是机器学习的算法在数据库中的优化。

统计学习
统计学习近似等于机器学习。统计学习是个与机器学习高度重叠的学科。因为机器学习中的大多数方法来自统计学,甚至可以认为,统计学的发展促进机器学习的繁荣昌盛。例如著名的支持向量机算法,就是源自统计学科。但是在某种程度上两者是有分别的,这个分别在于:统计学习者重点关注的是统计模型的发展与优化,偏数学,而机器学习者更关注的是能够解决问题,偏实践,因此机器学习研究者会重点研究学习算法在计算机上执行的效率与准确性的提升。

计算机视觉
计算机视觉=图像处理+机器学习。图像处理技术用于将图像处理为适合进入机器学习模型中的输入,机器学习则负责从图像中识别出相关的模式。计算机视觉相关的应用非常的多,例如百度识图、手写字符识别、车牌识别等等应用。这个领域是应用前景非常火热的,同时也是研究的热门方向。随着机器学习的新领域深度学习的发展,大大促进了计算机图像识别的效果,因此未来计算机视觉界的发展前景不可估量。

语音识别
语音识别=语音处理+机器学习。语音识别就是音频处理技术与机器学习的结合。语音识别技术一般不会单独使用,一般会结合自然语言处理的相关技术。目前的相关应用有苹果的语音助手siri等。

自然语言处理
自然语言处理=文本处理+机器学习。自然语言处理技术主要是让机器理解人类的语言的一门领域。在自然语言处理技术中,大量使用了编译原理相关的技术,例如词法分析,语法分析等等,除此之外,在理解这个层面,则使用了语义理解,机器学习等技术。作为唯一由人类自身创造的符号,自然语言处理一直是机器学习界不断研究的方向。按照百度机器学习专家余凯的说法“听与看,说白了就是阿猫和阿狗都会的,而只有语言才是人类独有的”。如何利用机器学习技术进行自然语言的的深度理解,一直是工业和学术界关注的焦点。

可以看出机器学习在众多领域的外延和应用。机器学习技术的发展促使了很多智能领域的进步,改善着我们的生活。

 

4.机器学习的方法

通过上节的介绍我们知晓了机器学习的大致范围,那么机器学习里面究竟有多少经典的算法呢?在这个部分我会简要介绍一下机器学习中的经典代表方法。这部分介绍的重点是这些方法内涵的思想,数学与实践细节不会在这讨论。

1、回归算法

在大部分机器学习课程中,回归算法都是介绍的第一个算法。原因有两个:一.回归算法比较简单,介绍它可以让人平滑地从统计学迁移到机器学习中。二.回归算法是后面若干强大算法的基石,如果不理解回归算法,无法学习那些强大的算法。回归算法有两个重要的子类:即线性回归和逻辑回归。

线性回归就是我们前面说过的房价求解问题。如何拟合出一条直线最佳匹配我所有的数据?一般使用“最小二乘法”来求解。“最小二乘法”的思想是这样的,假设我们拟合出的直线代表数据的真实值,而观测到的数据代表拥有误差的值。为了尽可能减小误差的影响,需要求解一条直线使所有误差的平方和最小。最小二乘法将最优问题转化为求函数极值问题。函数极值在数学上我们一般会采用求导数为0的方法。但这种做法并不适合计算机,可能求解不出来,也可能计算量太大。

计算机科学界专门有一个学科叫“数值计算”,专门用来提升计算机进行各类计算时的准确性和效率问题。例如,著名的“梯度下降”以及“牛顿法”就是数值计算中的经典算法,也非常适合来处理求解函数极值的问题。梯度下降法是解决回归模型中最简单且有效的方法之一。从严格意义上来说,由于后文中的神经网络和推荐算法中都有线性回归的因子,因此梯度下降法在后面的算法实现中也有应用。

逻辑回归是一种与线性回归非常类似的算法,但是,从本质上讲,线型回归处理的问题类型与逻辑回归不一致。线性回归处理的是数值问题,也就是最后预测出的结果是数字,例如房价。而逻辑回归属于分类算法,也就是说,逻辑回归预测结果是离散的分类,例如判断这封邮件是否是垃圾邮件,以及用户是否会点击此广告等等。

实现方面的话,逻辑回归只是对对线性回归的计算结果加上了一个Sigmoid函数,将数值结果转化为了0到1之间的概率(Sigmoid函数的图像一般来说并不直观,你只需要理解对数值越大,函数越逼近1,数值越小,函数越逼近0),接着我们根据这个概率可以做预测,例如概率大于0.5,则这封邮件就是垃圾邮件,或者肿瘤是否是恶性的等等。从直观上来说,逻辑回归是画出了一条分类线,见下图。


  图7 逻辑回归的直观解释

假设我们有一组肿瘤患者的数据,这些患者的肿瘤中有些是良性的(图中的蓝色点),有些是恶性的(图中的红色点)。这里肿瘤的红蓝色可以被称作数据的“标签”。同时每个数据包括两个“特征”:患者的年龄与肿瘤的大小。我们将这两个特征与标签映射到这个二维空间上,形成了我上图的数据。

当我有一个绿色的点时,我该判断这个肿瘤是恶性的还是良性的呢?根据红蓝点我们训练出了一个逻辑回归模型,也就是图中的分类线。这时,根据绿点出现在分类线的左侧,因此我们判断它的标签应该是红色,也就是说属于恶性肿瘤。

逻辑回归算法划出的分类线基本都是线性的(也有划出非线性分类线的逻辑回归,不过那样的模型在处理数据量较大的时候效率会很低),这意味着当两类之间的界线不是线性时,逻辑回归的表达能力就不足。下面的两个算法是机器学习界最强大且重要的算法,都可以拟合出非线性的分类线。

  2、神经网络

神经网络(也称之为人工神经网络,ANN)算法是80年代机器学习界非常流行的算法,不过在90年代中途衰落。现在,携着“深度学习”之势,神经网络重装归来,重新成为最强大的机器学习算法之一。

神经网络的诞生起源于对大脑工作机理的研究。早期生物界学者们使用神经网络来模拟大脑。机器学习的学者们使用神经网络进行机器学习的实验,发现在视觉与语音的识别上效果都相当好。在BP算法(加速神经网络训练过程的数值算法)诞生以后,神经网络的发展进入了一个热潮。BP算法的发明人之一是前面介绍的机器学习大牛Geoffrey Hinton(图1中的中间者)。

具体说来,神经网络的学习机理是什么?简单来说,就是分解与整合。在著名的Hubel-Wiesel试验中,学者们研究猫的视觉分析机理是这样的。


 图8 Hubel-Wiesel试验与大脑视觉机理

比方说,一个正方形,分解为四个折线进入视觉处理的下一层中。四个神经元分别处理一个折线。每个折线再继续被分解为两条直线,每条直线再被分解为黑白两个面。于是,一个复杂的图像变成了大量的细节进入神经元,神经元处理以后再进行整合,最后得出了看到的是正方形的结论。这就是大脑视觉识别的机理,也是神经网络工作的机理。

让我们看一个简单的神经网络的逻辑架构。在这个网络中,分成输入层,隐藏层,和输出层。输入层负责接收信号,隐藏层负责对数据的分解与处理,最后的结果被整合到输出层。每层中的一个圆代表一个处理单元,可以认为是模拟了一个神经元,若干个处理单元组成了一个层,若干个层再组成了一个网络,也就是”神经网络”。


图9 神经网络的逻辑架构

在神经网络中,每个处理单元事实上就是一个逻辑回归模型,逻辑回归模型接收上层的输入,把模型的预测结果作为输出传输到下一个层次。通过这样的过程,神经网络可以完成非常复杂的非线性分类。

下图会演示神经网络在图像识别领域的一个著名应用,这个程序叫做LeNet,是一个基于多个隐层构建的神经网络。通过LeNet可以识别多种手写数字,并且达到很高的识别精度与拥有较好的鲁棒性。

图10 LeNet的效果展示

右下方的方形中显示的是输入计算机的图像,方形上方的红色字样“answer”后面显示的是计算机的输出。左边的三条竖直的图像列显示的是神经网络中三个隐藏层的输出,可以看出,随着层次的不断深入,越深的层次处理的细节越低,例如层3基本处理的都已经是线的细节了。LeNet的发明人就是前文介绍过的机器学习的大牛Yann LeCun(图1右者)。

进入90年代,神经网络的发展进入了一个瓶颈期。其主要原因是尽管有BP算法的加速,神经网络的训练过程仍然很困难。因此90年代后期支持向量机(SVM)算法取代了神经网络的地位。

  3、SVM(支持向量机)

支持向量机算法是诞生于统计学习界,同时在机器学习界大放光彩的经典算法。

支持向量机算法从某种意义上来说是逻辑回归算法的强化:通过给予逻辑回归算法更严格的优化条件,支持向量机算法可以获得比逻辑回归更好的分类界线。但是如果没有某类函数技术,则支持向量机算法最多算是一种更好的线性分类技术。

但是,通过跟高斯“核”的结合,支持向量机可以表达出非常复杂的分类界线,从而达成很好的的分类效果。“核”事实上就是一种特殊的函数,最典型的特征就是可以将低维的空间映射到高维的空间。

例如下图所示:

          

图11 支持向量机图例

我们如何在二维平面划分出一个圆形的分类界线?在二维平面可能会很困难,但是通过“核”可以将二维空间映射到三维空间,然后使用一个线性平面就可以达成类似效果。也就是说,二维平面划分出的非线性分类界线可以等价于三维平面的线性分类界线。于是,我们可以通过在三维空间中进行简单的线性划分就可以达到在二维平面中的非线性划分效果。

图12 三维空间的切割

支持向量机是一种数学成分很浓的机器学习算法(相对的,神经网络则有生物科学成分)。在算法的核心步骤中,有一步证明,即将数据从低维映射到高维不会带来最后计算复杂性的提升。于是,通过支持向量机算法,既可以保持计算效率,又可以获得非常好的分类效果。因此支持向量机在90年代后期一直占据着机器学习中最核心的地位,基本取代了神经网络算法。直到现在神经网络借着深度学习重新兴起,两者之间才又发生了微妙的平衡转变。

  4、聚类算法

前面的算法中的一个显著特征就是我的训练数据中包含了标签,训练出的模型可以对其他未知数据预测标签。在下面的算法中,训练数据都是不含标签的,而算法的目的则是通过训练,推测出这些数据的标签。这类算法有一个统称,即无监督算法(前面有标签的数据的算法则是有监督算法)。无监督算法中最典型的代表就是聚类算法。

让我们还是拿一个二维的数据来说,某一个数据包含两个特征。我希望通过聚类算法,给他们中不同的种类打上标签,我该怎么做呢?简单来说,聚类算法就是计算种群中的距离,根据距离的远近将数据划分为多个族群。

聚类算法中最典型的代表就是K-Means算法。

  5、降维算法

降维算法也是一种无监督学习算法,其主要特征是将数据从高维降低到低维层次。在这里,维度其实表示的是数据的特征量的大小,例如,房价包含房子的长、宽、面积与房间数量四个特征,也就是维度为4维的数据。可以看出来,长与宽事实上与面积表示的信息重叠了,例如面积=长 × 宽。通过降维算法我们就可以去除冗余信息,将特征减少为面积与房间数量两个特征,即从4维的数据压缩到2维。于是我们将数据从高维降低到低维,不仅利于表示,同时在计算上也能带来加速。

刚才说的降维过程中减少的维度属于肉眼可视的层次,同时压缩也不会带来信息的损失(因为信息冗余了)。如果肉眼不可视,或者没有冗余的特征,降维算法也能工作,不过这样会带来一些信息的损失。但是,降维算法可以从数学上证明,从高维压缩到的低维中最大程度地保留了数据的信息。因此,使用降维算法仍然有很多的好处。

降维算法的主要作用是压缩数据与提升机器学习其他算法的效率。通过降维算法,可以将具有几千个特征的数据压缩至若干个特征。另外,降维算法的另一个好处是数据的可视化,例如将5维的数据压缩至2维,然后可以用二维平面来可视。降维算法的主要代表是PCA算法(即主成分分析算法)。

6、推荐算法

推荐算法是目前业界非常火的一种算法,在电商界,如亚马逊,天猫,京东等得到了广泛的运用。推荐算法的主要特征就是可以自动向用户推荐他们最感兴趣的东西,从而增加购买率,提升效益。推荐算法有两个主要的类别:

一类是基于物品内容的推荐,是将与用户购买的内容近似的物品推荐给用户,这样的前提是每个物品都得有若干个标签,因此才可以找出与用户购买物品类似的物品,这样推荐的好处是关联程度较大,但是由于每个物品都需要贴标签,因此工作量较大。

另一类是基于用户相似度的推荐,则是将与目标用户兴趣相同的其他用户购买的东西推荐给目标用户,例如小A历史上买了物品B和C,经过算法分析,发现另一个与小A近似的用户小D购买了物品E,于是将物品E推荐给小A。

两类推荐都有各自的优缺点,在一般的电商应用中,一般是两类混合使用。推荐算法中最有名的算法就是协同过滤算法。

7、其他

除了以上算法之外,机器学习界还有其他的如高斯判别,朴素贝叶斯,决策树等等算法。但是上面列的六个算法是使用最多,影响最广,种类最全的典型。机器学习界的一个特色就是算法众多,发展百花齐放。

下面做一个总结,按照训练的数据有无标签,可以将上面算法分为监督学习算法和无监督学习算法,但推荐算法较为特殊,既不属于监督学习,也不属于非监督学习,是单独的一类。

监督学习算法:
线性回归,逻辑回归,神经网络,SVM

无监督学习算法:
聚类算法,降维算法

特殊算法:
推荐算法

除了这些算法以外,有一些算法的名字在机器学习领域中也经常出现。但他们本身并不算是一个机器学习算法,而是为了解决某个子问题而诞生的。你可以理解他们为以上算法的子算法,用于大幅度提高训练过程。其中的代表有:梯度下降法,主要运用在线型回归,逻辑回归,神经网络,推荐算法中;牛顿法,主要运用在线型回归中;BP算法,主要运用在神经网络中;SMO算法,主要运用在SVM中。


5.机器学习的应用–大数据

说完机器学习的方法,下面要谈一谈机器学习的应用了。无疑,在2010年以前,机器学习的应用在某些特定领域发挥了巨大的作用,如车牌识别,网络攻击防范,手写字符识别等等。但是,从2010年以后,随着大数据概念的兴起,机器学习大量的应用都与大数据高度耦合,几乎可以认为大数据是机器学习应用的最佳场景。

譬如,但凡你能找到的介绍大数据魔力的文章,都会说大数据如何准确准确预测到了某些事。例如经典的Google利用大数据预测了H1N1在美国某小镇的爆发。

 

图13 Google成功预测H1N1


百度预测2014年世界杯,从淘汰赛到决赛全部预测正确。

图14 百度世界杯成功预测了所有比赛结果

这些实在太神奇了,那么究竟是什么原因导致大数据具有这些魔力的呢?简单来说,就是机器学习技术。正是基于机器学习技术的应用,数据才能发挥其魔力。

大数据的核心是利用数据的价值,机器学习是利用数据价值的关键技术,对于大数据而言,机器学习是不可或缺的。相反,对于机器学习而言,越多的数据会越 可能提升模型的精确性,同时,复杂的机器学习算法的计算时间也迫切需要分布式计算与内存计算这样的关键技术。因此,机器学习的兴盛也离不开大数据的帮助。 大数据与机器学习两者是互相促进,相依相存的关系。

机器学习与大数据紧密联系。但是,必须清醒的认识到,大数据并不等同于机器学习,同理,机器学习也不等同于大数据。大数据中包含有分布式计算,内存数据库,多维分析等等多种技术。单从分析方法来看,大数据也包含以下四种分析方法:

1.大数据,小分析:即数据仓库领域的OLAP分析思路,也就是多维分析思想。
2.大数据,大分析:这个代表的就是数据挖掘与机器学习分析法。
3.流式分析:这个主要指的是事件驱动架构。
4.查询分析:经典代表是NoSQL数据库。

也就是说,机器学习仅仅是大数据分析中的一种而已。尽管机器学习的一些结果具有很大的魔力,在某种场合下是大数据价值最好的说明。但这并不代表机器学习是大数据下的唯一的分析方法。

机器学习与大数据的结合产生了巨大的价值。基于机器学习技术的发展,数据能够“预测”。对人类而言,积累的经验越丰富,阅历也广泛,对未来的判断越准确。例如常说的“经验丰富”的人比“初出茅庐”的小伙子更有工作上的优势,就在于经验丰富的人获得的规律比他人更准确。而在机器学习领域,根据著名的一个实验,有效的证实了机器学习界一个理论:即机器学习模型的数据越多,机器学习的预测的效率就越好。见下图:

图15 机器学习准确率与数据的关系

通过这张图可以看出,各种不同算法在输入的数据量达到一定级数后,都有相近的高准确度。于是诞生了机器学习界的名言:成功的机器学习应用不是拥有最好的算法,而是拥有最多的数据!

在大数据的时代,有好多优势促使机器学习能够应用更广泛。例如随着物联网和移动设备的发展,我们拥有的数据越来越多,种类也包括图片、文本、视频等非结构化数据,这使得机器学习模型可以获得越来越多的数据。同时大数据技术中的分布式计算Map-Reduce使得机器学习的速度越来越快,可以更方便的使用。种种优势使得在大数据时代,机器学习的优势可以得到最佳的发挥。

6.机器学习的子类–深度学习

近来,机器学习的发展产生了一个新的方向,即“深度学习”。

虽然深度学习这四字听起来颇为高大上,但其理念却非常简单,就是传统的神经网络发展到了多隐藏层的情况。

在上文介绍过,自从90年代以后,神经网络已经消寂了一段时间。但是BP算法的发明人Geoffrey Hinton一直没有放弃对神经网络的研究。由于神经网络在隐藏层扩大到两个以上,其训练速度就会非常慢,因此实用性一直低于支持向量机。2006年,Geoffrey Hinton在科学杂志《Science》上发表了一篇文章,论证了两个观点:

  1.多隐层的神经网络具有优异的特征学习能力,学习得到的特征对数据有更本质的刻画,从而有利于可视化或分类;

  2.深度神经网络在训练上的难度,可以通过“逐层初始化” 来有效克服。


图16 Geoffrey Hinton与他的学生在Science上发表文章

通过这样的发现,不仅解决了神经网络在计算上的难度,同时也说明了深层神经网络在学习上的优异性。从此,神经网络重新成为了机器学习界中的主流强大学习技术。同时,具有多个隐藏层的神经网络被称为深度神经网络,基于深度神经网络的学习研究称之为深度学习。

由于深度学习的重要性质,在各方面都取得极大的关注,按照时间轴排序,有以下四个标志性事件值得一说:

2012年6月,《纽约时报》披露了Google Brain项目,这个项目是由Andrew Ng和Map-Reduce发明人Jeff Dean共同主导,用16000个CPU Core的并行计算平台训练一种称为“深层神经网络”的机器学习模型,在语音识别和图像识别等领域获得了巨大的成功。Andrew Ng就是文章开始所介绍的机器学习的大牛(图1中右者)。

2012年11月,微软在中国天津的一次活动上公开演示了一个全自动的同声传译系统,讲演者用英文演讲,后台的计算机一气呵成自动完成语音识别、英中机器翻译,以及中文语音合成,效果非常流畅,其中支撑的关键技术是深度学习;

2013年1月,在百度的年会上,创始人兼CEO李彦宏高调宣布要成立百度研究院,其中第一个重点方向就是深度学习,并为此而成立深度学习研究院(IDL)。

2013年4月,《麻省理工学院技术评论》杂志将深度学习列为2013年十大突破性技术(Breakthrough Technology)之首。


图17 深度学习的发展热潮

文章开头所列的三位机器学习的大牛,不仅都是机器学习界的专家,更是深度学习研究领域的先驱。因此,使他们担任各个大型互联网公司技术掌舵者的原因不仅在于他们的技术实力,更在于他们研究的领域是前景无限的深度学习技术。

目前业界许多的图像识别技术与语音识别技术的进步都源于深度学习的发展,除了本文开头所提的Cortana等语音助手,还包括一些图像识别应用,其中典型的代表就是下图的百度识图功能。

图18 百度识图

深度学习属于机器学习的子类。基于深度学习的发展极大的促进了机器学习的地位提高,更进一步地,推动了业界对机器学习父类人工智能梦想的再次重视。

7.机器学习的父类–人工智能

人工智能是机器学习的父类。深度学习则是机器学习的子类。如果把三者的关系用图来表明的话,则是下图:


图19 深度学习、机器学习、人工智能三者关系

毫无疑问,人工智能(AI)是人类所能想象的科技界最突破性的发明了,某种意义上来说,人工智能就像游戏最终幻想的名字一样,是人类对于科技界的最终梦想。从50年代提出人工智能的理念以后,科技界,产业界不断在探索,研究。这段时间各种小说、电影都在以各种方式展现对于人工智能的想象。人类可以发明类似于人类的机器,这是多么伟大的一种理念!但事实上,自从50年代以后,人工智能的发展就磕磕碰碰,未有见到足够震撼的科学技术的进步。

总结起来,人工智能的发展经历了如下若干阶段,从早期的逻辑推理,到中期的专家系统,这些科研进步确实使我们离机器的智能有点接近了,但还有一大段距离。直到机器学习诞生以后,人工智能界感觉终于找对了方向。基于机器学习的图像识别和语音识别在某些垂直领域达到了跟人相媲美的程度。机器学习使人类第一次如此接近人工智能的梦想。

事实上,如果我们把人工智能相关的技术以及其他业界的技术做一个类比,就可以发现机器学习在人工智能中的重要地位不是没有理由的。

人类区别于其他物体,植物,动物的最主要区别,作者认为是“智慧”。而智慧的最佳体现是什么?

是计算能力么,应该不是,心算速度快的人我们一般称之为天才。
是反应能力么,也不是,反应快的人我们称之为灵敏。
是记忆能力么,也不是,记忆好的人我们一般称之为过目不忘。
是推理能力么,这样的人我也许会称他智力很高,类似“福尔摩斯”,但不会称他拥有智慧。
是知识能力么,这样的人我们称之为博闻广,也不会称他拥有智慧。

想想看我们一般形容谁有大智慧?圣人,诸如庄子,老子等。智慧是对生活的感悟,是对人生的积淀与思考,这与我们机器学习的思想何其相似?通过经验获取规律,指导人生与未来。没有经验就没有智慧。

 

图20 机器学习与智慧

那么,从计算机来看,以上的种种能力都有种种技术去应对。

例如计算能力我们有分布式计算,反应能力我们有事件驱动架构,检索能力我们有搜索引擎,知识存储能力我们有数据仓库,逻辑推理能力我们有专家系统,但是,唯有对应智慧中最显著特征的归纳与感悟能力,只有机器学习与之对应。这也是机器学习能力最能表征智慧的根本原因。

让我们再看一下机器人的制造,在我们具有了强大的计算,海量的存储,快速的检索,迅速的反应,优秀的逻辑推理后我们如果再配合上一个强大的智慧大脑,一个真正意义上的人工智能也许就会诞生,这也是为什么说在机器学习快速发展的现在,人工智能可能不再是梦想的原因。

人工智能的发展可能不仅取决于机器学习,更取决于前面所介绍的深度学习,深度学习技术由于深度模拟了人类大脑的构成,在视觉识别与语音识别上显著性的突破了原有机器学习技术的界限,因此极有可能是真正实现人工智能梦想的关键技术。无论是谷歌大脑还是百度大脑,都是通过海量层次的深度学习网络所构成的。也许借助于深度学习技术,在不远的将来,一个具有人类智能的计算机真的有可能实现。

最后再说一下题外话,由于人工智能借助于深度学习技术的快速发展,已经在某些地方引起了传统技术界达人的担忧。真实世界的“钢铁侠”,特斯拉CEO马斯克就是其中之一。最近马斯克在参加MIT讨论会时,就表达了对于人工智能的担忧。“人工智能的研究就类似于召唤恶魔,我们必须在某些地方加强注意。”

图21 马斯克与人工智能

尽管马斯克的担心有些危言耸听,但是马斯克的推理不无道理。“如果人工智能想要消除垃圾邮件的话,可能它最后的决定就是消灭人类。”马斯克认为预防此类现象的方法是引入政府的监管。在这里作者的观点与马斯克类似,在人工智能诞生之初就给其加上若干规则限制可能有效,也就是不应该使用单纯的机器学习,而应该是机器学习与规则引擎等系统的综合能够较好的解决这类问题。因为如果学习没有限制,极有可能进入某个误区,必须要加上某些引导。正如人类社会中,法律就是一个最好的规则,杀人者死就是对于人类在探索提高生产力时不可逾越的界限。

在这里,必须提一下这里的规则与机器学习引出的规律的不同,规律不是一个严格意义的准则,其代表的更多是概率上的指导,而规则则是神圣不可侵犯,不可修改的。规律可以调整,但规则是不能改变的。有效的结合规律与规则的特点,可以引导出一个合理的,可控的学习型人工智能。

8.机器学习的思考–计算机的潜意识

最后,作者想谈一谈关于机器学习的一些思考。主要是作者在日常生活总结出来的一些感悟。

回想一下我在节1里所说的故事,我把小Y过往跟我相约的经历做了一个罗列。但是这种罗列以往所有经历的方法只有少数人会这么做,大部分的人采用的是更直接的方法,即利用直觉。那么,直觉是什么?其实直觉也是你在潜意识状态下思考经验后得出的规律。就像你通过机器学习算法,得到了一个模型,那么你下次只要直接使用就行了。那么这个规律你是什么时候思考的?可能是在你无意识的情况下,例如睡觉,走路等情况。这种时候,大脑其实也在默默地做一些你察觉不到的工作。

这种直觉与潜意识,我把它与另一种人类思考经验的方式做了区分。如果一个人勤于思考,例如他会每天做一个小结,譬如“吾日三省吾身”,或者他经常与同伴讨论最近工作的得失,那么他这种训练模型的方式是直接的,明意识的思考与归纳。这样的效果很好,记忆性强,并且更能得出有效反应现实的规律。但是大部分的人可能很少做这样的总结,那么他们得出生活中规律的方法使用的就是潜意识法。

举一个作者本人关于潜意识的例子。作者本人以前没开过车,最近一段时间买了车后,天天开车上班。我每天都走固定的路线。有趣的是,在一开始的几天,我非常紧张的注意着前方的路况,而现在我已经在无意识中就把车开到了目标。这个过程中我的眼睛是注视着前方的,我的大脑是没有思考,但是我手握着的方向盘会自动的调整方向。也就是说。随着我开车次数的增多,我已经把我开车的动作交给了潜意识。这是非常有趣的一件事。在这段过程中,我的大脑将前方路况的图像记录了下来,同时大脑也记忆了我转动方向盘的动作。经过大脑自己的潜意识思考,最后生成的潜意识可以直接根据前方的图像调整我手的动作。假设我们将前方的录像交给计算机,然后让计算机记录与图像对应的驾驶员的动作。经过一段时间的学习,计算机生成的机器学习模型就可以进行自动驾驶了。这很神奇,不是么。其实包括Google、特斯拉在内的自动驾驶汽车技术的原理就是这样。

除了自动驾驶汽车以外,潜意识的思想还可以扩展到人的交际。譬如说服别人,一个最佳的方法就是给他展示一些信息,然后让他自己去归纳得出我们想要的结论。就好比在阐述一个观点时,用一个事实,或者一个故事,比大段的道理要好很多。古往今来,但凡优秀的说客,无不采用的是这种方法。春秋战国时期,各国合纵连横,经常有各种说客去跟一国之君交流,直接告诉君主该做什么,无异于自寻死路,但是跟君主讲故事,通过这些故事让君主恍然大悟,就是一种正确的过程。这里面有许多杰出的代表,如墨子,苏秦等等。

基本上所有的交流过程,使用故事说明的效果都要远胜于阐述道义之类的效果好很多。为什么用故事的方法比道理或者其他的方法好很多,这是因为在人成长的过程,经过自己的思考,已经形成了很多规律与潜意识。如果你告诉的规律与对方的不相符,很有可能出于保护,他们会本能的拒绝你的新规律,但是如果你跟他讲一个故事,传递一些信息,输送一些数据给他,他会思考并自我改变。他的思考过程实际上就是机器学习的过程,他把新的数据纳入到他的旧有的记忆与数据中,经过重新训练。如果你给出的数据的信息量非常大,大到调整了他的模型,那么他就会按照你希望的规律去做事。有的时候,他会本能的拒绝执行这个思考过程,但是数据一旦输入,无论他希望与否,他的大脑都会在潜意识状态下思考,并且可能改变他的看法。

如果计算机也拥有潜意识(正如本博客的名称一样),那么会怎么样?譬如让计算机在工作的过程中,逐渐产生了自身的潜意识,于是甚至可以在你不需要告诉它做什么时它就会完成那件事。这是个非常有意思的设想,这里留给各位读者去发散思考吧。


9.总结

本文首先介绍了互联网界与机器学习大牛结合的趋势,以及使用机器学习的相关应用,接着以一个“等人故事”展开对机器学习的介绍。介绍中首先是机器学习的概念与定义,然后是机器学习的相关学科,机器学习中包含的各类学习算法,接着介绍机器学习与大数据的关系,机器学习的新子类深度学习,最后探讨了一下机器学习与人工智能发展的联系以及机器学习与潜意识的关联。经过本文的介绍,相信大家对机器学习技术有一定的了解,例如机器学习是什么,它的内核思想是什么(即统计和归纳),通过了解机器学习与人类思考的近似联系可以知晓机器学习为什么具有智慧能力的原因等等。其次,本文漫谈了机器学习与外延学科的关系,机器学习与大数据相互促进相得益彰的联系,机器学习界最新的深度学习的迅猛发展,以及对于人类基于机器学习开发智能机器人的一种展望与思考,最后作者简单谈了一点关于让计算机拥有潜意识的设想。

机器学习是目前业界最为Amazing与火热的一项技术,从网上的每一次淘宝的购买东西,到自动驾驶汽车技术,以及网络攻击抵御系统等等,都有机器学习的因子在内,同时机器学习也是最有可能使人类完成AI dream的一项技术,各种人工智能目前的应用,如微软小冰聊天机器人,到计算机视觉技术的进步,都有机器学习努力的成分。作为一名当代的计算机领域的开发或管理人员,以及身处这个世界,使用者IT技术带来便利的人们,最好都应该了解一些机器学习的相关知识与概念,因为这可以帮你更好的理解为你带来莫大便利技术的背后原理,以及让你更好的理解当代科技的进程。

10.后记

这篇文档花了作者两个月的时间,终于在2014年的最后一天的前一天基本完成。通过这篇文章,作者希望对机器学习在国内的普及做一点贡献,同时也是作者本人自己对于所学机器学习知识的一个融汇贯通,整体归纳的提高过程。作者把这么多的知识经过自己的大脑思考,训练出了一个模型,形成了这篇文档,可以说这也是一种机器学习的过程吧(笑)。

作者所在的行业会接触到大量的数据,因此对于数据的处理和分析是平常非常重要的工作,机器学习课程的思想和理念对于作者日常的工作指引作用极大,几乎导致了作者对于数据价值的重新认识。想想半年前,作者还对机器学习似懂非懂,如今也可以算是一个机器学习的Expert了(笑)。但作者始终认为,机器学习的真正应用不是通过概念或者思想的方式,而是通过实践。只有当把机器学习技术真正应用时,才可算是对机器学习的理解进入了一个层次。正所谓再“阳春白雪”的技术,也必须落到“下里巴人”的场景下运用。目前有一种风气,国内外研究机器学习的某些学者,有一种高贵的逼格,认为自己的研究是普通人无法理解的,但是这样的理念是根本错误的,没有在真正实际的地方发挥作用,凭什么证明你的研究有所价值呢?作者认为必须将高大上的技术用在改变普通人的生活上,才能发挥其根本的价值。一些简单的场景,恰恰是实践机器学习技术的最好地方。

最后,作者很感谢能够阅读到这里的读者。如果看完觉得好的话,还请轻轻点一下赞,你们的鼓励就是作者继续行文的动力。

 

对EasyPR做下说明:EasyPR,一个开源的中文车牌识别系统,代码托管在github。其次,在前面的博客文章中,包含EasyPR至今的开发文档与介绍。在后续的文章中,作者会介绍EasyPR中基于机器学习技术SVM的应用即车牌判别模块的核心内容,欢迎继续阅读。

参考文献:

  1.Andrew Ng Courera Machine Learning

  2.LeNet Homepage

  3.pluskid svm

从机器学习谈起,首发于博客 - 伯乐在线

20 Jan 00:46

Extracting the abstract syntax tree from GCC

20 Jan 00:32

Java常见内存溢出异常分析

by importnewzz

Java虚拟机规范规定JVM的内存分为了好几块,比如堆,栈,程序计数器,方法区等,而Hotspot jvm的实现中,将堆内存分为了三部分,新生代,老年代,持久带,其中持久带实现了规范中规定的方法区,而内存模型中不同的部分都会出现相应的OOM错误,接下来我们就分开来讨论一下。

栈溢出(StackOverflowError)

栈溢出抛出java.lang.StackOverflowError错误,出现此种情况是因为方法运行的时候栈的深度超过了虚拟机容许的最大深度所致。

出现这种情况,一般情况下是程序错误所致的,比如写了一个死递归,就有可能造成此种情况。 下面我们通过一段代码来模拟一下此种情况的内存溢出。

import java.util.*;
import java.lang.*;
public class OOMTest{

  public void stackOverFlowMethod(){
      stackOverFlowMethod();
  }

  public static void main(String... args){
      OOMTest oom = new OOMTest();
      oom.stackOverFlowMethod();
  }

}

运行上面的代码,会抛出如下的异常:

Exception in thread "main" java.lang.StackOverflowError
        at OOMTest.stackOverFlowMethod(OOMTest.java:6)

堆溢出(OutOfMemoryError:java heap space)

堆内存溢出的时候,虚拟机会抛出java.lang.OutOfMemoryError:java heap space,出现此种情况的时候,我们需要根据内存溢出的时候产生的dump文件来具体分析(需要增加-XX:+HeapDumpOnOutOfMemoryErrorjvm启动参数)。出现此种问题的时候有可能是内存泄露,也有可能是内存溢出了。
如果内存泄露,我们要找出泄露的对象是怎么被GC ROOT引用起来,然后通过引用链来具体分析泄露的原因。
如果出现了内存溢出问题,这往往是程序本生需要的内存大于了我们给虚拟机配置的内存,这种情况下,我们可以采用调大-Xmx来解决这种问题。

下面我们通过如下的代码来演示一下此种情况的溢出:

import java.util.*;
import java.lang.*;
public class OOMTest{

        public static void main(String... args){
                List<byte[]> buffer = new ArrayList<byte[]>();
                buffer.add(new byte[10*1024*1024]);
        }

}

我们通过如下的命令运行上面的代码:

java -verbose:gc -Xmn10M -Xms20M -Xmx20M -XX:+PrintGC OOMTest

程序输入如下的信息:

[GC 1180K->366K(19456K), 0.0037311 secs]
[Full GC 366K->330K(19456K), 0.0098740 secs]
[Full GC 330K->292K(19456K), 0.0090244 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at OOMTest.main(OOMTest.java:7)

从运行结果可以看出,JVM进行了一次Minor gc和两次的Major gc,从Major gc的输出可以看出,gc以后old区使用率为134K,而字节数组为10M,加起来大于了old generation的空间,所以抛出了异常,如果调整-Xms21M,-Xmx21M,那么就不会触发gc操作也不会出现异常了。

通过上面的实验其实也从侧面验证了一个结论:当对象大于新生代剩余内存的时候,将直接放入老年代,当老年代剩余内存还是无法放下的时候,出发垃圾收集,收集后还是不能放下就会抛出内存溢出异常了

持久带溢出(OutOfMemoryError: PermGen space)

我们知道Hotspot jvm通过持久带实现了Java虚拟机规范中的方法区,而运行时的常量池就是保存在方法区中的,因此持久带溢出有可能是运行时常量池溢出,也有可能是方法区中保存的class对象没有被及时回收掉或者class信息占用的内存超过了我们配置。当持久带溢出的时候抛出java.lang.OutOfMemoryError: PermGen space
我在工作可能在如下几种场景下出现此问题。

  1. 使用一些应用服务器的热部署的时候,我们就会遇到热部署几次以后发现内存溢出了,这种情况就是因为每次热部署的后,原来的class没有被卸载掉。
  2. 如果应用程序本身比较大,涉及的类库比较多,但是我们分配给持久带的内存(通过-XX:PermSize和-XX:MaxPermSize来设置)比较小的时候也可能出现此种问题。
  3. 一些第三方框架,比如spring,hibernate都通过字节码生成技术(比如CGLib)来实现一些增强的功能,这种情况可能需要更大的方法区来存储动态生成的Class文件。

我们知道Java中字符串常量是放在常量池中的,String.intern()这个方法运行的时候,会检查常量池中是否存和本字符串相等的对象,如果存在直接返回对常量池中对象的引用,不存在的话,先把此字符串加入常量池,然后再返回字符串的引用。那么我们就可以通过String.intern方法来模拟一下运行时常量区的溢出.下面我们通过如下的代码来模拟此种情况:

import java.util.*;
import java.lang.*;
public class OOMTest{

        public static void main(String... args){
                List<String> list = new ArrayList<String>();
                while(true){
                        list.add(UUID.randomUUID().toString().intern());
                }
        }

}

我们通过如下的命令运行上面代码:

java -verbose:gc -Xmn5M -Xms10M -Xmx10M -XX:MaxPermSize=1M -XX:+PrintGC OOMTest

运行后的输入如下图所示:

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
        at java.lang.String.intern(Native Method)
        at OOMTest.main(OOMTest.java:8)

通过上面的代码,我们成功模拟了运行时常量池溢出的情况,从输出中的PermGen space可以看出确实是持久带发生了溢出,这也验证了,我们前面说的Hotspot jvm通过持久带来实现方法区的说法。

OutOfMemoryError:unable to create native thread

最后我们在来看看java.lang.OutOfMemoryError:unable to create natvie thread这种错误。 出现这种情况的时候,一般是下面两种情况导致的:

  1. 程序创建的线程数超过了操作系统的限制。对于Linux系统,我们可以通过ulimit -u来查看此限制。
  2. 给虚拟机分配的内存过大,导致创建线程的时候需要的native内存太少。我们都知道操作系统对每个进程的内存是有限制的,我们启动Jvm,相当于启动了一个进程,假如我们一个进程占用了4G的内存,那么通过下面的公式计算出来的剩余内存就是建立线程栈的时候可以用的内存。 线程栈总可用内存=4G-(-Xmx的值)- (-XX:MaxPermSize的值)- 程序计数器占用的内存 通过上面的公式我们可以看出,-Xmx 和 MaxPermSize的值越大,那么留给线程栈可用的空间就越小,在-Xss参数配置的栈容量不变的情况下,可以创建的线程数也就越小。因此如果是因为这种情况导致的unable to create native thread,那么要么我们增大进程所占用的总内存,或者减少-Xmx或者-Xss来达到创建更多线程的目的。

相关文章

20 Jan 00:32

Java高效读取大文件

by 进林

1、概述

本教程将演示如何用Java高效地读取大文件。这篇文章是Baeldunghttp://www.baeldung.com/“Java——回归基础”系列教程的一部分。

2、在内存中读取

读取文件行的标准方式是在内存中读取,Guava 和Apache Commons IO都提供了如下所示快速读取文件行的方法:

Files.readLines(new File(path), Charsets.UTF_8);

FileUtils.readLines(new File(path));

这种方法带来的问题是文件的所有行都被存放在内存中,当文件足够大时很快就会导致程序抛出OutOfMemoryError 异常。

例如:读取一个大约1G的文件:

@Test
public void givenUsingGuava_whenIteratingAFile_thenWorks() throws IOException {
    String path = ...
    Files.readLines(new File(path), Charsets.UTF_8);
}

这种方式开始时只占用很少的内存:(大约消耗了0Mb内存

[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Total Memory: 128 Mb
[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Free Memory: 116 Mb

然而,当文件全部读到内存中后,我们最后可以看到(大约消耗了2GB内存)

[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Total Memory: 2666 Mb
[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Free Memory: 490 Mb

这意味这一过程大约耗费了2.1GB的内存——原因很简单:现在文件的所有行都被存储在内存中。

把文件所有的内容都放在内存中很快会耗尽可用内存——不论实际可用内存有多大,这点是显而易见的。

此外,我们通常不需要把文件的所有行一次性地放入内存中——相反,我们只需要遍历文件的每一行,然后做相应的处理,处理完之后把它扔掉。所以,这正是我们将要做的——通过行迭代,而不是把所有行都放在内存中。

3、文件流

现在让我们看下这种解决方案——我们将使用java.util.Scanner类扫描文件的内容,一行一行连续地读取:

FileInputStream inputStream = null;
Scanner sc = null;
try {
    inputStream = new FileInputStream(path);
    sc = new Scanner(inputStream, "UTF-8");
    while (sc.hasNextLine()) {
        String line = sc.nextLine();
        // System.out.println(line);
    }
    // note that Scanner suppresses exceptions
    if (sc.ioException() != null) {
        throw sc.ioException();
    }
} finally {
    if (inputStream != null) {
        inputStream.close();
    }
    if (sc != null) {
        sc.close();
    }
}

这种方案将会遍历文件中的所有行——允许对每一行进行处理,而不保持对它的引用。总之没有把它们存放在内存中(大约消耗了150MB内存)

[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Total Memory: 763 Mb
[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Free Memory: 605 Mb

4、Apache Commons IO

同样也可以使用Commons IO库实现,利用该库提供的自定义LineIterator:

LineIterator it = FileUtils.lineIterator(theFile, "UTF-8");
try {
    while (it.hasNext()) {
        String line = it.nextLine();
        // do something with line
    }
} finally {
    LineIterator.closeQuietly(it);
}

由于整个文件不是全部存放在内存中,这也就导致相当保守的内存消耗:(大约消耗了150MB内存)

[main] INFO  o.b.java.CoreJavaIoIntegrationTest - Total Memory: 752 Mb
[main] INFO  o.b.java.CoreJavaIoIntegrationTest - Free Memory: 564 Mb

5、结论

这篇短文介绍了如何在不重复读取与不耗尽内存的情况下处理大文件——这为大文件的处理提供了一个有用的解决办法。

所有这些例子的实现和代码片段都可以在我的github项目上获取到——这是一个基于Eclipse的项目,所以它应该很容易被导入和运行。

相关文章

20 Jan 00:30

实现TeX的算法:回首编程技术的过去三十年

by Sergio De Simone

Glenn Vanderburg是LivingSocial的工程主管,在最近的ClojureConj会议上,对他使用Clojure实现TeX算法的工作做了很有趣的叙述。在那个过程中,他发现在过去三十年间,编程技术已经发生了非常大的变化。

TeX简史

首先,一些历史可能有助于说明TeX的重要性。Donald Knuth在1982年发布了TeX 1.0,32年后,它仍然代表了计算机排版的先进状态,Glenn说。此外,TeX一直是少数几个提供了源代码的大型项目,人们可以从中学习。

TeX是一部鸿篇巨制:它运行快速、可移植、产出优秀的结果,并且在三十年后它还在广泛使用中,只发现了很少缺陷。

Glenn特别提到,很有趣的是,当Knuth收到他的巨著《计算机编程艺术》的第一页样张,就决定开始编写TeX,因为他发现排版“丑陋得让人失望”。这样,Knuth开始编写程序,使得他的书看起来自己能够接受。当TeX可用之后,因为快速排序而为人所知的Tony Hoare建议Knuth发布源代码,那样从此可以为学生所用,那时是1982年,互联网还没有出现,也没有太多源代码示例。这个目标让Knuth开始有了迭代编程的冲动,最终TeX的源代码在1986年公诸于众。直到Linux内核出现之前,它一直是世界上被最广泛阅读的代码,Glenn说。

TeX内部

TeX架构是一个处理文本的管道,它会把文本切分成多种类型的对象,如页、段落、行、词等等,最终生成一个DIV文件。从TeX出现到现在已经有超过三十年,回顾过去,我们会惊奇的发现它仍然是非常“早期”的东西,Glenn说。

TeX源代码中有很多现在并不被认为是好的编程风格的例子,像:

  • 全局变量;
  • 一个字母的变量;
  • goto语句;
  • 数百行长的过程;
  • 大量宏;
  • 重复代码;
  • 局部变量重用;
  • 到处都是单线程假设;
  • 可变性代码普遍存在

阅读这样的代码就像是在访问另一个时代[…]在那本书出版的1986年,它代表了非常不错的编程方式,但很多方面现在已经过时了。

当时很多方式都是因为受到了当时可用硬件的限制,只有有限的计算能力和可用内存,据Glenn所说,Knuth为了减少函数调用到最小程度做了非常大的努力,而那实在是太昂贵了。这使得TeX的代码库高度整合,从而“抽取出任何一个部分都无法独立使用”。

TeX积极地使用技术来改善手动的操作,我们今天可能会真的看不起那些技术,但我们更应该仰视那些技术,因为要考虑到摩尔定律,不仅仅是那个定律,还有当时的语言实现技术。

使用Clojure重新实现TeX:Cló

因此,Tex可能并非是今天指导新手程序员的最佳示例,然而,Glenn之所以要重新实现它,是因为它能够展示出编程技术已经发生了多大改变,并且能够提供真实的示例,说明从过程化转换到函数式语言的时候,算法会发生多大改变。

据Glenn所说,想要理解TeX代码的功能很困难,大多是因为它的简洁和极度优化,正如上面所概述的。最初,他试图让自己的设计尽可能和TeX保持一致。正如上面所说,Tex严格地单线程执行,而在当今计算机领域,非常重要的一个目标就是利用已经可用的多核硬件。Clojure非常有用的一种特性是,让他可以把TeX的基本管道实现为一系列函数,然后他可以替换线程宏,从而从串行执行模式转换到并行执行模式。“那让我开始做一些像比较两个苹果的工作。” Cló的实现当然要比TeX慢很多,但转换到并行执行带来了“巨大的收益”。

Glenn发现另一个有趣的点是,他在某些时候不得不实现和TeX一样的优化。然而,不久他就意识到,那会让他无法使用在函数式语言中天然存在的非常好的抽象,从而让事情比应该的情况更加复杂。这也让他觉得不知道TeX的API受到了语言模式多么大的影响,特别是普遍的不可变性和单线程的假设。

对于Glenn最重要的反映来自于意识到编程技术有了多大的发展。如果我们回顾1982年的编程技术,就会看到:

  • 计算机运行缓慢,内存非常小;
  • 大多数程序员从未见过多核处理器,CPU的字节和比特的大小都不一样;
  • 对于浮点算法没有IEEE标准;
  • 可移植性意味着要支持差不多40个不同的操作系统,每个都拥有不同的文件系统结构、不同的目录语法、不同的I/O和分配API、字符集;
  • 不可能动态载入代码;
  • 优化编译器还是研究性项目;
  • 没有开源和免费的软件,你需要从头开始实现不可想象的基本内容、常用数据结构和流程。
  • 当版本控制可用的时候,功能还非常初级;
  • 人们还从未听说过自动化测试;
  • 今天的工具非常豪华,但也是多年来小步的改进逐渐得来的。

因此,对于我们还要继续改进编程技术的工作,Glenn邀请大家首先享受已经存在的良好基础。

查看英文原文:Implementing TeX's Algorithms: Looking Back at Thirty Years of Programming

19 Jan 01:06

Write a Shell in C

19 Jan 01:06

Linux kernel booting process, part 2

19 Jan 01:06

Command-line tools can be faster than your Hadoop cluster (2014)

19 Jan 01:00

[图]NASA证实地球周围存在时空漩涡 符合广义相对论

据美国宇航局网站报道,爱因斯坦的预言再一次得到了证实!科学家们经过仔细的检测,发现地球周围确实存在时空漩涡,并且其各项参数和爱因斯坦广义相对论预言的完全符合。这是此间在美国宇航局总部举行的一场新闻发布会上公布的消息,探测的结果来自对该局实施的引力探测卫星B(GP-B)计划的数据分析结果。