线程、多线程以及线程池详解与总结

什么是进程?

进程是程序一次动态执行的过程。一个程序一般是一个进程,也可以有多个进程。一个进程可以有多个线程,但只有一个主线程。进程与程序不是一 一对应的。系统中没有相同的进程(开始时间不同)。

什么是线程?

线程(thread): 是操作系统能够进行运算调度的最小单位。线程是进程的一部分,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。线程是独立调度和分派的基本单位。同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。一个进程可以有很多线程,每条线程并行执行不同的任务。

创建线程的4种方式

  • 继承Thread类,重写run()方法
public class ThreadTest extends Thread{
    
    
	@Override
	public void run() {
    
    
		super.run();
	}
	public static void main(String[] args) {
    
    
		ThreadTest thread = new ThreadTest();
		thread.start();
	}
}
  • 实现Runnable接口,实现run() 方法
public class ThreadTest implements Runnable{
    
    
	@Override
	public void run() {
    
    
		System.out.println("实现Runnable接口");
	}
	public static void main(String[] args) {
    
    
		Thread thread = new Thread(new ThreadTest());
		thread.start();
	}
}
  • 实现Callable接口并通过FutureTask创建线程
public class ThreadTest implements Callable<Integer>{
    
    
	@Override
	public Integer call() throws Exception {
    
    
		int i = 0;
		System.out.println("实现Callable接口");
		return i;
	}
	public static void main(String[] args) {
    
    
		// 创建Callable实现类
		ThreadTest threadTest = new ThreadTest();
		// 将实现类封装到FutureTask中
		FutureTask<Integer> futureTask = new FutureTask<Integer>(threadTest);
		// 创建线程
		Thread thread = new Thread(futureTask);
		// 启动线程
		thread.start();
	}
}
  • 使用线程池
    Java通过Executors提供四种线程池,分别为:
    (1)newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
    (2)newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
    (3)newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
    (4)newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
    如果想让线程池执行任务的话需要实现Runnable或Callable接口。
public class ThreadTest implements Callable<Integer>{
    
    

	@Override
	public Integer call() throws Exception {
    
    
		int i = 0;
		System.out.println("实现Callable接口");
		return i;
	}
	public static void main(String[] args) throws InterruptedException, ExecutionException {
    
    
		
		// 获取线程池
		ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
		// 获取Future,里面封装了实现Callable的ThreadTest
		Future<Integer> submit = cachedThreadPool.submit(new ThreadTest());
		// 运行线程,获取结果
		Integer i = submit.get();
	}
}

Runnable接口和Callable接口的区别

Runnable接口的run()方法没有返回值,Callable接口的call()方法有返回值。

执行execute()方法和submit()方法的区别

(1)execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程执行成功与否;
(2)submit()方法用于提交需要返回值的任务,线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功,并且可以通过Future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit) 方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完成。

线程的状态

