JAVA多线程面试总结之如何控制多线程执行顺序

前言

程序员1~3年是一个从新手走向成熟期的过程,必不可少需要面试或是跳槽等问题,在这里我想给大家分享一下面试相关的经典题目,就以多线程的话题为切入点,针对于常见的面试题目做一个分享,希望对大家有所帮助!

多线程基础

java基础,也是核心。面试必考,如果面试官不问的话,这个工作岗位可能只是做CRUD(然而大厂的crud也被要问这个,不用多说,你们懂)。线程作为操作系统进程的最小单位。jdk对于汇编语言的封装,再到硬件上CPU调度与CPU多核的一级缓存等,设计知识比较广泛,这个时候不要懵逼,缕清思路,多敲代码,善于总结就ok了~

线程的生命周期

在这里插入图片描述

实现多线程都有哪些方式?

Java多线程实现方式主要有四种:继承Thread类、实现Runnable接口、实现Callable接口通过FutureTask包装器来创建Thread线程、使用ExecutorService、Callable、Future实现有返回结果的多线程。
其中前两种方式线程执行完后都没有返回值,后两种是带返回值的。

  1. 继承Thread类创建线程

Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。

public class MyThread extends Thread {  
  public void run() {  
   System.out.println("thread run...");  
  }  
}  
 
MyThread myThread1 = new MyThread();  
MyThread myThread2 = new MyThread();  
myThread1.start();  
myThread2.start();  

2.实现Runnable接口创建线程

public class MyThread extends OtherClass implements Runnable {  
  public void run() {  
   System.out.println("thread run...");  
  }  
}  

为了启动MyThread,需要首先实例化一个Thread,并传入自己的MyThread实例:

MyThread myThread = new MyThread();  
Thread thread = new Thread(myThread);  
thread.start();   

事实上,当传入一个Runnable target参数给Thread后,Thread的run()方法就会调用target.run(),参考JDK源代码:

public void run() {  
  if (target != null) {  
   target.run();  
  }  
}  

3.实现Callable接口通过FutureTask包装器来创建Thread线程
Callable接口(也只有一个方法)定义如下:

public interface Callable<V> { 
 	V call() throws Exception; 
} 

public class SomeCallable<V> extends OtherClass implements Callable<V> {

   @Override
   public V call() throws Exception {
       // TODO Auto-generated method stub
       return null;
   }

}
Callable<V> oneCallable = new SomeCallable<V>();
  //由Callable<Integer>创建一个FutureTask<Integer>对象:   
FutureTask<V> oneTask = new FutureTask<V>(oneCallable);   //注释:FutureTask<Integer>是一个包装器,它通过接受Callable<Integer>来创建,它同时实现了Future和Runnable接口。 
 //由FutureTask<Integer>创建一个Thread对象:   
Thread oneThread = new Thread(oneTask);   
oneThread.start();   //至此,一个线程就创建完成了。
  1. 使用ExecutorService、Callable、Future实现有返回结果的线程
    ExecutorService、Callable、Future三个接口实际上都是属于Executor框架。返回结果的线程是在JDK1.5中引入的新特征,有了这种特征就不需要再为了得到返回值而大费周折了。而且自己实现了也可能漏洞百出。
    可返回值的任务必须实现Callable接口。类似的,无返回值的任务必须实现Runnable接口。
    执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了。
    注意:get方法是阻塞的,即:线程无返回结果,get方法会一直等待。
    再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。
    下面提供了一个完整的有返回结果的多线程测试例子,在JDK1.5下验证过没问题可以直接使用。代码如下:
