根据自己学的知识加上从各个网站上收集的资料分享一下关于java高并发编程的知识点。对于代码示例会以Maven工程的形式分享到个人的GitHub上面。
首先介绍一下这个系列的东西是什么,这个系列自己总结的东西比较多,主要参考的内容是《Java高并发编程详解》这本书,当然也结合了很多的其他书籍,以及网站。现在多元化的学习途径,导致的问题就是没有一个系列的教程来讲解关于某一个点的详细的内容。通过这种系列分享的形式将自己的能力进一步的提升。
本系列主要有29篇的内容,从最基础的线程入门到高级的线程设计模式。通过详细的代码示例加上图表示例希望可以对大家的能力提升有一定的帮助。
对于所有的操作系统来说几乎都是支持多个任务的同时执行的,在计算机中,每执行一个任务就存在一个进程,每一个进程中又有很多的线程在协调工作。例如下图
我们看到在Windows10操作系统中默认就启动了很多的进程来支持我们计算机的运行。
在Java中线程是程序运行的一个途径,在Java虚拟机中除了有多线程共享的方法区、堆内存等资源,每个线程中还有属于自己的程序计数器、虚拟机栈等资源。而对于Java虚拟机本省来说就是一个进程。在运行Java虚拟机的时候本省也会附带的创建很多的线程。可以通过在JDK的bin路径下面找到对应的虚拟机工具jvisualvm 通过JDK自带的虚拟机工具来查看,如下图
在工作中生活中经常会遇到很多的两件事情同时做的时候,例如你下班回家之后一边吃饭一边看电视。在单线程的情况下你的操作步骤是先吃饭,然后再去看电视。但是多线程的情况下就是一边看电视一边吃饭,可以说两个都不耽误。
public interface DoSomething {
//看电视操作
public void watch() throws InterruptedException;
//吃饭操作
public void eat() throws InterruptedException;
}
public class WatchAndEat implements DoSomething{
public void watch() throws InterruptedException {
for (;;) {
doWatch();
sleep(1000);
}
}
public void eat() throws InterruptedException {
for (;;){
doEat();
sleep(1000);
}
}
private void doWatch() {
System.out.println("我在看电视");
}
private void doEat(){
System.out.println("我在吃饭");
}
}
public class MainTest {
/**
* 首先描述一个单线程场景
* 1.创建一个主线程入口
* 2.创建一个看电视方法
* 3.创建一个吃饭的方法
*
* 4.测试结果
*/
public static void main(String[] args) throws InterruptedException {
WatchAndEat watchAndEat = new WatchAndEat();
watchAndEat.watch();
watchAndEat.eat();
}
}
结果如下,在单线程中并没有看到吃饭的操作。也就是说吃饭的操作是永远都不可能被执行到的。因为我们知道Java代码始终是从上到下依次执行的,也就是说只要看电视的方法得不到返回,那么就永远不会执行吃饭的方法。
这里我们需要编写一个新的测试类。
public class MainThreadTest {
public static void main(String[] args) throws InterruptedException {
final WatchAndEat watchAndEat = new WatchAndEat();
new Thread(()->{
try {
watchAndEat.eat();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
watchAndEat.watch();
}
}
在这里可以看到吃饭和看电视的方法同时交替输出。这也就是说可以边看电视边吃饭了。
在java提供了很多的查看线程的的工具例如之前看到的jvisualvm、还有Jconsole等。
1.点击链接对应的进程
2.点击对应的线程进行查看操作
主线程
名称: main
状态: TIMED_WAITING
总阻止数: 77, 总等待数: 204
堆栈跟踪:
java.lang.Thread.sleep(Native Method)
com.example.charp01.demo01.WatchAndEat.watch(WatchAndEat.java:18)
com.example.charp01.demo01.MainThreadTest.main(MainThreadTest.java:20)
自己创建的线程
名称: Thread-0
状态: TIMED_WAITING
总阻止数: 86, 总等待数: 256
堆栈跟踪:
java.lang.Thread.sleep(Native Method)
com.example.charp01.demo01.WatchAndEat.eat(WatchAndEat.java:26)
com.example.charp01.demo01.MainThreadTest.lambda$main$0(MainThreadTest.java:15)
com.example.charp01.demo01.MainThreadTest$$Lambda$1/990368553.run(Unknown Source)
java.lang.Thread.run(Thread.java:748)
从上面可以看出JVM确实创建了两个线程,一个是main主线程,一个是Thread-0(这个名字可以自己指定)的用户自定义线程。并且两者也执行的自己对应的方法,一个是watch,一个是eat。
看完线程的运行情况,就要研究一下线程的生命周期情况了,在上面我们看到了两个线程的状态都是TIMED_WAITING,而在Java文档官方定义TIMED_WAITING状态为:“一个线程在一个特定的等待时间内等待另一个线程完成一个动作会在这个状态”,也就是说两个线程在相互等待完成,
提示,在调用以下方法的时候线程会进入到这个状态
1、Thread#sleep()
2、Object#wait() 并加了超时参数
3、Thread#join() 并加了超时参数
4、LockSupport#parkNanos()
5、LockSupport#parkUntil()
根据Java API文档的分类可以将Java线程在在JVM中的状态分为六个
New状态
通过new 关键字创建一个线程的时候,也就是说线程被创建的时候
terminated状态
这个状态也就是说run方法执行完毕线程就结束了
runnable状态
对于Runable状态其实还可以细致的分为Runnable状态和running状态,所有的线程运行的时候都在这两个状态下抢占资源
waiting状态
当然如果线程调用了wait方法之后就会进入到waiting,这个状态的时候线程会释放自己获取到的锁。直到调用了notify方法或者是notifyall方法不然永远都不会得到CPU资源。
timed waiting状态
这个状态也是我们通过工具看到的线程的状态,在这个状态的时候线程不会放弃锁,会一直等待其他线程执行完成之后才会执行。
blocking状态
对于这个状态来说就是进入了阻塞状态,对于阻塞来说一般情况下回出现在IO请求的时候由于收不到输入或者输出不到对应的操作中会出现阻塞。例如A线程获取到的cpu进入到了同步方法中,但是由于B线程获得的一半的资源,只有等待A线程运行完成之后才能继续获取到另一半的资源。
以上就是对于Java线程的生命周期的六个状态的简单的介绍,更为详细的介绍在后面的文章中会详细说明。
之前之前我们看到,在每个Thread执行的时候会调用一个strat方法,而这个Start方法和Run方法到底是什么样的关系呢!我们可以通过它的源码来分析这个问题
public synchronized void start() {
//首先线程调用Start方法的时候会判断线程的状态
//也就是说线程不能被两次激活,也就是不能调用两次start方法。
if (threadStatus != 0)
throw new IllegalThreadStateException();
//默认添加一个线程组
group.add(this);
//判断线程是否启动
boolean started = false;
try {
//真正的执行逻辑在这里,调用了一个start0的方法
//这个方法被native修饰,也就说是一个底层方法
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then it will be passed up the call stack */
}
}
}
通过上面的代码可以看到其实在java调用start方法的时候底层调用的是一个start0的方法,也就是说在线程执行的时候JVM就会调用该线程的run方法也就说其实在调用start方法的时候start0方法其实是调用run方法的。
首先我们通过分析一个Runable接口源码来分析一下什么是模板设计模式
public abstract void run();
@Override
public void run() {
if (target != null) {
target.run();
}
}
/* What will be run. */
private Runnable target;
可以看到在其中Runnable准备了一个抽象的run方法,而在Thread中继承了Runnable接口重写了run方法,其中可以看到有一个target的变量,而这个变量正是我们的接口Runnable的对象,这个也就给了我们一个启示,在我们使用的时候可以定义一个模板方法。而在我们之后的实现中都是实现了不同功能的run方法,但是实际上这个模板类Runnable并没有发生变化,当我们需要进行程序结构的改变的时候只需要将父类的结构改变即可。子类只负责实现具体的逻辑就可以了。例如Runnable和Thread的关系其实就是一个类似于子类和父类的关系。只不过它是被JDK封装好的方法而已。这里我们根据这思路给出了下面的实例
1、首先创建一个模板类
public interface PrintMessage {
abstract void print();
}
2、创建一个记者类
public class Repoter implements PrintMessage {
public void printNews() {
print();
}
@Override
public void print() {
System.out.println("show news");
}
}
3、创建最终的测试类
public class TemplateMain {
public static void main(String[] args) {
Repoter repoter = new Repoter(){
@Override
public void print() {
System.out.println("这个是一个设计模板方法");
}
};
repoter.printNews();
}
由于这里是模仿Runable和Thread写的,所以在实现上没有太多的细节上的处理,但是实际上在我们实际开发中所使用的时候对于细节的处理还是比较到位的,所以说要从简单的例子中理解原理,然后应用到实际开发程序设计中。
通过继承Thread类实现多线程操作
public class TicketWindow extends Thread {
private final String name;
private static final int MAX = 50;
private int index = 1;
public TicketWindow(String name) {
this.name = name;
}
@Override
public void run() {
while (index<MAX){
System.out.println(name+" 号柜台,出号 "+index++);
}
}
public static void main(String[] args){
TicketWindow ticketWindow1 = new TicketWindow("一");
ticketWindow1.start();
TicketWindow ticketWindow2 = new TicketWindow("二");
ticketWindow2.start();
TicketWindow ticketWindow3 = new TicketWindow("三");
ticketWindow3.start();
TicketWindow ticketWindow4 = new TicketWindow("四");
ticketWindow4.start();
}
}
通过继承Runnable接口实现多线程操作
在之前的例子中提到了Runable的作用就是为子类实现提供了一个程序结构的模板,在Java中比较常见的实现多线程的方式就是通过Thread和Runable的方式实现。而这两者都是重写了run方法。也就是说无论从那个角度上讲其目的就是要将线程本身的控制权和业务逻辑分开。
这里介绍一个新的设计模式策略模式
例如
1、创建一个结果类
public class Result {
}
2、创建要给处理类
public interface Handler<T>{
T handle(Result rs);
}
3、创建实现具体的操作类
public class GetResult {
public <T> T get(Handler<T> handler,String other){
Result result = new Result();
return handler.handle(result);//这个地方只负责获取结果
}
}
在我们实际使用获取结果的时候,可以这样子做一个操作,我们可以在实现get方法的时候传入一个继承了Handler接口的实现了,至于具体的实现什么样的处理我们不用关心,可以根据需要创建自己的Handler处理器。然后传入到get方法中,这样实现了处理逻辑和方法执行的分离。使用这个方法可以处理任何结果集。
继承Runable接口实现买票操作
public class TicketRunable implements Runnable {
private int index = 1;
private final static int MAX = 50;
@Override
public void run() {
while (index<=50){
System.out.println(Thread.currentThread()+" 的号码是 :"+index++);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
TicketRunable ticketRunable = new TicketRunable();
Thread thread1 = new Thread(ticketRunable,"一");
Thread thread2 = new Thread(ticketRunable,"二");
Thread thread3 = new Thread(ticketRunable,"三");
Thread thread4 = new Thread(ticketRunable,"四");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}