面试总结

Java相关

Java GC机制(重要程度:★★★★★)

主要从三个方面回答:GC是针对什么对象进行回收(可达性分析法),什么时候开始GC(当新生代满了会进行Minor GC,升到老年代的对象大于老年代剩余空间时会进行Major GC),GC做什么(新生代采用复制算法,老年代采用标记-清除或标记-整理算法),感觉回答这些就差不多了,也可以补充一下可以调优的参数(-XX:newRatio,-Xms,-Xmx等等)。详细的可以看我另一篇博客(Java中的垃圾回收机制)。

如何线程安全的使用HashMap(重要程度:★★★★★)

作为Java程序员还是经常和HashMap打交道的,所以HashMap的一些原理还是搞搞清除比较好。这个问题感觉主要就是问HashMap,HashTable,ConcurrentHashMap,sychronizedMap的原理和区别。具体的可以看我另一篇博客(如何线程安全的使用HashMap)。

HashMap是如何解决冲突的(重要程度:★★★★☆)

其实就是链接法,将索引值相同的元素存放到一个单链表里。但为了解决在频繁冲突时HashMap性能降低的问题,Java 8中做了一个小优化,在冲突的元素个数超过设定的值(默认为8)时,会使用平衡树来替代链表存储冲突的元素。具体的可以看我另一篇博客(Java 8中HashMap和LinkedHashMap如何解决冲突)。

Java创建对象有哪几种(重要程度:★★★★☆)

这个问题还算好回答,大概有四种—new、工厂模式、反射和克隆,不过这个问题有可能衍生出关于设计模式,反射,深克隆,浅克隆等一系列问题。。。要做好准备~
参考资料:
设计模式Java版
Java反射详解
深克隆与浅克隆的区别

注解(重要程度:★★★☆☆)

如果简历中有提到过曾自定义过注解,还是了解清楚比较好。主要是了解在自定义注解时需要使用的两个主要的元注解@Retention和@Target。@Retention用来声明注解的保留策略,有CLASS,RUNTIME,SOURCE三种,分别表示注解保存在类文件,JVM运行时刻和源代码中。@Target用来声明注解可以被添加到哪些类型的元素上,如类型,方法和域等。
参考资料:
Java注解

异常(重要程度:★★★☆☆)

一道笔试题,代码如下,问返回值是什么。

1
2
3
4
5
6
7
8
9
10
11
int ret = 0;
try{
throw new Exception();
}
catch(Exception e){
ret = 1;
return ret;
}
finally{
ret = 2;
}

主要的考点就是catch中的return在finally之后执行 但是会将return的值放到一个地方存起来,所以finally中的ret=2会执行,但返回值是1。
参考资料:
深入理解Java异常处理机制
Java异常处理

悲观锁和乐观锁区别,乐观锁适用于什么情况(重要程度:★★★★☆)

悲观锁,就是总觉得有刁民想害朕,每次访问数据的时候都觉得会有别人修改它,所以每次拿数据时都会上锁,确保在自己使用的过程中不会被他人访问。乐观锁就是很单纯,心态好,所以每次拿数据的时候都不会上锁,只是在更新数据的时候去判断该数据是否被别人修改过。
大多数的关系数据库写入操作都是基于悲观锁,缺点在于如果持有锁的客户端运行的很慢,那么等待解锁的客户端被阻塞的时间就越长。Redis的事务是基于乐观锁的机制,不会在执行WATCH命令时对数据进行加锁,只是会在数据已经被其他客户端抢先修改了的情况下,通知执行WATCH命令的客户端。乐观锁适用于读多写少的情况,因为在写操作比较频繁的时候,会不断地retry,从而降低性能。
参考资料:
关于悲观锁和乐观锁的区别
乐观锁和悲观锁

单例模式找错误(重要程度:★★★★☆)

错误是没有将构造函数私有化,单例还是比较简单的,把它的饿汉式和懒汉式的两种实现方式看明白了就可以了。
单例模式

__

Spring相关

关于Spring的问题主要就是围绕着Ioc和AOP,它们真是Spring的核心啊。

Spring Bean的生命周期(重要程度:★★★★★)

就不写我那么low的回答了,直接看参考资料吧。
参考资料:
Spring Bean的生命周期
Top 10 Spring Interview Questions Answers J2EE

Spring中用到的设计模式(重要程度:★★★★★)

工厂模式:IOC容器
代理模式:AOP
策略模式:在spring采取动态代理时,根据代理的类有无实现接口有JDK和CGLIB两种代理方式,就是采用策略模式实现的
单例模式:默认情况下spring中的bean只存在一个实例
只知道这四个。。。。
参考资料:
Design Patterns Used in Java Spring Framework

讲一讲Spring IoC和AOP(重要程度:★★★★★)

IoC的核心是依赖反转,将创建对象和对象之间的依赖管理交给IoC容器来做,完成对象之间的解耦。
AOP主要是利用代理模式,把许多接口都要用的又和接口本身主要的业务逻辑无关的部分抽出来,写成一个切面,单独维护,比如权限验证。这样可以使接口符合“单一职责原则”,只关注主要的业务逻辑,也提高了代码的重用性。

AOP的应用场景(重要程度:★★★★☆)

权限,日志,处理异常,事务等等,个人理解就是把许多接口都要用的又和接口本身主要的业务逻辑无关的部分抽出来,写成一个切面,单独维护,比如权限验证。这样可以使接口符合“单一职责原则”,只关注主要的业务逻辑,也提高了代码的重用性。

Spring中编码统一要如何做(重要程度:★★★☆☆)

