高并发编程基础(线程池基础)

线程池简单基础介绍:

Executor:

  Executor是Java工具类,执行提交给它的Runnable任务。该接口提供了一种基于任务运行机制的任务提交方法,包括线程使用详细信息,时序等等。Executor通常用于替代创建多线程。

  提供一个execute(Runnable command)方法;我们一般用它的继承接口ExecutorService。里面就只有一个执行任务的接口,源码如下:

public interface Executor {

    void execute(Runnable command);
}

ExecutorService:

  它是线程池定义的一个接口,继承自Executor。有两个实现类,分别为ThreadPoolExecutor,ScheduledThreadPoolExecutor。除了继承自父类的 execute 执行方法 ,自己还定义了一系列方法,其中有两个重载提交任务的方法 submit 方法,参数可以是 Runnable 或者是 Callable类型的。

<T> Future<T> submit(Callable<T> task);

<T> Future<T> submit(Runnable task, T result);

  这里既然提到了Callable 那就先来看一下这了接口跟Runnable:

Callable

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

Runnable:

public interface Runnable {
    public abstract void run();
}

相同点:

  1. 两者都是接口;
  2. 两者都可用来编写多线程程序;
  3. 两者都需要调用Thread.start()启动线程;

不同点:

  1. 两者最大的不同点是:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;
  2. Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;

注意点:

  Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!

Executors:

  是java.util.concurrent包下的一个类,提供了若干个静态方法,用于生成不同类型的线程池的工具类,有点类似与Arrays。Executors一共可以创建下面这四类线程池:

  1. newCacheThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  2. newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  3. newScheduledThreadPool 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
  4. newSingleThreadExecutor 创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

ThreadPool:

  合理利用线程池能够带来三个好处。第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。但是要做到合理的利用线程池,必须对其原理了如指掌。

  线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。用线程池来管理的好处是,可以保证系统稳定运行,适用与有大量线程,高工作量的情景下使用,假如要展示1000张图片如果创建1000个线程去加载,系统肯定会死掉。用线程池就可以避免这个问题,可以用5个线程轮流执行,5个一组,执行完的线程不直接回收而是等待下次执行,这样对系统的开销就可以减小不少。

public class T05_ThreadPool {
	public static void main(String[] args) {
		//创建一个5个线程的线程池
		ExecutorService service = Executors.newFixedThreadPool(5);
		for(int i=0;i<6;i++) {//往池子里仍了6个任务
			service.execute(()->{//睡500毫秒后打印线程名
			try {
				TimeUnit.MILLISECONDS.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName());
			});
		}
		//java.util.concurrent.ThreadPoolExecutor@119d7047[Running, pool size = 5, active threads = 5, queued tasks = 1, completed tasks = 0]
		System.out.println(service);
		service.shutdown();//关闭线程池 等待任务都执行完再关闭
		System.out.println(service.isTerminated());//false 任务是否都执行完
		System.out.println(service.isShutdown());//true 是不是关闭? 关闭了不代表任务执行完。
		//java.util.concurrent.ThreadPoolExecutor@119d7047[Shutting down, pool size = 5, active threads = 5, queued tasks = 1, completed tasks = 0]
		System.out.println(service);
		try {
			TimeUnit.SECONDS.sleep(5);//睡5秒
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(service.isTerminated());//true
		System.out.println(service.isShutdown());//true
		//java.util.concurrent.ThreadPoolExecutor@119d7047[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 6]
		System.out.println(service);
		
	}
}

Futrue: 

  在并发编程中,我们经常用到非阻塞的模型,在之前的多线程的三种实现中,不管是继承thread类还是实现runnable接口,都无法保证获取到之前的执行结果。通过实现Callback接口,并用Future可以来接收多线程的执行结果。

        Future表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback以便在任务执行成功或失败后作出相应的操作。

        举个例子:比如去吃早点时,点了包子和凉菜,包子需要等3分钟,凉菜只需1分钟,如果是串行的一个执行,在吃上早点的时候需要等待4分钟,但是因为你在等包子的时候,可以同时准备凉菜,所以在准备凉菜的过程中,可以同时准备包子,这样只需要等待3分钟。那Future这种模式就是后面这种执行模式。

  
public class T06_Future {

