Java 并发编程系列——线程

用户头像
孙苏勇
关注
发布于: 2020 年 04 月 26 日
Java并发编程系列——线程

这几年比较折腾的日子暂告一段落,有时间正好也做做回顾,技术也是一方面,陆续做一些整理。



第一篇写写Java并发有关的,就从线程开始。开始之前,先明确几个概念。



1、CPU的核心线程数

CPU的核心线程数决定了同一时间可处理任务的能力,现在的CPU基本采用超线程方式,一核对应2个或多个超线程,所以一般CPU的核心线程数就是标称的几核几线程中的线程数量,比如四核八线程,那核心线程数就是八。



2、线程的执行

线程的执行由操作系统进行调度,通常是时间片轮转机制,通过时间片的轮转切换线程的执行,使得各线程都有执行机会,当然切换线程本身也会有开销,所以程序中执行时并非线程数越多越好。



3、进程与线程的关系

通常进程是操作系统分配资源的最小单位,一个进程中可以包含多个线程,多个线程共享同一进程中的资源。

而线程通常是操作系统进行任务调度的最小单位。



4、并行与并发的区别

并行指同一时刻能够处理的任务数量,比如同时可以处理4个任务,则并行计算能力是4。

并发是一定时间或说单位时间内能处理的任务数,比如常说的QPS度量时长就是1秒钟。

举个例子,现并行计算能力为4,平均每个任务处理时长为50毫秒,则计算1秒钟内的平均并发处理能力为4*(1000/50)=80。



并发编程中最需要注意的就是资源冲突和死锁,使用不当会造成计算甚至是宕机,在这一篇中暂不讨论。



到这里一些概念暂时清楚了,现在来看看Java中的线程。



1、虚拟机一运行就是多线程的,哪怕我们并没有编写任何多线程的代码。看代码示例,其中使用ThreadMXBean的dumpAllThreads方法可查看所有线程。

public class MainShow {
public static void main(String[] args) {
ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = mxBean.dumpAllThreads(true, true);
for (ThreadInfo t : threadInfos) {
System.out.printf("[%s] %s\n", t.getThreadId(), t.getThreadName());
}
}
}



可以看下输出,其中包含了多个线程。

[1] main
[2] Reference Handler
[3] Finalizer
[4] Signal Dispatcher
[9] Common-Cleaner
[10] Monitor Ctrl-Break



2、实现线程的几个主要方式:Thread,Runnable,Callable

Java中实现线程主要有三种方式(Lambda本身也是实现接口,至于线程池后续再写),Thread,Runnable,Callable,其中Thread是类,另外两个接口,两个接口最大的差别是一个无返回值,一个有返回值。



第一种方式,继承Thread类,覆写run方法,如下代码所示:

public class ThreadShow {
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
public static void main(String[] args) {
Thread t = new MyThread();
t.setName("t");
t.start();
}
}



第二种方式是实现Runnable接口,覆写run方法,如下代码所示:

public class RunnableImplShow {
public static class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.setName("t");
t.start();
}
}



第三种方式是实现Callable接口,而覆写的方法与前两个不同,是call方法。当需要有返回值时,使用Callable较为方便,Callable需要配合FutureTask一块使用,要取得返回结果使用FutureTask的get方法,从代码上看,get方法将阻塞等到结果返回。如下代码所示:

public class CallableImplShow {
public static class MyCallable implements Callable {
@Override
public String call() throws Exception {
return Thread.currentThread().getName();
}
}
public static void main(String[] args) {
FutureTask ft = new FutureTask<>(new MyCallable());
Thread t = new Thread(ft);
t.setName("t");
t.start();
try {
System.out.println(ft.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}



3、如何结束线程

线程结束通常有三种情况:运行完毕结束,线程内异常导致结束,人为结束



前两种结束较为明确,重点看下如何人为结束线程的执行。线程类中有几个和线程停止相关的方法,比如stop,suspend,resume字面上看似乎是停止、挂起和恢复线程,而这些方法无法保证线程资源的正常释放,实际上这几个方法早被废弃,不推荐使用,接下来就看下通常推荐的线程结束方法。



线程仅执行一次的,也谈不上需要结束,而很多线程是需要长时间执行的,则有必要提前结束。提前结束线程推荐使用interrupt方法来告知线程准备结束,而在线程内使用isInterrupted方法判断是否应该结束线程的执行。这也意味并非强行结束线程的执行,线程是否结束由线程自身决定。如下代码所示:

public class ThreadStopShow {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
};
Thread t1 = new Thread(runnable);
t1.setName("t1");
Thread t2 = new Thread(runnable);
t2.setName("t2");
t1.start();
t2.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.interrupt();
t2.interrupt();
System.out.println("main thread over");
}
}



执行结果如下:

t2
t1
main thread over
t1
t2
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at com.sthlike.java.review.ThreadStopShow$1.run(ThreadStopShow.java:10)
at java.base/java.lang.Thread.run(Thread.java:830)
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at com.sthlike.java.review.ThreadStopShow$1.run(ThreadStopShow.java:10)
at java.base/java.lang.Thread.run(Thread.java:830)



在使用interrupt方法时需要注意,在线程中如果sleep失败,中断标志位会变为false,这样会造成线程没有按预期被结束,所以要再调用一次interrupt方法。其中有个方法需要特别注意一下,interrupted,这个方法将返回当前线程的结束标志,但执行成功同时会重置结束标志位为false,所以判断时不能用该方法作为结束条件。



4、线程优先级

线程的优先级通过setPriority来设置,优先级是1到10,默认是5,但通常不推荐设置优先级。对于线程优先级,并不能期望通过设置优先级来保证线程一定被优先执行,所以也很少使用。



5、守护线程

通过setDaemon(true)来设置守护线程,而且要在线程start前设置。守护线程与主线程生命周期相同,主线程结束则守护线程也结束,正因为如此,在这里需要注意的一点是,守护线程的finally方法不保证一定会被执行。如下代码所示:

public class DaemonShow {
public static void main(String[] args) {
long start = System.currentTimeMillis();
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
System.out.printf("print in %s\n", Thread.currentThread().getName());
}
} finally {
System.out.printf("%s final\n", Thread.currentThread().getName());
}
}
};
Thread t1 = new Thread(runnable);
t1.setName("t1");
t1.setDaemon(true);
t1.start();
Thread t2 = new Thread(runnable);
t2.setName("t2");
t2.start();
t2.interrupt();
System.out.printf("main over after %d millis", System.currentTimeMillis() - start);
}
}



可见类似如下输出:

print in t1
print in t1
main over after 20 millis
t2 final
print in t1
print in t1
print in t1
print in t1
print in t1
Process finished with exit code 0



可以看到线程1被设置为守护线程,当主线程结束后,其finally方法并未被执行。



讲技术的文章很多,自己写主要是形成个总结的习惯,印象也会更深刻。

发布于: 2020 年 04 月 26 日 阅读数: 192
用户头像

孙苏勇

关注

不读书,思想就会停止。 2018.04.05 加入

公众号“像什么",记录想记录的。

评论

发布
暂无评论
Java并发编程系列——线程