配置一个拦截器就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<filter> 
       <filter-name>CharacterEncodingFilter</filter-name> 
       <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class
       <init-param> 
           <param-name>encoding</param-name> 
           <param-value>UTF-8</param-value> 
       </init-param> 
       <init-param> 
           <param-name>forceEncoding</param-name> 
           <param-value>true</param-value> 
       </init-param> 
   </filter> 
   <filter-mapping> 
       <filter-name>CharacterEncodingFilter</filter-name> 
       <url-pattern>/*</url-pattern> 
   </filter-mapping>

数据库相关

Mysql索引的内部结构(重要程度:★★★★☆)

B+树,三层,真实的数据存储在叶子节点
参考资料:
MySQL索引原理及慢查询优化

如果一个SQL执行时间比较长怎么办(重要程度:★★★★☆)

可以利用pt-query-digest等工具分析慢查询日志,也可以用explain查看SQL的执行计划。具体可看我的另一篇博客MySQL调优

如果一张表中有上千万条数据应该怎么做分页(重要程度:★★★☆☆)

肯定不能直接limit,offset,主要就是要想办法避免在数据量大时扫描过多的记录。具体可看我的另一篇博客【译】优化MySQL中的分页

什么样的列适合加索引,如果一个列的值只有1和2,那么它适合加索引么(重要程度:★★★☆☆)

  • 在where从句,group by从句,order by从句,on从句中出现的列
  • 索引的字段越小越好
  • 在建立联合索引时,离散度大的列放大联合索引的前面

只有1和2不适合建索引

Redis相关

Redis提供哪几种数据结构(重要程度:★★★★★)

一共有5种,字符串,散列,列表,集合,有序集合。
参考资料:
Redis中文官网

Redis支持集群么,从哪个版本开始支持集群的(重要程度:★★☆☆☆)

支持集群,从3.0版本开始。当然面试时我也没记住版本。。。

Redis集群中,如何将一个对象映射到对应的缓存服务器(重要程度:★★★★☆)

一般就是hash%N,就是用对象的hash值对缓存服务器的个数取余

接上个问题,缓存集群中如果新增一台服务器,怎么才能不影响大部分缓存数据的命中?(重要程度:★★★★☆)

其实就是一致性Hash算法。以前有看过,可惜面试的时候脑袋就空了,只记得一个环,果然还是要实践啊。
参考资料:
Consistent Hashing
五分钟理解一致性哈希算法(consistent hashing)

项目中具体是怎样使用Redis的(重要程度:★★★★☆)

根据实际情况回答吧。。。。我是主要做权限控制时用到了Redis,将用户Id和权限Code拼接在一起作为一个key,放到Redis的集合中,在验证某一用户是否有指定权限时,只需验证集合中是否有用户Id和权限Code拼接的key即可

算法相关

判断一个数字是否为快乐数字(重要程度:★☆☆☆☆)

leetcode第202题
链接

给定一个乱序数组和一个目标数字 找到和为这个数字的两个数字 时间复杂度是多少(重要程度:★☆☆☆☆)

leetcode第一题
链接

如何判断一个链表有没有环(重要程度:★☆☆☆☆)

用快慢指针

删除字符串中的空格 只留一个(重要程度:★☆☆☆☆)

这个比较简单。。。。

二叉树层序遍历(重要程度:★★☆☆☆)

利用队列就可以了

地铁票价是如何计算的(重要程度:★★☆☆☆)

不知道正确答案,感觉是图的最短路径算法相关的。

Elasticsearch相关

为什么要用Elasticsearch(重要程度:★★★★☆)

其实对Es的了解还是比较少的,因为没做多久就去写坑爹代理商了。个人觉得项目中用Es的原因一是可以做分词,二是Es中采用的是倒排索引所以性能比较好,三是Es是个分布式的搜索服务,对各个节点的配置还是很简单方便的

Elasticsearch中的数据来源是什么,如何做同步(重要程度:★★★★☆)

数据是来自其他部门的数据库,会在一开始写python脚本做全量更新,之后利用RabbitMQ做增量更新,就是数据更改之后,数据提供方将更改的数据插入到指定消息队列,由对应的消费者索引到Es中

接上个问题,利用消息队列是会对对方代码造成侵入的,还有没有别的方式(重要程度:★★★☆☆)

还可以读MySQL的binlog

发散思维的题

以下题都是没有正确答案的,不知道是想考思维,还是压力面试,就只写题目,不写回答了。。。

画一下心中房树人的关系(重要程度:★☆☆☆☆)
给你一块地建房如何规划(重要程度:★☆☆☆☆)
估计二号线有几辆车在运行(重要程度:★☆☆☆☆)

其他

Thrift通信协议(重要程度:★★★☆☆)

这个问题被问了两遍,然而现在还是不知道。。。什么东西都不能停留在只会用的阶段啊~

git相关(重要程度:★★★★★)

一些git相关的问题,比如如何做分支管理(git flow),rebase和merge的区别(merge操作会生成一个新的节点)等等。。。

如何学习一门新技术(重要程度:★★★☆☆)

google+官网+stackoverflow+github

比较爱逛的网站和爱看的书(重要程度:★★★☆☆)

根据实际情况回答吧。。。

了不了解微服务(重要程度:★★☆☆☆)

简单了解过。。。
参考资料:
基于微服务的软件架构模式

针对简历中的项目问一些问题(重要程度:★★★★★)

就是根据简历上的项目问一些东西,比如权限控制是怎么做的,有没有碰到过比较难解决的问题之类的,不具体列举了,只要简历上的内容是真实的基本都没啥问题

为什么要离职(重要程度:★★★★★)

被问了n遍,挺不好回答的一个问题,毕竟不算实习期工作还没满一年,这个时候跳槽很容易让人觉得不安稳。。。。

对公司还有什么问题(重要程度:★★★★★)

基本每轮面试结束都会问的一个问题,一开始也没当回事,直到有家公司居然挂在四面的这个问题上,我也是蛮醉的,果然言多必失啊。。。

在***公司最大的收获是什么(重要程度:★★★★★)

对于我来说,觉得最大的收获就是对企业级的应用有了一个大致的了解,企业里的项目不像学校的课程作业,只要jdbc连接数据库完成功能就可以了,企业的项目要考虑很多东西,比如说为了提高可用性,要部署在多台服务器上,用nginx做负载均衡,还有就是用缓存,异步之类来提高接口性能。然后,也是第一次接触到SOA,这种面向服务的架构。也了解到一个好的应用,除了开发本身,一些自动化发布系统和监控系统也是必不可少的。当然,还认识了一群三观合的小伙伴~~~

Java 8简明教程

ImportNew注:有兴趣第一时间学习Java 8的Java开发者,欢迎围观《征集参与Java 8原创系列文章作者》。

以下是《Java 8简明教程》的正文。

“Java并没有没落,人们很快就会发现这一点”

欢迎阅读我编写的Java 8介绍。本教程将带领你一步一步地认识这门语言的新特性。通过简单明了的代码示例,你将会学习到如何使用默认接口方法,Lambda表达式,方法引用和重复注解。看完这篇教程后,你还将对最新推出的API有一定的了解,例如:流控制,函数式接口,map扩展和新的时间日期API等等。

允许在接口中有默认方法实现

Java 8 允许我们使用default关键字,为接口声明添加非抽象的方法实现。这个特性又被称为扩展方法。下面是我们的第一个例子:

1
2
3
4
5
6
7
interface Formula {
    double calculate(int a);
    default double sqrt(int a) {
        return Math.sqrt(a);
    }
}

在接口Formula中,除了抽象方法caculate以外,还定义了一个默认方法sqrt。Formula的实现类只需要实现抽象方法caculate就可以了。默认方法sqrt可以直接使用。

1
2
3
4
5
6
7
8
9
Formula formula = new Formula() {
    @Override
    public double calculate(int a) {
        return sqrt(a * 100);
    }
};
formula.calculate(100);     // 100.0
formula.sqrt(16);           // 4.0

formula对象以匿名对象的形式实现了Formula接口。代码很啰嗦:用了6行代码才实现了一个简单的计算功能:a*100开平方根。我们在下一节会看到,Java 8 还有一种更加优美的方法,能够实现包含单个函数的对象。

Lambda表达式

让我们从最简单的例子开始,来学习如何对一个string列表进行排序。我们首先使用Java 8之前的方法来实现:

1
2
3
4
5
6
7
8
List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");
Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return b.compareTo(a);
    }
});

静态工具方法Collections.sort接受一个list,和一个Comparator接口作为输入参数,Comparator的实现类可以对输入的list中的元素进行比较。通常情况下,你可以直接用创建匿名Comparator对象,并把它作为参数传递给sort方法。

除了创建匿名对象以外,Java 8 还提供了一种更简洁的方式,Lambda表达式。

1
2
3
Collections.sort(names, (String a, String b) -> {
    return b.compareTo(a);
});

你可以看到,这段代码就比之前的更加简短和易读。但是,它还可以更加简短:

1
Collections.sort(names, (String a, String b) -> b.compareTo(a));

只要一行代码,包含了方法体。你甚至可以连大括号对{}和return关键字都省略不要。不过这还不是最短的写法:

1
Collections.sort(names, (a, b) -> b.compareTo(a));

Java编译器能够自动识别参数的类型,所以你就可以省略掉类型不写。让我们再深入地研究一下lambda表达式的威力吧。

函数式接口

Lambda表达式如何匹配Java的类型系统?每一个lambda都能够通过一个特定的接口,与一个给定的类型进行匹配。一个所谓的函数式接口必须要有且仅有一个抽象方法声明。每个与之对应的lambda表达式必须要与抽象方法的声明相匹配。由于默认方法不是抽象的,因此你可以在你的函数式接口里任意添加默认方法。

任意只包含一个抽象方法的接口,我们都可以用来做成lambda表达式。为了让你定义的接口满足要求,你应当在接口前加上@FunctionalInterface 标注。编译器会注意到这个标注,如果你的接口中定义了第二个抽象方法的话,编译器会抛出异常。

举例:

1
2
3
4
5
6
7
8
@FunctionalInterface
interface Converter<F, T> {
    T convert(F from);
}
Converter<String, Integer> converter = (from) -> Integer.valueOf(from);
Integer converted = converter.convert("123");
System.out.println(converted);    // 123

注意,如果你不写@FunctionalInterface 标注,程序也是正确的。

方法和构造函数引用

上面的代码实例可以通过静态方法引用,使之更加简洁:

1
2
3
Converter<String, Integer> converter = Integer::valueOf;
Integer converted = converter.convert("123");
System.out.println(converted);   // 123

Java 8 允许你通过::关键字获取方法或者构造函数的的引用。上面的例子就演示了如何引用一个静态方法。而且,我们还可以对一个对象的方法进行引用:

1
2
3
4
5
6
7
8
9
10
class Something {
    String startsWith(String s) {
        return String.valueOf(s.charAt(0));
    }
}
Something something = new Something();
Converter<String, String> converter = something::startsWith;
String converted = converter.convert("Java");
System.out.println(converted);    // "J"

让我们看看如何使用::关键字引用构造函数。首先我们定义一个示例bean,包含不同的构造方法:

1
2
3
4
5
6
7
8
9
10
11
class Person {
    String firstName;
    String lastName;
    Person() {}
    Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

接下来,我们定义一个person工厂接口,用来创建新的person对象:

1
2
3
interface PersonFactory<P extends Person> {
    P create(String firstName, String lastName);
}

然后我们通过构造函数引用来把所有东西拼到一起,而不是像以前一样,通过手动实现一个工厂来这么做。

1
2
PersonFactory<Person> personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");

我们通过Person::new来创建一个Person类构造函数的引用。Java编译器会自动地选择合适的构造函数来匹配PersonFactory.create函数的签名,并选择正确的构造函数形式。

Lambda的范围

对于lambdab表达式外部的变量,其访问权限的粒度与匿名对象的方式非常类似。你能够访问局部对应的外部区域的局部final变量,以及成员变量和静态变量。

访问局部变量

我们可以访问lambda表达式外部的final局部变量:

1
2
3
4
5
final int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);
stringConverter.convert(2);     // 3

但是与匿名对象不同的是,变量num并不需要一定是final。下面的代码依然是合法的:

1
2
3
4
5
int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);
stringConverter.convert(2);     // 3

然而,num在编译的时候被隐式地当做final变量来处理。下面的代码就不合法:

1
2
3
4
int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);
num = 3;

在lambda表达式内部企图改变num的值也是不允许的。

访问成员变量和静态变量

与局部变量不同,我们在lambda表达式的内部能获取到对成员变量或静态变量的读写权。这种访问行为在匿名对象里是非常典型的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Lambda4 {
    static int outerStaticNum;
    int outerNum;
    void testScopes() {
        Converter<Integer, String> stringConverter1 = (from) -> {
            outerNum = 23;
            return String.valueOf(from);
        };
        Converter<Integer, String> stringConverter2 = (from) -> {
            outerStaticNum = 72;
            return String.valueOf(from);
        };
    }
}

访问默认接口方法

还记得第一节里面formula的那个例子么? 接口Formula定义了一个默认的方法sqrt,该方法能够访问formula所有的对象实例,包括匿名对象。这个对lambda表达式来讲则无效。

默认方法无法在lambda表达式内部被访问。因此下面的代码是无法通过编译的:

1
Formula formula = (a) -> sqrt( a * 100);

内置函数式接口

JDK 1.8 API中包含了很多内置的函数式接口。有些是在以前版本的Java中大家耳熟能详的,例如Comparator接口,或者Runnable接口。对这些现成的接口进行实现,可以通过@FunctionalInterface 标注来启用Lambda功能支持。

此外,Java 8 API 还提供了很多新的函数式接口,来降低程序员的工作负担。有些新的接口已经在Google Guava库中很有名了。如果你对这些库很熟的话,你甚至闭上眼睛都能够想到,这些接口在类库的实现过程中起了多么大的作用。

Predicates

Predicate是一个布尔类型的函数,该函数只有一个输入参数。Predicate接口包含了多种默认方法,用于处理复杂的逻辑动词(and, or,negate)

1
2
3
4
5
6
7
8
9
10
Predicate<String> predicate = (s) -> s.length() > 0;
predicate.test("foo");              // true
predicate.negate().test("foo");     // false
Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();

Functions

Function接口接收一个参数,并返回单一的结果。默认方法可以将多个函数串在一起(compse, andThen)

1
2
3
4
Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
backToString.apply("123");     // "123"

Suppliers

Supplier接口产生一个给定类型的结果。与Function不同的是,Supplier没有输入参数。

1
2
Supplier<Person> personSupplier = Person::new;
personSupplier.get();   // new Person

Consumers

Consumer代表了在一个输入参数上需要进行的操作。

1
2
Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));

Comparators

Comparator接口在早期的Java版本中非常著名。Java 8 为这个接口添加了不同的默认方法。

1
2
3
4
5
6
7
Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName);
Person p1 = new Person("John", "Doe");
Person p2 = new Person("Alice", "Wonderland");
comparator.compare(p1, p2);             // > 0
comparator.reversed().compare(p1, p2);  // < 0

Optionals

Optional不是一个函数式接口,而是一个精巧的工具接口,用来防止NullPointerEception产生。这个概念在下一节会显得很重要,所以我们在这里快速地浏览一下Optional的工作原理。

Optional是一个简单的值容器,这个值可以是null,也可以是non-null。考虑到一个方法可能会返回一个non-null的值,也可能返回一个空值。为了不直接返回null,我们在Java 8中就返回一个Optional.

1
2
3
4
5
6
7
Optional<String> optional = Optional.of("bam");
optional.isPresent();           // true
optional.get();                 // "bam"
optional.orElse("fallback");    // "bam"
optional.ifPresent((s) -> System.out.println(s.charAt(0)));     // "b"

Streams

java.util.Stream表示了某一种元素的序列,在这些元素上可以进行各种操作。Stream操作可以是中间操作,也可以是完结操作。完结操作会返回一个某种类型的值,而中间操作会返回流对象本身,并且你可以通过多次调用同一个流操作方法来将操作结果串起来(就像StringBuffer的append方法一样————译者注)。Stream是在一个源的基础上创建出来的,例如java.util.Collection中的list或者set(map不能作为Stream的源)。Stream操作往往可以通过顺序或者并行两种方式来执行。

我们先了解一下序列流。首先,我们通过string类型的list的形式创建示例数据:

1
2
3
4
5
6
7
8
9
List<String> stringCollection = new ArrayList<>();
stringCollection.add("ddd2");
stringCollection.add("aaa2");
stringCollection.add("bbb1");
stringCollection.add("aaa1");
stringCollection.add("bbb3");
stringCollection.add("ccc");
stringCollection.add("bbb2");
stringCollection.add("ddd1");

Java 8中的Collections类的功能已经有所增强,你可以之直接通过调用Collections.stream()或者Collection.parallelStream()方法来创建一个流对象。下面的章节会解释这个最常用的操作。

Filter

Filter接受一个predicate接口类型的变量,并将所有流对象中的元素进行过滤。该操作是一个中间操作,因此它允许我们在返回结果的基础上再进行其他的流操作(forEach)。ForEach接受一个function接口类型的变量,用来执行对每一个元素的操作。ForEach是一个中止操作。它不返回流,所以我们不能再调用其他的流操作。

1
2
3
4
5
6
stringCollection
    .stream()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);
// "aaa2", "aaa1"

Sorted

Sorted是一个中间操作,能够返回一个排过序的流对象的视图。流对象中的元素会默认按照自然顺序进行排序,除非你自己指定一个Comparator接口来改变排序规则。

1
2
3
4
5
6
7
stringCollection
    .stream()
    .sorted()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);
// "aaa1", "aaa2"

一定要记住,sorted只是创建一个流对象排序的视图,而不会改变原来集合中元素的顺序。原来string集合中的元素顺序是没有改变的。

1
2
System.out.println(stringCollection);
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1

Map

map是一个对于流对象的中间操作,通过给定的方法,它能够把流对象中的每一个元素对应到另外一个对象上。下面的例子就演示了如何把每个string都转换成大写的string. 不但如此,你还可以把每一种对象映射成为其他类型。对于带泛型结果的流对象,具体的类型还要由传递给map的泛型方法来决定。

1
2
3
4
5
6
7
stringCollection
    .stream()
    .map(String::toUpperCase)
    .sorted((a, b) -> b.compareTo(a))
    .forEach(System.out::println);
// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"

Match

匹配操作有多种不同的类型,都是用来判断某一种规则是否与流对象相互吻合的。所有的匹配操作都是终结操作,只返回一个boolean类型的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
boolean anyStartsWithA =
    stringCollection
        .stream()
        .anyMatch((s) -> s.startsWith("a"));
System.out.println(anyStartsWithA);      // true
boolean allStartsWithA =
    stringCollection
        .stream()
        .allMatch((s) -> s.startsWith("a"));
System.out.println(allStartsWithA);      // false
boolean noneStartsWithZ =
    stringCollection
        .stream()
        .noneMatch((s) -> s.startsWith("z"));
System.out.println(noneStartsWithZ);      // true

Count

Count是一个终结操作,它的作用是返回一个数值,用来标识当前流对象中包含的元素数量。

1
2
3
4
5
6
7
long startsWithB =
    stringCollection
        .stream()
        .filter((s) -> s.startsWith("b"))
        .count();
System.out.println(startsWithB);    // 3

Reduce

该操作是一个终结操作,它能够通过某一个方法,对元素进行削减操作。该操作的结果会放在一个Optional变量里返回。

1
2
3
4
5
6
7
8
Optional<String> reduced =
    stringCollection
        .stream()
        .sorted()
        .reduce((s1, s2) -> s1 + "#" + s2);
reduced.ifPresent(System.out::println);
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

Parallel Streams

像上面所说的,流操作可以是顺序的,也可以是并行的。顺序操作通过单线程执行,而并行操作则通过多线程执行。

下面的例子就演示了如何使用并行流进行操作来提高运行效率,代码非常简单。

首先我们创建一个大的list,里面的元素都是唯一的:

1
2
3
4
5
6
int max = 1000000;
List<String> values = new ArrayList<>(max);
for (int i = 0; i < max; i++) {
    UUID uuid = UUID.randomUUID();
    values.add(uuid.toString());
}

现在,我们测量一下对这个集合进行排序所使用的时间。

顺序排序

1
2
3
4
5
6
7
8
9
10
11
long t0 = System.nanoTime();
long count = values.stream().sorted().count();
System.out.println(count);
long t1 = System.nanoTime();
long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("sequential sort took: %d ms", millis));
// sequential sort took: 899 ms

并行排序

1
2
3
4
5
6
7
8
9
10
11
long t0 = System.nanoTime();
long count = values.parallelStream().sorted().count();
System.out.println(count);
long t1 = System.nanoTime();
long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("parallel sort took: %d ms", millis));
// parallel sort took: 472 ms

如你所见,所有的代码段几乎都相同,唯一的不同就是把stream()改成了parallelStream(), 结果并行排序快了50%。

Map

正如前面已经提到的那样,map是不支持流操作的。而更新后的map现在则支持多种实用的新方法,来完成常规的任务。

1
2
3
4
5
6
7
Map<Integer, String> map = new HashMap<>();
for (int i = 0; i < 10; i++) {
    map.putIfAbsent(i, "val" + i);
}
map.forEach((id, val) -> System.out.println(val));

上面的代码风格是完全自解释的:putIfAbsent避免我们将null写入;forEach接受一个消费者对象,从而将操作实施到每一个map中的值上。

下面的这个例子展示了如何使用函数来计算map的编码

1
2
3
4
5
6
7
8
9
10
11
map.computeIfPresent(3, (num, val) -> val + num);
map.get(3);             // val33
map.computeIfPresent(9, (num, val) -> null);
map.containsKey(9);     // false
map.computeIfAbsent(23, num -> "val" + num);
map.containsKey(23);    // true
map.computeIfAbsent(3, num -> "bam");
map.get(3);             // val33

接下来,我们将学习,当给定一个key值时,如何把一个实例从对应的key中移除:

 

1
2
3
4
5
map.remove(3, "val3");
map.get(3);             // val33
map.remove(3, "val33");
map.get(3);             // null

另一个有用的方法:

1
map.getOrDefault(42, "not found");  // not found

将map中的实例合并也是非常容易的:

1
2
3
4
5
map.merge(9, "val9", (value, newValue) -> value.concat(newValue));
map.get(9);             // val9
map.merge(9, "concat", (value, newValue) -> value.concat(newValue));
map.get(9);             // val9concat

合并操作先看map中是否没有特定的key/value存在,如果是,则把key/value存入map,否则merging函数就会被调用,对现有的数值进行修改。

时间日期API

Java 8 包含了全新的时间日期API,这些功能都放在了java.time包下。新的时间日期API是基于Joda-Time库开发的,但是也不尽相同。下面的例子就涵盖了大多数新的API的重要部分。

Clock

Clock提供了对当前时间和日期的访问功能。Clock是对当前时区敏感的,并可用于替代System.currentTimeMillis()方法来获取当前的毫秒时间。当前时间线上的时刻可以用Instance类来表示。Instance也能够用于创建原先的java.util.Date对象。

1
2
3
4
5
Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();
Instant instant = clock.instant();
Date legacyDate = Date.from(instant);   // legacy java.util.Date

Timezones

时区类可以用一个ZoneId来表示。时区类的对象可以通过静态工厂方法方便地获取。时区类还定义了一个偏移量,用来在当前时刻或某时间与目标时区时间之间进行转换。

1
2
3
4
5
6
7
8
9
10
System.out.println(ZoneId.getAvailableZoneIds());
// prints all available timezone ids
ZoneId zone1 = ZoneId.of("Europe/Berlin");
ZoneId zone2 = ZoneId.of("Brazil/East");
System.out.println(zone1.getRules());
System.out.println(zone2.getRules());
// ZoneRules[currentStandardOffset=+01:00]
// ZoneRules[currentStandardOffset=-03:00]

LocalTime

本地时间类表示一个没有指定时区的时间,例如,10 p.m.或者17:30:15,下面的例子会用上面的例子定义的时区创建两个本地时间对象。然后我们会比较两个时间,并计算它们之间的小时和分钟的不同。

1
2
3
4
5
6
7
8
9
10
LocalTime now1 = LocalTime.now(zone1);
LocalTime now2 = LocalTime.now(zone2);
System.out.println(now1.isBefore(now2));  // false
long hoursBetween = ChronoUnit.HOURS.between(now1, now2);
long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);
System.out.println(hoursBetween);       // -3
System.out.println(minutesBetween);     // -239

LocalTime是由多个工厂方法组成,其目的是为了简化对时间对象实例的创建和操作,包括对时间字符串进行解析的操作。

1
2
3
4
5
6
7
8
9
10
LocalTime late = LocalTime.of(23, 59, 59);
System.out.println(late);       // 23:59:59
DateTimeFormatter germanFormatter =
    DateTimeFormatter
        .ofLocalizedTime(FormatStyle.SHORT)
        .withLocale(Locale.GERMAN);
LocalTime leetTime = LocalTime.parse("13:37", germanFormatter);
System.out.println(leetTime);   // 13:37

LocalDate

本地时间表示了一个独一无二的时间,例如:2014-03-11。这个时间是不可变的,与LocalTime是同源的。下面的例子演示了如何通过加减日,月,年等指标来计算新的日期。记住,每一次操作都会返回一个新的时间对象。

1
2
3
4
5
6
7
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
LocalDate yesterday = tomorrow.minusDays(2);
LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4);
DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();
System.out.println(dayOfWeek);    // FRIDAY<span style="font-family: Georgia, 'Times New Roman', 'Bitstream Charter', Times, serif; font-size: 13px; line-height: 19px;">Parsing a LocalDate from a string is just as simple as parsing a LocalTime:</span>

解析字符串并形成LocalDate对象,这个操作和解析LocalTime一样简单。

1
2
3
4
5
6
7
DateTimeFormatter germanFormatter =
    DateTimeFormatter
        .ofLocalizedDate(FormatStyle.MEDIUM)
        .withLocale(Locale.GERMAN);
LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter);
System.out.println(xmas);   // 2014-12-24

LocalDateTime

LocalDateTime表示的是日期-时间。它将刚才介绍的日期对象和时间对象结合起来,形成了一个对象实例。LocalDateTime是不可变的,与LocalTime和LocalDate的工作原理相同。我们可以通过调用方法来获取日期时间对象中特定的数据域。

1
2
3
4
5
6
7
8
9
10
LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59);
DayOfWeek dayOfWeek = sylvester.getDayOfWeek();
System.out.println(dayOfWeek);      // WEDNESDAY
Month month = sylvester.getMonth();
System.out.println(month);          // DECEMBER
long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);
System.out.println(minuteOfDay);    // 1439

如果再加上的时区信息,LocalDateTime能够被转换成Instance实例。Instance能够被转换成以前的java.util.Date对象。

1
2
3
4
5
6
Instant instant = sylvester
        .atZone(ZoneId.systemDefault())
        .toInstant();
Date legacyDate = Date.from(instant);
System.out.println(legacyDate);     // Wed Dec 31 23:59:59 CET 2014

格式化日期-时间对象就和格式化日期对象或者时间对象一样。除了使用预定义的格式以外,我们还可以创建自定义的格式化对象,然后匹配我们自定义的格式。

1
2
3
4
5
6
7
DateTimeFormatter formatter =
    DateTimeFormatter
        .ofPattern("MMM dd, yyyy - HH:mm");
LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", formatter);
String string = formatter.format(parsed);
System.out.println(string);     // Nov 03, 2014 - 07:13

不同于java.text.NumberFormat,新的DateTimeFormatter类是不可变的,也是线程安全的。

更多的细节,请看这里

 

Annotations

Java 8中的注解是可重复的。让我们直接深入看看例子,弄明白它是什么意思。

首先,我们定义一个包装注解,它包括了一个实际注解的数组

1
2
3
4
5
6
7
8
@interface Hints {
    Hint[] value();
}
@Repeatable(Hints.class)
@interface Hint {
    String value();
}

只要在前面加上注解名:@Repeatable,Java 8 允许我们对同一类型使用多重注解,

变体1:使用注解容器(老方法)

1
2
@Hints({@Hint("hint1"), @Hint("hint2")})
class Person {}

变体2:使用可重复注解(新方法)

1
2
3
@Hint("hint1")
@Hint("hint2")
class Person {}

使用变体2,Java编译器能够在内部自动对@Hint进行设置。这对于通过反射来读取注解信息来说,是非常重要的。

1
2
3
4
5
6
7
8
Hint hint = Person.class.getAnnotation(Hint.class);
System.out.println(hint);                   // null
Hints hints1 = Person.class.getAnnotation(Hints.class);
System.out.println(hints1.value().length);  // 2
Hint[] hints2 = Person.class.getAnnotationsByType(Hint.class);
System.out.println(hints2.length);          // 2

尽管我们绝对不会在Person类上声明@Hints注解,但是它的信息仍然可以通过getAnnotation(Hints.class)来读取。并且,getAnnotationsByType方法会更方便,因为它赋予了所有@Hints注解标注的方法直接的访问权限。

1
2
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface MyAnnotation {}

先到这里

我的Java 8编程指南就到此告一段落。当然,还有很多内容需要进一步研究和说明。这就需要靠读者您来对JDK 8进行探究了,例如:Arrays.parallelSort, StampedLock和CompletableFuture等等 ———— 我这里只是举几个例子而已。

我希望这个博文能够对您有所帮助,也希望您阅读愉快。完整的教程源代码放在了GitHub上。您可以尽情地fork,并请通过Twitter告诉我您的反馈。

原文链接: winterbe 翻译: ImportNew.com 黄小非
译文链接: http://www.importnew.com/10360.html

Java命令学习系列(2):Jstack

来源:Hollis

链接:http://www.hollischuang.com/archives/110

jstack是java虚拟机自带的一种堆栈跟踪工具。

功能

jstack用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。另外,jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。

So,jstack命令主要用来查看Java线程的调用堆栈的,可以用来分析线程问题(如死锁)。

线程状态

想要通过jstack命令来分析线程的情况的话,首先要知道线程都有哪些状态,下面这些状态是我们使用jstack命令查看线程堆栈信息时可能会看到的线程的几种状态:

NEW,未启动的。不会出现在Dump中。

RUNNABLE,在虚拟机内执行的。

BLOCKED,受阻塞并等待监视器锁。

WATING,无限期等待另一个线程执行特定操作。

TIMED_WATING,有时限的等待另一个线程的特定操作。

TERMINATED,已退出的。

Monitor

在多线程的 JAVA程序中,实现线程之间的同步,就要说说 Monitor。 Monitor是 Java中用以实现线程之间的互斥与协作的主要手段,它可以看成是对象或者 Class的锁。每一个对象都有,也仅有一个 monitor。下 面这个图,描述了线程和 Monitor之间关系,以 及线程的状态转换图:

进入区(Entrt Set):表示线程通过synchronized要求获取对象的锁。如果对象未被锁住,则迚入拥有者;否则则在进入区等待。一旦对象锁被其他线程释放,立即参与竞争。

拥有者(The Owner):表示某一线程成功竞争到对象锁。

等待区(Wait Set):表示线程通过对象的wait方法,释放对象的锁,并在等待区等待被唤醒。

从图中可以看出,一个 Monitor在某个时刻,只能被一个线程拥有,该线程就是 “Active Thread”,而其它线程都是 “Waiting Thread”,分别在两个队列 “ Entry Set”和 “Wait Set”里面等候。在 “Entry Set”中等待的线程状态是 “Waiting for monitor entry”,而在 “Wait Set”中等待的线程状态是 “in Object.wait()”。 先看 “Entry Set”里面的线程。我们称被 synchronized保护起来的代码段为临界区。当一个线程申请进入临界区时,它就进入了 “Entry Set”队列。对应的 code就像:

synchronized(obj) {

………

}

调用修饰

表示线程在方法调用时,额外的重要的操作。线程Dump分析的重要信息。修饰上方的方法调用。

locked <地址> 目标:使用synchronized申请对象锁成功,监视器的拥有者。

waiting to lock <地址> 目标:使用synchronized申请对象锁未成功,在迚入区等待。

waiting on <地址> 目标:使用synchronized申请对象锁成功后,释放锁幵在等待区等待。

parking to wait for <地址> 目标

locked

at oracle.jdbc.driver.PhysicalConnection.prepareStatement

– locked <0x00002aab63bf7f58> (a oracle.jdbc.driver.T4CConnection)

at oracle.jdbc.driver.PhysicalConnection.prepareStatement

– locked <0x00002aab63bf7f58> (a oracle.jdbc.driver.T4CConnection)

at com.jiuqi.dna.core.internal.db.datasource.PooledConnection.prepareStatement

通过synchronized关键字,成功获取到了对象的锁,成为监视器的拥有者,在临界区内操作。对象锁是可以线程重入的。

waiting to lock

at com.jiuqi.dna.core.impl.CacheHolder.isVisibleIn(CacheHolder.java:165)

– waiting to lock <0x0000000097ba9aa8> (a CacheHolder)

at com.jiuqi.dna.core.impl.CacheGroup$Index.findHolder

at com.jiuqi.dna.core.impl.ContextImpl.find

at com.jiuqi.dna.bap.basedata.common.util.BaseDataCenter.findInfo

通过synchronized关键字,没有获取到了对象的锁,线程在监视器的进入区等待。在调用栈顶出现,线程状态为Blocked。

waiting on

at java.lang.Object.wait(Native Method)

– waiting on <0x00000000da2defb0> (a WorkingThread)

at com.jiuqi.dna.core.impl.WorkingManager.getWorkToDo

– locked <0x00000000da2defb0> (a WorkingThread)

at com.jiuqi.dna.core.impl.WorkingThread.run

通过synchronized关键字,成功获取到了对象的锁后,调用了wait方法,进入对象的等待区等待。在调用栈顶出现,线程状态为WAITING或TIMED_WATING。

parking to wait for

park是基本的线程阻塞原语,不通过监视器在对象上阻塞。随concurrent包会出现的新的机制,不synchronized体系不同。

线程动作

线程状态产生的原因

runnable:状态一般为RUNNABLE。

in Object.wait():等待区等待,状态为WAITING或TIMED_WAITING。

waiting for monitor entry:进入区等待,状态为BLOCKED。

waiting on condition:等待区等待、被park。

sleeping:休眠的线程,调用了Thread.sleep()。

Wait on condition 该状态出现在线程等待某个条件的发生。具体是什么原因,可以结合 stacktrace来分析。 最常见的情况就是线程处于sleep状态,等待被唤醒。 常见的情况还有等待网络IO:在java引入nio之前,对于每个网络连接,都有一个对应的线程来处理网络的读写操作,即使没有可读写的数据,线程仍然阻塞在读写操作上,这样有可能造成资源浪费,而且给操作系统的线程调度也带来压力。在 NewIO里采用了新的机制,编写的服务器程序的性能和可扩展性都得到提高。 正等待网络读写,这可能是一个网络瓶颈的征兆。因为网络阻塞导致线程无法执行。一种情况是网络非常忙,几 乎消耗了所有的带宽,仍然有大量数据等待网络读 写;另一种情况也可能是网络空闲,但由于路由等问题,导致包无法正常的到达。所以要结合系统的一些性能观察工具来综合分析,比如 netstat统计单位时间的发送包的数目,如果很明显超过了所在网络带宽的限制 ; 观察 cpu的利用率,如果系统态的 CPU时间,相对于用户态的 CPU时间比例较高;如果程序运行在 Solaris 10平台上,可以用 dtrace工具看系统调用的情况,如果观察到 read/write的系统调用的次数或者运行时间遥遥领先;这些都指向由于网络带宽所限导致的网络瓶颈。(来自http://www.blogjava.net/jzone/articles/303979.html)

线程Dump的分析

原则

结合代码阅读的推理。需要线程Dump和源码的相互推导和印证。

造成Bug的根源往往丌会在调用栈上直接体现,一定格外注意线程当前调用之前的所有调用。

入手点

进入区等待

“d&a-3588” daemon waiting for monitor entry [0x000000006e5d5000]

java.lang.Thread.State: BLOCKED (on object monitor)

at com.jiuqi.dna.bap.authority.service.UserService$LoginHandler.handle()

– waiting to lock <0x0000000602f38e90> (a java.lang.Object)

at com.jiuqi.dna.bap.authority.service.UserService$LoginHandler.handle()

线程状态BLOCKED,线程动作wait on monitor entry,调用修饰waiting to lock总是一起出现。表示在代码级别已经存在冲突的调用。必然有问题的代码,需要尽可能减少其发生。

同步块阻塞

一个线程锁住某对象,大量其他线程在该对象上等待。

“blocker” runnable

java.lang.Thread.State: RUNNABLE

at com.jiuqi.hcl.javadump.Blocker$1.run(Blocker.java:23)

– locked <0x00000000eb8eff68> (a java.lang.Object)

“blockee-11” waiting for monitor entry

java.lang.Thread.State: BLOCKED (on object monitor)

at com.jiuqi.hcl.javadump.Blocker$2.run(Blocker.java:41)

– waiting to lock <0x00000000eb8eff68> (a java.lang.Object)

“blockee-86” waiting for monitor entry

java.lang.Thread.State: BLOCKED (on object monitor)

at com.jiuqi.hcl.javadump.Blocker$2.run(Blocker.java:41)

– waiting to lock <0x00000000eb8eff68> (a java.lang.Object)

持续运行的IO IO操作是可以以RUNNABLE状态达成阻塞。例如:数据库死锁、网络读写。 格外注意对IO线程的真实状态的分析。 一般来说,被捕捉到RUNNABLE的IO调用,都是有问题的。

以下堆栈显示: 线程状态为RUNNABLE。 调用栈在SocketInputStream或SocketImpl上,socketRead0等方法。 调用栈包含了jdbc相关的包。很可能发生了数据库死锁

“d&a-614” daemon prio=6 tid=0x0000000022f1f000 nid=0x37c8 runnable

[0x0000000027cbd000]

java.lang.Thread.State: RUNNABLE

at java.net.SocketInputStream.socketRead0(Native Method)

at java.net.SocketInputStream.read(Unknown Source)

at oracle.net.ns.Packet.receive(Packet.java:240)

at oracle.net.ns.DataPacket.receive(DataPacket.java:92)

at oracle.net.ns.NetInputStream.getNextPacket(NetInputStream.java:172)

at oracle.net.ns.NetInputStream.read(NetInputStream.java:117)

at oracle.jdbc.driver.T4CMAREngine.unmarshalUB1(T4CMAREngine.java:1034)

at oracle.jdbc.driver.T4C8Oall.receive(T4C8Oall.java:588)

分线程调度的休眠

正常的线程池等待

“d&a-131” in Object.wait()

java.lang.Thread.State: TIMED_WAITING (on object monitor)

at java.lang.Object.wait(Native Method)

at com.jiuqi.dna.core.impl.WorkingManager.getWorkToDo(WorkingManager.java:322)

– locked <0x0000000313f656f8> (a com.jiuqi.dna.core.impl.WorkingThread)

at com.jiuqi.dna.core.impl.WorkingThread.run(WorkingThread.java:40)

可疑的线程等待

“d&a-121” in Object.wait()

java.lang.Thread.State: WAITING (on object monitor)

at java.lang.Object.wait(Native Method)

at java.lang.Object.wait(Object.java:485)

at com.jiuqi.dna.core.impl.AcquirableAccessor.exclusive()

– locked <0x00000003011678d8> (a com.jiuqi.dna.core.impl.CacheGroup)

at com.jiuqi.dna.core.impl.Transaction.lock()

入手点总结

wait on monitor entry: 被阻塞的,肯定有问题

runnable : 注意IO线程

in Object.wait(): 注意非线程池等待

使用

想要学习一个命令,先来看看帮助,使用jstack -help查看帮助:

hollis@hos:~$ jstack -help

Usage:

jstack [-l] <pid>

(to connect to running process)

jstack -F [-m] [-l] <pid>

(to connect to a hung process)

jstack [-m] [-l] <executable> <core>

(to connect to a core file)

jstack [-m] [-l] [server_id@]<remote server IP or hostname>

(to connect to a remote debug server)

 

Options:

-F  to force a thread dump. Use when jstack <pid> does not respond (process is hung)

-m  to print both java and native frames (mixed mode)

-l  long listing. Prints additional information about locks

-h or -help to print this help message

-F当’jstack [-l] pid’没有相应的时候强制打印栈信息 -l长列表. 打印关于锁的附加信息,例如属于java.util.concurrent的ownable synchronizers列表. -m打印java和native c/c++框架的所有栈信息. -h | -help打印帮助信息 pid 需要被打印配置信息的java进程id,可以用jps查询.

首先,我们分析这么一段程序的线程情况:

/**

* @author hollis

*/