	public static void main(String[] args) throws InterruptedException, ExecutionException {
		//FutureTask 区分 RunnableTask  也是实现了Runnable接口的
		FutureTask<Integer> task = new FutureTask<>(()-> {
			try {// 该任务将来会有个返回值是Integer类型  
				TimeUnit.MILLISECONDS.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return 1000;
		}); // new Callable()
		
		new Thread(task).start();
		
		System.out.println(task.get());//阻塞
		
		//************************************
		ExecutorService service = Executors.newFixedThreadPool(5);
		Future<Integer> f = service.submit(()->{// callable
			try {
				TimeUnit.MILLISECONDS.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return 1;
		});
		System.out.println(f.isDone());// 任务执行完没有啊?
		System.out.println(f.get()); // 阻塞  1
		System.out.println(f.isDone());
	}
}

  原理流程图如下:

CompletableFuture:

  通过Future同步等待执行结果,CompletionStage,增强异步回调的功能。

构建一个CompletableFuture

  • supplyAsync 异步执行一个任务,提供返回值
  • supplyAsync(runnable,Executor executor) 提供返回值
  • runAsync(runnable,Executor executor) -> 异步执行一个任务,但是可以自定义线程池,没有返 回值
  • runAsync(runnable) -> 异步执行一个任务, 默认用ForkJoinPool.commonPool();,没有返回值

CompletionStage:

  纯消费类型的方法:指依赖上一个异步任务的结果作为当前函数的参数进行下一步计算,它的特点 是不返回新的计算值,这类的方法都包含 Accept 这个关键字。在CompletionStage中包含9个 Accept关键字的方法,这9个方法又可以分为三类:依赖单个CompletionStage任务完成,依赖两 个CompletionStage任务都完成,依赖两个CompletionStage中的任何一个完成。

//当前线程同步执行
public CompletionStage<Void> thenAccept(Consumer<? super T> action);
//使用ForkJoinPool.commonPool线程池执行action
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action);
//使用自定义线程池执行action
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T>
action,Executor executor);
public <U> CompletionStage<Void> thenAcceptBoth(CompletionStage<? extends U>
other,BiConsumer<? super T, ? super U> action);
public <U> CompletionStage<Void> thenAcceptBothAsync(CompletionStage<?
extends U> other,BiConsumer<? super T, ? super U> action);
public <U> CompletionStage<Void> thenAcceptBothAsync(CompletionStage<?
extends U> other,BiConsumer<? super T, ? super U> action,Executor executor);
public CompletionStage<Void> acceptEither(CompletionStage<? extends T>
other,Consumer<? super T> action);
public CompletionStage<Void> acceptEitherAsync(CompletionStage<? extends T>
other,Consumer<? super T> action);
public CompletionStage<Void> acceptEitherAsync(CompletionStage<? extends T>
other,Consumer<? super T> action,Executor executor);

  有返回值类型的方法:就是用上一个异步任务的执行结果进行下一步计算,并且会产生一个新的有 返回值的CompletionStage对象。 在CompletionStage中,定义了9个带有返回结果的方法,同样也可以分为三个类型:依赖单个 CompletionStage任务完成,依赖两个CompletionStage任务都完成,依赖两个CompletionStage 中的任何一个完成。

public <U> CompletionStage<U> thenApply(Function<? super T,? extends U> fn);
public <U> CompletionStage<U> thenApplyAsync(Function<? super T,? extends U>
fn);
public <U> CompletionStage<U> thenApplyAsync(Function<? super T,? extends U>
fn,Executor executor);
public <U,V> CompletionStage<V> thenCombine(CompletionStage<? extends U>
other,BiFunction<? super T,? super U,? extends V> fn);
public <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends
U> other,BiFunction<? super T,? super U,? extends V> fn);
public <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends
U> other,BiFunction<? super T,? super U,? extends V> fn,Executor executor);
public <U> CompletionStage<U> applyToEither(CompletionStage<? extends T>
other,Function<? super T, U> fn);
public <U> CompletionStage<U> applyToEitherAsync(CompletionStage<? extends
T> other,Function<? super T, U> fn);
public <U> CompletionStage<U> applyToEitherAsync(CompletionStage<? extends
T> other,Function<? super T, U> fn,Executor executor);

  不消费也不返回的方法

