Java并发1(线程基础|Callable和Future|Atomic|线程池)

14 篇文章 0 订阅


线程基础


Runnable和Thread

单独线程执行一个简单的任务:

Runnable r = () -> {
	try {
		for(int i =1;i<STEPS;i++){
			……
			Thread.sleep(DELAY);
		}
	}
	catch(InterruptedException e){
	}
};
//由Runnable创建一个Thread对象
Thread t = new Thread(r);
t.start();
public static class RunableTask implements Runnable{
	@Override
	public void run(){
		System.out.println("I am a child thread");
	}
}

Runnable接口是一个非常简单的接口,只有一个void run()方法。

或者通过构建Thread类的子类定义一个线程

class MyThread extends Thread{
	public void run(){
		//balabala
	}
}

不过这种方式不推荐,应该要将并行运行的任务和运行机制解耦合。


不要直接run!

不要调用Thread类或Runnable对象的run方法,
直接调用run方法,只会执行同一个线程中的任务,而不会启动新线程。
应该调用Thread.start()方法,这个方法将创建一个执行run方法的新线程


notify 和 wait

当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:

  • 其他线程调用了该共享对象的notify()或者notifyAll()方法;
  • 其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。

另外需要注意的是,如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常。

简单来说wait要在sychronized后用。

线程获取共享变量的监视器锁

(1) 执行synchronized同步代码块时,使用该共享变量作为参数。
synchronized(共享变量){
	//doSomething
}

(2) 调用该共享变量的方法,并且该方法使用synchronized修饰
synchronized void add(int a, int b){
	//doSomething
}

虚假唤醒

另外需要注意的是,一个线程可以从挂起状态变为可以运行状态(也就是被唤醒),即使该线程没有被其他线程调用notify()notifyAll()方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒

虽然虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是不停地去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个循环中调用wait()方法进行防范。

synchronized (obj){
	while(条件不满足){
		obj.wait();
	}
}

如上代码是经典的调用共享变量wait()方法的实例,首先通过同步块获取obj上面的监视器锁,然后在while循环内调用obj的wait()方法。

//生产线程
sychronized(queue){
	//消费队列满,则等待队列空闲
	while(queue.size()==MAX_SIZE){
		try{
			//挂起当前线程,并释放通过同步块获取的queue上的锁,让消费者线程可以获取该锁,然后获取队列里的元素
			queue.wait();
		}catch(Exception ex){
			ex.printStackTrace();
		}
	}
	//空闲则生成元素,并通知消费者线程
	queue.add(ele);
	queue.notifyAll();
}

//消费者线程
sychronized(queue){
	//消费队列为空
	while(queue.size()==0){
		try{
			//挂起当前线程,并释放通过同步块获取的queue上的锁,让生产者线程可以获取该锁,将生产元素放入队列
			queue.wait();
		}catch(Exception ex){
			ex.printStackTrace();
		}
	}
	//消费元素,并通知唤醒生产者线程
	queue.take();
	queue.notifyAll();
}

wait(long timeout)函数
该方法相比wait()方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享对象的该方法挂起后,没有在指定的timeout ms时间内被其他线程调用该共享变量的notify()\notifyAll()唤醒,函数会因为超时返回
wait()方法内部就是调用了wait(0)
如果传入参数为负的timeout则会抛出IllegalArgumentException异常。

wait(long timeout, int nanos)函数
在其内部调用的是wait(long timeout)函数,如下代码只有在nanos>0时才使参数timeout递增1。

public final void wait(long timeout, int nanos)throws InterruptedException{
	if(timeout<0){
		throw new IllegalArgumentException("timeout value is negative");
	}if(nanos<0||nanos>999999){
		throw new IllegalArgumentException("nanosecond timeout value out of range");
	}
	if(nanos>0){
		timeout++;
	}
	wait(timeout);
}

notify()函数
一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
此外,被唤醒的线程不能马上从wait方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回。
只有当线程获取到了共享变量的监视器锁后,才可以调用共享变量的notify()方法,否则会抛出IllegalMonitorStateException异常。