public class JStackDemo1 {

public static void main(String[] args) {

while (true) {

//Do Nothing

}

}

}

先是有jps查看进程号:

hollis@hos:~$ jps

29788 JStackDemo1

29834 Jps

22385 org.e clipse.equinox.launcher_1.3.0.v20130327-1440.jar

然后使用jstack 查看堆栈信息:

hollis@hos:~$ jstack 29788

2015-04-17 23:47:31

…此处省略若干内容…

“main” prio=10 tid=0x00007f197800a000 nid=0x7462 runnable [0x00007f197f7e1000]

java.lang.Thread.State: RUNNABLE

at javaCommand.JStackDemo1.main(JStackDemo1.java:7)

我们可以从这段堆栈信息中看出什么来呢?我们可以看到,当前一共有一条用户级别线程,线程处于runnable状态,执行到JStackDemo1.java的第七行。 看下面代码:

/**

* @author hollis

*/

public class JStackDemo1 {

public static void main(String[] args) {

Thread thread = new Thread(new Thread1());

thread.start();

}

}

class Thread1 implements Runnable{

@Override

public void run() {

while(true){

System.out.println(1);

}

}

}

线程堆栈信息如下:

“Reference Handler” daemon prio=10 tid=0x00007fbbcc06e000 nid=0x286c in Object.wait() [0x00007fbbc8dfc000]