在这里插入图片描述

  1. 新建状态(New):新创建了一个线程对象。
  2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。
  3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    (一)、等待阻塞:运行的线程执行wait() 方法,JVM会把该线程放入到等待池中。(注意:wait() 方法会释放持有的锁)。
    (二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把线程放入锁池中。
    (三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程设置为阻塞状态。当sleep() 状态超时、join() 等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态(注意:sleep()不会释放持有的锁)。
  5. 死亡状态(Dead):线程执行完了或者因异常退出了run() 方法,该线程结束生命周期。

一般线程和守护线程的区别

所谓守护线程是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。
区别:唯一的区别是判断JVM何时离开,Daemon(守护线程)是为其他线程服务的,如果全部的User Thread已经撤离,Daemon没有可服务的线程,JVM撤离。也可以理解为守护线程是JVM自动创建的线程(但是不一定,也可以由用户自定义创建),用户线程是程序创建的线程。
使用守护线程需要注意的几点:
(1)thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
(2)在Daemon线程中产生的新线程也是Daemon的。
(3)守护线程应该永远不去访问固有资源,如文件、数据库等,因为它会在任何时候甚至一个操作的过程中发生中断。

线程方法

(一)、sleep和wait的区别
(1)sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时候会自动恢复。调用sleep不会释放对象锁。sleep()使当前线程进入阻塞状态,在指定时间内不会执行。
(2)wait是Object类的方法,对此对象调用wait方法会导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。
区别:
1、sleep是线程类(Thread)的方法,wait是Object类的方法
2、调用sleep方法不会释放所持有的所有对象锁,而wait方法会释放所持有的所有对象锁。
3、wait、notify、notifyAll只能在同步控制方法或者同步控制块中使用,而sleep可以在任何地方使用(使用范围不同)
4、
(1)sleep会让一个线程进入睡眠状态,等待一定的时间后自动醒来进入到可运行状态(就绪状态),但是不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间。注意,sleep方法是一个静态的方法,也就是说它只对当前对象有效,通过thread.sleep(5000)让thread线程对象进入sleep的做法是错误的。它只会使当前线程(main线程)被sleep,而不是thread线程。下面代码的结果是thread线程执行完for循环100次后,main线程还在等待几秒后才打印 “main执行完!”

public class SleepTest implements Runnable{
    
    

	@Override
	public void run() {
    
    
		for (int i = 0; i < 100; i++) {
    
    
			System.out.println("测试线程执行:"+i);
		}
	}
	public static void main(String[] args) throws InterruptedException {
    
    
		// 创建测试的线程
		Thread thread = new Thread(new SleepTest());
		// 线程开始
		thread.start();
		// 让主线程(main)睡眠5秒
		thread.sleep(5000);
		System.out.println("main执行完!");
	}
}

(2)一旦一个对象调用了wait方法,则必须要使用notify()或notifyAll()来唤醒该对象。
(二)、yield()、join()、notify()、notifyAll()
(1)yield()方法是停止当前线程,让同等优先级或更高优先级的线程有执行的机会。如果没有的话,那么yield()方法将不会起作用,并且由可执行状态后马上又被执行。
(2)join()方法是用于在某一个线程的执行过程中调用另一个线程执行,等到被调用的线程执行结束后,再继续执行当前线程。如:tt.join() //主要用于等待线程tt运行结束,若无此句,main则会执行完毕。

未使用tt.join()时

public class JoinTest implements Runnable{
    
    
	
	@Override
	public void run() {
    
    
		
		System.out.println("tt线程执行开始!");
		System.out.println("tt线程执行结束!");
	}
	public static void main(String[] args) throws InterruptedException {
    
    
		// 创建测试的线程
		Thread tt = new Thread(new JoinTest());
		System.out.println("main开始执行!");
		// 线程开始
		tt.start();
		// 在main线程中加入tt线程
		// tt.join();
		System.out.println("main执行完!");
	}
}

运行结果是:

main开始执行!
main执行完!
tt线程执行开始!
tt线程执行结束!

使用tt.join()时

public class JoinTest implements Runnable{
    
    
	
	@Override
	public void run() {
    
    
		
		System.out.println("tt线程执行开始!");
		System.out.println("tt线程执行结束!");
	}
	public static void main(String[] args) throws InterruptedException {
    
    
		// 创建测试的线程
		Thread tt = new Thread(new JoinTest());
		System.out.println("main开始执行!");
		// 线程开始
		tt.start();
		// 在main线程中加入tt线程
		tt.join();
		System.out.println("main执行完!");
	}
}

运行结果是:

main开始执行!
tt线程执行开始!
tt线程执行结束!
main执行完!

(3)notify()方法只唤醒一个在此对象上等待的线程并使该线程开始执行。如果有多个线程等待一个对象,这个方法只会唤醒一个线程,选择哪一个线程取决于操作系统对多线程管理的实现。
(4)notifyAll()会唤醒在此对象上等待的所有线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态,没有获得锁的线程则继续等待。

什么是多线程?

在一个程序中同时运行多个线程完成不同的工作,称为多线程

多线程的优点及问题

优点:
(1)资源利用率更好
(2)程序设计在某些情况下更简单
(3)程序响应更快
了解详情:http://ifeve.com/benefits/
问题:
(一)线程同步问题
(二)线程安全问题

什么是死锁?

所谓死锁,就是指各并发进程互相等待对方所拥有的资源,且这些并发进程在得到对方的资源之前不会释放自己所拥有的资源,从而造成大家都想得到资源而又都得不到资源,各并发进程不能继续向前推进的状态。

死锁产生的必要条件

(1)互斥条件。即某个资源在一段时间内只能由一个进程占有,不能同时被两个或两个以上的进程占有。这种独占资源如CD-ROM驱动器,打印机等等,必须在占有该资源的进程主动释放它之后,其它进程才能占有该资源。这是由资源本身的属性所决定的。如独木桥就是一种独占资源,两方的人不能同时过桥。

(2)不可剥夺条件。进程所获得的资源在未使用完毕之前,资源申请者不能强行地从资源占有者手中夺取资源,而只能由该资源的占有者进程自行释放。如过独木桥的人不能强迫对方后退,也不能非法地将对方推下桥,必须是桥上的人自己过桥后空出桥面(即主动释放占有资源),对方的人才能过桥。

(3)占有且申请条件。进程至少已经占有一个资源,但又申请新的资源;由于该资源已被另外进程占有,此时该进程阻塞;但是,它在等待新资源之时,仍继续占用已占有的资源。还以过独木桥为例,甲乙两人在桥上相遇。甲走过一段桥面(即占有了一些资源),还需要走其余的桥面(申请新的资源),但那部分桥面被乙占有(乙走过一段桥面)。甲过不去,前进不能,又不后退;乙也处于同样的状况。

(4)循环等待条件。存在一个进程等待序列{P1,P2,…,Pn},其中P1等待P2所占有的某一资源,P2等待P3所占有的某一源,…,而Pn等待P1所占有的的某一资源,形成一个进程循环等待环。就像前面的过独木桥问题,甲等待乙占有的桥面,而乙又等待甲占有的桥面,从而彼此循环等待。

上面我们提到的这四个条件在死锁时会同时发生。也就是说,只要有一个必要条件不满足,则死锁就可以排除。

死锁的预防

(1)打破互斥条件。即允许进程同时访问某些资源。但是,有的资源是不允许被同时访问的,像打印机等等,这是由资源本身的属性所决定的。所以,这种办法并无实用价值。

(2)打破不可抢占条件。即允许进程强行从占有者那里夺取某些资源。就是说,当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再重新申请。它所释放的资源可以分配给其它进程。这就相当于该进程占有的资源被隐蔽地强占了。这种预防死锁的方法实现起来困难,会降低系统性能。

(3)打破占有且申请条件。可以实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要的全部资源。如果某个进程所需的全部资源得不到满足,则不分配任何资源,此进程暂不运行。只有当系统能够满足当前进程的全部资源需求时,才一次性地将所申请的资源全部分配给该进程。由于运行的进程已占有了它所需的全部资源,所以不会发生占有资源又申请资源的现象,因此不会发生死锁。但是,这种策略也有如下缺点:

1)在许多情况下,一个进程在执行之前不可能知道它所需要的全部资源。这是由于进程在执行时是动态的,不可预测的;
2)资源利用率低。无论所分资源何时用到,一个进程只有在占有所需的全部资源后才能执行。即使有些资源最后才被该进程用到一次,但该进程在生存期间却一直占有它们,造成长期占着不用的状况。这显然是一种极大的资源浪费;
3)降低了进程的并发性。因为资源有限,又加上存在浪费,能分配到所需全部资源的进程个数就必然少了。