public CompletionStage<Void> thenRun(Runnable action);
public CompletionStage<Void> thenRunAsync(Runnable action);
public CompletionStage<Void> thenRunAsync(Runnable action,Executor
executor);
public CompletionStage<Void> runAfterBoth(CompletionStage<?> other,Runnable
action);
public CompletionStage<Void> runAfterBothAsync(CompletionStage<?>
other,Runnable action);
public CompletionStage<Void> runAfterBothAsync(CompletionStage<?>
other,Runnable action,Executor executor);
public CompletionStage<Void> runAfterEither(CompletionStage<?>
other,Runnable action);
public CompletionStage<Void> runAfterEitherAsync(CompletionStage<?>
other,Runnable action);
public CompletionStage<Void> runAfterEitherAsync(CompletionStage<?>
other,Runnable action,Executor executor);

   多任务组合:

public <U> CompletionStage<U> thenCompose(Function<? super T, ? extends
CompletionStage<U>> fn);
public <U> CompletionStage<U> thenComposeAsync(Function<? super T, ? extends
CompletionStage<U>> fn);
public <U> CompletionStage<U> thenComposeAsync(Function<? super T, ? extends
CompletionStage<U>> fn,Executor executor);

  异常处理:

  • whenComplete表示当任务执行完成后,会触发的方法,它的特点是,不论前置的CompletionStage任务是正常执行结束还是出现异常,都能够触发特定的 action 方法,主要方法 如下。
  • handle表示前置任务执行完成后,不管前置任务执行状态是正常还是异常,都会执行handle中的 fn 函数,它和whenComplete的作用几乎一致,不同点在于,handle是一个有返回值类型的方 法。
  • exceptionally接受一个 fn 函数,当上一个CompletionStage出现异常时,会把该异常作为参数传 递到 fn 函数

ParallerComputing(并行计算)

newFixedThreadPool:(固定线程的线程池)

  测试例子:计算1-20W之间的质数数量,用一个线程的话计算时间会很长。我们可以用线程池来解决:

public class T07_ParallerComputing {
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		long start = System.currentTimeMillis();
		// 获取 1-200000的质数,只能被1跟自身整除
		List<Integer> results = getPrime(1,200000);
		long end = System.currentTimeMillis();
		System.out.println(end - start);// 2000 左右
		
		final int cpuCoreNum =4;
		
		ExecutorService service = Executors.newFixedThreadPool(cpuCoreNum);
		MyTask task1 = new MyTask(1, 80000);
		MyTask task2 = new MyTask(80001, 130000);
		MyTask task3 = new MyTask(130001, 170000);
		MyTask task4 = new MyTask(170001, 200000);
		
		start = System.currentTimeMillis();
		Future<List<Integer>> submit1 = service.submit(task1);
		Future<List<Integer>> submit2 = service.submit(task2);
		Future<List<Integer>> submit3 = service.submit(task3);
		Future<List<Integer>> submit4 = service.submit(task4);
		
		submit1.get();
		submit2.get();
		submit3.get();
		submit4.get();
		
		end = System.currentTimeMillis();
		System.out.println(end - start); // 800左右
	}

	static class MyTask implements Callable<List<Integer>> {
		int startPos, endPos;

		private MyTask(int startPos, int endPos) {
			this.startPos = startPos;
			this.endPos = endPos;
		}

		@Override
		public List<Integer> call() throws Exception {
			List<Integer> results = getPrime(startPos, endPos);
			return results;
		}
	}

	static boolean isPrime(int num) {// 是否是质数
		for (int i = 2; i < num / 2; i++) {
			if (num % i == 0)
				return false;
		}
		return true;
	}

	static List<Integer> getPrime(int start, int end) {
		List<Integer> results = new ArrayList<Integer>();
		for (int i = start; i < end; i++) {
			if (isPrime(i))
				results.add(i);
		}
		return results;
	}
}

  两次执行结果大致为 2067  ,698 .这说明用线程池会大大提高计算效率。

newCachedThreadPool:

  刚刚开始线程池里面没有线程,来一个任务启动一个线程,如果有空闲的线程就直接执行任务,没有空闲就另起一个线程,每个线程超过 60 秒的空闲时间,线程消失,可以自己指定生存时间。

public class T08_CachedPool {

	public static void main(String[] args) throws InterruptedException {
		ExecutorService service = Executors.newCachedThreadPool();
		//java.util.concurrent.ThreadPoolExecutor@55f96302[Running, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]
		System.out.println(service);
		for(int i=0;i<2;i++) {
			service.execute(()->{//睡500毫秒后打印线程名
				try {
					TimeUnit.MILLISECONDS.sleep(500);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName());
				});
		}
		//java.util.concurrent.ThreadPoolExecutor@55f96302[Running, pool size = 2, active threads = 2, queued tasks = 0, completed tasks = 0]
		System.out.println(service);
		
		TimeUnit.SECONDS.sleep(80);
		
		//java.util.concurrent.ThreadPoolExecutor@55f96302[Running, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 2]
		System.out.println(service);
	}
}