java.lang.Thread.State: WAITING (on object monitor)

at java.lang.Object.wait(Native Method)

– waiting on <0x0000000783e066e0> (a java.lang.ref.Reference$Lock)

at java.lang.Object.wait(Object.java:503)

at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:133)

– locked <0x0000000783e066e0> (a java.lang.ref.Reference$Lock)

我们能看到:

线程的状态: WAITING 线程的调用栈 线程的当前锁住的资源: <0x0000000783e066e0> 线程当前等待的资源:<0x0000000783e066e0>

为什么同时锁住的等待同一个资源:

线程的执行中,先获得了这个对象的 Monitor(对应于 locked <0x0000000783e066e0>)。当执行到 obj.wait(), 线程即放弃了 Monitor的所有权,进入 “wait set”队列(对应于 waiting on <0x0000000783e066e0> )。

死锁分析

学会了怎么使用jstack命令之后,我们就可以看看,如何使用jstack分析死锁了,这也是我们一定要掌握的内容。 啥叫死锁? 所谓死锁: 是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。 说白了,我现在想吃鸡蛋灌饼,桌子上放着鸡蛋和饼,但是我和我的朋友同时分别拿起了鸡蛋和病,我手里拿着鸡蛋,但是我需要他手里的饼。他手里拿着饼,但是他想要我手里的鸡蛋。就这样,如果不能同时拿到鸡蛋和饼,那我们就不能继续做后面的工作(做鸡蛋灌饼)。所以,这就造成了死锁。 看一段死锁的程序:

