线程问题

死锁问题

所谓死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

特殊的概念

饥饿指的线程无法访问到它需要的资源而不能继续执行时,引发饥饿最常见资源就是CPU时钟周期。
在程序中使用的Thread.yield或者Thread.sleep(0)表明该程序试图克服优先级调整问题,让优先级更低的线程拥有被CPU调度的机会。
Thread.sleep(0)不等于不写

活锁指的是线程不断重复执行相同的操作,但每次操作的结果都是失败的。
活锁通常发生在处理事务消息的应用程序中,如果不能成功处理这个事务那么事务将回滚整个操作。解决活锁的办法是在每次重复执行的时候引入随机机制,这样由于出现的可能性不同使得程序可以继续执行其他的任务。

Java中活锁和死锁有什么区别?
活锁和死锁类似,不同之处在于处于活锁的线程或进程的状态是不断改变的,活锁可以认为是一种特殊的饥饿。一个现实的活锁例子是两个人在狭小的走廊碰到,两个人都试着避让对方好让彼此通过,但是因为避让的方向都一样导致最后谁都不能通过走廊。简单的说就是,活锁和死锁的主要区别是前者进程的状态可以改变但是却不能继续执行。

怎么检测一个线程是否拥有锁?
在java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。

死锁产生的原因

1、系统资源的竞争。通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。
2、不剥夺性条件。只有对不可剥夺资源的竞争才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。
3、进程推进顺序非法。进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都会因为所需资源被占用而阻塞。

死锁产生的必要条件

产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。
1)互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
2)不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
3)请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
4)循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, Pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有。

如何避免死锁

加锁顺序(线程按照一定的顺序加锁,避免嵌套封锁)
加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)

发现死锁

死循环、死锁、阻塞、页面打开慢等问题,打印线程dump是最好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两步:
1)获取到线程的进程编号pid,可以通过使用jps命令,在Linux环境下还可以使用ps -ef | grep java
2)打印进程堆栈,可以通过使用jstack pid命令,在Linux环境下还可以使用kill -3 pid

死锁检测

Jconsole是JDK自带的图形化界面工具,使用JDK给我们的的工具JConsole,可以通过打开cmd然后输入jconsole打开,检测出该进程中造成死锁的线程

Jstack是JDK自带的命令行工具,主要用于线程Dump分析。
1、先用Jps来查看java进程id,例如6666
2、jstack输出线程dump信息到文件 jstack -l 6666 > aa.log
3、查看dump文件,然后进行分析。可以看到提示【Found 1 deadlock】,可以看到报错行数位置

总结

1、让程序每次至多只能获得一个锁。当然,在多线程环境下,这种情况通常并不现实。
2、设计时考虑清楚锁的顺序,尽量减少嵌入加锁交互数量。
3、既然死锁的产生是两个线程无限等待对方持有的锁,那么只要等待时间有个上限不就好了。当然synchronized不具备这个功能,但是可以使用Lock类中的tryLock方法去尝试获取锁,这个方法可以指定一个超时时限,在等待超过该时限之后便会返回一个失败信息。

数据结构

队列:FIFO先进先出
栈:FILO先进后出

线程相关的模型

volatile类型修饰符

保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
禁止进行指令重排序。(实现有序性)
volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性
volatile 变量的内存可见性是基于内存屏障Memory Barrier实现。
内存屏障,又称内存栅栏,是一个 CPU 指令。
在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM 为了保证在不同的编译器和 CPU 上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和 CPU:不管什么指令都不能和这条 Memory Barrier 指令重排序。
volatile 的应用场景
只有在状态真正独立于程序内其他内容时才能使用 volatile

AQS模型

Abstract Queued Synchronizer抽象队列同步器:AQS是JDK下提供的一套用于实现基于FIFO等待队列的阻塞锁和相关的同步器的一个同步框架。
AQS维护了一个原子变量 volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。许多同步类实现都依赖于它,如常用的ReentrantLock、Semaphore、CountDownLatch…
在这里插入图片描述
AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。CLH即克雷格Craig, 兰丁Landin, and 海格斯腾 Hagersten的简称
CLH同步队列遵循FIFO,首节点的线程释放同步状态后,将会唤醒它的后继节点next,而后继节点将会在获取同步状态成功时将自己设置为首节点,这个过程非常简单,head执行该节点并断开原首节点的next和当前节点的prev即可,注意在这个过程是不需要使用CAS来保证的,因为只有一个线程能够成功获取到同步状态

内部使用AQS的例子:
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

CAS模型