import java.util.concurrent.*;  import java.util.Date;  import java.util.List;  import java.util.ArrayList;  
  /** 
* 有返回值的线程 */  
@SuppressWarnings("unchecked")  public class Test {  public static void main(String[] args) throws ExecutionException,  
    InterruptedException {  
   System.out.println("----程序开始运行----");  
   Date date1 = new Date();  
  
   int taskSize = 5;  
   // 创建一个线程池  
   ExecutorService pool = Executors.newFixedThreadPool(taskSize);  
   // 创建多个有返回值的任务  
   List<Future> list = new ArrayList<Future>();  
   for (int i = 0; i < taskSize; i++) {  
    Callable c = new MyCallable(i + " ");  
    // 执行任务并获取Future对象  
    Future f = pool.submit(c);  
    // System.out.println(">>>" + f.get().toString());      list.add(f);  
   }  
   // 关闭线程池     pool.shutdown();  
  
   // 获取所有并发任务的运行结果  
   for (Future f : list) {  
    // 从Future对象上获取任务的返回值,并输出到控制台  
    System.out.println(">>>" + f.get().toString());  
   }  
  
   Date date2 = new Date();  
   System.out.println("----程序结束运行----,程序运行时间【"  
     + (date2.getTime() - date1.getTime()) + "毫秒】");  
}  
}  
  class MyCallable implements Callable<Object> {  private String taskNum;  
  
MyCallable(String taskNum) {  
   this.taskNum = taskNum;  
}  
  public Object call() throws Exception {  
   System.out.println(">>>" + taskNum + "任务启动");  
   Date dateTmp1 = new Date();  
   Thread.sleep(1000);  
   Date dateTmp2 = new Date();  
   long time = dateTmp2.getTime() - dateTmp1.getTime();  
   System.out.println(">>>" + taskNum + "任务终止");  
   return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】";  
}  
}  

代码说明:
上述代码中Executors类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。
public static ExecutorService newFixedThreadPool(int nThreads)
创建固定数目线程的线程池。
public static ExecutorService newCachedThreadPool()
创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
public static ExecutorService newSingleThreadExecutor()
创建一个单线程化的Executor。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

ExecutoreService提供了submit()方法,传递一个Callable,或Runnable,返回Future。如果Executor后台线程池还没有完成Callable的计算,这调用返回Future对象的get()方法,会阻塞直到计算完成。

在这里插入代码片

线程同步的3种方式

java线程的同步问题可以通过三种方式实现:
首先创建四个线程:

public class Test01 {
	public static void main(String[] args) {
		//创建接口实现类实例化对象
		Runnable r1 = new TicketRunnableImpl();
		//创建线程
		Thread t1 = new Thread(r1, "窗口一");
		Thread t2 = new Thread(r1, "窗口二");
		Thread t3 = new Thread(r1, "窗口三");
		Thread t4 = new Thread(r1, "窗口四");
		//启动线程
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

1.使用synchronized代码块

  public class TicketRunnableImpl implements Runnable{
    	private int ticketNum = 1000;
    	@Override
    	public void run() {
    		while (ticketNum > 0) {
    			//同步代码块
    			synchronized (this) {
    				//判断
    				if (ticketNum > 0) {
    					ticketNum--;
    					System.out.println(Thread.currentThread().getName() + "售出一张票,剩余:" + ticketNum);
    					try {
    						Thread.sleep(10);
    					} catch (InterruptedException e) {
    						e.printStackTrace();
    					}
    				}
    			}
    		}
    	}
    }

2.使用对象锁

import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
     
    public class TicketRunnableImpl implements Runnable{
    	private int ticketNum = 1000;
    	//创建锁对象
    	Lock lock = new ReentrantLock();
    	@Override
    	public void run() {
    		while (ticketNum > 0) {
    			//上锁
    			lock.lock();
    			//判断
    			if (ticketNum > 0) {
    				ticketNum--;
    				System.out.println(Thread.currentThread().getName() + "售出一张票,剩余:" + ticketNum);
    				try {
    					Thread.sleep(10);
    				} catch (InterruptedException e) {
    					e.printStackTrace();
    				}
    			}
    			//解锁
    			lock.unlock();
    		}
    	}
    }

3.使用同步方法

 public class TicketRunnableImpl implements Runnable{
    	private int ticketNum = 1000;
    	@Override
    	public void run() {
    		while (ticketNum > 0) {
    			sellTickets();
    		}
    	}
    	//同步方法
    	public synchronized void sellTickets() {
    		//判断
    		if (ticketNum > 0) {
    			ticketNum--;
    			System.out.println(Thread.currentThread().getName() + "售出一张票,剩余:" + ticketNum);
    			try {
    				Thread.sleep(100);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		}
    	}
}

什么是线程池?

java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池
多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。
假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。
如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。
一个线程池包括以下四个基本组成部分:
1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1,T3的开销了。
线程池不仅调整T1,T3产生的时间段,而且它还显著减少了创建线程的数目,看一个例子:
假设一个服务器一天要处理50000个请求,并且每个请求需要一个单独的线程完成。在线程池中,线程数一般是固定的,所以产生线程总数不会超过线程池中线程的数目,而如果服务器不利用线程池来处理这些请求则线程总数为50000。一般线程池大小是远小于50000。所以利用线程池的服务器程序不会为了创建50000而在处理请求时浪费时间,从而提高效率。

线程池有几种?实际应用场景以及如何选取?

①newSingleThreadExecutor
单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务
②newFixedThreadExecutor(n)
固定数量的线程池,没提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行
③newCacheThreadExecutor(推荐使用)
可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。
④newScheduleThreadExecutor
大小无限制的线程池,支持定时和周期性的执行线程

线程池的execute与submit的区别?

线程池中的execute方法大家都不陌生,即开启线程执行池中的任务。还有一个方法submit也可以做到,它的功能是提交指定的任务去执行并且返回Future对象,即执行的结果。下面简要介绍一下两者的三个区别:
1、接收的参数不一样
2、submit有返回值,而execute没有
用到返回值的例子,比如说我有很多个做validation的task,我希望所有的task执行完,然后每个task告诉我它的执行结果,是成功还是失败,如果是失败,原因是什么。
然后我就可以把所有失败的原因综合起来发给调用者。
个人觉得cancel execution这个用处不大,很少有需要去取消执行的。
而最大的用处应该是第二点。
3、submit方便Exception处理
意思就是如果你在你的task里会抛出checked或者unchecked exception,
而你又希望外面的调用者能够感知这些exception并做出及时的处理,那么就需要用到submit,通过捕获Future.get抛出的异常。
下面一个小程序演示一下submit方法

public class RunnableTestMain {

    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        
        /**
         * execute(Runnable x) 没有返回值。可以执行任务,但无法判断任务是否成功完成。
         */
        pool.execute(new RunnableTest("Task1")); 
        
        /**
         * submit(Runnable x) 返回一个future。可以用这个future来判断任务是否成功完成。请看下面:
         */
        Future future = pool.submit(new RunnableTest("Task2"));
        
        try {
            if(future.get()==null){//如果Future's get返回null,任务完成
                System.out.println("任务完成");
            }
        } catch (InterruptedException e) {
        } catch (ExecutionException e) {
            //否则我们可以看看任务失败的原因是什么
            System.out.println(e.getCause().getMessage());
        }

    }

}

public class RunnableTest implements Runnable {
    
    private String taskName;
    
    public RunnableTest(final String taskName) {
        this.taskName = taskName;
    }

    @Override
    public void run() {
        System.out.println("Inside "+taskName);
        throw new RuntimeException("RuntimeException from inside " + taskName);
    }

}

java提供的线程池更加强大,相信理解线程池的工作原理,看类库中的线程池就不会感到陌生了。
在这里插入图片描述
在这里插入图片描述

如何控制多线程执行顺序

 /**
     * jdk1.8 - Lambda简化匿名内部类
     */
    static Thread thread1 = new Thread(()-> System.out.println("thread1"));

    static Thread thread2 = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("thread2");
        }
    });

    static Thread thread3 = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("thread3");
        }
    });

    public static void main(String[] args) throws InterruptedException{
        /**
         * start方法直接使当前线程进入就绪状态
         * 但是要等到cpu进行调度,现在主线程是随机调用的
         */
        thread1.start();
        thread2.start();
        thread3.start();
        /**
         * 如何控制多线程执行顺序-使用join()
         * join的执行原理:join方法会使,当前主线程main放弃cpu的控制权,使join的线程执行完毕前,主线程陷入休眠状态
         * TODO:join源码跟踪发现调用的是Object的wait方法,但是join与wait方法有所不同
         */
        thread1.start();
        thread1.join();
        thread2.start();
        thread2.join();
        thread3.start();
        /**
         * 如何控制多线程执行顺序-线程池
         * FIFO-队列方式执行多线程
         */
        executorService.submit(thread1);
        executorService.submit(thread2);
        executorService.submit(thread3);
        executorService.shutdown();
    }

以上代码分享在github上:代码分享https://github.com/harrypottry/testgit

发布了47 篇原创文章 · 获赞 5 · 访问量 1889

猜你喜欢

转载自blog.csdn.net/qq_34361283/article/details/102905394