package javaCommand;

/**

* @author hollis

*/

public class JStackDemo {

public static void main(String[] args) {

Thread t1 = new Thread(new DeadLockclass(true));//建立一个线程

Thread t2 = new Thread(new DeadLockclass(false));//建立另一个线程

t1.start();//启动一个线程

t2.start();//启动另一个线程

}

}

class DeadLockclass implements Runnable {

public boolean falg;// 控制线程

DeadLockclass(boolean falg) {

this.falg = falg;

}

public void run() {

/**

* 如果falg的值为true则调用t1线程

*/

if (falg) {

while (true) {

synchronized (Suo.o1) {

System.out.println(“o1 ” + Thread.currentThread().getName());

synchronized (Suo.o2) {

System.out.println(“o2 ” + Thread.currentThread().getName());

}

}

}

}

/**

* 如果falg的值为false则调用t2线程

*/

else {

while (true) {

synchronized (Suo.o2) {

System.out.println(“o2 ” + Thread.currentThread().getName());

synchronized (Suo.o1) {

System.out.println(“o1 ” + Thread.currentThread().getName());

}

}

}

}

}

}

 

class Suo {

static Object o1 = new Object();

static Object o2 = new Object();

}

当我启动该程序时,我们看一下控制台:

我们发现,程序只输出了两行内容,然后程序就不再打印其它的东西了,但是程序并没有停止。这样就产生了死锁。 当线程1使用synchronized锁住了o1的同时,线程2也是用synchronized锁住了o2。当两个线程都执行完第一个打印任务的时候,线程1想锁住o2,线程2想锁住o1。但是,线程1当前锁着o1,线程2锁着o2。所以两个想成都无法继续执行下去,就造成了死锁。

