多线程技术可以更好地利用系统资源,减少用户的响应时间,提高系统的性能和效率,但同时也增加了系统的复杂性和运维难度,特别是在高并发、大压力、高可靠性的项目中。线程资源的同步、抢占、互斥都需要谨慎考虑,以避免产生性能损耗和线程死锁。
建议118:不推荐覆写start方法
建议119:启动线程前stop方法是不可靠的
建议120:不使用stop方法停止线程
1、stop方法是过时的:从Java编码规则来说,已经过时的方法不建议采用,弃了。
2、stop方法会导致代码逻辑不完整:stop方法是一种“恶意”的中断,一旦执行stop方法,即终止当前正在运行的线程,不管线程逻辑是否完整,这是非常危险的。
3、stop方法破坏原子逻辑
多线程为了解决共享资源抢占的问题,使用了锁概念,避免资源不同步,但是正因为如此,stop带了更大了麻烦,它会丢弃所有的锁,导致原子逻辑受损。
如何关闭线程呢?
if (!thread.isInterrupted()) {
thread.interrupt();
}
如果使用的是线程池,可以通过shutdown方法逐步关闭池中的线程。
建议121:线程优先级只使用三个等级
线程的优先级(Priority)决定了线程获取CPU运行的机会,优先级越高获取的运行机会越大,优先级月底获取的机会越小。
package OSChina.Multithread;
public class TestThread implements Runnable {
public void start(int _priority) {
Thread t = new Thread(this);
// 设置优先级别
t.setPriority(_priority);
t.start();
}
@Override
public void run() {
// 消耗CPU的计算
for (int i = 0; i < 100000; i++) {
Math.hypot(924526789, Math.cos(i));
}
// 输出线程优先级
System.out.println("Priority:" + Thread.currentThread().getPriority());
}
public static void main(String[] args) {
//启动20个不同优先级的线程
for (int i = 0; i < 20; i++) {
new TestThread().start(i % 10 + 1);
}
}
}
创建了20个线程,优先级设置的不同,执行起来是这样的,5和6反了。
1、并不是严格按照线程优先级来执行的
因为优先级只是表示线程获取CPU运行的机会,并不是代码强制的排序号。
2、优先级差别越大,运行机会差别越明显
Java的缔造者们也觉察到了线程优先问题,于是Thread类中设置了三个优先级,此意就是告诉开发者,建议使用优先级常量,而不是1到10的随机数字。常量代码如下:
public class Thread implements Runnable {
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
}
开发时只使用此三类优先级就可以了。
建议122:使用线程异常处理器提升系统可靠性
编写一个socket应用,监听指定端口,实现数据包的接收和发送逻辑,这在早起系统间进行数据交互是经常使用的,这类接口通常考虑两个问题:一个是避免线程阻塞,保证接收的数据尽快处理;二是接口的稳定性和可靠性,数据包很复杂,接口服务的系统也很多,一旦守候线程出现异常就会导致socket停止,这是非常危险的,那我们有什么办法避免呢?
Java1.5版本以后在thread类中增加了setUncaughtExceptionHandler方法,实现了线程异常的捕捉和处理。
代码实例:
package OSChina.Multithread;
public class TcpServer implements Runnable {
public TcpServer() {
Thread t = new Thread(this);
t.setUncaughtExceptionHandler(new TcpServerExceptionHandler());
t.start();
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
try{
Thread.sleep(1000);
System.out.println("系统正常运行:"+i);
}catch (InterruptedException e){
e.printStackTrace();
}
}
throw new RuntimeException();
}
private static class TcpServerExceptionHandler implements Thread.UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("线程"+t.getName()+" 出现异常,自行重启,请分析原因。");
e.printStackTrace();
new TcpServer();
}
}
public static void main(String[] args) {
TcpServer tcpServer = new TcpServer();
}
}
这段代码的逻辑比较简单,在TcpServer类创建时启动一个线程,提供TCP服务,例如接收和发送文件,具体逻辑在run方法中实现。同时,设置了该线程出现运行期异常时,由TcpServerExceptionHandler异常处理器来处理异常。那么TcpServerExceptionHandler做什么呢?两件事:
1、记录异常信息,以便查找问题
2、重新启动一个新线程,提供不间断的服务
有了这两点,TcpServer就可以稳定的运行了,即使出现异常也能自动重启,客户代码比较简单,只需要new TcpServer()即可,运行结果如下:
从运行结果可以看出,当Thread-0出现异常时,系统自动重启了Thread-1线程,继续提供服务,大大提高了系统的性能。
这段代码只是一个示例程序,若要在实际环境中应用,则需要注意以下三个方面:
1、共享资源锁定:如果线程产生异常的原因是资源被锁定,自动重启应用会增加系统的负担,无法提供不间断服务。例如一个即时通信服务出现信息不能写入的情况,即时再怎么重启服务,也无法解决问题。在此情况下最好的办法是停止所有的线程,释放资源。
2、脏数据引起系统逻辑混乱:异常的产生中断了正在执行的业务逻辑,特别是如果正在处理一个原子操作,但如果此时抛出了运行期异常就有可能会破坏正常的业务逻辑,例如出现用户认证通过了,但签到不成功的情况,在这种情况下重启应用程序,虽然可以提供服务,但对部分用户产生了逻辑异常。
3、内存溢出:线程异常了,但由该线程创建的对象并不会马上回收,如果再重新启动新线程,再创建一批对象,特别是加入了场景接管,就非常危险了,例如即时通信服务,重新启动一个新线程必须保证原在线用户的透明性,即用户不会察觉服务重启,在这种情况下,就需要在线程初始化时加载大量对象以保证用户的状态信息,但是如果线程反复重启,很可能会引起OutOfMemory内存泄漏问题。
建议123:volatile不能保证数据同步
volatile关键字比较少用,原因无外乎两点,一是在Java1.5之前该关键字在不同的操作系统上有不同的表现,所带来的问题就是移植性较差;而且比较难设计,误用较多,这也导致它的“名誉”受损。
我们知道,每个线程都运行在栈内存中,每个线程都有自己的工作内存(Working Memory,比如寄存器Register、高速缓存存储器Cache等),线程的计算一般是通过工作内存进行交互的,其示意图如下图所示:
从示意图中我们可以看到,线程在初始化时从主内存中加载需要的变量值到工作内存中,然后在线程运行时,如果是读取,直接从工作内存中读取,如果是写入,则先写入工作内存中,之后刷新到主内存中,这是JVM的一个简单的内存模型,但是这样的结构在多线程的情况下有可能会出现问题,比如:A线程修改变量的值,也刷新到了主内存,但B、C线程在此时间内读取的还是本线程的工作内存,也就是说它们读取的不是最新的值,此时就会出现不同线程持有的公共资源不同步的情况。
对于此问题有很多解决的办法,比如使用synchronized同步代码块,或者使用Lock锁来解决该问题,不过,Java可以使用volatile更简单的解决此类问题,比如在一个变量前加上volatile关键字,可以确保每个线程对本地变量的访问和修改都是直接与内存交互的,而不是与本线程的工作内存交互的,保证每个线程都能获取到最新的变量值,其示意图如下:
明白了volatile变量的原理,那我们来思考一下:volatile变量是否能够保证数据的同步性呢?两个线程同时修改volatile变量是否会产生脏数据呢?代码如下:
package OSChina.Multithread;
public class UnsafeThread implements Runnable {
//共享资源
private volatile int count = 0;
@Override
public void run() {
// 增加CPU的繁忙程度,不必关心其逻辑含义
for (int i = 0; i < 1000; i++) {
Math.hypot(Math.pow(92456789,i),Math.cos(i));
}
count++;
}
public int getCount(){
return count;
}
}
上面的代码定义了一个多线程,run方法的主要逻辑是共享资源count的自加运算,而且我们还为count变量加上了volatile关键字,确保是从内存中读取和写入的,如果有多个线程运行,也就是多个线程执行count变量的自加操作,count变量会产生脏数据吗?模拟多线程代码如下:
public static void main(String[] args) {
// 理想值,并作为最大循环次数
int value = 1000;
// 循环次数,防止造成无限循环或者死循环
int loops = 0;
// 主线程组,用于估计活动线程数
ThreadGroup tg = Thread.currentThread().getThreadGroup();
while (loops++
此段代码的逻辑如下:
1、启动1000个线程,修改共享资源count的值
2、暂停15毫秒,观察活动线程数是否为1(即只剩下主线程再运行),若不为1,则再等待15毫秒。
3、共享资源是否是不安全的,即实际值与理想值是否相同,若不相同,则发现目标,此时count的值为脏数据。
4、如果没有找到,继续循环,直到达到最大循环为止。
运行结果:
执行完了,没出现不安全的情况,证明volatile性能还是可以的。
书中自有黄金屋,书中自有颜如玉!
书中的运行结果:
循环到:40遍,出现不安全的情况
此时,count=999
这只是一种可能的结果,每次执行都有可能产生不同的结果。这也说明我们的count变量没有实现数据同步,在多个线程修改的情况下,count的实际值与理论值产生了偏差,直接说明了volatile关键字并不能保证线程的安全。
代码执行完毕,原本期望的结果为1000,但运行后的结果为999,这表示出现了线程不安全的情况。这也就说明了:volatile关键字只能保证当前线程需要该变量的值时能够获得最新的值,并不能保证线程修改的安全性。
顺便说一下,上面的代码中,UnsafeThread类消耗CPU计算时必须的,其目的是加重线程的负荷,以便出现单个线程抢占整个CPU资源的情景,否者很难模拟出volatile线程不安全的情况,大家可以实际测试一下。
UnsafeThread消耗CPU很严重,慎用啊。
建议124:异步运算考虑使用Callable接口
多线程应用有两种实现方式,一种是实现runnable接口,另一种是继承Thread类,这两种方法都有缺点:run方法没有返回值,不能抛出异常(这两个缺点归根到底就是runnable接口的缺陷,Thread类也是实现了runnable接口),如果需要知道一个线程的运行结果就需要用户自行设计,线程类本身并不能提供返回值和异常。但是Java1.5引入了一个新的接口callable,它类似于runnable接口,实现它也可以实现多线程任务。
好不好测一下:
package OSChina.Multithread;
import java.util.concurrent.*;
public class TaxCalculator implements Callable {
//本金
private int seedMoney;
//接收主线程提供的参数
public TaxCalculator(int _seedMoney){
seedMoney = _seedMoney;
}
@Override
public Integer call() throws Exception {
// 复杂计算,运行一次需要2秒
TimeUnit.MILLISECONDS.sleep(2000);
return seedMoney/10;
}
}
模拟一个复杂运算:税款计算器,该运算可能要花费10秒的时间,用户此时一直等啊等,很烦躁,需要给点提示,让用户知道程序在运行,没卡死。
public static void main(String[] args) throws InterruptedException, ExecutionException {
//生成一个单线程的异步执行器
ExecutorService es = Executors.newSingleThreadExecutor();
//线程执行后的期望值
Future future = es.submit(new TaxCalculator(100));
while (!future.isDone()){
// 还没有运算完成,等待50毫秒
TimeUnit.MILLISECONDS.sleep(50);
System.out.print("*");
}
System.out.println("\n计算完成,税金是:"+future.get()+" 元");
es.shutdown();
}
Executors是一个静态工具类,提供了异步执行器的创建能力,如单线程异步执行器newSingleThreadExecutor、固定线程数量的执行器newFixedThreadPool等,一般它是异步计算的入口类。future关注的是线程执行后的结果,比如运行十分完毕,结果是多少等。
执行时,"*"会依次递增,表示系统正在运算,为用户提供了运算进度,此类异步计算的好处是:
1、尽可能多的占用系统资源,提高运算速度
2、可以监控线程的执行情况。比如执行是否完毕、是否有返回值、是否有异常等。
3、可以为用户提供更好的支持,比如例子中的运算进度等。