Java必知必会(一)——多线程并发

开一个系列来记录我的Java学习之路,这些东西都是从自己在学习的过程中总结出来的,应该算是针对面试中遇到的问题,有不正确的地方还望大佬们指出,算是自己的学习分享吧。

1、线程的四种实现方式

多线程的实现方式有四种,面试也只需要答出来这四种即可。

  1. 继承Thread类,重写Run方法,创建线程。
public static class MyThread extends Thread{
		@Override
		public void run() {
			// TODO 自动生成的方法存根
			System.out.println("线程一");
		}
	}
	public static void main(String[] args) {
		MyThread t1 = new MyThread();
		t1.start();
	}
  1. 实现Runnable接口,重写Run方法,创建线程。
public static class MyThread implements Runnable{
		@Override
		public void run() {
			// TODO 自动生成的方法存根
			System.out.println("线程一");
		}
	}
	public static void main(String[] args) {
		MyThread mt = new MyThread();
		Thread t = new Thread(mt);
		t.start();
	}
  1. 实现Callable接口,重写Run方法,通过FutureTask包装器,创建线程。
public static class MyThread implements Callable<Integer>{
		@Override
		public Integer call() throws Exception {
			// TODO 自动生成的方法存根
			return null;
		}
	}
	public static void main(String[] args) {
		MyThread mt = new MyThread();
		FutureTask<Integer> ft = new FutureTask<>(mt);
		Thread t = new Thread(ft);
		t.start();
	}
  1. 通过线程池Executor创建线程。
public static class MyThread extends Thread{
		@Override
		public void run() {
			// TODO 自动生成的方法存根
			super.run();
		}
	}
	public static void main(String[] args) {
		ExecutorService pool = Executors.newFixedThreadPool(5);
		MyThread t = new MyThread();
		pool.submit(t);
		pool.shutdown();
	}
  • 启动线程的方法有两种,一种是调用Thread类的start()方法,另一种是通过线程池。(注意:线程的run()方法是执行过程,启动线程的方法是start(),假如直接调用run()方法,线程只会执行一次)。
  • Callable接口与Runnable接口的区别在于Callable接口有返回值,而前两种方法没有返回值,因此我们通常通过Callable、Executor框架、Future实现有结果返回的线程,比如异步计算等。
2、线程池参数

可以看到,部分线程池在创建时是初始化参数

java竞争条件与并发 java实现并发的方式_java


虽然看起来在调用线程池的构造函数时最多只需要两个参数,但实际上可以通过设置函数(Setter)来修改线程池的配置,常见的参数有5个:

  1. corepoolSize:线程池的基本大小,即核心线程数
  2. maximumPoolSize:线程池的最大大小,即最大线程数
  3. keepAliveTime:空闲线程的存活时间。
  4. workQueue:缓存工作队列
  5. threadFactory:线程工厂,用来创建线程池中的工作线程
3、四种线程池

通过Executor框架实现线程创建的方法中,有四种线程池可以使用,分别应用于不同场景:ExecutorService pool = Executors.newFixedThreadPool(5);

  1. newFixedThreadPool:创建一个定长线程池。即设置corepoolSize和maximumPoolSize的大小,并且线程池中的线程数不超过maximumPoolSize。
  2. newCachedThreadPool:创建一个可缓存线程池。即设置corepoolSize为0,maximumPoolSize为Integer.MAX_VALUE,不限制线程池中线程数量。
  3. newScheduledThreadPool:创建一个定长的线程池,并且以延迟或定时的方式执行任务,一般用于执行定时任务。
  4. newSingleThreadExecutor:创建一个单线程的Executor。即线程池中只有一个工作线程,当该线程异常结束时,会创建另一个线程替代,一般用于确保队列任务的顺序执行。
4、线程的生命周期
  • 新建状态(New):
    当你创建一个线程对象时,线程便进入了新建状态Thread theard = new MyThread();
  • 就绪状态(Runnable):
    当你调用线程对象的start(),方法时,线程便进入就绪状态。thread.start();注意:调用了start()方法并不等于立即执行,而是在等待CPU调度。
  • 运行状态(Running):
    当CPU调度处于就绪状态的线程时,线程便进入执行状态,此时执行的是线程的run()。注意:线程只有处于就绪状态才能进入到执行状态,其他状态都不可以直接进入执行状态。
  • 阻塞状态(Blocked):
    当执行状态的线程由于某种原因暂时放弃对CPU的使用权,线程便会进入阻塞状态。阻塞状态根据原因可以分为三种:
  1. 等待阻塞:线程执行wait()方法。
  2. 同步阻塞:线程获取锁失败时。
  3. 其他阻塞:线程调用sleep()、join()方法、发出I/O请求时。注意:当sleep()状态超时、join()终止或超时、I/O处理完毕时进入就绪状态。
  • 死亡状态(Dead):
    线程执行完成或因异常退出run()方法,线程结束生命周期。