然后,我们使用jstack来看一下线程堆栈信息:

Found one Java-level deadlock:

=============================

“Thread-1”:

waiting to lock monitor 0x00007f0134003ae8 (object 0x00000007d6aa2c98, a java.lang.Object),

which is held by “Thread-0”

“Thread-0”:

waiting to lock monitor 0x00007f0134006168 (object 0x00000007d6aa2ca8, a java.lang.Object),

which is held by “Thread-1”

 

Java stack information for the threads listed above:

===================================================

“Thread-1”:

at javaCommand.DeadLockclass.run(JStackDemo.java:40)

– waiting to lock <0x00000007d6aa2c98> (a java.lang.Object)

– locked <0x00000007d6aa2ca8> (a java.lang.Object)

at java.lang.Thread.run(Thread.java:745)

“Thread-0”:

at javaCommand.DeadLockclass.run(JStackDemo.java:27)

– waiting to lock <0x00000007d6aa2ca8> (a java.lang.Object)

– locked <0x00000007d6aa2c98> (a java.lang.Object)

at java.lang.Thread.run(Thread.java:745)

 

Found 1 deadlock.

哈哈,堆栈写的很明显,它告诉我们 Found one Java-level deadlock,然后指出造成死锁的两个线程的内容。然后,又通过 Java stack information for the threads listed above来显示更详细的死锁的信息。 他说

