Java面试总结-线程

2016年03月04日

Java面试总结-线程

1、创建一个线程

  • 创建线程主要分为两个方法
    • implements Runnable接口并实现run()方法,然后由Runnable对象创建一个Thread对象,调用Tread的start()方法启动线程。
    • extends Thread 构建一个Thread类的子类,复写run()方法。该方法目前已不再推荐,应该从运行机制上减少需要并行运行的任务数量。
    • 警告:不要调用Thread类或Runnable对象的run方法。直接调用run方法,只会执行同一个线程中的任务,而不会启动新线程??。应该调用Tread.start方法,这个方法将创建一个执行run方法的新线程。
 /**
 * 创建线程的两种方式
 */
public class TestCreateTread 
{
	public static void main(String[] args)
	{
		MyRunnable one = new MyRunnable("one");
		Thread thread1 = new Thread(one);
		
		MyRunnable two = new MyRunnable("two");
		Thread thread2 = new Thread(two);
		
		thread2.start();
		thread1.start();
		
		Thread three = new MyThread("three");
		Thread four = new MyThread("four");
		three.start();
		four.start();
	}
}

class MyRunnable implements Runnable
{
	private String name;
	public MyRunnable(String name) 
	{
		this.name = name;
	}
	@Override
	public void run() {
		System.out.println("excute runnable " + this.name);
	}
	
}

class MyThread extends Thread
{
	private String name;
	public MyThread(String name)
	{
		this.name = name;
	}
	
	@Override
	public void run()
	{
		System.out.println("excute thread " + this.name);
	}
}
  • 线程的一些常见问题:
    • 线程的名字,一个运行中的线程总是有名字的,名字有两个来源,一个是虚拟机自己给的名字,一个是你自己的定的名字。在没有指定线程名字的情况下,虚拟机总会为线程指定名字,并且主线程的名字总是mian,非主线程的名字不确定。
    • 获取当前线程的对象的方法是:Thread.currentThread();
    • 每个线程都将启动,每个线程都将运行直到完成。一系列线程以某种顺序启动并不意味着将按该顺序执行。对于任何一组启动的线程来说,调度程序不能保证其执行次序,持续时间也无法保证。
    • 一旦线程启动,它就永远不能再重新启动。只有一个新的线程可以被启动,并且只能一次。一个可运行的线程或死线程可以被重新启动。

2、线程的同步

在大多数实际的多线程应用中,两个或两个以上的线程需要共享对同一数据的存取。根据各线程访问数据的次序,可能会产生讹误的对象。为避免多线程引起的对共享数据的讹误,必须学习如何同步存取。


  • 使用ReentrantLock类对线程进行加锁:
    • ReentrantLock() 构建一个可以被用来保护临界区的可重入锁;
    • ReentrantLock(boolean fair) 构建一个带有公平策略的锁。一个公平锁偏爱等待时间最长的线程。但是,这一公平的保证将大大降低性能。所以默认情况下,锁没有被强制为公平的。
    • void lock() 获取这个锁;如果锁同时被另一个线程拥有则发生阻塞;
    • void unlock() 释放这个锁。
//这一结构确保任何时刻只有一个线程进入临界区,一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。
//当其他线程调用lock时,他们被阻塞,直到第一个线程释放锁对象。
	private Lock myLock = new ReentrantLock();
	public void run()
	{
		myLock.lock();
		try {
			
		} finally {
          //把解锁操作括在finally子句之内是至关重要的。如果在临界区的代码抛出异常,锁必须被释放。否则其他线程将永远阻塞。
			myLock.unlock();
		}
	}
  • 使用条件对象Condition阻塞线程
    • Condition newCondition() 返回一个与该锁相关的条件对象。
    • void await() 将该线程放到条件的等待集中,阻塞该线程。当一个线程调用await时,它没有办法重新激活自身,只能寄希望于其他线程,最终可能导致死锁(deadlock)现象。
    • void signalAll() 解除该条件的等待集中的所有线程的阻塞状态。
    • void signal() 从该条件的等待集中随机地选择一个线程,解除其阻塞状态。
	private Lock myLock = new ReentrantLock();
	private Condition condition = myLock.newCondition();
	public void run()
	{
		myLock.lock();
		try {
			while(!(ok to proceed))//判断是否满足线程继续执行的条件
				condition.wait();//当前线程被阻塞了,并放弃了锁。
			
			//
			condition.signalAll();
			
		} finally {
			myLock.unlock();
		}
	}

  • synchronized关键字
    • Java中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。
    • Objcet类的final方法wait() 添加一个线程到等待集中;
    • Objcet类的final方法notifyAll()/notify() 解除等待线程的阻塞状态。
    • 静态static方法也可以声明为synchronized,如果调用这种方法,该方法获得相关的类对象的内部锁。

