@(架构之路之Java多线程编程实战)
欢迎关注作者
csdn传送门
[TOC]
前言
最近打算就多线程编程做个笔记整理与学习记录,以此打卡,激励自我驱动,fighting!
第一章
1、 jconsole监控工具
- 想验证你对 jvm 配的一些调优参数(比如 Xms、Xmx 等)有没有起作用吗?
- 想不想实时监控你自定义的线程池的在实际运行时的线程个数、有没有死锁?
- 应用出现 java.lang.OutOfMemoryError: Java heap space,你知道需要去调整 Xms、Xmx。想不想实时监控你的 Java 应用的堆内存使用情况,并根据峰值等数据设置最适合你的 Xms、Xmx 等参数?
- 应用出现 java.lang.OutOfMemoryError: PermGen space,你知道需要去调整 XX:PermSize、XX:MaxPermSize。想不想找到你的应用的永久区 PermGen 的使用峰值,并根据其去设置合理的 XX:PermSize、XX:MaxPermSize 等参数?
- 我们都知道,JVM 堆内存划分为年轻代和年老代。JVM 默认下的年老代与年轻代的比例(即 XX:NewRatio,这个名字容易让人产生混淆,即认为是年轻代比年老代)为 2(即把 JVM 堆内存平均分成了三份,年老大占用了两份,而年轻代占用一份。参考资料 Sun Java System Application Server Enterprise Edition 8.2 Performance Tuning Guide),这个比例并不适合所有情况,特别是当你的应用里局部变量远远大于全局变量,而且大量局部变量生命周期都很短的时候。如何根据应用实时的运行运行情况合理配置年轻代(Young Generation,即 Eden 区和两个 Survivor 区之和)和年老代(Old Generation,即 Tenured 区)的比例 XX:NewRatio 值?
Java 自带性能监控工具:监视和管理控制台 jconsole,它可以提供 Java 某个进程的内存、线程、类加载、jvm 概要以及 MBean 等的实时信息,也许能够对以上问题提供参考。
1.1、JVM一些参数
在启动 jconsole 之前我们先来回顾一下 JVM 的一些主要参数:
- -Xms 初始/最小堆内存大小
- -Xmx 最大堆内存大小
- -Xmn 年轻代大小
- -XX:NewSize 年轻代大小
- -XX:MaxNewSize 年轻代最大值
- -XX:NewRatio 年老代与年轻代比值
- -XX:MaxPermSize 持久代最大值
- -XX:PermSize 持久代初始值
有些资料说,Xms、Xmx 设置的是 JVM 内存大小,是不对的,JVM 除了留给开发人员使用的堆内存之外还有非堆内存。
读者可能发现,有三种方式可以划分年轻代大小:-Xmn 方式、-XX:NewSize + -XX:MaxNewSize 方式、-XX:NewRatio 方式。三种都可以,优先级从高到低依次是 -XX:NewSize + -XX:MaxNewSize 方式、-Xmn 方式、-XX:NewRatio 方式,也就是说配置了前面优先级高的后面的优先级低的就被覆盖掉了。
1.2、启用 jconsole 以监控 Java 进程
CMD 切换到 %JAVA_HOME%/bin 目录,直接执行 jconsole
即可打开 Java 监视和管理控制台:
本地进程列表里显示了所有本地执行中的 Java 进程,双击你感兴趣的那个进程(比如 PID 为 8504 那个),即可对该进程进行监控了:
1.3、远程监控 Java 进程
要对 Java 进程进行远程监控,在启动它的时候需要启用 JMX。
以远程主机上的 tomcat 为例,先为 jmx 找一个可用的远程端口,比如 9999:
No news is good news~在 %TOMCAT_HOME%/bin/catalina.sh 文件的前面加上以下配置:
JAVA_OPTS="-Xms1024m -Xmx2048m -XX:MaxPermSize=128m -Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false"
如图
这样写在 tomcat 关闭的时候(执行 %tomcat%/bin/shutdown.sh)会报端口已使用异常:
错误: 代理抛出异常错误: java.rmi.server.ExportException: Port already in use: 9999; nested exception is:
java.net.BindException: 地址已在使用
这是因为 tomcat 在启动、停止的时候都会执行 JAVA_OPTS 配置。这样就只能使用 kill -9 来关闭 tomcat 了...
解决办法是把监控配置写在 CATALINA_OPTS 里:
JAVA_OPTS="-Xms1024m -Xmx2048m -XX:MaxPermSize=128m"
CATALINA_OPTS="-Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false"
然后重启 tomcat,在本机打开 Java 监视和管理控制台,"远程进程" 输入远程主机名和 jmx 端口号:
点击 "连接" 按钮,即可对远程主机上的 tomcat 进行实时监控了:
1.4、jconsole 提供的一些有用信息
1.4.1、JVM 设定信息是否起作用检查
点击 "VM 概要" 可以查看到刚才我们设定的 JAVA_OPTS 的一些参数已经奏效了:
1.4.2、tomcat 线程池、自定义线程池数量情况实时监控
还在为 tomcat 线程池的神秘面纱而头疼?还在为自己定义的线程池 "黑盒" 一般而苦恼?看看下图:
我们的 tomcat 刚启动,从上图可以看出只有一个 http-8080-Acceptor-0 线程,我们去访问一下我们的项目,然后再回来看看:
http-8080 线程一下子增长到了 8 个。是不是一切一目了然,尽在掌握之中?
1.4.3、内存使用实际消耗
点击 Java 监视和管理控制台 "内存" 叶项,可以看到 tomcat 堆内存的使用情况:
图表里有很多选项:
我们看一下 Eden 区:
Eden 区基本和整个堆内存的走势差不多。再看 Survivor 区:
Survivor 区在较短时间内的走势相对平稳。再看 Old Gen 区:
这个走势更加平稳,而且对比 Survivor 区、Old Gen 区两张图,可以很明显地看出,在大约 19:58 那个时刻有将一批对象从 Survivor 区移到 Old Gen 区。最后看 Perm Gen 区。
这个走势最平稳了。可以明显看出,在大约 19:58,在我们访问一下我们的项目的时候,一些新的 class 等静态资源加载到了 JVM 中。1.4.4 的加载类数的图也证实了这一点。
1.4.4、tomcat 加载类的情况
1.5、配合 jmap 的使用
先找到我们 tomcat 进程的 PID 是 13863,然后执行 jmap -heap 13863:
Heap Configuration 里列的基本就是我们刚才配的那些,比如 MaxHeapSize 是 2048 MB,MaxPermSize 是 128 MB。这个和 5.1 里的是一样的。
2、策略模式在Thread和Runnable中的应用分析
2.1、策略模式
传送门
2.2、源代码
抽象策略(Strategy)角色类
package com.scmd.concurrency.chap2;
@FunctionalInterface
public interface CalculatorStrategy {
double calculate(double salary, double bouns);
}
具体策略(Strategy)角色类
package com.scmd.concurrency.chap2;
/**
* @program: scmd-knb-common
* @description:
* @author: zhouzhixiang
* @create: 2019-03-16 18:18
*/
public class SimpleCalculatorStrategy implements CalculatorStrategy {
private static final Double SALARY_RATE = 0.1;
private static final Double BOUNS_RATE = 0.2;
@Override
public double calculate(double salary, double bouns) {
return SALARY_RATE * salary + BOUNS_RATE * bouns;
}
}
环境(Context)角色类
package com.scmd.concurrency.chap2;
/**
* @program: scmd-knb-common
* @description:
* @author: zhouzhixiang
* @create: 2019-03-16 18:09
*/
public class TaxCalculator {
private final double salary;
private final double bonus;
private final CalculatorStrategy strategy;
public TaxCalculator(double salary, double bonus, CalculatorStrategy strategy) {
this.salary = salary;
this.bonus = bonus;
this.strategy = strategy;
}
public double calTax() {
return strategy.calculate(salary, bonus);
}
protected double calculate() {
return this.calTax();
}
public double getSalary() {
return salary;
}
public double getBonus() {
return bonus;
}
}
客户端
package com.scmd.concurrency.chap2;
/**
* @program: scmd-knb-common
* @description:
* @author: zhouzhixiang
* @create: 2019-03-16 18:12
*/
public class TaxCalculatorMain {
public static void main(String[] args) {
// 策略模式
// 非lambd
// TaxCalculator t = new TaxCalculator(1000d, 2000d, new SimpleCalculatorStrategy());
// lambd表达式 + 函数式接口
TaxCalculator t = new TaxCalculator(1000d, 2000d, (s, b) -> s * 0.3 + b * 0.5);
double tax = t.calculate();
System.out.println(tax);
}
}
3、Thread与ThreadGroup
3.1、Thread和ThredGroup的关系
因为Thread的构造函数中有关于ThradGroup的,所以了解它们之间的关系是有必要的。ThradGroup之间的关系是树的关系,而Thread与ThradGroup的关系就像元素与集合的关系。关系图简单如下:
其中有一点要明确一下:main方法执行后,将自动创建system线程组合main线程组,main方法所在线程存放在main线程组中。
3.2、Thread API
3.2.1、基本属性
首先应该了解线程的基本属性:
- name:线程名称,可以重复,若没有指定会自动生成。
- id:线程ID,一个正long值,创建线程时指定,终生不变,线程终结时ID可以复用。
- priority:线程优先级,取值为1到10,线程优先级越高,执行的可能越大,若运行环境不支持优先级分10级,如只支持5级,那么设置5和设置6有可能是一样的。
- state:线程状态,Thread.State枚举类型,有NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED 5种。
- ThreadGroup:所属线程组,一个线程必然有所属线程组。
- UncaughtExceptionHandler:未捕获异常时的处理器,默认没有,线程出现错误后会立即终止当前线程运行,并打印错误。
3.2.2、字段摘要
Thread类有三个字段,设置线程优先级时可使用:
- MIN_PRIORITY:1,最低优先级
- NORM_PRIORITY:5,普通优先级
- MAX_PRIORITY:10,最高优先级
3.2.3、构造方法
现只介绍参数最多的一个:
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
- group:指定当前线程的线程组,未指定时线程组为创建该线程所属的线程组。
- target:指定运行其中的Runnable,一般都需要指定,不指定的线程没有意义,或者可以通过创建Thread的子类并重新run方法。
- name:线程的名称,不指定自动生成。
- stackSize:预期堆栈大小,不指定默认为0,0代表忽略这个属性。与平台相关,不建议使用该属性。
3.2.4、方法摘要
静态方法
- Thread Thread.currentThread() :获得当前线程的引用。获得当前线程后对其进行操作。
- Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() :返回线程由于未捕获到异常而突然终止时调用的默认处理程序。
- int Thread.activeCount():当前线程所在线程组中活动线程的数目。
- void dumpStack() :将当前线程的堆栈跟踪打印至标准错误流。
- int enumerate(Thread[] tarray) :将当前线程的线程组及其子组中的每一个活动线程复制到指定的数组中。
- Map
getAllStackTraces() :返回所有活动线程的堆栈跟踪的一个映射。 - boolean holdsLock(Object obj) :当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
- boolean interrupted() :测试当前线程是否已经中断。
- void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh) :设置当线程由于未捕获到异常而突然终止,并且没有为该线程定义其他处理程序时所调用的默认处理程序。
- void sleep(long millis) :休眠指定时间
- void sleep(long millis, int nanos) :休眠指定时间
- void yield() :暂停当前正在执行的线程对象,并执行其他线程。意义不太大
普通方法
- void checkAccess() :判定当前运行的线程是否有权修改该线程。
- ClassLoader getContextClassLoader() :返回该线程的上下文 ClassLoader。
- long getId() :返回该线程的标识符。
- String getName() :返回该线程的名称。
- int getPriority() :返回线程的优先级。
- StackTraceElement[] getStackTrace() :返回一个表示该线程堆栈转储的堆栈跟踪元素数组。
- Thread.State getState() :返回该线程的状态。
- ThreadGroup getThreadGroup() :返回该线程所属的线程组。
- Thread.UncaughtExceptionHandler getUncaughtExceptionHandler() :返回该线程由于未捕获到异常而突然终止时调用的处理程序。
- void interrupt() :中断线程。
- boolean isAlive() :测试线程是否处于活动状态。
- boolean isDaemon() :测试该线程是否为守护线程。
- boolean isInterrupted():测试线程是否已经中断。
- void join() :等待该线程终止。
- void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。
- void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
- void run() :线程启动后执行的方法。
- void setContextClassLoader(ClassLoader cl) :设置该线程的上下文 ClassLoader。
- void setDaemon(boolean on) :将该线程标记为守护线程或用户线程。
- void start():使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
- String toString():返回该线程的字符串表示形式,包括线程名称、优先级和线程组。
作废方法
- int countStackFrames() :没有意义不做解释。
- void destroy() :破坏线程,不释放锁,已经不能再使用,使用会抛出NoSuchMethodError。
- void suspend() :挂起线程,不要使用。
- void resume() :恢复线程,不要使用。
- void stop() :停止线程释放锁,不要使用。
- void stop(Throwable obj) :同上。
3.2.5、setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
首先要了解什么是Thread.UncaughtExceptionHandler,默认来说当线程出现未捕获的异常时,会中断并抛出异常,抛出后的动作只有简单的堆栈输出。如:
public class ThreadTest{
public static void main(String[] args) throws Exception{
Thread t1=new Thread(new Runnable(){
public void run(){
int a=1/0;
}
});
t1.start();
}
}
那么代码运行到int a=1/0;就会报错:
Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
at yiwangzhibujian.ThreadTest$1.run(ThreadTest.java:11)
at java.lang.Thread.run(Thread.java:662)
这时候如果设置了Thread.UncaughtExceptionHandler,那么处理器会将异常进行捕获,捕获后就可以对其进行处理:
public class ThreadTest{
public static void main(String[] args) throws Exception{
Thread t1=new Thread(new Runnable(){
public void run(){
int a=1/0;
}
});
t1.setUncaughtExceptionHandler(new UncaughtExceptionHandler(){
@Override
public void uncaughtException(Thread t,Throwable e){
System.out.println("线程:"+t.getName()+"出现异常,异常信息:"+e);
}
});
t1.start();
}
}
那么当线程抛出异常后就可以对其抓取并进行处理,最终结果如下:
线程:Thread-0出现异常,异常信息:java.lang.ArithmeticException: / by zero
如果自己写线程,那么完全可以在run方法内,将所有代码进行try catch,在catch里做相同的操作。UncaughtExceptionHandler的意义在于不对(或者不能对)原有线程进行修改的情况下,为其增加一个错误处理器。
3.2.6、interrupt() 、interrupted() 、isInterrupted()作用
因为stop()方法已经不建议使用了,下面的3.5.4进行详解,所以如何中断一个线程就成了一个问题,一种简单的办法是设置一个全局变量needStop,如下:
@Override
public void run(){
while(!needStop){
//执行某些任务
}
}
或者需要操作耗时较长的方法内,每一步执行之前进行判断:
@Override
public void run(){
//耗时较长步骤1
if(needStop) return;
//耗时较长步骤2
if(needStop) return;
//耗时较长步骤3
}
这样在其他的地方将此线程停止掉,因为停止是在自己的预料下,所以不会有死锁或者数据异常问题(当然你的程序编写的时候要注意)。
其实Thread类早就有类似的功能,那就是Thread具有中断属性。可以通过调用interrupt()方法对线程中断属性设置为true,这将导致如下两种情况:
- 当线程正常运行时,中断属性设置为true,调用其isInterrupted()方法会返回true。
- 当线程阻塞时(wait,join,sleep方法),会立即抛出InterruptedException异常,并将中断属性设置为false。此时再调用isInterrupted()会返回false。
这样就由程序来决定当检测到中断属性为true时,怎么对线程中断进行处理。因此,上面的代码可以改成:
@Override
public void run(){
while(!Thread.currentThread().isInterrupted()){
//执行某些任务
}
}
---------------------------------------------------------
@Override
public void run(){
//耗时较长步骤1
if(Thread.currentThread().isInterrupted()) return;
//耗时较长步骤2
if(Thread.currentThread().isInterrupted()) return;
//耗时较长步骤3
}
interrupted()的方法名容易给人一种误解,看似和interrupt()方法一样,但是其实际含义是,返回当前中断状态,并将其设置为false。
3.2.7、yield()和sleep(0)
yield()方法的API容易给人一种误解,它的实际含义是停止执行当前线程(立即),让CPU重新选择需要执行的线程,因为具有随机性,所以也有可能重新执行该线程,通过下面例子了解:
public class ThreadTest{
public static void main(String[] args) throws Exception{
Thread t1=new Thread(new Runnable(){
@Override
public void run(){
while(true){
System.out.println(1);
Thread.yield();
}
}
});
Thread t2=new Thread(new Runnable(){
public void run(){
while(true){
System.out.println(2);
Thread.yield();
}
}
});
t1.start();
t2.start();
}
}
程序执行结果并不是121212而是有,有连续的1和连续的2。
经过测试yield()和sleep(0)的效果是一样的,sleep(0)底层要么是和yield()一样,要么被过滤掉了(纯靠猜测),不过sleep(0)没有任何意义。要是真打算让当前线程暂停还是应该使用sleep(long millis,int nanos)这个方法,设置几纳秒表示下诚意,或者找到想要让步的线程,调用它的join方法更实际一些。
3.2.8、stop()、suspend()、resume()为什么不建议使用
stop方法会立即中断线程,虽然会释放持有的锁,但是线程的运行到哪是未知的,假如在具有上下文语义的位置中断了,那么将会导致信息出现错误,比如:
@Override
public void run(){
try{
//处理资源并插入数据库
}catch(Exception e){
//出现异常回滚
}
}
如果在调用stop时,代码运行到捕获异常需要回滚的地方,那么将会因为没有回滚,保存了错误的信息。
而suspend会将当前线程挂起,但是并不会释放所持有的资源,如果恢复线程在调用resume也需要那个资源,那么就会形成死锁。当然可以通过你精湛的编程来避免死锁,但是这个方法具有固有的死锁倾向。所以不建议使用。其他暂停方法为什么可用:
- wait方法会释放锁,所以不会有死锁问题
- sleep方法虽然不释放锁,但是它不需要唤醒,在使用的时候已经指定想要的睡眠时间了。
jdk的文章详细介绍了方法禁用的原因:文章地址,有空可以看一看,如果你足够大胆,也是可以使用的。
3.3、ThreadGroup API
3.3.1、基本属性
name:当前线程的名称。
parent:当前线程组的父线程组。
MaxPriority:当前线程组的最高优先级,其中的线程优先级不能高于此。
3.3.2、构造方法
只介绍一个构造方法:
ThreadGroup(ThreadGroup parent, String name) :
- parent:父线程组,若为指定则是创建该线程组的线程所需的线程组。
- name:线程组的名称,可重复。
3.3.3、常用方法摘要
API详解(中文,英文)。
- int activeCount():返回此线程组中活动线程的估计数。
- void interrupt():中断此线程组中的所有线程。
- void uncaughtException(Thread t, Throwable e) :设置当前线程组的异常处理器(只对没有异常处理器的线程有效)。
3.3.4、ThreadGroup作用
这个线程组可以用来管理一组线程,通过activeCount() 来查看活动线程的数量。其他没有什么大的用处。
4、Thread API 综合实战
4.1、优雅结束线程
- 全局变量
package com.scmd.concurrency.chap6;
/**
* @program: scmd-knb-common
* @description:
* @author: zhouzhixiang
* @create: 2019-03-20 20:02
*/
public class ThreadCloseGraceful {
static class Worker extends Thread {
private volatile boolean open = true;
@Override
public void run() {
while (open) {
// code ......
try {
Thread.sleep(1000);
System.out.println(".........");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void shutdown (boolean open) {
this.open = open;
}
}
public static void main(String[] args) {
Worker worker = new Worker();
worker.start();
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
worker.shutdown(false);
}
}
- interrupt()
package com.scmd.concurrency.chap6;
/**
* @program: scmd-knb-common
* @description:
* @author: zhouzhixiang
* @create: 2019-03-20 20:02
*/
public class ThreadCloseGraceful2 {
static class Worker extends Thread {
@Override
public void run() {
while (!Thread.interrupted()) {
// code ......
try {
Thread.sleep(1000);
System.out.println(".........");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Worker worker = new Worker();
worker.start();
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
worker.interrupt();
}
}
4.2、暴力结束线程
package com.scmd.concurrency.chap6;
/**
* @program: scmd-knb-common
* @description: 暴力停止
* @author: zhouzhixiang
* @create: 2019-03-20 20:40
*/
public class ThreadService {
private Thread excuteThread;
private volatile boolean finished = false;
public void excute(Runnable task) {
excuteThread = new Thread() {
@Override
public void run() {
Thread daemon = new Thread(task);
daemon.setDaemon(true);
daemon.start();
try {
daemon.join();
finished = true;
} catch (InterruptedException e) {
// e.printStackTrace();
}
}
};
excuteThread.start();
}
public void shutdown(long millis) {
long currentTime = System.currentTimeMillis();
while (!finished) {
if ((System.currentTimeMillis() - currentTime) > millis) {
System.out.println("任务超时,需要结束!");
excuteThread.interrupt();
break;
}
try {
excuteThread.sleep(1);
} catch (InterruptedException e) {
System.out.println("执行线程被打断!");
break;
}
}
finished = false;
}
}
package com.scmd.concurrency.chap6;
/**
* @program: scmd-knb-common
* @description: 暴力
* @author: zhouzhixiang
* @create: 2019-03-20 20:38
*/
public class ThreadCloseForce {
public static void main(String[] args) {
ThreadService service = new ThreadService();
long startTime = System.currentTimeMillis();
service.excute(() -> {
// load a very heavy resouce
// while (true) {
//
// }
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
service.shutdown(10000);
long endTime = System.currentTimeMillis();
System.out.println(endTime - startTime);
}
}
欢迎加入Java猿社区!
免费领取我历年收集的所有学习资料哦!