Thread-1在想要执行第40行的时候,当前锁住了资源<0x00000007d6aa2ca8>,但是他在等待资源<0x00000007d6aa2c98> Thread-0在想要执行第27行的时候,当前锁住了资源<0x00000007d6aa2c98>,但是他在等待资源<0x00000007d6aa2ca8> 由于这两个线程都持有资源,并且都需要对方的资源,所以造成了死锁。 原因我们找到了,就可以具体问题具体分析,解决这个死锁了。

其他

虚拟机执行Full GC时,会阻塞所有的用户线程。因此,即时获取到同步锁的线程也有可能被阻塞。 在查看线程Dump时,首先查看内存使用情况。

本系列:

Java命令学习系列(1):Jps

Java命令学习系列(2):Jstack

Java命令学习系列(1):Jps

英文:Antonio

译文:Hollis(@Hollis_Chuang)

链接:http://www.hollischuang.com/archives/105

jps位于jdk的bin目录下,其作用是显示当前系统的java进程情况,及其id号。 jps相当于Solaris进程工具ps。不象”pgrep java”或”ps -ef grep java”,jps并不使用应用程序名来查找JVM实例。因此,它查找所有的Java应用程序,包括即使没有使用java执行体的那种(例如,定制的启动 器)。另外,jps仅查找当前用户的Java进程,而不是当前系统中的所有进程。