newSingleThreadExecutor:

  线程池里面只有一个线程,代码如下:

public class T09_SingleThreadPool {

	public static void main(String[] args) throws InterruptedException {
		ExecutorService service = Executors.newSingleThreadExecutor();
		for (int i = 0; i < 5; i++) {
			final int j = i;
			service.execute(() -> { 
				System.out.println(j + " " + Thread.currentThread().getName());
			});
//			0 pool-1-thread-1
//			1 pool-1-thread-1
//			2 pool-1-thread-1
//			3 pool-1-thread-1
//			4 pool-1-thread-1
		}
	}
}

  这个可以保证任务的先后执行顺序,打印出来的结果是按顺序的,且只有一个线程去执行。

newScheduledThreadPool:定时器线程池

  以下小程序是启动后0秒开始执行,每隔500毫秒执行一次任务

public class T09_SingleThreadPool {

	public static void main(String[] args) throws InterruptedException {
		  ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
		  service.scheduleAtFixedRate(()->{//以固定的频率来执行任务
			  try {
				TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			  System.out.println(Thread.currentThread().getName());
		  }, 0, 500, TimeUnit.MILLISECONDS);// 起始延迟多久后执行 ,每隔500毫秒执行一次,时间单位
	}
}

newWorkStealingPool:精灵线程(守护线程,后台线程)

  任务窃取:线程池中每个线程都维护着自己的任务队列,当某一个线程队列执行空了,他会去另外的线程中去拿一个任务来执行,不用去分配:

public class T11_WorkStealingPool {

	public static void main(String[] args) throws InterruptedException, IOException {
		ExecutorService service = Executors.newWorkStealingPool();
		//查看CPU是几核 我这里是8  默认启动8个线程java.util.concurrent.ForkJoinPool@55f96302[Running, parallelism = 8, size = 0, active = 0, running = 0, steals = 0, tasks = 0, submissions = 0]
		System.out.println(Runtime.getRuntime().availableProcessors());
		//java.util.concurrent.ForkJoinPool@55f96302[Running, parallelism = 8, size = 0, active = 0, running = 0, steals = 0, tasks = 0, submissions = 0]
		System.out.println(service);
		
		service.execute(new R(1000));
		service.execute(new R(1000));
		service.execute(new R(1000));
		service.execute(new R(1000));//daemon 精灵线程Debug 可以查看
		service.execute(new R(1000));
		
		//由于产生的是精灵线程(守护线程,后台线程),主线程不阻塞的话看不到输出
		System.in.read();
	}
	static class R implements Runnable{
		
		int time;
		
		public R(int time) {
			this.time =time;
		}

		@Override
		public void run() {
			try {
				TimeUnit.MILLISECONDS.sleep(time);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			  System.out.println(time +" "+ Thread.currentThread().getName());
		}
	}
}

   newWorkStealingPool的实现是ForkJoinPool;

ForkJoin的基本应用:

   Java 7开始引入了一种新的Fork/Join线程池,它可以执行一种特殊的任务:把一个大任务拆成多个小任务并行执行。我们举个例子:如果要计算一个超大数组的和,最简单的做法是用一个循环在一个线程内完成。还有一种方法,可以把数组拆成两部分,分别计算,最后加起来就是最终结果。这个过程可以反复“裂变”成一系列小任务。

  我们来看如何使用Fork/Join对大数据进行并行求和:

public class ForkJoinPoolTest {
    static int[] nums= new int[1000000];
    static final int MAX_NUM =50000;
    static Random r=new Random();

    static {
        for(int i=0;i<nums.length;i++) {
            nums[i] = r.nextInt(100);
        }
        System.out.println(Arrays.stream(nums).sum());
    }
    //无返回值
    static class AddTask extends RecursiveAction {
        int start, end;

        private AddTask(int start, int end) {
            this.start = start;
            this.end = end;
        }
        @Override
        protected void compute() {
            if(end -start <MAX_NUM) {
                long sum =0L;
                for(int i=start;i<end ;i++) sum += nums[i];
                System.out.println("from "+start +" to "+end+" = "+sum);
            }else {
                int middle =start +(end-start)/2;
                AddTask task1 = new AddTask(start, middle);
                AddTask task2 = new AddTask(middle, end);
                task1.fork();
                task2.fork();
            }

        }

    }
    //有返回值
    static class AddTask2 extends RecursiveTask<Long> {
        int start, end;