5、各种线程方法的区别
  • Thread.join():作用是同步线程。举例:A线程中调用B线程的join()方法,则只有执行完B线程才能继续执行A线程。可有参数,参数为等待B线程执行的时间长短。注意当参数为0时,表示等待B线程执行完毕。
  • Thread.sleep():较为常用的方法,作用是使正在执行的线程暂停执行,休眠指定时间。注意:使用sleep()方法并不会释放锁。我们经常使用sleep()方法手动触发系统分配时间片的操作,从而应对某条线程经常获取CPU控制权的情况。
  • Thread.yield():作用是放弃当前CPU资源,使线程回到就绪状态。与sleep()方法的区别在于yield()不会控制放弃资源的时间长短,仅仅是使线程回到就绪状态,也就是说有可能出现刚放弃就又获得CPU资源的情况。注意:yield()同样不会释放锁。
  • Thread.wait():作用是使线程进入等待队列,前提是线程必须占有锁。注意:调用wait()方法会释放当前线程占有的锁。可以通过其他线程调用notify()方法回到执行状态。
  • Thread.notify():作用是唤醒处于沉睡状态下的线程,且只唤醒一个。
  • Thread.notifyAll():作用是唤醒处于沉睡状态下的所有线程。
  • 一些区别:sleep()与yield()方法是属于线程的静态方法,不可被重写;而wait()与notify()与notifyAll()是对象类Object下的方法,既然是对象下的方法,那么这三个方法便常用来操作锁。
6、同步工具类
  • CountDownLatch
    闭锁是一种同步工具类,可以延迟线程的进度直到其到达终止状态。我们比较熟悉的CountDownLatch就是一种灵活的闭锁实现。简单地描述它的作用就是使线程进行等待一组事件发生。(也可描述为等待其他线程执行完毕)。
    闭锁状态包含一个计数器,初始化为一个正数,表示等待的事件数。countDown方法会递减计数器,表示一个事件发生。当计数器为0时,表示等待的事件全部发生了,非0时,线程会一直阻塞。
  • CyclicBarrier
    栅栏类似闭锁,可以阻塞一组线程直到某个事件发生。CyclicBarrier就是栅栏的一种实现。它的作用就是当所有线程到达栅栏处,都进行等待,直到所有线程都准备好进入await()方法之后,所有线程同时开始执行。
  • Semaphore
    信号量。
  • Excharnger
    另一种形式的栅栏,两方栅栏。
7、CountDownLatch和CyclicBarrier区别
  • CountDownLatch的计数器只能使用一次,CyclicBarrier的计数器可以使用reset()方法重置。
  • CyclicBarrier提供了许多方法。getNumberWaiting()方法:CyclicBarrier阻塞的线程数量。isBroken方法:阻塞线程是否被中断。
8、ThreadLocal
  • 我们经常会听到这个名词,那么这个ThreadLocal是个什么东西?
  • 定义:ThreadLocal是线程内部的一个存储类,提供了线程存储变量的能力,每一个线程存储的变量都是互相独立的。
  • 实现:ThreadLocal的实现依赖于三个类,分别是Thread、ThreadLocal、ThreadLocalMap。ThreadLocal就是它自身了,那么需要其他两个类干什么?ThreadLocal当中最重要的两个方法就是get()和set()方法,我们可以从它们的源码中看出关联。
//set 方法
public void set(T value) {
      //获取当前线程
      Thread t = Thread.currentThread();
      //实际存储的数据结构类型
      ThreadLocalMap map = getMap(t);
      //如果存在map就直接set,没有则创建map并set
      if (map != null)
          map.set(this, value);
      else
          createMap(t, value);
  }
  
//getMap方法
ThreadLocalMap getMap(Thread t) {
      //thred中维护了一个ThreadLocalMap
      return t.threadLocals;
 }
 
//createMap
void createMap(Thread t, T firstValue) {
      //实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
      t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//get 方法
public T get() {
	//获取当前线程
	 Thread t = Thread.currentThread();
	 //实际存储的数据结构类型
	 ThreadLocalMap map = getMap(t);
	 if (map != null) {
	 	ThreadLocalMap.Entry e = map.getEntry(this);
	 if (e != null) {
	 	@SuppressWarnings("unchecked")
	 	T result = (T)e.value;
	 	return result;
	 	}
	 }
	 return setInitialValue();
}

private T setInitialValue() {
	 T value = initialValue();
	 Thread t = Thread.currentThread();
	 ThreadLocalMap map = getMap(t);
	 if (map != null)
	 	map.set(this, value);
	 else
		createMap(t, value);
	 return value;
}
  • 可以看出,Thread用于获取当前线程,ThreadLocalMap则是每一个线程中实际存储ThreadLocal传过来的值的数据结构,并且每个线程有且仅有一个ThreadLocalMap。
  • 应用场景:
  1. 解决多线程并发过程中,共享变量冲突问题
  2. 数据库连接的安全问题(《Java并发编程实战》P37页实例)
    代码如下
private static ThreadLocal<Connection> connectionHolder
	= new ThreadLocal<Connection>() {
		public Connection initialValue(){
			return DriverManager.getConnection(DB_URL);
		}
	};
public static Connection getConnection(){
	return connectionHolder.get();
}
  • 总结:ThreadLocal常常用于多线程当中对全局共享变量的安全访问。