Java并发之多线程


Java并发之多线程

什么是线程?

通常我们在使用桌面操作系统的时候,说的都是XXX进程。比如我们启动一个Java程序,那操作系统中就会新建一个Java进程。那线程是什么呢?线程是比进程更加轻量级的调度单位,在现代操作系统中,线程就是最小的调度单位,又被称为“轻量级进程”。

在一个进程中是可以创建多个线程的,这些线程拥有自己的虚拟机栈,本地方法栈,程序计数器。如下图JVM的运行时内存划分中绿色的部分,就是线程私有的。CPU在多个线程中高速切换,让用户感觉像是在同时执行。总结来说,操作系统中可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程。


有些初次学习Java的同学可能会很疑惑,好像在日常的开发中,很少用到多线程啊?其实多线程就伴随着我们的日常开发,举个栗子,如果只用单线程,那么在SpringMVC中,前端每发起一个HTTP请求,那么后端接口就会进入阻塞,等待这个线程执行完成,后面的请求才能继续执行。这样的情况下效率将会非常低下。之所以SpringMVC能同时处理多个请求,当然是使用了多线程。

其实Java程序天生就是多线程程序,让我们来看一段简单的Java代码:

public static void main(String[] args) {
    //获取Java线程管理的MXBean
    ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
    //仅获取线程和堆栈信息
    ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
    //遍历线程信息,仅打印线程ID和线程名称信息
    for (ThreadInfo threadInfo : threadInfos) {
        System.out.println("[" + threadInfo.getThreadId() + "]" + threadInfo.getThreadName());
    }
    //打印当前线程名字
    System.out.println(Thread.currentThread().getName());
}

结果如下(不同版本的JDK可能不同):

[5]Monitor Ctrl-Break
[4]Signal Dispatcher
[3]Finalizer
[2]Reference Handler
[1]main
ThreadId:1  ThreadName:main

可以看出,我们仅仅跑了一个main方法,但是却有多个其他线程在同时执行。

为什么使用多线程?

  1. 发挥多处理器核心的优势
    现在的计算机核心数量已经越来越多,单核的计算机几乎已经不存在了。一个程序可以作为一个进程来运行,程序运行过程中可以创建多个线程,而一个线程在同一时刻只能运行在一个处理器核心上。如果是单线程程序,那么同一时间只能有一个进程的一个线程运行,即时有再多的核心,也无法发挥出多核处理器的优势。如果使用多线程,可以在不同的核心上运行不同的计算逻辑,将会显著的提升性能。
  2. 提升响应时间
    在有一些业务逻辑中,会涉及到复杂的流程,比如创建一个用户,要初始化很多数据,用户信息,用户菜单等等。用户在使用这个功能的时候,如果要等到所有流程执行完才能看到返回成功,那么很多用户是不能忍受这么长时间等待的。这时候就可以利用多线程,异步地去执行某些用户不关心的操作,尽快返回结果,提升用户体验。
  3. 合理利用系统资源
    进程在系统中是相互分隔的,而线程之间隔离程度比进程小,而且线程可以共享内存,进程公有数据,相互之间很容易就能实现通信。同时,创建线程的代价比进程要小很多,而且多线程执行效率也比多进程更高更节省系统资源。

多线程的好处不仅仅是这些,正是因为多线程带来的诸多好多,Java在语言内就内置了多线程支持,Java为多线程提供了良好的变成模型,让开发者能够专注对于问题的解决,为所遇到的问题建立合适的模型,而不是绞尽脑汁去思考如何将程序多线程化。

Java多线程的创建

在Java中有三种方式来实现多线程,但是都离不开Thread这个类,所有的线程对象都必须是Thread类或其子类的实例。每个线程都是执行一段程序流,Java使用线程执行体来代表这段程序流。
* 继承Thread类创建线程
步骤如下:
1. 定义一个类继承Thread,并且重写其run()方法,run()方法就是我们所说的线程执行体。
2. 创建Thread子类的实例,就相当于创建了线程对象。
3. 调用实例的start()方法来启动线程。

具体代码如下:

public class ThreadTest {
    public static void main(String[] args) throws Exception {
      for (int i = 0; i < 5; i++) {
          MyThread thread = new MyThread("MyThread-" + i);
          thread.start();
      }
    }
}

class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        //这里可以直接使用getName()方法获取线程的名称,该方法是Thread类的实例方法
        System.out.println(this.getName() + ":created success");
    }
}

结果如下:

MyThread-0:created success
MyThread-1:created success
MyThread-2:created success
MyThread-3:created success
MyThread-4:created success
  • 实现Runnable接口来创建线程
    1. 定义Runnable接口的实现类,并重写该类的run()方法,该run()方法的方法体同样是该线程的线程执行体。
    2. 创建Runnable实现类的实例,并且以此实例作为Thread类的target来创建Thread对象,这个Thread对象才是真正的线程对象。