notifyAll()函数
不同于在共享变量上调用notify()函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll()方法会唤醒所有在该共享变量上由于调用wait方法被挂起的线程。
注意:在共享变量上调用notifyAll()方法只会唤醒调用这个方法前调用了wait系列函数而被放入共享变量等待集合里的线程。如果是只后的话,该线程是不会被唤醒的。


生产者-消费者模型

一个内存队列,多个生产者线程往内存队列中放数据;多个消费者线程从内存队列中取数据。
实现这样一个编程模型,需要做下面几件事情:

  • 内存队列本身需要加锁
  • 阻塞,当队列满了,生产者放不进去,阻塞;当队列是空的,消费者无法消费,阻塞
  • 双向通知,消费者被阻塞后,生产者放入新数据,要notify()消费者;生成者被阻塞后,消费者消费也要notify()生产者

当然也可以不阻塞不通知,就设置睡眠一会再重试。

  1. 如何阻塞?
    线程自己阻塞自己,也就是生产者、消费者线程各种调用wait()和notify()。
  2. 如何双向通知?
    用wait()与notify()机制或Condition机制。
public void enqueue(){
	synchronized(queue){
		while(queue.full()) queue.wait();
		//入队列……
		queue.notify();
	}
}

public void dequeue(){
	synchronized(queue){
		while(queue.empty()) queue.wait();
		//出队列……
		queue.notify();
	}
}

不过生产者本来只想通知消费者,但它把其他的生产者也通知了;消费者本来只想通知生产者,但它被其他的消费者通知了。
原因就是wait()和notify()所作用的对象和synchronized所作用的对象是一个,只能有一个对象,无法区分队列空和队列满两个条件。
这正是Condition要解决的问题。


wait和notify必须和synchronized一起使用

在Java里,wait和notify是Object的成员函数。
并且wait和notify必须和synchronized一起使用。
synchronized关键字可以加在任何对象的成员函数上,任何对象都可以成为锁,那wait和notify要同样如此普及,就只能放在Object里了。

wait()时必须释放锁,

//wait伪代码
wait(){
	//释放锁
	//阻塞,等待被其他线程notify
	//重新拿锁
}

中断线程 interrupt方法

Java早期版本有一个stop方法,不过被弃用了。
所以,没有可以强制线程终止的方法。

interrupt方法可以用来请求终止线程。
当对一个线程调用interrupt方法时,线程的中断状态将被置位。

这是每一个线程都具有的boolean标志
每个线程都应该不时地检查这个标志,以判断线程是否被中断。

while(!Thread.currentThread().isInterrupted()){
	//balabala
}

如果线程被阻塞,无法检测中断状态
这是产生InterruptedException异常的地方。
当在一个被阻塞的线程(调用sleepwait)上调用interrupt方法时,阻塞调用将会被InterruptedException异常中断。
(存在不能被中断的阻塞I/O调用,应该考虑可中断的调用)

没有任何语言方面的需求要求一个被中断的线程应该终止。
中断一个线程不过是引起它的注意,被中断的线程可以决定如何响应中断。

Runable r = () -> {
	try{
		while(!Thread.currentThread().isInterrupted()){
			//balabala
		}
		catch(InterruptedException e){
			//Thread was interrupted during sleep or wait
		}finally{
			cleanup,if required
		}
		//exiting the run method terminates the thread
	}
};

如果每次工作迭代之后都调用sleep方法(或者其他的可中断方法),isInterrupted检测既没有必要,也没有用处。
如果在中断状态被置位时调用sleep方法,它不会休眠。相反它将清除这一状态并抛出InterruptedException。
因此,如果你循环调用sleep,不会检测中断状态。

Runnable r = () -> {
	try{
		while(more work to do){
			Thread.sleep(1000);
		}
	}
	catch(InterruptedException e){
		//balabala
	}
	//balabala
}

Thread的interrupted()方法可以检测当前的线程是否被中断,并且会清除该线程的中断状态。

