一.进程和多线程的概述以及使用场景
进程:一个正在操作系统中运行的exe程序可以理解为一个进程,完全可以将运行在内存中的exe文件理解为进程-----进程就是受操作系统管理的基本运行单元。一个最简单的Java程序的运行也可以叫做一个进程。
所有的应用程序都是由CPU执行的,对于一个CPU而言,在某一个时间点只能运行一个程序,也就是说只能执行一个进程。操作系统会给每一个进程分配一段有限的cpu使用时间,cpu在这段时间中执行某个进程。由于CPU的执行速度非常快,能在极短的时间内在不同的进程之间进行切换,所以给人同时执行多个程序的感觉。
线程:在一个进程中,还可以有多个执行单元同时运行,来同时完成一个或多个程序任务。这些执行单元可以看作是一条条线索,被称为线程。
多线程技术的使用场景:(1)阻塞。一旦系统出现了阻塞现象,则可以根据实际情况来使用多线程技术提高开发效率。(2)依赖。业务如果分为2个执行过程,分别是A和B。当A业务发生阻塞时,B业务的执行不依赖于A业务的执行结果,这时就可以通过多线程来提高运行效率。如果B业务不依赖于A业务的结果,则不必使用多线程技术,A业务出结果后再运行B业务,按顺来就行。
二.并发和并行
并发:指两个或多个事件在同一个时间段内发生,同一时刻只能发生一件事。
并行:指两个或多个事件在同一时刻发生。(同时发生)
特别注意:Java程序属于优先抢占式地调度,哪个线程的优先级高,哪个线程就有可能优先执行。优先级相同时,就随机选择执行。优先级大小只是指CPU被优先执行的概率的大小,并不一定优先级大就一定先被执行。
三.线程的创建
Java为多线程开发提供了非常优秀的支持,在java中,可以通过以下三个方式来实现多线程:
1.Thread类实现多线程
Thread类是java.lang包下的一个线程类,用来实现Java多线程。其实步骤非常简单。如下
(1)创建一个Thread线程类的子类,同时重写Thread类的run()方法。
(2)创建该子类的实例对象,并通过调用start()方法来启动线程。
start()方法使线程开始执行,Java虚拟机调用该线程的run()方法。结果是程序的main入口方法和创建的线程并发的运行。多次启动同一个线程是非法的,特别是当线程已经结束执行后,不能再重新启动。
package demo01; public class Test { public static void main(String[] args) { //创建MyThread1线程实例对象 MyThread1 thread1=new MyThread1("thread1"); thread1.start(); MyThread1 thread2=new MyThread1("thread2"); thread2.start(); } } //定义一个线程类继承自Thread类 class MyThread1 extends Thread{ //构造方法 public MyThread1(String name){ super(name); } //重写run()方法 @Override public void run() { int i=0; while (i++<5){ //打印输出当时运行的线程的名字 System.out.println(Thread.currentThread().getName()); } } }
2.Runnable接口实现多线程
通过继承Thread类来实现多线程的方式有一些局限性,因为java只支持类的单继承。在这种情况下就可以考虑通过实现Runnable接口的方式来实现多线程。
步骤如下:
(1)创建一个Runnable接口的实现类,同时重写接口中的run()方法。
(2)创建Runnable接口的实现类对象。
(3)使用Thread有参构造方法来创建线程实例,并将Runnable接口的实现类对象作为参数传入。
(4)调用线程实例的start()方法来启动线程
package demo02; public class test { public static void main(String[] args) { //(2)创建Runnable接口的实现类对象。 myThread mythread1=new myThread(); myThread mythread2=new myThread(); //(3)使用Thread有参构造方法来创建线程实例,并将Runnable接口的实现类对象作为参数传入。 Thread thread1=new Thread(mythread1,"thread1"); Thread thread2=new Thread(mythread2,"thread2"); //(4)调用线程实例的start()方法来启动线程 thread1.start(); thread2.start(); } } //(1)定义一个Runnable接口的实现类 class myThread implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName()+"的run()方法正在运行"); } }
3.Callable接口实现多线程
通过Thread类和Runnable接口实现多线程时都要要重写run()方法。但是,该方法却没有返回值,因此无法从多个线程中获取返回值,但实际应用中肯定会用到返回值的。为了解决这个问题,从jdk5开始,java提供了一个Callable接口,来满足这种既能创建多线程,又能有返回值的需求。
通过这个方式实现多线程和Runnable的方式实现多线程差不多,都是通过Thread类的有参构造方法传入各自接口对象为参数来实现。只不过这里传入的不是Runnable对象了,而是Runnable类的子类FutureTask对象作为参数,而FutureTask对象中则封装带有返回值的Callable接口实现类。
实现步骤如下:
(1)创建一个Callable的实现类,同时重写Callable接口的call()方法。
(2)创建实现了Callable接口的实现类对象。
(3)通过FutureTask线程结果处理类的有参构造方法来封装Callable接口实现类对象
(4)使用参数为FutureTask类对象的Thread有参构造方法创建Thread实例。
(5)调用线程实例的start()方法启动线程。
package demo03; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; //(1)创建一个Callable的实现类,同时重写Callable接口的call()方法。 class MyThread implements Callable<Object>{ @Override public Object call() throws Exception { int i=0; while (i++<5){ //打印输出当时运行的线程的名字 System.out.println(Thread.currentThread().getName()); } return i; } } public class test { public static void main(String[] args) throws ExecutionException, InterruptedException { //(2)创建实现了Callable接口的实现类对象。 MyThread mythread1=new MyThread(); //(3)通过FutureTask线程结果处理类的有参构造方法来封装Callable接口实现类对象 FutureTask<Object> ft1=new FutureTask<>(mythread1); //(4)使用参数为FutureTask类对象的Thread有参构造方法创建Thread实例。 Thread thread1=new Thread(ft1,"thread1"); //(5)调用线程实例的start()方法启动线程。 thread1.start(); //(2)创建实现了Callable接口的实现类对象。 MyThread mythread2=new MyThread(); //(3)通过FutureTask线程结果处理类的有参构造方法来封装Callable接口实现类对象 FutureTask<Object> ft2=new FutureTask<>(mythread2); //(4)使用参数为FutureTask类对象的Thread有参构造方法创建Thread实例。 Thread thread2=new Thread(ft2,"thread2"); //(5)调用线程实例的start()方法启动线程。 thread2.start(); System.out.println("thread1返回的结果为:"+ft1.get()); System.out.println("thread2返回的结果为:"+ft2.get()); } }
FutureTask实现了RunnableFuture接口,RunnableFuture接口继承自Runnable接口和Future接口。所以FutureTask的本质是Runnable接口和Future接口的实现类。
Future接口的方法:
boolean cancel(boolean running)用于取消任务,running参数为是否取消还在运行的任务
boolean isCancelled()用于判断任务是否被取消成功
boolean isDone()判断任务是否已经完成
V get()用于获取执行结果(返回值),这个方法会发生阻塞,一直等到任务执行完才会执行结果。
V get(long timeout,TimeUnit unit)如果指定时间内没有得到结果就返回null
四.3种多线程实现方式的对比分析。
实现Runnable接口比继承Thread类所更具有的优势:
1.可以避免Java中的单继承的局限性
2.线程池只能放入实现Runnable或Callable类的线程,不能直接放入继承Thread类的线程
其它方面的对比,我通过下面一个多窗口售票的案例来对比分析更多优略:
4个窗口卖100张票,这100张票可以看作是共享资源。4个售票窗口可以看成是4个线程。
我们先用第1种方法(继承Thread类)来实现这个案例:
package demo04; //定义一个继承自thread类的子类 class TicketWindow extends Thread{ private int tickets=100; @Override public void run() { while (true){ if (tickets>0){ System.out.println(Thread.currentThread().getName()+"正在发售第"+tickets--+"张票"); } } } } public class test01 { public static void main(String[] args) { //创建4个线程对象来作为窗口卖票 new TicketWindow().start(); new TicketWindow().start(); new TicketWindow().start(); new TicketWindow().start(); } }
运行后会发现重复卖票了,也就是说票没有共享。同一张票被卖了四次。这很明显不是我们想要的答案吧。
这时候就要通过实现Runnable接口来实现多线程:
package demo04; //定义一个实现了Runnable接口的实现类 class TicketWindow implements Runnable{ private int tickets=100; @Override public void run() { while (true){ if (tickets>0){ System.out.println(Thread.currentThread().getName()+"正在发售第"+tickets--+"张票"); } } } } public class test02 { public static void main(String[] args) { //创建TicketWindow实例对象 TicketWindow ticketWindow=new TicketWindow(); new Thread(ticketWindow,"窗口1").start(); new Thread(ticketWindow,"窗口2").start(); new Thread(ticketWindow,"窗口3").start(); new Thread(ticketWindow,"窗口4").start(); } }
这次就可以共享资源了,不会出现票被重复卖的情况。
五.后台线程
新创建的线程默认是前台线程,对于java程序来说,只要有前台线程在运行,这个进程就不会关闭。如果某个线程在启动之前(start()方法)调用了setDaemon(true)语句,这个线程就会变成一个后台线程。
java中,每次程序运行至少启动2个线程,1个是main线程,另一个是垃圾收集线程。因为每当执行一个java程序时,都会启动一个jvm,每一个jvm其实就是操作系统中启动了一个进程