Java5中引入了AutomicInteger、AutomicLong、AutomicReference等特殊的原子性变量类,它们提供的如compareAndSet、incrementAndSet和getAndIncrement等方法都使用了CAS操作。都是由硬件指令来保证的原子方法。
CAS即比较并交换。是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置V、预期原值A和新值B。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。
比较和交换(Conmpare And Swap)是用于实现多线程同步的原子指令。 它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。这是作为单个原子操作完成的。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成
CAS 的特性:
通过调用 JNI 的代码实现
非阻塞算法
非独占锁
CAS 存在的问题:
ABA
循环时间长开销大
只能保证一个共享变量的原子操作
典型案例:原子量
所谓的原子量即操作变量的操作是原子的,该操作不可再分,因此是线程安全的。

为何要使用原子变量呢,
原因是多个线程对单个变量操作也会引起一些问题。在Java5之前,可以通过volatile、synchronized关键字来解决并发访问的安全问题,但这样太麻烦。
Java5后,专门提供了用来进行单变量多线程并发安全访问的工具包java.util.concurrent.atomic,其中的类也很简单。
例如AtomicLong aLong=new AtomicLong(10000); //原子量,每个线程都可以自由操作

原子量实现的计数器

public class Test2 {
	public static void main(String[] args) {
		Counter counter = new Counter();
		ExecutorService service = Executors.newCachedThreadPool();
		for (int i = 0; i < 1000000; i++) {
			service.execute(new Runnable() {
				public void run() {
					System.out.println(counter.increase());
				}
			});
		}
		service.shutdown();
	}
}

class Counter {
	private AtomicInteger counter = new AtomicInteger(0);

	public int increase() {
		return counter.incrementAndGet();
	}
}

CAS模型的问题

ABA问题
解决方法:JAVA中提供了AtomicStampedReference / AtomicMarkableReference来处理会发生ABA问题的场景,主要是在对象中额外再增加一个标记来标识对象是否有过变更。

CAS应用场景隐含竞争是短暂的,否则不断的自旋尝试会过度消耗CPU
解决方法加入超时设置

CAS只能保证一个共享变量的原子操作,解决方法是使用锁或者合并多个变量

//AtomicReference提供了以无锁方式访问共享资源的能力
AtomicReference<Integer> ar=new AtomicReference<>(new Integer(1000));
List<Thread> list=new ArrayList<>();
for(int i=0; i<1000; i++){
    Thread t=new Thread( ()->{
        while(true){ //自旋操作
Integer oldValue=ar.get();
            Thread.sleep(10);
            //具体处理逻辑
            Integer newValue=oldValue+1;
            if(ar.compareAndSet(oldValue,newValue))//CAS操作
                break;
        }
    });
    list.add(t);
    t.start();
}
for(Thread temp:list) temp.join();
System.out.println(ar.get());

信号量Semaphore

Java的信号量实际上是一个功能完备的计数器,能监控有多少数目的线程等待获取资源,并且通过信号量可以得知可用资源的数目等,这里强调数目二字,但不能指出来有哪些在等待,哪些资源可用。
用来控制对有限资源的访问数量。
应用场景:有限资源的使用限制
public Semaphore(int permits) permits表示初始可用的资源,注意,只是初始值为permits,并不是资源的最大数,通过释放资源的操作可以使可用资源数量超过初始值
public Semaphore(int permits, boolean fair) fair等待资源的线程是否采用公平策略获取锁,true即是先来先得。(公平锁)
acquire()申请资源,当申请的资源>现有可用资源时,申请资源的线程将被阻塞,直到有可用资源或者申请线程被打断,若线程被打断,则抛出InterruptedException异常
release()释放一个资源

public class Test1 {
	public static void main(String[] args) throws Exception {
		Random r = new Random();// 使用随机整数模拟不定期的出现要上厕所的人
		Semaphore wc = new Semaphore(3, true);// 信号量这种编程方式一般用于多个共享资源,
		但是资源数小于线程数
		for (int i = 1; i <= 6; i++) {
			new Thread(new Person("阿" + i, wc)).start();
			Thread.sleep(r.nextInt(100));
		}
	}
}