        private AddTask2(int start, int end) {
            this.start = start;
            this.end = end;
        }
        @Override
        protected Long compute() {
            if(end -start <MAX_NUM) {
                long sum =0L;
                for(int i=start;i<end ;i++) sum += nums[i];
                return sum;
            }
            int middle =start +(end-start)/2;
            AddTask2 task1 = new AddTask2(start, middle);
            AddTask2 task2 = new AddTask2(middle, end);
            task1.fork();//启动新线程
            task2.fork();
            return task1.join() + task2.join();

        }

    }
    public static void main(String[] args) throws IOException {
        ForkJoinPool fjp = new ForkJoinPool();

//        AddTask task = new AddTask(0, nums.length);
//        fjp.execute(task);
        AddTask2 task2 = new AddTask2(0, nums.length);
        fjp.execute(task2);
        long result= task2.join();//阻塞的
        System.out.println(result);
        System.in.read();
    }
}

  在刚刚的案例中,涉及到几个重要的API, ForkJoinTask , ForkJoinPool .

  ForkJoinTask : 基本任务,使用fork/join框架必须创建的对象,提供fork,join操作,常用的三个子类

  1. RecursiveAction : 无结果返回的任务
  2. RecursiveTask : 有返回结果的任务
  3. CountedCompleter :无返回值任务,完成任务后可以触发回调。

  ForkJoinTask提供了两个重要的方法:

  1. fork : 让task异步执行
  2. join : 让task同步执行,可以获取返回值

  ForkJoinPool : 专门用来运行 ForkJoinTask 的线程池,(在实际使用中,也可以接收 Runnable/Callable 任务,但在真正运行时,也会把这些任务封装成 ForkJoinTask 类型的任务)

  ForkJoinTask 在不显示使用 ForkJoinPool.execute/invoke/submit() 方法进行执行的情况下,也可 以使用自己的fork/invoke方法进行执行

ForkJoin API工作流程:

  图中最顶层的任务使用submit方式被提交到Fork/Join框架中,Fork/Join把这个任务放入到某个线程 中运行,工作任务中的compute方法的代码开始对这个任务T1进行分析。如果当前任务需要累加的数 字范围过大(代码中设定的是大于200),则将这个计算任务拆分成两个子任务(T1.1和T1.2),每个 子任务各自负责计算一半的数据累加,请参见代码中的fork方法。如果当前子任务中需要累加的数字范 围足够小(小于等于200),就进行累加然后返回到上层任务中。

ThreadPoolExecutor:

  在上诉讲到的线程池中 :newFixedThreadPool ,newCachedThreadPool,newSingleThreadExecutor,newScheduledThreadPool 的底层实现中都是用到了 ThreadPoolExecutor 来创建线程池。而newWorkStealingPool 的底层用的是 ForkJoinPool ,ForkJoinPool是1.8以后才加入的。

简单看一下几种线程池构造函数的简单实现:

  • newFixedThreadPool :new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()); 指定的线程数,最大线程数,多长时间空闲消失,时间单位,队列。 0L代表永远不会消失。

  • newCachedThreadPool:new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());

  • newSingleThreadExecutor:new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())

  • newScheduledThreadPool: super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());

  • newWorkStealingPool:new ForkJoinPool (Runtime.getRuntime().availableProcessors(),ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true)第一个是CPU核数

ThreadLocal的使用:

  ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。解决线程安全性问题。

public class ThreadLocalDemo {
    //希望每个线程获得到的都是0
    private static int num = 0;

    static ThreadLocal<Integer> local = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        Thread[] thread = new Thread[5];
        for (int i = 0; i < 5; i++) {
            thread[i] = new Thread(() -> {

                int num = local.get().intValue();
                local.set(num += 5);
                System.out.println(Thread.currentThread().getName() + " " + num);
            });
            System.out.println();

        }
        for (int i = 0; i < 5; i++) {
            thread[i].start();
        }
    }
}

   在上述的代码中,五个线程获取到的num 全部都是0,实现了线程的隔离。

  另外再看一下 另外的一个 关于 SimpleDateFormat 的例子。

public class ThreadLocalExample {