同步器

中文名类名它能做什么何时使用
信号量Semaphore允许线程集等待直到被允许继续运行为止限制访问资源总数。如果许可数是1,常常阻塞线程直到另一个线程给出许可为止。
倒计时门栓CountDownLatch允许线程集等待直到计数器减为0.当一个或多个线程需要等待直到指定数目的事件发生。
障栅CyclicBarrier允许线程集等待直至其中预订数目的线程到达一个公共障栅(barrier),然后可以选择执行一个处理障栅的动作。当大量的线程需要在它们的结果可用之前完成时。
交换器Exchanger允许两个线程在要交换的对象准备好时交换对象。当两个线程工作在同一个数据结构的两个实例上的时候,一个向实例添加数据而另一从实例清除数据。
同步队列SynchronousQueue允许线程把一对象交给另一个线程。在没有显式同步的情况下,当两个线程准备好将一个对象从一个线程传递另一个时。

3、线程池

  • 执行器(Executor)类用来构建线程池的静态工厂方法:
方法描述
newCachedThreadPool必要时创建新线程;空闲线程会被保留60秒。
newFixedThreadPool该池包含固定数量的线程;空闲线程会被一直保留。
newSingleThreadExecutor只有一个线程的“池”,该线程顺序执行每一个提交的任务。
newScheduledThreadPool用于预定执行而构建的固定线程池,替代java.util.Timer
newSingleThreadScheduledExecutor用于预定执行而构建的单线程“池”。
  • 合理利用线程池能的好处
    • 第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
    • 第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
    • 第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  • 线程池的创建:

一个ThreadPoolExecutor的创建需要考虑三个方面:线程池的大小任务队列的大小饱和策略

new  ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler);
  • Executor核心的思想就是将请求处理任务的提交线程和任务的实际执行解耦开来。利用execute来传递一个具体执行的Runnable任务类,或者利用submit来传递一个Runnable任务类或Callable获取任务返回值的任务。
  • 对于每次通过execute方法提交的任务执行顺序如下:
    • 1、会判断当前池线程以及核心数目的大小,当池中当前的线程数小于核心线程数时,会创建新的线程。具体创建新线程流程如:获取内置锁,将任务添加到内部的BlockingQueue任务队列中,再利用工厂方法产生一个执行该任务的线程,这个线程是非守护及优先级是NORM的线程。
    • 2、当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
    • 3、当线程数大于等于核心线程数,且任务队列已满,采用以下处理方式: 若线程数小于最大线程数,创建线程; 若线程数等于最大线程数,抛出异常,拒绝任务,进行饱和处理策略。
  • 饱和策略解释:
    • AbortPolicy,即中止策略,是默认的饱和策略,该策略将抛出未检查的RejectedExecutionException。
    • DiscardPolicy,即抛弃策略,会丢弃队列满后请求的任务。
    • DiscardOldestPolicy,即抛弃最旧的策略,会抛弃下一个将要被执行的任务,然后尝试重新提交新任务。
    • CallerRunsPolicy,即调用者策略,既不会抛弃任务,也不会抛出异常,而是将任务回退到调用者。它不会在线程池的某个线程执行新提交的任务,而是在一个调用execute的线程中执行该任务。

参考文档

1、 Java多线程:Java多线程同步与synchronized
2、 Java并发编程:线程池的使用
3、Java线程池Executors
4、聊聊并发(三)——JAVA线程池的分析和使用