Java线程的分类
java线程可以分为用户线程和守护线程。
用户线程
这就是普通的java线程,可以通过重新Thread的run方法来实现,这是Java里面非常常用的,这里也就不多说了。
守护线程
守护线程的名字是根据守护进程的概念来的。在Linux系统中,有一类进程会在系统中一直运行到系统关闭,这有点类似于Windows系统的服务,由于这些进程大部分都是提供一些服务来支持其他应用,就好像是在”守护“其他的进程,所以被称之为守护进程。常见的守护进程有邮件通知系统,数据库相关服务等等。
在Java中,守护线程的概念跟守护进程类似,守护线程会一直运行直到Java虚拟机退出,不过与守护线进程有一些不同:
- 当Java虚拟机发现所有的线程都是守护线程时会释放资源退出。
- 守护进程无法使用标准输入输出,但是Java守护线程无此限制,但是还是不建议在Java守护线程中对文件进行操作,因为线程本身不知道虚拟机什么时候退出。
使用守护线程的例子不是很多,所以网上有些人说”守护线程和普通线程区别不大“,但是还是有一些场景是普通线程没办法满足或者很难满足的,比如GC线程,没有用户线程在运行的话GC线程的存在是没有意义的,如果用普通线程来实现,必须要不停的检测虚拟机的运行状态。除了GC线程,另外一个比较常用的场景是Web应用中的服务线程,比如说消息线程,为不同线程之间提供消息传递功能。这种Web容器服务线程一般都采用守护线程来实现,否则在关闭Web容器的时候,服务线程拒绝退出,这样一来Web应用停止了,但是Java虚拟机却不退出。
守护线程的使用方法也很简单
Thread thread=new Thread();
thread.setDaemon(true); //设置守护线程,必须在线程启动之前声明,否则线程会在检查状态的时候抛出异常。
thread.start(); //开始执行分线程
我比较好奇运行时退出钩子中添加守护线程是否会执行,所以做了一个小实验。
Thread t = new Thread() {
public void run() {
System.out.println("exit");
}
};
t.setDaemon(true);
Runtime.getRuntime().addShutdownHook(t);
结果还是输出了”exit“字符,哈哈。
守护线程还是比较好玩的,写服务器的同学可能会用到,但是一般应用场景应该不多,大概也是一门“屠龙之技”。
线程同步
线程中最头痛的问题大概就是数据同步问题了,因为我才写了几个demo程序就已经感受到了这其中深深的痛。当然,如果没有性能要求的话,synchronized可以拯救你于水火之中。
问题由来
由于Java程序的原子操作是字节码级别的,所以像修改对象属性等操作是一个指令无法完成的。
this.a+= 10;
Code:
0: getstatic #2; //Field a:I
3: bipush 10
5: iadd //可能某个线程在此处中断,另外一个线程又来执行了add操作。此线程继续导致出错。
6: putstatic #2; //Field a:I
synchronized机制
synchronized语法是
synchronized(object) {
// do something
}
当程序修改object的属性的时候,其他线程将被阻塞,等待属性修改完成。通过synchronized特性,我们可以写成
synchronized(this) {
thia.a += 10;
}
// 编译之后为
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2; //Field a:I
7: bipush 10
9: iadd
10: putstatic #2; //Field a:I
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
Exception table:
from to target type
4 15 18 any
18 21 18 any
// 等价于下面的代码
Object tmp = this;
monitorenter(this); // 对应monitorenter字节码的解释,对this对象加上监视器
try {
this.a += 10;
monitorexit(tmp); // 释放监视器
} catch(Exception e) {
while (true) {
try {
monitorexit(tmp); // 对应monitorexit字节码的解释,释放this对象的监视器
throw e;
} catch(e) {
}
}
}
Java对象监视器的功能是保证同一时间只有一个线程去修改对象的数据的机制,详情参考[3]。注意,synchronized是一个重量级的操作,当对象被锁定时,当前线程会进入阻塞状态,线程的阻塞和唤醒依赖于操作系统,需要从用户态切换到内核态,所以在对性能要求比较高而且并发量大的场景下需要尽量避免使用它。
重用锁
重用锁Reentrantlock是另一种同步解决方案,synchronized关键字的实现是属于悲观锁,它会锁定该对象只允许一个线程修改,而重用锁则是一种乐观锁,它使用CAS操作来保证同步。详情参看[4][5]。CAS是CompareAndSwap的缩写。
CAS算法:
- 先获取内存中的值m保存到临时变量t
- 执行操作将结果更新临时变量t
- 检查内存值m是否改变,如果t=m,更新m为t,否则重新回到第1步操作。
其中3的操作是依赖CPU底层实现的,CPU提供了一种指令保证多核情况下对内存值修改的原子性。参考[4]。CAS有一个ABA的问题,[5]中给出了详细描述和解决方案。CAS在java.util.concurrent.atomic包封装的类库中有大量应用。
在线程竞争明显的情况下,重用锁的性能要好于synchronized特性,详情参看[6]。
下面是synchronized机制和ReentrantLock对比的例子
// class ReentrantLockTest
public void run() {
try {
lock.lock();
this.a += 10;
this.a -= 10;
System.out.println(this.getName()+ ":" + this.a);
} finally {
lock.unlock();
}
}
// class SynchronizedTest
public void run() {
// 使用class对象作为一个全局锁
synchronized(SynchronizedTest.class) {
this.a += 10;
this.a -= 10;
System.out.println(this.getName()+ ":" + this.a);
}
}
volatile关键字
volatile提供了语言层面的可见性保障。我们知道,多核CPU每个CPU都有自己的缓存,而缓存与主存之间的同步并不受应用程序控制,一旦多个线程运行在不同的CPU上操作同一个内存地址,那么就很难保证一致性了,除此之外,Java虚拟机还会针对指令集进行指令重排的优化,这些操作对于单线程而言是安全的,但是对于多线程就不能保证了。为了解决这个问题,Java提供了volatile类型来保证变量的可见性。
volatile的主要功能:
1. 避免指令重排
2. 保证变量值立即写入主存并使其他CPU上对应的缓存失效
当然需要注意的是volatile仅能保证单个变量的一致性,而且相对使用锁而言比较隐晦,会使代码更脆弱,也不利于理解,所以千万不要作为锁的替代品滥用。
参考资料
- Core Java, Volume I-Fundamentals (9th Edition)
- 深入JAVA虚拟机第二版
- Java监视器 http://ifeve.com/monitors-java-synchronization-mechanism/
- JAVA CAS原理深度分析,从JVM底层实现讨论了CAS原理 http://zl198751.iteye.com/blog/1848575
- Java CAS 和ABA问题 http://www.cnblogs.com/549294286/p/3766717.html
- synchronized和ReentrantLock机制对比 http://heaven-arch.iteye.com/blog/1738212
- Java并发编程实战