位置

我们知道,很多Java命令都在jdk的JAVA_HOME/bin/目录下面,jps也不例外,他就在bin目录下,所以,他是java自带的一个命令。

功能

jps(Java Virtual Machine Process Status Tool)是JDK 1.5提供的一个显示当前所有java进程pid的命令,简单实用,非常适合在linux/unix平台上简单察看当前java进程的一些简单情况。

原理

jdk中的jps命令可以显示当前运行的java进程以及相关参数,它的实现机制如下:
java程序在启动以后,会在java.io.tmpdir指定的目录下,就是临时文件夹里,生成一个类似于hsperfdata_User的文件夹,这个文件夹里(在Linux中为/tmp/hsperfdata_{userName}/),有几个文件,名字就是java进程的pid,因此列出当前运行的java进程,只是把这个目录里的文件名列一下而已。 至于系统的参数什么,就可以解析这几个文件获得。

hollis@hos:/tmp/hsperfdata_hollis$ pwd

/tmp/hsperfdata_hollis

hollis@hos:/tmp/hsperfdata_hollis$ ll

total 48

drwxr-xr-x 2 hollis hollis  4096  4月 16 10:54 ./

drwxrwxrwt 7 root   root   12288  4月 16 10:56 ../

-rw——- 1 hollis hollis 32768  4月 16 10:57 2679

hollis@hos:/tmp/hsperfdata_hollis$

上面的内容就是我机器中/tmp/hsperfdata_hollis目录下的内容,其中2679就是我机器上当前运行中的java的进程的pid,我们执行jps验证一下:

hollis@hos:/tmp/hsperfdata_hollis$ jps

2679 org.ec lipse.equinox.launcher_1.3.0.v20130327-1440.jar

4445 Jps

执行了jps命令之后,我们发现有两个java进程,一个是pid为2679的Eclipse运行的进程,另外一个是pid为4445的jps使用的进程(他也是java命令,也要开一个进程)

使用

想要学习一个命令,先来看看帮助,使用jps -help查看帮助:

hollis@hos:/tmp/hsperfdata_hollis$ jps -help

usage: jps [-help]

jps [-q] [-mlvV] [<hostid>]

 

Definitions:

<hostid>:      <hostname>[:<port>]

接下来,为了详细介绍这些参数,我们编写几个类,在main方法里写一个while(true)的循环,查看java进程情况。代码如下:

package com.JavaCommand;

/**

* @author hollis

*/

public class JpsDemo {

public static void main(String[] args) {

while(true){

System.out.println(1);

}

}

}

-q 只显示pid,不显示class名称,jar文件名和传递给main 方法的参数

hollis@hos:/tmp/hsperfdata_hollis$ jps -q

2679

11421

-m 输出传递给main 方法的参数,在嵌入式jvm上可能是null, 在这里,在启动main方法的时候,我给String[] args传递两个参数。hollis,chuang,执行jsp -m:

hollis@hos:/tmp/hsperfdata_hollis$ jps -m

12062 JpsDemo hollis,chuang

-l 输出应用程序main class的完整package名 或者 应用程序的jar文件完整路径名

hollis@hos:/tmp/hsperfdata_hollis$ jps -l

12356 sun.tools.jps.Jps

2679 /home/hollis/tools/eclipse//plugins/org.eclipse.equinox.launcher_1.3.0.v20130327-1440.jar

12329 com.JavaCommand.JpsDemo

-v 输出传递给JVM的参数 在这里,在启动main方法的时候,我给jvm传递一个参数:-Dfile.encoding=UTF-8,执行jps -v:

hollis@hos:/tmp/hsperfdata_hollis$ jps -v

2679 org.eclipse.equinox.launcher_1.3.0.v20130327-1440.jar -Djava.library.path=/usr/lib/jni:/usr/lib/x86_64-linux-gnu/jni -Dosgi.requiredJavaVersion=1.6 -XX:MaxPermSize=256m -Xms40m -Xmx512m

13157 Jps -Denv.class.path=/home/hollis/tools/java/jdk1.7.0_71/lib:/home/hollis/tools/java/jdk1.7.0_71/jre/lib: -Dapplication.home=/home/hollis/tools/java/jdk1.7.0_71 -Xms8m

13083 JpsDemo -Dfile.encoding=UTF-8

PS:jps命令有个地方很不好,似乎只能显示当前用户的java进程,要显示其他用户的还是只能用unix/linux的ps命令。

jps是我最常用的java命令。使用jps可以查看当前有哪些Java进程处于运行状态。如果我运行了一个web应用(使用tomcat、jboss、jetty等启动)的时候,我就可以使用jps查看启动情况。有的时候我想知道这个应用的日志会输出到哪里,或者启动的时候使用了哪些javaagent,那么我可以使用jps -v 查看进程的jvm参数情况。

JPS失效处理

现象: 用ps -ef|grep java能看到启动的java进程,但是用jps查看却不存在该进程的id。待会儿解释过之后就能知道在该情况下,jconsole、jvisualvm可能无法监控该进程,其他java自带工具也可能无法使用

分析: jps、jconsole、jvisualvm等工具的数据来源就是这个文件(/tmp/hsperfdata_userName/pid)。所以当该文件不存在或是无法读取时就会出现jps无法查看该进程号,jconsole无法监控等问题

原因:

(1)、磁盘读写、目录权限问题 若该用户没有权限写/tmp目录或是磁盘已满,则无法创建/tmp/hsperfdata_userName/pid文件。或该文件已经生成,但用户没有读权限

(2)、临时文件丢失,被删除或是定期清理 对于linux机器,一般都会存在定时任务对临时文件夹进行清理,导致/tmp目录被清空。这也是我第一次碰到该现象的原因。常用的可能定时删除临时目录的工具为crontab、redhat的tmpwatch、ubuntu的tmpreaper等等

这个导致的现象可能会是这样,用jconsole监控进程,发现在某一时段后进程仍然存在,但是却没有监控信息了。

(3)、java进程信息文件存储地址被设置,不在/tmp目录下 上面我们在介绍时说默认会在/tmp/hsperfdata_userName目录保存进程信息,但由于以上1、2所述原因,可能导致该文件无法生成或是丢失,所以java启动时提供了参数(-Djava.io.tmpdir),可以对这个文件的位置进行设置,而jps、jconsole都只会从/tmp目录读取,而无法从设置后的目录读物信息,这是我第二次碰到该现象的原因

附:

1.如何给main传递参数 在eclipse中,鼠标右键->Run As->Run COnfiguations->Arguments->在Program arguments中写下要传的参数值

2.如何给JVM传递参数 在eclipse中,鼠标右键->Run As->Run COnfiguations->Arguments->在VM arguments中写下要传的参数值(一般以-D开头)