class Person implements Runnable {
	private String name;
	private Semaphore sp;
	public Person(String name, Semaphore sp) {
		this.name = name;
		this.sp = sp;
	}
	public void run() {
		System.out.println(name + ":憋死老子了!");
		// 查看是否有空闲资源
		if (sp.availablePermits() > 0) {
			System.out.println("天助我也,有坑位!");
		} else {
			System.out.println("卧槽,没坑位了,等会吧....");
		}
		// 没有资源则需要阻塞当前线程,等待别的线程释放资源
		try {
			sp.acquire();// 申请坑位,如果有资源则直接使用,否则阻塞等待---AQS
		} catch (Exception e) {
			e.printStackTrace();
		}
		// 模拟上厕所的过程----具体使用资源进行业务逻辑处理
		System.out.println(name + ":终于轮到我了,拉屎就是爽!");
		try {
			Thread.sleep(new Random().nextInt(1000));// 模拟上厕所时间
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(name + ":拉完了,好臭!");
		// 释放资源
		sp.release();
	}

}

障碍器CyclicBarrier

它的作用就是会让所有线程都等待完成后才会继续下一步行动。
Java5中添加了障碍器类,为了适应一种新的设计需求,比如一个大型的任务,常常需要分配好多子任务去执行,只有当所有子任务都执行完成时候,才能执行主任务,这时候,就可以选择障碍器了。障碍器是多线程并发控制的一种手段

CyclicBarrier(int parties)参与线程的个数
CyclicBarrier(int parties, Runnable barrierAction) Runnable参数是最后一个到达线程要做的任务
int await() 线程调用await()表示自己已经到达栅栏
int await(long timeout, TimeUnit unit)

public class Test2 {
	public static void main(String[] args) {
		//等待5个线程执行完毕后执行()->{}中的任务,如果线程数大于5会有多次执行【执行次数=到达barrier数目/5】
	    CyclicBarrier cb=new CyclicBarrier(5, ()->{
	        System.out.println("Main: 最终执行的任务");
	    });
	    for(int i=0;i<10;i++)
	        new MyThread(cb).start();
	}
}

class MyThread extends Thread {
	// 所有执行线程需要共享同一个CyclicBarrier对象
	private CyclicBarrier cb;

	public MyThread(CyclicBarrier cb) {
		this.cb = cb;
	}