我们可以查看Thread的构造函数

public Thread(Runnable target, String name) {
    init(null, target, name, 0);
}

具体代码如下:

public class ThreadTest {
    public static void main(String[] args) throws Exception {
        MyThread myThread;
        for (int i = 0; i < 5; i++) {
            myThread = new MyThread();
            new Thread(myThread, "MyThread-").start();
        }
    }
}

class MyThread implements Runnable {

    @Override
    public void run() {
        //这里必须使用Thread.currentThread()方法来获取当前线程
        System.out.println(Thread.currentThread().getName() + ":created success");
    }
}

执行结果同上

  • 实现Callable接口创建线程

在上面的两种实现方式,都是在日常开发中经常见到的方式,但是从Java5开始,提供了Callable接口,它提供了一个call()方法来作为线程执行体,但不同的是call()方法比run()方法更加强大。
call()方法可以有返回值,同时call()方法可以声明抛出异常。

Callable不能直接作为Thread的target,因为他不是Runnable的子接口,所以Java提供了一个FutureTask实现类,该实现类同时实现了Future接口和Runnable接口,Future接口代表了call()方法的返回值。使用Callable的步骤如下:
1. 创建Callable接口的实现类,实现call()方法,再创建该类的实例。
2. 使用FutureTask来包装Callable对象,FutureTask封装了Callable对象的call()方法的返回值。
3. 使用FutureTask的对象作为Thread对象的target来启动新线程。
4. 调用FutureTask对象的get()方法来实现线程类,并启动新线程。

具体代码如下:

public class ThreadTest {
    public static void main(String[] args) throws Exception {
        FutureTask<Integer> task;
        for (int i = 0; i < 5; i++) {
            task = new FutureTask<>(new MyThread());
            String name = "MyThread-" + i;
            new Thread(task, name).start();
            System.out.println(name + " return:" + task.get());
        }
    }
}

class MyThread implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        Integer i = new Random().nextInt(10);
        System.out.println(Thread.currentThread().getName() + ":created success");
        return i;
    }
}

结果如下:

MyThread-0:created success
MyThread-0 return:8
MyThread-1:created success
MyThread-1 return:9
MyThread-2:created success
MyThread-2 return:1
MyThread-3:created success
MyThread-3 return:3
MyThread-4:created success
MyThread-4 return:7

Runnable和Callable在JDK1.8之后已经变成了函数式接口,可以使用lambda表达式来创建他们的对象,会使代码更加的简洁。通过上述三种方式都可以实现多线程,实现Runnable和Callable接口的方式基本上相似,只是Callable的功能更加强大一些。在实际开发中可以根据自己的需求进行选择。

线程的生命周期

在知道了怎么创建线程之后,我们还需要搞清楚线程的生命周期。线程需要经历新建(new),就绪(Runnable),运行(Running),阻塞(Blocked)和死亡(Dead)这5种状态。下面这张图描述了线程生命周期各个状态的转换:

- 在我们通过new创建了一个线程的实例过后,该线程就处于新建状态,此时这个线程对象就和其他Java对象一样,JVM为其分配内存,初始化成员变量的值。
- 调用线程对象的start()方法之后,线程进入就绪状态,Java虚拟机会为其创建栈帧和程序计数器,但是这个状态的线程也并没有开始运行,只是表明这个线程已经可以开始运行了,具体运行时间要看JVM的调度。这里千万要注意,启动线程要使用start()方法,而不是run()方法,使用start()方法启动系统会把run()方法当做线程执行体来执行,但是如果使用run()方法,相当于会立即执行run()方法,线程对象也只是一个普通对象,不会把run()方法包装成线程执行体来执行。
- 处于就绪状态的线程如果获取了CPU,那么就会进入运行状态,在这个状态的线程可能会调用sleep()方法进入阻塞,也可能调用yield()方法再次进入就绪状态,也可能完整地执行完成后进入死亡状态。如果线程进入死亡状态,就不能再次调用start()方法来启动它了,否则会抛出IllegalThreadStateExcetion异常。
- 处于阻塞状态的线程在sleep()时间结束、线程调用的阻塞式IO方法已经返回、线程成功获取锁、被notify()方法唤醒或者调用resume()方法之后会重新进入就绪状态。

结束前

以上内容都是个人学习的总结,后面可能会补充更多,如果有错误,请指出。

参考:
  • 《Java并发编程的艺术》
  • 《疯狂Java讲义》
智能推荐

注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
© 2014-2019 ITdaan.com 粤ICP备14056181号  

赞助商广告