关于Java中线程的总结

操作系统

  • 什么是操作系统?

操作系统就是一个硬件之上的软件。

  • 为什么要有操作系统?

对于一个只有硬件的计算机,同样也可以让他去执行任务,但是一次只能执行一个任务,或者是很少的任务,效率也低,同时计算机慢慢从军队中走入人们的生活,多用户也变成了需求,而计算机硬件完全有能力去做,硬件们蕴含着巨大的潜能,迫切需要一个管理者 —— 操作系统就慢慢被发明。
操作系统是一个做管理的软件。

  • 操作系统管理什么?

操作系统主要管理的是硬件和软件。
硬件:CPU、内存、硬盘、GPU、各种外设
软件:任务/进程(Process)

程序 VS 进程

  • 程序其实就是一组 要执行的指令 + 需要的数据 组成的符合一定格式的文件。
  • 进程的概念是操作系统的一个抽象的概念,是程序的一次执行过程
  • 进程是操作系统进行资源分配的最小单位

进程通信

  • 进程和进程之间相互隔离,互相不可见。
  • 但是,进程和进程之间又有交换数据的需求,我们最好理解的就是正常的网络通信(本地的线程和远端服务器线程的通信),因此,有了进程通信的机制。
  • 进程通信的方式:

①管道
②共享内存
③ 文件
④网络
⑤有名管道
⑥信号量
⑦信号

管理进程、进程调度

  • 进程调度/进程切换

首先我们来设想一个场景:一个进程现在跑在CPU上,当他执行结束的时候,应该让下一个进程开始执行,那么什么时候下一个进程可以去执行,又是谁来让他开始执行的,这个时候就需要一个德高望重的人物来主持一下这个局面了,毫无疑问,操作系统是最佳人选。

  • 操作系统如何管理进程?

通过对每个进程的一组数据进行管理。
这些数据可以粗略的理解为:进程号,所关联程序,给予的资源,上下文切换信息等等,这一组数据称为PCB(Process Control Block)进程控制块。
现在,我们又可以想到,在许多个进程中,其实可以分类,哪些准备好了,哪些现在在阻塞,等待IO,哪些在运行,等等。所以还需要一个状态,来标识这个进程是什么状态的。

  • 为什么需要进程状态?

操作系统需要依赖状态来对进程进行管理。

进程转移图

  • 哪些情况可以使一个进程从CPU上下来
  1. 他分配到的时间片已经到了。
  2. 主动放弃CPU。
  3. IO阻塞。
  4. 进程结束。

线程

  • 什么是线程?

线程是属于进程之下的单位,可以将其理解为轻量级进程。
线程是操作系统调度的最小单位。
进程是操作系统资源分配的最小单位,是操作系统调度的单位之一。

  • 为什么要有线程?
  1. 我们需要额外的调度单位。假如现在已经有一个进程在CPU上跑了,我们的进程也要运行,现在就等操作系统的调度了,因此,我们所占用CPU的时间为总时间的 1 2 {1\over 2} 21,那么假如我们在来8个调度的单位,加上原来的一个,现在总共有9个,我们占用CPU的时间为总时间的 9 10 {9 \over 10} 109,占用CPU的时间变多了,如果我们新增的调度单位为进程,首先是创建和销毁的成本太大了,同时资源共享也出现了很大的问题,所以线程就被提出来了,可以提升效率。
  2. 在一个程序中,可能因为某些场景,导致IO阻塞,导致失去CPU时,还需要同时进行其他工作时,如果没有线程,就不好去做了。举例一个场景:当你要计算一些数的时候(假如计算时间很长),你把这个数字输入之后,本来应该的效果是,你可在输入框进行一些其他的简单的运算或者是选择这个应用的其他功能,但是,就是因为计算没有结束,整个程序都在没有往下执行了,体现出来的就是程序卡死了。
  • Java中的线程使用
  1. 创建类,继承Thread类,并重写run()方法,然后实例化这个类。
  2. 创建类,实现Runnable接口,并实现run()方法,将该类的实例对象作为参数,传递给Thread的构造方法。
  3. Lambda表达式可以选择使用,然后使得创建线程变的简洁。