	@Override
	public void run() {
		System.out.println(Thread.currentThread() + "开始...");
		// 线程的具体执行逻辑
		try {
			Thread.sleep(500);
			System.out.println(Thread.currentThread() + "完毕...");
			// 用于告知栅栏已经到达,会阻塞当前线程,直到到达次数达到barrier要求后才能继续执行后续程序
			cb.await();
		} catch (Exception e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread() + "退出操作...");
	}
}

主要工作原理,通过构造函数创建一个指定屏障数的屏障类,在各线程中调用await(),调用后当前线程将被阻塞,直到调用的次数到指定屏障数后,所有阻塞的线程将恢复继续执行。
yield暂停正在执行的线程
Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。
yield()应该是让当前运行线程回到可运行状态以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果
join阻塞当前线程等待线程
t.join()方法阻塞调用此方法的线程(calling thread)进入TIMED_WAITING状态,直到线程t完成,此线程再继续;通常用于在main()主线程内,等待其它线程完成再结束main()主线程
Join方法实现是通过wait()。 当main线程调用t.join时候,main线程会获得线程对象t的锁(wait意味着拿到该对象的锁),调用该对象的wait(),直到该对象唤醒main线程 ,比如退出后。这就意味着main线程调用t.join时,必须能够拿到线程t对象的锁。

CountDownLatch闭锁

CountDownLatch是一个同步工具类,join的增强版。允许一个或多个线程,等待其他一组线程完成操作,再继续执行

public CountDownLatch(int count); 构造函数,初始化计数器值为count,count只能被设置一次
public void await()throws InterruptedException; 调用await()方法的线程会被挂起,直到count值为0才继续执行
public boolean await(longtimeout, TimeUnit unit)throws InterruptedException; 和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public void countDown();将count值减1

public class Test4 {
	public static void main(String[] args) {
		CountDownLatch latch = new CountDownLatch(10);
		System.out.println(latch.getCount());
		for (int i = 0; i < 10; i++) {
			Thread t = new Thread(() -> {
				System.out.println(Thread.currentThread() + "开始");
				try {
					Thread.sleep(500);  //模拟具体的处理逻辑
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				latch.countDown();// 计数器减1
				System.out.println(Thread.currentThread() + "结束");
			});
			t.start();
		}
		try {
			latch.await();// 阻塞主线程,直到计数器值为0时才会继续执行
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("最终执行结果:");
		System.out.println(latch.getCount());
	}
}

它是通过控制计数器的值来达到等待的目的。当计数器的值>0时,调用countDownLatch.await()会阻塞当前线程,直到其他线程调用countDownLatch.countDown()将计数器的值减到0时,阻塞线程将被唤醒。计数器的值>0时调用await()方法不会阻塞当前线程。主线程必须在启动其他线程后立即调用CountDownLatch.await()方法。这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务。

join与countDownLatch区别

join用于让当前执行线程等待join线程执行结束。
其实现原理是不停检查join线程是否存活,如果join线程存活则让当前线程永远wait。

countDownLatch没有这个线程,只要count减小到0,不管被等待线程是否执行结束,等待线程都可以继续执行(被唤醒,进入可执行状态)。

yield与join方法的区别

yield()方法:暂停当前正在执行的线程对象,并允许执行其他线程。
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

join()方法:线程实例的join()方法可以使得一个线程在另一个线程结束后再执行,即也就是说使得当前线程可以阻塞其他线程执行;
thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

线程池和并行处理

线程是稀缺资源,如果被无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,合理的使用线程池对线程进行统一分配、调优和监控,有以下好处:1、降低资源消耗;2、提高响应速度;3、提高线程的可管理性。

Java1.5中引入的Executor框架把任务的提交和执行进行解耦,只需要定义好任务,然后提交给线程池,而不用关心该任务是如何执行、被哪个线程执行,以及什么时候执行。

线程池可以节约系统资源,包括线程、内存资源等,这样可以避免创建过多的线程导致线程资源匮乏、系统频繁进行上下文切换以及内存溢出等问题,因为线程池中的每一个线程可能会轮询地执行多个任务
线程池可以节省重新创建线程的时间,进而提高响应速度。

ExecutorService是Java中对线程池定义的一个接口,它java.util.concurrent包中。Java API对ExecutorService接口的实现有两个(ThreadPoolExecutor和ScheduledThreadPoolExecutor),所以这两个即是Java线程池具体实现类。除此之外,ExecutorService还继承了Executor接口(注意区分Executor接口和Executors工厂类),这个接口只有一个execute()方法
Executors只是一个工厂类,它所有的方法返回的都是ThreadPoolExecutor、ScheduledThreadPoolExecutor这两个类的实例
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。

通过Executors提供6种线程池

可缓存线程池

newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收重用时则新建线程。
工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger.MAX_VALUE), 这样可灵活的往线程池中添加线程。
如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。

在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。

ExecutorService pool=Executors.newCachedThreadPool();
for(int k=0;k<10;k++)
    pool.execute(()->{
        for(int i=0;i<10;i++)
            System.out.println(Thread.currentThread()+":"+i);
    });
pool.shutdown();

特点:
重用之前的线程
适合执行许多短期异步任务的程序。
调用execute() 将重用以前构造的线程
如果没有可用的线程,则创建一个新线程并添加到池中
默认为60s未使用就被终止和移除
长期闲置的池将会不消耗任何资源

定长线程池

newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

newFixedThreadPool可控制线程最大并发数,当线程池中的线程数达到其设定大小时,其余新创建的线程会在LinkedBlockingQueue队列中等待
当线程池中的某个线程失败而终止时,新的线程会代替它执行剩下的任务
线程池中的线程只有在显式调用shutdown函数时才会退出线程池

ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);创建确定大小的线程池
for (int i = 0; i < 10; ++i) {
    newFixedThreadPool.execute(new Runnable() {
        public void run() {
            System.out.println(Thread.currentThread().getName());
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}

特点:
创建重用固定数量线程的线程池,不能随时新建线程
当所有线程都处于活动状态时,如果提交了其他任务,他们将在队列中等待一个线程可用
线程会一直存在,直到调用shutdown

周期定长线程池

newScheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行。

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
DateFormat df=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(df.format(new Date()));
//定时
scheduledThreadPool.schedule(new Runnable() {
    public void run() {
        System.out.println("thread:" + Thread.currentThread().getName() + ",time:" + sdf1.format(new Date()));
    	Thread.sleep(10);
    }
}, 3, TimeUnit.SECONDS);

特点:
设定延迟时间
定期执行
空闲线程会进行保留

单线程化线程池

newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
特点:
在任何情况下都不会有超过一个任务处于活动状态
与newFixedThreadPool(1)不同是不能重新配置加入线程,使用FinalizableDelegatedExecutorService进行包装
能保证执行顺序,先提交的先执行
当线程执行中出现异常,去创建一个新的线程替换之

周期单线程池

newSingleThreadScheduledExecutor创建一个单线程化的线程池,可以在指定延迟后指定线程任务。

工作窃取算法的线程池

newWorkStealingPool(int)/()创建持有足够数量线程的线程池来支持给定的并行级别,该方法还会使用多个队列来减少竞争,无参则是根据CPU个数定义并行级别。
工作窃取核心思想是,自己的活干完了去看看别人有没有没干完的活,如果有就拿过来帮他干。
实现机制是:为每个工作线程分配一个双端队列(本地队列)用于存放需要执行的任务,当自己的队列没有数据的时候从其它工作者队列中获得一个任务继续执行。

常见的五种线程池的适应场景

newCachedThreadPool:用来创建一个可以无限扩大的线程池,适用于服务器负载较轻,执行很多短期异步任务。
newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于可以预测线程数量的业务中,或者服务器负载较重,对当前线程数量进行限制。
newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务,并且在任意时间点,不会有多个线程是活动的场景。
newScheduledThreadPool:可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景。
newWorkStealingPool:创建一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行,适用于大耗时的操作,可以并行来执行

ThreadPoolExecutor参数含义

在具体开发中不建议直接使用Oracle所提供的默认实现。而是通过ThreadPoolExecutor进行自定义
在这里插入图片描述

任务调度算法

如果当前线程池线程个数小于corePoolSize则开启新线程
否则添加任务到阻塞队列
如果任务队列满了,则尝试新开启线程执行任务
如果线程数量>maximumPoolSize则执行RejectedExecutionHandler。

JAVA提供4种饱和策略

1、AbortPolicy直接抛出异常。
2、CallerRunsPolicy只用调用所在的线程运行任务。
3、DiscardOldestPolicy丢弃队列里最近的一个任务,并执行当前任务。
4、DiscardPolicy不处理,丢弃掉。

任务队列、核心线程数、最大线程数的逻辑关系

当线程数小于核心线程数时,创建线程
当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列
当线程数大于等于核心线程数,且任务队列已满
若线程数小于最大线程数,创建线程
若线程数等于最大线程数,调用拒绝执行处理程序(默认效果为:抛出异常,拒绝任务)

阿里开发规范为什么不允许Executors快速创建线程池

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor方法,这样的处理方式让编程人员更加明确线程池的运行规则,规避资源耗尽的风险说明: Executors返回的线程池对象的弊端:
FixedThreadPool和SingleThreadPool允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
CachedThreadPool允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM

线程池相关的主要方法

java.util.concurrent.ExecutorService是java线程池框架的主要接口,用Future保存任务的运行状态及计算结果,主要方法有:
void execute(Runnable)提交任务到线程池
Future submit(Runnable)提交任务到线程池并返回Future
Future submit(Runnable, T) 提交任务到线程池并返回Future, 第二个参数会作为计算结果封装到Future
Future submit(Callable)提交任务到线程池并返回Future,call方法的计算结果会封装到Future
List invokeAll(Collection extends Callable>) 批量提交任务并返回计算结果
T invokeAny(Collectionextends Callable> tasks) 只执行其中一个任务并返回结果

线程池的关闭

shutdownNow:对正在执行的任务全部发出interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表
shutdown:当调用shutdown后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务

如何合理配置线程池的大小

任务性质可分为:CPU密集型任务,IO密集型任务,混合型任务。
对于计算密集型的任务,一个有N cpu个处理器的系统通常通过使用一个N cpu +1个线程的线程池来得最优的利用率
对于IO密集型任务,包含了I/O和其他阻塞操作的任务,可以设置为2Ncpu
最优的线程池的大小等于 【((线程等待时间+线程CPU时间)/线程CPU时间 )
CPU数目 】。比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32

可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进行适当调整。
Runtime.getRuntime().availableProcessors():int获取CPU的核数

线程通信

当线程在系统内运行时,线程的调度具有一定的透明性,程序通常无法精确控制线程的轮换执行,但Java也有一些机制来保证线程协调执行。

多线程有什么用

1)发挥多核CPU的优势。
2)防止阻塞。
3)便于建模。

什么是线程安全

如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
(1)不可变:
(2)绝对线程安全
(3)相对线程安全
(4)线程非安全:ArrayList、LinkedList、HashMap等都是线程非安全的类,可以引入Collections中的方法将线程非安全转换为线程安全

传统的线程通信:生产者消费者模式

Object类的wait()/notify/notifyAll
使用Lock和Condition控制线程通信

public class Basket{
    private String data;
    private final Lock lock=new ReentrantLock();
    private Condition prod=lock.newCondition();
    private Condition cons=lock.newCondition();
    
    public void put(String data){
        lock.lock();
        try{
            while(this.data!=null)
                prod.await();
            this.data=data;
            System.out.println(Thread.currentThread()+"生产"+data);
            cons.signal();
        } catch(Exception e){
            e.printStackTrace();
        } finally{
            lock.unlock();
        }
    }
    public void get(){
        lock.lock();
        try{
            while(this.data==null)
                cons.await();
            System.out.println(Thread.currentThread()+"消费"+data);
            data=null;
            prod.signal();
        } catch(Exception e){
            e.printStackTrace();
        } finally{
            lock.unlock();
        }
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值