多线程
基本相关概念
进程:是指计算机在执行的程序的实体。
线程:是指一个程序内部的顺序控制流。
进程与线程之间的关系:一个进程中可以包含一个或多个线程,一个线程就是一个程序内部的一条执行线索。
进程和线程的区别:
1.每个进程都有独立的代码和数据空间,进程的切换会有很大的开销。
2.同一类线程共享代码和数据空间,每个线程有独立运行的栈和程序计数器,线程切换的开销小。
多进程:在操作系统中能同时运行多个任务(程序)。
多线程:在同一应用程序中有多个顺序流同时执行。
并行:多个处理器或多核处理器同时处理多个任务。
并发:多个任务在同一个CPU核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时进行的。
(并发和并行可以抽象的理解为:排队等咖啡,并发就是两个队列和一台咖啡机;并行就是两个队列和两台咖啡机。)
多线程实现的方式
实现多线程有三种方式:
1.继承Thread类,重写run()方法。
2.实现Runnable接口。
3.实现Callable接口。
注意:1.Runnable接口的run()方法没有返回值,Callable可以拿到返回值,所以Callable可以看做是Runnable的补充。
2.无论是实现Runnable接口还是Callable接口,在实例化实现类后,都要将实例化的对象注入到Thread类的有参构造中。也就是说还需要实例化Thread对象,只有Thread类下的start()方法才能使线程启动。
线程状态及其生命周期
线程的状态:
1.new 尚未启动
2.runnable 正在执行
3.blocked 阻塞的(被同步锁或者IO锁阻塞)
4.waiting 永久等待状态
5.timed-waiting 等待指定的时间重新被唤醒的状态
6.terminated 执行完成
线程的生命周期图:
线程类中的主要方法
注意:启动一个线程是调用 start()方法,使线程就绪状态,以后可以被调度为运行状态,一个线程必须关联一些具体的执行代码,run()方法是该线程所关联的执行代码。
sleep()和wait()有什么区别:
1.类的不同:sleep()来自Thread,wait()来自Object。
2.释放锁:sleep()不释放锁;wait()释放锁。
3.用法不同:sleep()时间到会自动恢复;wait()可以使用notify()/notifyAll()直接唤醒。
notify()和notifyAll()有什么区别:
1.notifyAll()会唤醒所有的线程,notify()之后唤醒一个线程。
2.notifyAll()调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。
线程的优先级
1.Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照线程的优先级来决定应调度哪个线程来执行。
2.Java线程的优先级用1~10的整数来表示,越小则优先级越低。(但是Java的优先级是高度依赖于操作系统的实现的)
Thread类的三个常量,表示常用的线程优先级:
Thread.MIN_PRIORITY //1
Thread.NORM_PRIORITY // 5
Thread.MAX_PRIORITY // 10
缺省时线程具有NORM_PRIORITY
线程的终止
自动终止:一个线程完成执行后,不能再次运行
手动终止:
stop( ) —— 已过时,基本不用。
interrupt( ) —— 粗暴的终止方式
可通过使用一个标志指示 run 方法退出,从而终止线程。
线程同步
有时两个或多个线程可能会试图同时访问一个资源。例如,一个线程可能尝试从一个文件中读取数据,而另一个线程则尝试在同一文件中修改数据,在此情况下,数据可能会变得不一致。
为了确保在任何时间点一个共享的资源只被一个线程使用,使用了“同步”
当一个线程运行到需要同步的语句后,CPU不去执行其他线程中的、可能影响当前线程中的下一句代码的执行结果的代码块,必须等到下一句执行完后才能去执行其他线程中的相关代码块,这就是线程同步。
实现同步的两种方式:
1.将该方法用synchronized修饰。
synchronized void methodA() {
......
}
2.利用synchronized代码块。
synchronized (Object) {
//要同步的语句
}
实现同步的两种方式的优缺点:
使用synchronized方法:
优点:可以显示的知道哪些方法是被synchronized关键字保护的。
缺点:方法中有些内容是不需要同步的,如果该方法执行会花很长时间,那么其他人就要花较多时间等待锁被归还;只能取得自己对象的锁,有时候程序设计的需求,可能会需要取得其他对象的锁。
使用synchronized代码块:
优点:可以针对某段程序代码同步,不需要浪费时间在别的程序代码上;
可以取得不同对象的锁。
缺点:无法显示的得知哪些方法是被synchronized关键字保护的。
线程池
线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。 例如,线程数一般取cpu数量+2比较合适,线程数过多会导致额外的线程切换开销。
线程池的创建方式
线程池创建有七种方式,最核心的是最后一种:
1.newSingleThreadExecutor():它的特点在于工作线程数目被限制为1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目。
2.newCachedThreadPool():它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列。
3.newFixedThreadPool(int nThreads):重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads。
4.newSingleThreadScheduledExecutor():创建单线程池,返回 ScheduledExecutorService,可以进行定时或周期性的工作调度;
5.newScheduledThreadPool(int corePoolSize):和newSingleThreadScheduledExecutor()类似,创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。
6.newWorkStealingPool(int parallelism):这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序;
7.ThreadPoolExecutor():是最原始的线程池创建,上面1-3创建方式都是对ThreadPoolExecutor的封装。
线程池的状态
RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。
TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。
线程池中 submit() 和 execute() 方法的区别
execute():只能执行 Runnable 类型的任务。
submit():可以执行 Runnable 和 Callable 类型的任务。
Callable 类型的任务可以获取执行的返回值,而 Runnable 执行无返回值。