(4)打破循环等待条件,实行资源有序分配策略。采用这种策略,即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。进程占用了小号资源,才能申请大号资源,就不会产生环路,从而预防了死锁。这种策略与前面的策略相比,资源的利用率和系统吞吐量都有很大提高,但是也存在以下缺点:

1)限制了进程对资源的请求,同时给系统中所有资源合理编号也是件困难事,并增加了系统开销;
2)为了遵循按编号申请的次序,暂不使用的资源也需要提前申请,从而增加了进程对资源的占用时间。

死锁避免

银行家算法

原文地址:https://blog.csdn.net/abigale1011/article/details/6450845

什么是线程池

在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。

线程池的优点

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能执行。

第三:提高线程的可管理性,线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

创建线程池

Java 5+中的Executor接口定义一个执行线程的工具。它的子类型即线程池接口是ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类Executors面提供了一些静态工厂方法,生成一些常用的线程池,如下所示:
1、newFixedThreadPool创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
2、newCachedThreadPool创建一个可缓存的线程池。这种类型的线程池特点是:
1).工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
2).如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
3、newSingleThreadExecutor创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,如果这个线程异常结束,会有另一个取代它,保证顺序执行(我觉得这点是它的特色)。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的 。
4、newScheduleThreadPool创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于Timer。

Executors返回线程池对象的弊端

  • FixedThreadPool和SingleThreadExecutor:允许请求队列的长度为Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM(Out Of Memory,内存溢出);
  • CachedThreadPool和ScheduleThreadPool:允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM(Out Of Memory,内存溢出);

线程池运行流程

推荐这个博主写的线程池运行流程:
https://blog.csdn.net/u011240877/article/details/73440993

猜你喜欢

转载自blog.csdn.net/qq_47768542/article/details/109129897