    private static final SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

//    private static ThreadLocal<SimpleDateFormat> local = new ThreadLocal<SimpleDateFormat>() {
//        @Override
//        protected SimpleDateFormat initialValue() {
//            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//        }
//    };

    public static /*synchronized*/ Date parse(String strData) throws ParseException {
        return sdf.parse(strData);
    }
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for(int i=0;i<20;i++){
            executorService.execute(()->{
                try {
                    //用全局的变量,会存在线程安全问题,抛出异常
                    System.out.println(parse("2021-05-30 20:12:20"));

                    //基于 ThreadLocal  线程独立的 SimpleDateFormat
//                    System.out.println(local.get().parse("2021-05-30 20:12:20"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

  为了避免 SimpleDateFormat 带来的 线程安全问题,可以通过加锁或者使用 ThreadLocal 来解决线程安全问题。

ThreadLocal的原理:

  那么我们来看看ThreadLocal 的原理。主要是基于其操作API

  • set() :在当前线程范围内,设置一个值存储到ThreadLocal中,这个值仅对当前线程可见。 相当于在当前线程范围内建立了副本。
  • get() :从当前线程范围内取出set方法设置的值.
  • remove() :移除当前线程中存储的值
  • withInitial :java8中的初始化方法

  既然 ThreadLocal 能够实现线程安全,能够实现线程的隔离,当前保存的数据,只会存储在当前线程范围内。是线程私有的。存储结构 key 应该就是当前线程。

  带着这些猜测我们来看看源码:

public void set(T value) {
        // 获取到当前线程
        Thread t = Thread.currentThread();
        // 获取到缓存的值 // 如果没有初始化,则进行初始化。
        ThreadLocalMap map = getMap(t);
        if (map != null)//修改value
            map.set(this, value);
        else//初始化
            createMap(t, value);
}  

  按照未初始化的情况,这个时候需要进入初始化:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY]; //默认长度为16的数组
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //计算数组下标
    table[i] = new Entry(firstKey, firstValue); //把key/value存储到i的位置.
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

   就这样变完成了初始化且设置好了值,那么我们回头看一下set方法:

private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            //计算数组下标()
            int i = key.threadLocalHashCode & (len-1);
            //线性探索.() 就是依次往数组下标右移
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                // i的位置已经存在了值, 就直接替换。
                if (k == key) {
                    e.value = value;
                    return;
                }
                //如果key==null,则进行replaceStaleEntry(替换空余的数组)
                if (k == null) { // 因为是弱引用,有可能被回收
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
}

  replaceStaleEntry 把当前的value保存到entry数组中,清理无效的key。如果当前值对应的entry数组中key为null,那么该方法会向前查找到还存在key失效的entry,进行 清理。 通过线性探索的方式,解决hash冲突的问题。

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            // Back up to check for prior stale entry in current run.
            // We clean out whole runs at a time to avoid continual
            // incremental rehashing due to garbage collector freeing
            // up refs in bunches (i.e., whenever the collector runs).
// 一直向前查找
int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; // Find either the key or trailing null slot of run, whichever // occurs first for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); // If we find key, then we need to swap it // with the stale entry to maintain hash table order. // The newly stale slot, or any other stale slot // encountered above it, can then be sent to expungeStaleEntry // to remove or rehash all of the other entries in run. if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // If we didn't find stale entry on backward scan, the // first stale entry seen while scanning for key is the // first still present in the run. if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // If key not found, put new entry in stale slot tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // If there are any other stale entries in run, expunge them if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }

内存泄漏的问题:

  通过上面的分析,我们知道 expungeStaleEntry() 方法是帮助垃圾回收的,根据源码,我们可以发现 get 和set 方法都可能触发清理方法 expungeStaleEntry() ,所以正常情况下是不会有内存溢出的,但是如果我们没有调用get 和set 的时候就会可能面临着内存溢出,养成好习惯不再使用的时候调用 remove(),加快垃圾回收,避免内存溢出。退一步说,就算我们没有调用get 和set 和remove 方法,线程结束的时候,也就没有强引用再指向 ThreadLocal 中的ThreadLocalMap了,这样ThreadLocalMap 和里面的元素也会被回收掉,但是有一 种危险是,如果线程是线程池的, 在线程执行完代码的时候并没有结束,只是归还给线程池,这个时候 ThreadLocalMap 和里面的元素是不会回收掉的。

 

posted @ 2018-11-08 13:55  吴振照  阅读(1422)  评论(0编辑  收藏  举报