注:线程被创建前,存在子父关系,创建之后,就是平等的了。

  • Java Thread VS Native Thread

Java中的线程是JVM中的,而自然线程是真实操作系统中的线程。他们之间可能是一一对应的,也有可能不是,这和JVM版本还有操作系统都有关系,因此不要将他们混为一谈。

  • 通知进程停止的机制

首先我们设想一个场景,线程1中启动了线程2。当我们需要在线程1中,停止线程2怎么做呢?

  1. 在线程1中,调用线程2.interrupt();
  2. 前提是,在线程2中,有类似的代码:while(!Thread.interrupted()){……}

注意:xxx.interrupt()会导致xxx线程的中断标志被置位(理解为boolean类型的标志位),而Thread.interrupted()会响应这个标志位,一旦他响应了,他也就连带把这个标志位清除了。Thread.interrupted()是一个静态方法,它的源码如下:

public static boolean interrupted() {
        return currentThread().isInterrupted(ClearInterrupted:true);
    }

因此,我们很明白的可以看出来,他获取的线程为这个方法出现的那个线程的isInterrupted()方法,且参数为true,即清除标志位。

  • 进程的退出(一个Java进程/JVM进程什么时候就退出了)
  • 所有前台线程都退出了,就退出了。
  • 对于OS级的进程,主线程退出了,进程就退出了。

线程安全

  • 什么是线程安全?

在我们的理想中,我们所写的代码,是符合我们的理想的,100%正确的。但是,理想终究是理想,由于种种原因,使得程序的执行,其实不是按我们所设想的那样,运行可能会出现错误,简单理解为多个线程一起打架。

  • 为什么会线程不安全?
  1. 站在系统的角度:①原子性②内存可见性③代码重排序
  2. 站在代码的角度:原子性,例如n++,这类代码就不是原子的(读->修改->写),还有long,double类型的数据的简单赋值也不是原子的,因为JVM的操作数据单位为32位的,而他俩位64位的,还得分2次,才能完成赋值。
  3. 其实究其原因,就是因为操作系统对线程的调度,对我们程序员是透明的,不可见的,也可以说是线程的随机性。
  • 原子性

一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

  • 内存可见性

在JMM(Java内存模型)中,Java程序的运行,存在工作内存和主内存。
JMM
线程对数据的操作,必须把数据从主内存中加载到自己的工作内存中,一切修改必须在工作内存中进行,直到运行结束或主动刷新到主内存,否则不会同步回主内存。不在任何保护下,一个线程看到的变量的值不保证是最新的。
因此,就会出现问题,每个线程使用的数据不是不一样了吗?这就是内存可见性导致的线程不安全。

  • 代码重排序
  • 为什么要进行代码重排序?
    程序员所写的代码,并不是最优的解决问题的方案。因此,我们所写的语句到真正执行的时候,语句的顺序可能会发生改变。
  • 代码重排序的底线:
    保证在单线程下的执行结果和未进行排序前的结果一致。
  • 代码重排序导致线程不安全(多线程场景下)
    多线程下,无法满足我们的预期。举例:new 一个对象,可以分为3大步,①开辟空间②初始化③引用赋值,3大块的原子性先不说,代码重排序,可能会导致这三步变为:①开辟空间②引用赋值③初始化,这种情况,在单线程下是没有任何影响的,但是多线程可能会导致巨大的错误,假如在第二部结束的时候,被操作系统调度,一个没有初始化的对象,就被其他线程进行了使用,想想都恐怖。
  • 从代码的角度分析线程安全问题
  1. 线程和线程之间是否有数据共享,如果没有,那么天生线程安全。
  2. 线程和线程之间有数据共享,但是,都是进行读操作,那么也是天生线程安全。
  3. 线程和线程之间有数据共享,并且至少有一个线程在修改数据。那么需要考虑:
    ①是否破坏了原子性
    ②是否具备内存可见性
    ③代码重排序引起的问题
  • 哪些数据是线程共享的呢?
  • Java运行时内存结构
  1. PC(程序计数器)
  2. 栈(Java栈,本地方法栈)
  3. 方法区