isInerrupted方法是一个实例方法,可用来检验是否有线程被中断。调用这个方法不会改变中断状态。

java.lang.Thread

.void interrupt()
向线程发送中断请求,线程的中断状态被置为true,
 如果被sleep调用阻塞,抛出InterruptedException异常

.static boolean interrupted()
测试当前线程(即正在执行这一命令的线程) 是否被中断,
该方法会产生副作用:将当前线程的中断状态重置为false

.boolean isInterrupted()
测试线程是否被终止,
不改变线程中断状态

.static Thread currentThread()
返回代表当前执行线程的Thread对象


如何安全地让所有线程停止

先判断再中断。

先用Thread.isInterrupted()判断线程是否被中断
然后使用Thread.interrupt()方法处理现场中断


等待线程执行终止 jion

Thread的join方法可以让主线程等待子线程的终止。等待子线程结束了,才能继续执行thread.join()之后的代码块

join是无参且返回值为void的方法。

public class TestJoin {
    public static void main(String[] args){
        IntStream.rangeClosed(0,4).forEach(
                (i)->{
                    Thread thread = new Thread(
                            ()->{
                                try {
                                    Thread.sleep(1000);
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                                System.out.println(i+"is over");
                            }
                    );
                    thread.start();
                    try {
                        thread.join();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("等待");
                }
        );
    }
}


如果不用join方法会:


线程睡眠 sleep

Thread类的静态方法sleep(),暂时让出指定时间的执行权、在此期间内不参与CPU的调度,但是锁持有不让出,时间到了继续参与CPU的调度。

如果在睡眠期间其他线程调用了该线程的interrupt()方法中断了该线程,则该线程会在调用sleep方法的地方抛出InterruptedException异常而返回。


wait()和sleep()

  1. Thread.sleep() Object.wait()
  2. sleep方法不会释放lock,wait会释放,并且加入等待队列
  3. wait需要依赖sychronized
  4. sleep不需要被唤醒,wait需要
  5. sleep是线程被调用时,占着CPU去睡觉,其他线程不能占用CPU,OS认为该线程正在工作,不会让出系统资源
  6. wait是进入等待池等待,让出系统资源,其他线程可以占用CPU

让出CPU执行权 yield

Thread类的静态方法yield。

当一个线程调用了Thread类的静态方法yield时,是在告诉线程调度器自己占用的时间片中还没有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度。

当一个线程调用yield方法时,当前线程会让出CPU使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出CPU的那个线程来获取CPU执行权。

public class YieldTest implements Runnable{
	YieldTest(){
		//创建并启动线程
		Thread t = new Thread(this);
		t.start();
	}
	public void run(){
		for(int i=0; i<5; i++){
			//当i=0时让出CPU执行权,放弃时间片,进入下一轮调度
			if((i%5)==0){
				System.out.println(Thread.currentThread()+"yield cpu …");
				Thread.yield();
			}
		}
		System.out.println(Thread.currentThread()+"is over");
	}
	public static void main(String[] args){
		new YieldTest();
		new YieldTest();
		new YieldTest();
	}
}

一般很少使用这个方法,在调试或测试时这个方法或许可以帮助复现由于并发竞争条件导致的问题,在设计并发控制的时候可能有用。

sleep和yield的区别

  • sleep调用的线程会被阻塞挂起指定时间,在此期间内线程调度器不会去调度该线程

  • yield线程只是让出自己剩余的时间片,并没有被阻塞挂起,
    而是处于就绪状态,线程调度器下一次调度就有可能调度到当前线程。


线程状态

  • new
  • Runnable
  • Blocked
  • Waiting
  • Timed Waiting
  • Terminated

创建新线程

new Thread(x) 该线程的状态是new

可运行线程

一旦调用start方法,线程处于runnable状态。
至于运不运行,取决于操作系统给线程提供运行的时间。

被阻塞线程和等待线程

当线程处于被阻塞或等待状态时,它暂时不活动。它不运行任何代码且消耗最少的资源。直到线程调度器重新激活它。

  • 当一个线程试图获取一个内部的对象锁(不是java.util.concurrent库中的锁),而该锁被其他线程持有

  • 线程等待另一个线程通知调度器一个条件时,自己进入等待状态

  • 有几个方法有一个超时参数,调用它们导致线程进入计时等待

被终止的线程

  • 因为run方法正常退出而自然死亡
  • 因为一个没有捕获的异常终止了run方法而意外死亡

虽然可以调用stop方法杀死一个线程,但是该方法抛出ThreadDeath错误对象,由此杀死进程。
但是stop方法已经过时,不要用了。

java.lang.Thread 

.void join()
等待终止指定的线程

.void join(long millis)
等待指定的线程死亡或经过指定的毫秒数

.Thread.State getState()
得到这一线程的状态

.void stop() 
停止该线程,方法已过时

.void suspend()
暂停这一线程的执行,方法已过时

.void resume()
恢复线程,仅仅在suspend之后调用,已过时

为什么弃用stop,suspend

stop方法终止所有未结束的方法,包括run方法,当线程被终止,立即释放被它锁住的所有对象的锁,会导致对象处于不一致的状态。
例如,假定TSThread从一个账户向另一个账户转账的过程中被终止,钱款已经转出,却没有转入目标账户,现在银行对象就被破坏了。因为锁已经被释放,这种破坏会被其他尚未停止的线程观察到。

当线程要终止另一个线程时,无法知道什么时候调用stop方法是安全的,什么时候导致对象被破坏。

经验证明suspend方法会经常导致死锁。
suspend方法不会破坏对象,但是用suspend挂起一个持有一个锁的线程,锁在恢复之前是不可用的。如果调用suspend方法的线程试图获得同一个锁,那么程序死锁:被挂起的线程等着被恢复,其他挂起的线程等待获得锁。

在希望停止线程的时候应该中断线程,被中断的线程会在安全的时候停止


线程多次调用start()方法会怎么样

一个线程被调用两次start()方法,会出现Exception in thread “main” java.lang.IllegalThreadStateException的错误。

Java的线程是不允许启动两次的,启动第二次必然会出现Exception in thread “main” java.lang.IllegalThreadStateException异常,这是一种运行时异常,多次调用start被认为是编程错误。

为什么呢?是因为在Java5以后,线程的状态会被明确的写入其公共内部枚举类型Java.lang.Thread.State中,分别是:新建(NEW),就绪(RUNNABLE),阻塞(BLOCKED),等待(WAITING)计时等待(TIMED_WAIT),终止(TERMINATED)
在第二次调用start() 方法的时候,已经被start的线程已经不再是(NEW)状态了,所以无论如何是不能再次启动的。


线程优先级

Java中每个线程有一个优先级,默认情况下线程继承父线程的优先级。可以使用setPriority方法提高或降低任何一个线程的优先级。

优先级可以设置为MIN_PRIORITY(1)到MAX_PRIORITY(10)之间的任何值。NORM_PRIORITY被定义为5。

线程优先级是依赖于系统的,当虚拟机依赖于宿主机平台的线程实现机制时,Java线程的优先级被映射到宿主机平台的优先级上,优先级个数也许更多,也许更少。


Callable和Future


Callable

有两种创建线程的方法:

  • 一种是通过使用Thread类
  • 另一种是通过使用Runnable创建线程(启动还是要Thread.start())

但是,Runnable缺少的一项功能是,当线程终止时(即run()完成时),我们无法使线程返回结果。为了支持此功能,Java中提供了Callable接口。

为了实现Runnable,需要实现不返回任何内容的run()方法,而对于Callable,需要实现在完成时返回结果的call()方法。

请注意,不能直接使用Callable创建线程,只能使用Runnable创建线程。可以用FutureTask包装器

另一个区别是call()方法可以引发异常,而run()则不能。
为实现Callable而必须重写call方法。

Callable接口是一个参数化的类型,只有一个方法call()

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

类型参数是返回值的类型。例如,Callable< Interger >表示一个最终返回Interger对象的异步计算。

call()方法完成时,结果必须存储在主线程已知的对象中,以便主线程可以知道该线程返回的结果。可以使用Future对象。


Future

Future类代表了一个异步计算的未来结果——处理完成后最终会出现在Future中的结果。

将Future视为保存结果的对象–它可能暂时不保存结果,但将来会保存(一旦Callable返回)。因此,Future基本上是主线程可以跟踪进度以及其他线程的结果的一种方式。要实现此接口,必须重写5种方法。

Future接口具有下面的方法:

public interface Future<V>{
	V get throws …… ;
	V get(long timeout, TimeUnit unit)throws ……;
	void cancel(boolean mayInterrupt);
	boolean isCancelled();
	boolean isDone();
}
  • 第一个get方法的调用被阻塞,直到计算完成。如果在计算完成之前,第二个方法的调用超时,抛出一个TimeoutException异常。

  • 如果运行该计算的线程被中断,两个方法都将抛出InterruptedException。
    如果计算已经完成,那么get方法立即返回。

  • 如果计算还在进行,isDone方法返回false;
    如果完成了返回true。

  • 可以用cancel方法取消该计算。


FutureTask

FutureTask包装器一种非常便利的机制,可将Callable转换成Future和Runnable,它同时实现二者的接口。

Callable<Integer> myComputation = ……;
FutureTask<Integer> task = new FutureTask<Integer>(myComputation);
Thread t = new Thread(task); 
t.start();
// balabala
Integer result = task.get();

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

public class FutureTest {

	public static void main(String[] args) {
		try(Scanner in = new Scanner(System.in)){
			System.out.println("Enter base directory(e.g. /usr/local/jdk1.8/src)");
			String directory = in.nextLine();
			System.out.println("Enter keyword (e.g. volatile):");
			String keyword = in.nextLine();
			
			MatchCounter counter = new MatchCounter(new File(directory),keyword);
			FutureTask<Integer> task = new FutureTask<>(counter);
			Thread t = new Thread(task);
			t.start();
			try {
				System.out.println(task.get()+"matching files");
			}catch(ExecutionException | InterruptedException e) {
				e.printStackTrace();
			}
		}
		
	}

}

class MatchCounter implements Callable<Integer>{

	private File directory;
	private String keyword;
	
	public MatchCounter(File directory, String keyword) {
		this.directory = directory;
		this.keyword = keyword;
	}
	
	@Override
	public Integer call() throws Exception {
		int count = 0;
		try {
			File[] files = directory.listFiles();
			List<Future<Integer>> results = new ArrayList();
			
			for(File file : files) {
				if(file.isDirectory()) {
					MatchCounter counter = new MatchCounter(file,keyword);
					FutureTask<Integer> task = new FutureTask<>(counter);
					results.add(task);
					Thread t = new Thread(task);
					t.start();
				}
				else {
					if(search(file))
						count ++;
				}
				
				for(Future<Integer> result : results) {
					try {
						count += result.get();
					}catch(ExecutionException e) {
						
					}
				}
			}
		}catch(InterruptedException e) {
			
		}
		return count;
	}
	
	public boolean search(File file) {
		try {
			try(Scanner in = new Scanner(file,"UTF-8")){
				boolean found = false;
				while(!found && in.hasNextLine()) {
					String line = in.nextLine();
					if(line.contains(keyword))
						found = true;
				}
				return found;
			}
		}
		catch(IOException e) {
			return false;
		}
	}
	
}



Atomic类


AtomicInteger

对一个整数加减操作要保证线程安全可以用这个,比synchronized性能更好。其compareAndSet方法还是调的Unsafe的CAS方法。


AtomicBoolean和AtomicReference

为什么boolean类型还需要这样?
因为往往要实现这种功能:

if(flag==false){
	flag = true;
	……
}

就是实现compare和set两个操作合在一起的原子性,就是CAS的功能。
上面的代码就变为:

if(compareAndSet(false,true)){
……
}

AtomicReference也需要同样的功能。


如何支持boolean和double

Unsafe类中只提供了三种类型的CAS操作,int long Object
AtomicBoolean靠int实现,AtomicDouble靠long。


AtomicStampedReference和AtomicMarkableReference

ABA问题的解决方案。
解决ABA问题不仅要比较值,还要比较版本号。

AtomicStampReference是整型的累加,而AtomicMarkableReference的版本号是boolean类型的,不过这也导致了因为只有两个版本号不能完全避免ABA问题,只是降低ABA发生的概率。


AtomicIntegerArray\AtomicLongArray\AtomicReferenceArray

这不是说对整个数组的操作是原子的,而是针对数组中一个元素的原子操作而言的。


线程池


为什么要线程池

构建一个新的线程是有一定代价的,因为涉及与操作系统的交互。
因为JVM中的线程与操作系统的线程是一对一的关系,需要操作系统提供资源。

为什么要使用线程池?

  • 减少线程这样的重资源的开销,如果程序中创建了大量的生命期很短的线程,应该使用线程池
  • 线程数需要控制并不是越多越好,需要限制并发线程的总数
  • 线程的切换有开销,线程数的多少需要结合CPU核心数和I/O等待占比

手写线程池

public class MyThreadPool {
    BlockingQueue<Runnable> blockingQueueue;//阻塞队列
    List<MyThread> threadsList; // 线程列表

    MyThreadPool(BlockingQueue<Runnable> blockingQueue,int poolSize){
        this.blockingQueueue = blockingQueue;
        threadsList = new ArrayList<>(poolSize);
        IntStream.rangeClosed(1,poolSize).forEach((i)->
        {
        	MyThread thread = new MyThread("线程"+i);
            thread.start();
            threadsList.add(thread);
        });
    }
    
    public void execute(Runnable task) throws InterruptedException{
        blockingQueueue.put(task);
    }
    
    class MyThread extends Thread{
        public MyThread(String name){
            super(name);
        }

        @Override
        public void run(){
            while (true){
                Runnable task = null;
                try {
                    task = blockingQueueue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                task.run();
            }
        }
    }
}
public class TestMyThreadPool {
    public static void main(String[] args){
        MyThreadPool myThreadPool = new MyThreadPool(new LinkedBlockingDeque<>(8),4);
        IntStream.rangeClosed(1,6).forEach((i)->{
            try {
                myThreadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName());
                });
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
}


ThreadPoolExecutor

ThreadPoolExecutor是线程池的核心类。

线程池的参数

public ThreadPoolExecutor
(int corePoolSize, //核心线程数 最小线程数
int maximumPoolSize, //最大线程数
long keepAliveTime, //空闲线程存活的时间
TimeUnit unit, //时间单位
BlockingQueue<Runnable> workQueue, //工作队列 线程太多了就排个队
ThreadFactory threadFactory, //线程工厂  线程创造的地方
RejectedExecutionHandler handler) //拒绝策略

提交优先级和执行优先级

自己定义一个线程池,模仿原生的,new ThreadPoolExecutor,
CorePoolSize: 10
MaxinumPoolSize: 20
KeepAliveTime: 0
ArrayBlockingQueue: 10 这里是使用一个数组

拒绝策略其实是有30个任务然后就容纳不下了。

提交优先级:核心线程,工作队列,非核心线程
执行优先级:核心线程,非核心线程,工作队列

  • 任务提交到线程池之后,如果线程数小于核心线程数,那么就会新起一个线程来执行当前的任务。
  • 如果线程数大于核心线程数,将任务塞入阻塞队列中,等待被执行。
  • 阻塞队列满了,此时线程数小于最大线程数,那么会创建新线程来执行当前任务。
  • 如果阻塞队列满了,并且此时线程数大于最大线程数,采取拒绝策略。

    最后那个else if就是还是放不下就调用reject方法。

注意上面图里的注释1:
如果线程数小于核心线程数,并且线程都空闲,提交一个任务时会新建一个线程来执行。

clt是一个涵盖了两个概念的原子整数类,将工作线程数和线程池状态结合在一起维护,低29位存放workerCount,高3位存放runState。

如何理解核心线程

线程池虽然默认是懒创建线程,实际上是想要快速用于核心线程数的线程,核心线程指的是线程池承载日常任务的中坚力量,最大线程数是为了应付突发状况。

keepAliveTime

当线程数大于核心数后,如果线程空闲了一段时间,就回收线程,直到数量与核心数持平。

线程池的拒绝策略

  • AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
    这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。

  • DiscardPolicy:丢弃任务,但是不抛出异常。 如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃

  • DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务

  • CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务,
    短信平台的项目中,线程池就是采用了该种拒绝策略,因为我们的短信一条都不能漏发,所以每个任务必须处理。

当然,要做到完全不丢弃任务,还是要根据实际的线上业务量,合理配置线程池参数,比如核心线程数、最大线程数、任务队列长度等,保证线程池能够及时有效处理任务,这样才能够不丢弃任务。

用户还可以自定义拒绝策略:实现RejectExecutionHandler接口,实现rejectedExecution方法,自定义策略模式。

线程池的状态

  • RUNNING 能够接受新任务,并处理阻塞队列中的任务
  • SHUTDOWN 不接受新任务,但是可以处理阻塞队列中的任务
  • STOP 不接受新任务,并且不处理阻塞队列中的任务,并且还打断正在运行任务的线程
  • TIDYING 所有任务都终止,并且工作线程也为0,处于关闭之前的状态
  • TERMINATED 已关闭

其他

  • 如果线程池中的线程在执行任务的时候抛异常怎么办
    这个线程废了新建一个换掉它。

  • 核心线程一定伴随着任务慢慢创建吗
    线程池提供了了两个方法:
    prestartCoreThread:启动一个核心线程
    prestartAllCoreThreads:启动所有核心线程
    有些情况需要一个预热的

  • 线程池的核心线程在空闲的时候会不会被回收
    allowCoreThreadTimeOut方法,把它设置为true,则所有的线程都会超时。
    具体是InterruptIdleWorkers方法。


Executors

执行器(Executors)类有许多静态工厂方法用来构建线程池

方法描述
newCachedThreadPool必要时创建新线程;空闲线程会被保留60秒
newFixedThreadPool该池包含固定数量的线程;空闲线程会一直被保留
newSingleThreadExecutor只有一个线程的“池”,该线程顺序执行每一个提交的任务(类似于Swing事件分配线程)
newScheduledThreadPool用于预定执行而构建的固定线程池,替代java.util.Timer
newSingleThreadScheduledExecutor用于预定执行而构建的单线程”池“

newCachedThreadPool

  • 有空闲立即执行任务
  • 没有空闲线程创建一个新线程
  • 没有核心线程,最大线程数量很大
  • 使用只有一个节点的同步队列,来了就消费

newFixedThreadPool

  • 固定大小线程池,核心数=最大数
  • 没有空闲线程可用就去排队
  • 队可以排很长,LinkedBlockingQueue

newSingleThreadExecutor

  • 退化了的大小为1的线程池
  • 由一个线程执行提交的任务,一个接着一个

原生静态工厂方法的问题

  • newCachedThreadPool在任务很多的时候会不停地创建线程(线程不花多少资源),会占用CPU的资源,让CPU100%,程序很有可能会卡死,(但是不会出现内存溢出)。

  • newFixedThreadPool、newSingleThreadExecutor可能会出现内存溢出,因为LinkedBlockingQueue,它使用了近乎于无界的LinkedBlockingQueue阻塞队列。当核心线程用完后,任务会入队到阻塞队列,如果任务执行的时间比较长,没有释放,会导致越来越多的任务堆积到阻塞队列,最后导致机器的内存使用不停的飙升,造成JVM OOM。


怎样使用线程池

将一个Runnable对象或Callable对象提交给ExecutorService:

//调用submit时会得到一个Future对象,可用来查询该任务的状态

//可使用这样一个Future<?>对象来调用isDone、cancel、isCancelled
//但是get方法在完成时只是简单地返回null
Future<?> submit(Runnable task)

//Submit提交一个Runnable对象,并且Future的get方法在完成时返回指定的result对象
Future<T> submit(Runnable task, T result)

//Submit提交一个Callable,并且返回的Future对象将在计算结果准备好的时候得到它
Future<T> submit(Callable<T> task)
  • 当用完一个线程池的时候,调用shutdown。
    该方法启动该池的关闭序列。
    被关闭的执行器不再接受新的任务。
    当所有任务都完成以后线程中的线程死亡。

  • 另一种方式是调用shutdownNow。
    该池取消尚未开始的所有任务并试图中断正在运行的线程。

在使用连接池应该做的事

  • 调用Executors类中静态的方法newCachedThreadPool或newFixedThreadPool
  • 调用submit提交Runnable或Callable对象
  • 如果想要取消一个任务,或如果提交Callable对象,那就要保存好返回的Future对象
  • 当不再提交任何任务时,调用shutdown
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;

public class ThreadPoolTest {

	public static void main(String[] args) {
		try(Scanner in = new Scanner(System.in)){
			System.out.print("Enter base directory(e.g. /usr/local/jdk 1.8/src):");
			String directory = in.nextLine();
			System.out.println("Enter keyword (e.g volatile)");
			String keyword = in.nextLine();
			
			//1. 调用Executors类中静态方法newCachedThreadPool或newFixedThreadPool
			ExecutorService pool = Executors.newCachedThreadPool();
			MatchCounter2 counter = new MatchCounter2(new File(directory),keyword,pool);
			//2. 调用submit提交Runnable或Callable对象,保存返回的Future对象
			Future<Integer> result = pool.submit(counter);
			try {
				System.out.println(result.get()+"matching files.");
			}catch(ExecutionException e) {
				e.printStackTrace();
			}catch(InterruptedException e) {
				
			}
			//3. 不再提交任何任务时,调用shutdown
			pool.shutdown();
			
			//ExecutorService接口是不能getLargestPoolSize的,需要强制转换为ThreadPoolExector
			//返回的是线程池在生命周期中的最大尺寸
			int largestPoolSize = ((ThreadPoolExecutor) pool).getLargestPoolSize();
			//打印池中最大的线程数
			System.out.println("largest pool size="+ largestPoolSize);
			
		}
	}
}

class MatchCounter2 implements Callable<Integer>{
	private File directory;
	private String keyword;
	private ExecutorService pool;
	private int count;
	
	public MatchCounter2(File directory,String keyword,ExecutorService pool) {
		this.directory = directory;
		this.keyword = keyword;
		this.pool = pool;
	}

	@Override
	public Integer call() throws Exception {
		count = 0;
		File[] files = directory.listFiles();
		List<Future<Integer>> results = new ArrayList<>();
		
		for(File file : files) {
			if(file.isDirectory()) {
				MatchCounter2 counter = new MatchCounter2(file,keyword,pool);
				Future<Integer> result = pool.submit(counter);
				results.add(result);
			}else {
				if(search(file)) {
					count++;
				}
			}
			for(Future<Integer> result : results) {
				try {
					count += result.get();
				}catch(ExecutionException e) {
					e.printStackTrace();
				}
			}
		}
		return count;
	}
	
	public boolean search(File file) {
		try(Scanner in = new Scanner(file,"UTF-8")){
			boolean found = false;
			while(!found&&in.hasNextLine()){
				String line = in.nextLine();
				if(line.contains(keyword)) {
					found = true;
				}
				return found;
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
		return false;
	}
	
}


一个公式

工作线程数(线程池中线程数量)设多少合适?
Nthread=NCPU * UCPU * (1+W/C)

  • NCPU是处理器核的数目,可以通过Runtime.getRuntime().availableProcessors()得到
  • UCPU是期望的CPU利用率(该值应该介于0和1之间)
  • W/C是等待时间与计算时间的比率(压测,统计,调整)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值