其中的,PC和栈是线程独享的,而堆和方法区是线程共享的。

解决线程安全的一种方式 - 锁机制(synchronized)

  • 加锁和释放锁

如果我们在进入临界区的代码前,进行一个上锁操作,在离开临界区之后,再把锁释放了,那么如果中间就算被调度走,其他线程遇到需要上锁的语句,执行不成功,然后放弃CPU,直到第一个加锁成功的线程释放锁,不是就可以保证了线程安全吗。因此,Java提供 了很多锁,这里先介绍synchronized

  • synchronized(同步)
  • Java语法中,synchronized的使用(前提:在Java中,每一个对象内部,都有一把锁,默认开着)
  1. 修饰成员方法,对this进行加锁
  2. 修饰静态方法,对类对象进行加锁
  3. 同步代码块,对括号内的对象进行加锁(格式为:synchronized(对象){……}
  • 对于synchronized理解上,需要知道的事情,请看下面的这段代码:
语句1;
synchronized(xx对象){
	语句2;
}

从执行语句1,到执行语句2,需要经过多久呢?答案可能是马上,也可能是永远不可能,所以从执行语句1到执行语句2,期间可能会发生很多很多的事情,我们在编程的时候,需要注意考虑到这个点。

  • 如果多个线程竞争同一把锁,竞争失败的线程,该怎么办
  1. 首先,竞争失败之后,也就没有资格去继续执行代码
  2. 无法继续执行代码,就不该占着处理机的资源,应当放弃处理机
  3. 触发线程的调度,自己被调度器从处理机上调度下来
  4. 自己加入对应锁对象的阻塞队列,方便以后锁被释放了之后,被叫回来,同时也要把自己的状态从RUNNABLE 改为 BLOCKED
  • synchronized可以保护什么
  1. 原子性
    synchronized所包裹的那段临界区代码,可以保证,可能与之互斥的操作的线程,都无法执行,只有等到当前线程把这段代码执行结束,才会释放锁,其他线程才可以获取到锁,才会拥有执行代码的权限,保护了临界区代码的原子性。
  2. 内存可见性
    Java官方实现synchronized的时候,实现了当一个线程加锁成功之后,必须做一次从主内存到工作内存的同步工作,保证当前看到的数据是最新的;释放锁的时候,必须做一次从工作内存到主内存的刷新工作,保证主内存的数据是最新的,即为了让后续的线程看到的数据是最新的。对一把锁,加锁的线程只能是一个,所以,所有线程拿到锁的时候,都可保证数据的最新性,保证了内存可见性。
  3. 代码重排序
    Java可以保证synchronized修饰的那一段代码区域、其之前的代码,其之后的代码的相对顺序不会被重排序打乱。

解决线程安全的另一种方式 - volatile

  • volatile用法
  1. 修饰属性
  2. 修饰静态属性
  • volatile

volatile本意为易变的,被它修饰的变量也就是易变的。

  • volatile的作用
  1. 最主要也是最重要的作用:保证变量的内存可见性
    volatile修饰的变量,每次对它的读操作,都必须从主内存同步,每次写操作,都必须做一次到主内存的刷新。
  2. 非常局限的情况下,它还可以保证原子性
    对于longdouble类型的变量,他们都是64位的,正常情况,他们的赋值不是原子的,而只要被volatile修饰了之后,就可以保证他们的原子性。
  3. 非常局限的情况下,可以保护代码重排序引起的问题
    还是我们的new操作,被volatile修饰的对象,其后接new操作的时候,不会使引用赋值出现在初始化之前。

多线程的通知机制

首先,设想一个场景,如果线程1的继续运行,需要线程2中,某段代码的执行结果,那么,为了得知线程2的运行情况,我们有2种方案:

  1. 轮询模式
    每过一段时间,去查看一下,线程2的运行情况,如果还没有执行,那么继续等待。这种方案有很多的缺点,不停的查看,大多数情况下,都是在做无用功,而且查看的时候,还需要切换线程,效率太低了。
  2. 通知模式
    线程1只需要提供一个联系方式,然后当线程2执行完那段代码之后,由它自己来通知一下线程1,等待期间,没有无用的线程切换,效率比较高。
  • Java中通知机制的使用

Object类下提供(意味着每个类都有这3个方法)的3个方法

  1. wait() //等待
  2. notify() //通知
  3. notifyAll() //通知所有人

使用这3个方法需要注意:

  1. 要在某个对象上使用这3个方法,必须先持有该对象的锁。
    xx.wait()是把自己(线程)加入到该对象的等待队列中,那么当前线程就要修改对象中的信息,为了保证一系列操作的原子性,所以需要在加锁的情况下使用,如果没有加锁,之间使用,就会收到IllegalMoniterStateException异常。
  2. 先使用notify,在使用wait,那么会被wait吗?
    会的,notify不会被保留下来,最后执行的wait,那么就是wait状态。
  3. 调用wait会释放锁
    wait释放锁
    这里如果理解不了,可以试问自己一下,如果不释放锁,那么它该怎么被唤醒呢?
  4. notify()是会随机唤醒一个处于等待队列中的线程,而notifyAll()会唤醒等待队列中所有的线程。
  • wait()/wait(timeout)被唤醒的条件
  1. 其他线程调用了notify(),且自己被选中。
  2. 其他线程调用了notifyAll()
  3. 其他线程调用了xx.interrupt(),被通知停止且抛出异常。
  4. 对于wait(timeout),timeout的时间到了。
  5. 在某些系统上,可能存在虚假唤醒。
    我们一般调用wait()都是在期待着某些事情的发生/某些条件的发生,因此我们一般都会这么写代码:
while(某个条件未发生){
	xx.wait();
}

线程池

  • 为什么需要线程池

如果我们每次需要一个线程,直接去创建一个线程,然后让他做完事情之后,在把它销毁掉,之后又需要一个线程,然后再创建,再销毁,频繁的创建/销毁线程对性能的消耗比较高,会使得系统的效率变低。因此,我们有了一个新的想法,就是我们用线程的时候,可以直接去一个池子里面拿出来用,用完在放进去,一个线程就可以被反复的使用,效率自然就上来了,依赖池技术,线程池就出来了。

  • Java中提供的线程池

Executor(父接口)<— ExecutorService(子接口)<— ThreadPoolExecutor(实现类)
构造方法如下:

public ThreadPoolExecutor(
	int corePoolSize,
	int maximumPoolSize,
	long keepAliveTime,
	TimeUnit unit,
	BlockingQueue<Runnable> workQueue,
	ThreadFactory threadFactory,
	RejectedExecutionHandler handler){……}
  • 理解构造方法

我们先把线程池看做是一个公司

  1. corePoolSize 正式员工(不会被解雇)的最大人数
  2. maximumPoolSize 正式员工 + 临时员工的最大人数
  3. keepAliveTime 临时员工不工作之后,会在这个时间之后被解雇
  4. unit 不工作计时的时间单位
  5. workQueue 关于全体员工的任务队列,每个人来这里领任务
  6. threadFactory 当需要人的时候,来这里招人
  7. handler 当公司很忙很忙,没有时间,没有员工,去做其他人给公司的事情的时候,公司该怎么做,这里是拒绝策略

公司内部的流程是,起初公司里面一个人也没有,当有任务来了的时候,首先去招正式员工,招来之后,就顺便把任务给他,此后,每有新任务来了的时候,只要正式员工没有招够,就一直招正式员工,并且把任务给他,什么时候正式员工达到了corePoolSize的大小,再有新任务来,就放入阻塞队列(workQueue),期间前面所有的正式员工当他们的任务做完之后,都会在阻塞队列中去领取任务,什么时候阻塞队列也满了,再来新任务的时候,就开始招临时工了,并且把新任务给他,就这样不断的招人,什么时候当临时工也招满了(临时工总数 + 正式工总数 等于 maximumPoolSize),就要开始执行拒绝策略了(handler),从一个临时工没有活可干的那一刻起,且期间也一直没有活干,那么它会在(keepAliveTime)的时间之后被解雇。

  • 关于拒绝策略
    拒绝策略

AbortPolicy - 以异常的形式来通知
CallerRunsPolicy - 公司没能力执行,由你们自己去执行吧
DiscardOldestPolicy - 丢弃阻塞队列中最老的那一个(呆的最久的),然后把新任务加入队列
DiscardPolicy - 悄悄的丢弃掉任务

多种多样的锁

  • 乐观锁/悲观锁

乐观锁和悲观锁其实不是锁,仅仅只是面对并发场景的时候的态度:乐观/悲观
乐观锁:不上锁了,如果发生了错误,再说,或者是可以承受错误
悲观锁:就算发生错误的概率很低,还是持悲观态度,进行上锁

  • 读锁/写锁

就拿我们现实生活来说,作家的数量是远远小于读者的数量的,在大部分场景中,读操作也是远远多于写操作的,且读操作和读操作直接是不互斥的,但是因为读操作和写操作又是互斥的,不得不对其进行加锁操作,但是这会使得大量的读操作也需要参与进来,效率实在是太低了,为了优化,提出读写锁,即只要现在只有读锁,那么所有的读锁都可以加锁成功,此时如果需要写操作,那么就需要等待所有加读锁的线程释放写锁,写操作和写操作还是和原来的一样,互斥。其中读操作加的是读锁,写操作加的是写锁,虽然看起来是在两个对象上进行加锁,但是他们是有联系的,可以抽象理解为一把锁。

  • 可重入锁/不可重入锁

synchronized本身就是可重入锁,如果一个线程已经完成了加锁,而且其内部依然需要对该锁进行加锁怎么办呢?场景如下:

public class Demo{
	public synchronized void method1(){
		……
	}
	public synchronized void method2(){
		method1();
	}

	public static void main(String[] args){
		Demo demo = new Demo();
		demo.method2();
	}
}

主方法调用method2,需要对demo对象进行加锁,然后method2内部需要执行method1()方法,也要对demo进行加锁,那么它可以加成功吗?我们希望它加锁成功吗?
我们当然觉得它需要加锁成功,这就是可重入锁被设计的必要性。不可重入锁,与之相反,即在此场景下,method1()就无法被执行。

  • 公平锁/非公平锁

公平和不公平就和我们的生活中的意思一样。首先,我们知道当加锁失败之后,线程会进入阻塞队列,而当调用wait的时候,会进入等待队列,等锁释放的时候,或者等唤醒的时候,假如我们只唤醒一个,那么该唤醒哪一个呢?默认情况下,我们是随机唤醒(notify),显然这样就不公平,最公平的是,我们只唤醒最先排队的那一个线程,因此公平锁和非公平锁就出来了。synchronized是非公平锁,而Lock lock = new ReentrantLock(true);所新建的锁就是公平锁。

  • 自旋锁

加锁失败 到 放弃CPU,操作系统需要陷入内核态来完成线程的调度,这一过程其实是很费时间的,现实中,加锁的临界区其实是比较小的,放弃CPU不值当。可能会发生加锁失败-放弃CPU-获取CPU-加锁成功……,这样的情况,其实稍微等一下,就可能可以获取到锁了,可以这样:加锁失败-不放弃CPU-自己跑一些无意义代码-加锁成功……,自旋锁可以理解为自己兜圈的锁。

  • CAS(Compare And Set / Swap)

中文意思为:比较并设置/替换
可以把CAS理解为:

public boolean cas(数据的地址,期望值,要修改的值){
	if(数据 == 期望值){
		数据 = 要修改的值;
		return true;
	}
	return false;
}

上述的整个过程是原子的,从CPU的指令集开始到操作系统到JVM,可以保证这一套操作是原子操作。
有了CAS之后,我们就可以知道锁的设计原理了。

class MyLock{
	boolean isLock = false;//锁状态
	Thread[] threads = ……;//加锁失败的阻塞队列
	void lock(){
		boolean success = cas(&isLock,false,true);
		if(success){
			return;
		}else{//加锁失败
			thread.add(当前线程);
			//更改当前线程的状态 -> BLOCKED
			//放弃CPU
		}
	}
	void unLock(){
		//更改线程状态
		//从阻塞队列唤醒一个或多个线程
	}
}

synchronized的优化(HotSpot)

  • 锁消除

背景:大部分的类,方法都带有了锁,但是大部分应用场景又是单线程场景

因此,JVM为了优化这些无意义的加锁释放锁操作,它只要发现现在的场景是单线程,那么它会把所有的锁操作都消除掉,来完成优化。

  • 锁粗化

背景:理论上,锁的粒度越小越好,粒度越小,并发度就越高,但是如果锁的粒度实在是太小了,可能会出现 加锁-操作-释放锁-加锁-操作-释放锁……,在一个时间片内,发生了没有必要的加锁和释放锁,使得效率反而更低了。

因此,JVM为了优化这些多余的加锁,释放锁,它会把锁的粒度适当的提高,效果就是 加锁-操作-操作-操作-释放锁,效率就上来了。

  • 偏向锁

背景:在多线程场景下,互斥的操作会进行加锁操作,但是在现实场景中,可能往往只是那一个线程在获取锁,这样的加锁和释放锁会造成性能消耗。

因此,JVM为了优化,在锁一开始就有了偏向策略,当锁被第一次加锁的时候,会记录是哪一个线程,此后,每次加锁和释放锁都走快速通道(更快的加锁和释放锁),但是,一旦另外一个线程对该锁进行加锁,快速通道消失,以后的所有线程都走到正常的通道。

  • 轻量级锁

背景:线程的切换其实花费相对还是很多的,而临界区的代码又不长,所以稍微等下,就可以获取到锁。轻量级——自旋锁实现。
JVM采用了类似信用的机制,如果加锁失败,那么就自旋一会儿,自旋期间会不断获取锁,我们可以理解线程和线程之间存在信用机制,他们约定好了大致占用锁的时间,如果自旋了几次还没有加锁成功,那么会主动放弃CPU,进入阻塞状态,如果每次自旋之后,都可以获取到锁,甚至线程之间的关系还会变的更好,下次可以多自旋,等一会儿。

  • 重量级锁

也就是互斥锁,也是我们之前说的,加在对象上的监视器,来完成锁操作。

实际场景

  • 单例模式(这里举例为:懒汉模式)
public class SingletonClass {
    private static volatile SingletonClass instance = null;

    private SingletonClass() {
    }

    public static SingletonClass getInstance() {
        if (instance == null) {
            synchronized (SingletonClass.class) {
                if (instance == null) {
                    instance = new SingletonClass();
                }
            }
        }
        return instance;
    }
}

首先,单例模式,该类的对象是唯一的,因此,把实例化的权力收回(使用private修饰构造方法),即收回new对象的权力,保存这唯一的对象的地方,静态属性再合适不过了,我们有了这个对象的,同时也需要向外提供该对象,因此,编写getInstance()方法。考虑到多线程场景下,多个线程同时使用获取该类对象,因此需要考虑线程安全问题。因此我们这里使用synchronized关键字。解释如下:

  • 为什么synchronized放在if里面?
    如果把synchronized放在if的外面,那么每次调用getInstance方法,都要进行上锁,实际上我们99%的情况下,都是只读的,只有最一开始才要去写,为了效率,我们放在里面。
  • 为什么synchronized前已经判定了instancenull,里面还要进行一次判定呢?
    我们上面已经讲过了从synchronized前的代码到synchronized里面的代码,期间可能会进行很多的操作,我们每次都需要去考虑可能发生的事情。我们设想一下,如果线程1加锁执行getInstance方法,刚刚执行到synchronized的前一句话,还没有来的及上锁,操作系统进行线程调度(可能是时间片到了),然后线程2完成了整个getInstance方法,完成了instance的初始化,这个时候,线程1再次获取CPU,那么它不应该去初始化instance,所以再加一个判定null操作。
  • instance为什么要被volatile修饰?
    这里volatile修饰,不是保证内存可见性的问题,而是我们上面讲的,关于代码重排序的问题,这里保证了new操作的代码重排序。我们再设想,如果线程1执行到了new操作,而且代码重排序为①开辟空间②引用赋值③初始化,当执行完引用赋值之后,线程被调度下CPU,线程2,去执行getInstance方法,它执行到了if(instance == null),这里的instance已经不是null了,但是还没有被初始化,线程2就拿到了未初始化的实例对象,后果很严重,所以我们需要volatile来保证,这里不要被代码重排序引起问题。
  • 阻塞队列
public class MyBlockingQueue {
    private final Runnable[] queue;
    private int size;
    private int head;
    private int tail;

    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    public MyBlockingQueue(int initCap) {
        queue = new Runnable[initCap];
        size = 0;
        head = 0;
        tail = 0;
    }

    public void put(Runnable task) throws InterruptedException {
        lock.lock();
        try {
            while (size == queue.length) {
                notFull.await();
            }

            queue[tail] = task;
            size++;
            tail = (tail + 1) % queue.length;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public synchronized Runnable take() throws InterruptedException {
        lock.lock();
        try {
            while (size == 0) {
                notEmpty.await();
            }

            Runnable ret = queue[head];
            size--;
            head = (head + 1) % queue.length;
            notFull.signal();
            return ret;
        } finally {
            lock.unlock();
        }
    }

    public int size() {
        return size;
    }
}
  • 定时器
public class MyTimer {
    private final PriorityBlockingQueue<MyTimerTask> queue;
    private final WorkThread workThread;

    public MyTimer() {
        queue = new PriorityBlockingQueue<>();
        workThread = new WorkThread(queue);
        workThread.start();
    }

    public void schedule(MyTimerTask task, long delay) {
        task.runAt = System.currentTimeMillis() + delay;
        queue.put(task);
        synchronized (queue) {
            queue.notify();
        }
    }

}

class WorkThread extends Thread {
    private PriorityBlockingQueue<MyTimerTask> queue;

    public WorkThread(PriorityBlockingQueue<MyTimerTask> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            while (true) {
                MyTimerTask target = queue.take();
                if (target.runAt <= System.currentTimeMillis()) {
                    target.task.run();
                } else {
                    long timeToRun = target.runAt - System.currentTimeMillis();
                    synchronized (queue) {
                        queue.put(target);
                        queue.wait(timeToRun);
                    }
                }
            }
        } catch (InterruptedException exc) {
            exc.printStackTrace();
        }
    }
}
public class MyTimerTask implements Comparable<MyTimerTask> {
    public Runnable task;
    public long runAt;

    public MyTimerTask(Runnable task) {
        this.task = task;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        return (int) (this.runAt - o.runAt);
    }
}
  • 线程池

这里实现的是简单版本的线程池,没有临时工,只有正式员工,没有线程工厂,阻塞队列是自己规定的,拒绝策略为之间抛异常,即AbortPolicy

public class ThreadPool {
    private final BlockingQueue<Runnable> queue;
    private final Thread[] workers;
    private final int corePoolSize;
    private int size;

    public ThreadPool(int corePoolSize) {
        workers = new Thread[corePoolSize];
        this.corePoolSize = corePoolSize;
        queue = new ArrayBlockingQueue<>(5);
        size = 0;
    }

    public void execute(Runnable task) throws InterruptedException {
        if (size < corePoolSize) {
            workers[size] = new Worker(task, queue);
            workers[size].start();
            size++;
        } else if (queue.size() < 5) {
            queue.put(task);
        } else {
            throw new InterruptedException("The queue is full");
        }
    }

    public void shutDown() throws InterruptedException {
        for (int i = 0; i < corePoolSize; i++) {
            workers[i].interrupt();
        }
        for (int i = 0; i < corePoolSize; i++) {
            workers[i].join();
        }
    }
}

class Worker extends Thread {
    private Runnable firstTask;
    private final BlockingQueue<Runnable> queue;

    public Worker(Runnable task, BlockingQueue<Runnable> queue) {
        firstTask = task;
        this.queue = queue;
    }

    @Override
    public void run() {

        while (!Thread.interrupted()) {
            try {
                if (firstTask == null) {
                    Runnable task = queue.take();
                    task.run();
                } else {
                    firstTask.run();
                    firstTask = null;
                }
            } catch (InterruptedException e) {
                break;
            }
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值