进程:程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
线程:一个比进程更小的执行单位,是操作系统能够进行运算调度的最小单位,包含在进程之中,是进程中的实际运作单位。
两者的关系(区别):线程是进程的子集,一个进程在其执行的过程中可以产生多个线程,每条线程并发执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。进程中的线程共享进程的堆和方法区的资源,每个线程有自己的程序计数器、虚拟机栈和本地方法栈。
栈是一块和线程紧密相关的内存区域。每个线程都有自己的栈内存,用于存储本地变量,方法参数和栈调用,一个线程中存储的变量对其它线程是不可见的。
堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时volatile 变量就可以发挥作用了,它要求线程从主存中读取变量的值。
线程安全:是指多线程执行同一份代码每次执行结果都和单线程一样。
线程同步:对临界区的共享数据,A线程去操作数据,并且需要另一线程B的操作才能继续完成,这种线程之间协作的就是线程同步。
线程互斥:对临界区的共享数据,两个线程都有修改情况,如果没有加锁或cas等的操作会造成数据混乱异常,这种就是线程互斥。
线程通信:可以认为是线程同步的扩展,因为wait/notify必须获取了对象锁才能使用,通过wait/notify这种方式实现两个线程的等待唤醒。
新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态
class MyThread implements Runnable{
pulic void run(){
System.out.println("Thread Body");
}
}
public class Test{
public static void main(String[] args){
MyThread myThread = new MyThread;
Thread thread = new Thread(myThread);
thread.start();//启动线程
}
}
class MyThread extends Thread{//创建线程类
public void run(){
System.out.println("Thread Body");//线程的函数体
}
}
public class Test{
public static void main(String[] args){
MyThread thread = new Thread
thread.run();//开启线程
}
}
public class CallableAndFuture{
//创建线程类
public static class CallableTest implements Callable{
public String call() throws Exception{
return "Hello World!";
}
}
public static void main(String[] args){
ExecutorService threadPool = Executors.newSingleThreadExecutor();
Future<String> future = threadPool.submit(new CallableTest());
try{
System.out.println("waiting thread to finish");
System.out.println(future.get());
}catch{Exception e}{
e.printStackTrace
}
}
}
Java不支持类的多继承,但是允许实现多个接口,所以继承了Thread类就不能继承其他类,而且Thread类实际上也是实现了Runnable接口,所以最好使用Runnable接口实现线程。
Runnable和Callable都是创建线程的方式。
Runnable:可以处理同一资源,实现资源的共享;线程不能返回结果;run()方法的异常只能在内部消化,不能继续上抛。
Callable:可以获取到线程执行的返回值、是否执行完成等信息,即能返回执行结果;call()方法允许向上抛出异常。
线程调度器:一个操作系统服务,负责为Runnable状态的线程分配CPU时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。
时间分片:将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择(即最好不要让你的程序依赖于线程的优先级)。
Future:是一个接口。是对runnable、callable的运行结果的操作,可以判断运行是否完成、是否取消以及获取运行的结果,获取运行结果调用get方法,这种方式获取结果是同步的。
FutureTask:是一个实现类,实现了Future和Runnable接口。所以既可以作为Runnable去启动一个线程,也可以作为Future去获取线程运行结果,只有当运行完成的时候结果才能取回结果,如果运行尚未完成get方法将会阻塞。Future是接口不能直接操作运行结果,FutureTask可以,也是Future唯一的实现类。由于FutureTask也是调用了Runnable接口所以它可以提交给Executor来执行。
一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性。也就是说当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么这个类就可以称为是线程安全的。
如果多个线程,存在着共享数据,那么就有可能出现线程的安全问题:当其中一个线程操作共享数据时,还未操作完成,另外的线程就参与进来,导致对共享数据的操作出现问题。
解决线程不安全:
要求一个线程操作共享数据时,只有当其完成操作共享数据,其它线程才有机会执行共享数据。
下文多线程同步的知识即为了解决线程不安全。
在多线程应用中,多个线程需要共享对同一数据进行存取,会由于不正确的执行时序,而出现多个不正确的结果。这样计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件。
最常见的竞态条件类型就是先检查后执行操作,即通过一个可能失效的观测结果来决定下一步的动作。
如下图,对于main线程,如果文件a不存在,则创建文件a,但是在判断文件a不存在之后,Task线程创建了文件a,这时候先前的判断结果已经失效,(main线程的执行依赖了一个错误的判断结果)此时文件a已经存在了,但是main线程还是会继续创建文件a,导致Task线程创建的文件a被覆盖、文件中的内容丢失等等问题。
因此引出线程中的锁。
为了解决上面的竞态条件,当多个线程需要访问同一资源的时候,它们需要以某种顺序来确保该资源在某一时刻只能被一个线程使用,否则程序的运行结果将不可预料。也就是说,当线程A需要使用某个资源,如果该资源正被线程B使用,同步机制就会让线程A一直等待下去,直到线程B结束对该资源的使用,线程A才能使用。
要想实现同步操作,必须获得每一个线程对象的锁(lock)。获得锁可以保证同一时刻只有一个线程进入临界区(访问互斥资源的代码块),并且在这个锁被释放之前,其它线程都不能再进入这个临界区。如果还有其他线程想要获得该对象的锁,只能先进入等待队列。当拥有该对象锁的线程退出临界区,锁才会被释放,等待队列中优先级最高的线程才能获得该锁,从而进入共享代码区。
临界区:指访问那种一次只能有一个线程执行的资源的代码块。
实现同步的方式有以下三种方式。
每个Java对象都可以用作一个实现同步的对象锁,这些锁称为内置锁(synchronized)。
其使用方式就是使用synchronized关键字,表明在任何时候只允许被一个线程所拥有。线程在进入同步代码块之前自动获得锁,然后执行相应的代码,在退出同步代码块时自动释放锁,获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
public synchronize void test();
synchronized(syncObject){
//访问syncObject的代码块
}
在synchronized代码被执行期间,线程可以调用对象的wait()方法,释放对象锁,进入等待状态,并且可以调用notify()或者notifyAll()方法通知正在等待的其它线程。
使用synchronized来修饰某个共享资源时,如果线程1在执行synchronized代码,线程2也要执行同一对象的统同一synchronize的代码,线程2将要等到线程1执行完后执行,这种情况可以使用wai()和notify()进行线程唤醒。
class NumberPrint implements Runnable{
private int number;
public byte res[];
public static int count = 5;
public NumberPrint(int number, byte a[]){
this.number = number;
res = a;
}
public void run(){
synchronized (res){
while(count-- > 0){
try {
res.notify();//唤醒等待res资源的线程,把锁交给线程(该同步锁执行完毕自动释放锁)
System.out.println(" "+number);
res.wait();//释放CPU控制权,释放res的锁,本线程阻塞,等待被唤醒。
System.out.println("------线程"+Thread.currentThread().getName()+"获得锁,wait()后的代码继续运行:"+number);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}//end of while
return;
}//synchronized
}
}
public class WaitNotify {
public static void main(String args[]){
final byte a[] = {0};//以该对象为共享资源
new Thread(new NumberPrint((1),a),"1").start();
new Thread(new NumberPrint((2),a),"2").start();
}
}
1
2
------线程1获得锁,wait()后的代码继续运行:1
1
------线程2获得锁,wait()后的代码继续运行:2
2
------线程1获得锁,wait()后的代码继续运行:1
1
------线程2获得锁,wait()后的代码继续运行:2
JDK5新增加Lock接口以及它的一个实现类显式锁(ReentrantLock),实现多线程的同步
public int consume(){
int m = 0;
try {
lock.lock();
while(ProdLine.size() == 0){
System.out.println("队列是空的,请稍候");
empty.await();
}
m = ProdLine.removeFirst();
full.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();
return m;
}
}
Java语言中提供了两种锁机制的实现对某个共享资源的同步;synchronized和Lock。
其中synchronized使用Object类对象本身的notify()、wait()、notifyAll()调度机制。
Lock使用condition包进行线程之间的调度,完成synchronized实现的所有功能。
用法不一样:synchronized既可以加在方法上,也可以加在特定的代码块中,括号中表示需要的锁对象。而Lock需要显式的指定起始位置和终止位置。synchronized是托管给JVM执行的,而Lock的锁定是通过代码实现,他有比synchronized更精确的线程语义。
性能不一样:在JDK5中增加了一个Lock接口的实现类ReentrantLock。它不仅拥有和synchronized相同的并发性和内存语义、还多了锁投票、定时锁、等候锁和中断锁。它们的性能在不同的情况下会有所不同;在资源竞争不激烈的情况下,synchronized的性能要优于RenntrantLock,但是资源竞争激烈的情况下,synchronized性能会下降的非常快,而ReentrantLock的性能基本保持不变。
锁机制不一样:synchronized获得锁和释放锁的方式都是在块结构中,当获取多个锁时,必须以相反的顺序释放,并且自动解锁,而condition中的await()、signal()、signalAll()能够指定要释放的锁,不会因为异常而导致锁没有被释放从而引发死锁的问题;而Lock则需要开发人员手动释放,并且必须放在finally块中释放,否则会引起死锁问题,此外,Lock还提供了更强大的功能,他的tryLock()方法可以采用非阻塞的方式去获取锁。
虽然synchronized与Lock都可以实现多线程的同步,但是最好不要同时使用这两种同步机制给统一共享资源加锁(不起作用),因为ReentrantLock与synchronized所使用的机制不同,所以它们运行时独立的,相当于两个种类的锁,在使用的时候互不影响。
public class Test{
public static void main(String[] args) throws InterruptedException{
final Lock lock=new ReetrantLock();
lock.lock();
Thread t1=new Thread(new Runnable){
public void run(){
try{
lock.lockInterruptibly();
} catch(InterruptedException e){
System.out.println(" interrupted.");
}
}
});
t1.start();
t1.interrupt();
Thread.sleep(1000);
}
}
其他线程可进入此对象的非synchronized修饰的方法。如果其他方法有synchronized修饰,都用的是同一对象锁,就不能访问。
可以的,因为static修饰的方法,它用的锁是当前类的字节码,而非静态方法使用的是this,因此可以调用。
死锁是指多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们将一直互相等待而无法推进下去。也就是说,死锁会让你的程序挂起无法完成任务。
比如:线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
Output
Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1
线程A通过synchronized(resource1)获得resource1的监视器锁,然后通过Thread.sleep(1000)
让线程A休眠1s为的是让线程B得到执行然后获取到resource2的监视器锁。线程A和线程B休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。
产生死锁必须具备以下四个条件:
活锁(livelock):
活锁和死锁一样无法继续执行线程。
死锁不能自己改变线程状态,表现为等待外力作用。
活锁是由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。所谓的“活”就是处于活锁的实体是在不断的改变状态,有可能自行解开。
需要破坏产生死锁的四个条件中的其中一个即可。
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 2").start();
我们分析一下上面的代码为什么避免了死锁的发生?
线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。
一个类型修饰符(type specifier),被设计用来修饰被不同线程访问和修改的变量。volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。
变量定义volatile之后具备两种特性:
ThreadLocal是线程局部变量,为解决多线程的并发问题提供了一种新的思路。ThreadLocal提供了set、get等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。
在ThreadLocal类中有一个Map(ThreadLocalMap),用于存储每个线程的变量副本,Map中元素的key为线程对象,而value对应线程的变量副本。所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
实质上:它只是一个线程的局部变量(其实就是一个Map)(以空间换时间)。
CAS算法
CAS(Compareand Swap,比较并交换)是乐观锁的一种典型算法实现。其核心是对于修改操作,会有旧值、预期值和新值,当去修改内存中的旧值时,会先去判断是否和自己的预期值相等,如果相等说明没有被别的线程修改过,直接替换为新值;如果不相等说明被别的线程修改了,就舍弃本次操作。这种方式能优化锁,提高效率,但是也可能出现ABA(A值被改为B,有改为了A,CAS不能发现)的情况。
注意:启动线程使用start()方法,而不是run()方法。调用start()方法来启动线程,系统会把该run()方法当成线程执行体来处理;但如果直按调用线程对象的run()方法,则run()方法立即就会被执行,而且在run()方法返回之前其他线程无法并发执行。也就是说,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体。需要指出的是,调用了线程的run()方法之后,该线程已经不再处于新建状态,不要再次调用线程对象的start()方法。只能对处于新建状态的线程调用start()方法,否则将引发
IllegaIThreadStateExccption
异常。
new 一个 Thread,线程进入了新建状态;调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到CPU后就可以开始运行了。start()会执行线程的相应准备工作,然后自动执行run()方法的内容,这是真正的多线程工作。而直接执行 run() 方法,会把run()方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
sleep()和wait()都是使线程暂停执行一段时间的方法。
**区别一:**原理不同(面试时可不答,偏重2,3)
sleep()方法是Thread类的静态方法,是线程用来控制自身流程的。
wait()方法是Object类的方法,用于线程间的通信。
**区别二:**对锁的处理机制不同
调用wait()的时候方法会释放当前持有的锁
调用sleep()的时候不会释放锁
**区别三:**使用地方不同
sleep()方法可以放在任何地方使用,通常被用于暂停执行,且必须捕获异常
wait()方法必须放在同步方法或者同步代码块中使用,通常被用于线程间交互/通信,不需要捕获异常
**区别四:**对锁的处理机制不同
sleep()方法执行完成后,线程会自动苏醒。
wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。
推荐:由于sleep()不会释放锁标志,容易导致死锁问题的发生,一般情况下,不推荐使用sleep()方法,而推荐使用wait()方法。
调用notify(),JVM会从这个条件队列上等待的多个线程中选择一个来唤醒,选择哪个取决于线程调度器。
调用notifyAll(),则会唤醒所有在这个锁上等待的线程,并允许他们争夺锁确保至少有一个线程能继续运行。
Wait()一般和notify/notifyAll一起使用,这三个方法都是Object的方法,一般用于多个线程对共享数据的获取,并且只能在synchrnoized中使用,因为wait和notify方法使用的前提是必须先获取一个锁。
由于多个线程可以基于不同的条件谓词在同一个条件队列上等待,如果使用notify,容易导致类似于信号丢失的问题,因此大多数情况下,应该优先选择notifyAll而不是单个的notify。
yield()方法会使当前线程从运行状态变为就绪状态,把运行机会让给其它相同优先级的线程。它是一个静态的原生(native)方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能会被再次继续执行的。
**区别一:**sleep()给其他线程运行机会时,不考虑线程的优先级;yield()方法只会给相同优先级或更高优先级的线程以运行的机会。
**区别二:**sleep()方法执行后,线程会转入阻塞状态,在指定的时间内该线程不会被执行;yield()方法是使当前线程从运行状态回到可执行状态,执行yield()方法的线程很可能在进入到可执行状态后马上又被执行。
join()的作用是让调用该方法的线程在执行完run()方法后,再执行join方法后面的代码。简单点说就是将两个线程合并,并实现同步功能。
比如:为了确保三个线程的顺序为T1,T2,T3,应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。
stop():会释放已经锁定的所有监视资源,如果当前任何一个受监视资源保护的对象处于一个不一致的状态(执行了一部分),其他线程将会获取到修改了的部分值,这个时候就可能导致程序执行结果的不确定性,并且这种问题很难被定位。
suspend():调用suspend()方法不会释放锁,会导致此线程挂起,容易发生死锁。
鉴于以上两种方法的不安全性,不建议使用以上两种方法来终止线程。
建议采用的方法是让线程自行结束进入Dead状态。一个线程进入Dead状态,既执行完run()方法,也就是说提供一种能够自动让run()方法结束的方式,在实际中,我们可以通过flag标志来控制循环是否执行,从而使线程离开run方法终止线程。
public class MyThread implements Runnable{
private volatile Boolean flag;
public void stop(){
flag=false;
}
public void run(){
while(flag);//do something
}
}
当线程处于阻塞状态时(sleep()被调用或wait()方法被调用或当被I/O阻塞时),上面的方法就不可用了。此时使用interrupt()方法来打破阻塞的情况,当interrupt()方法被调用时,会抛出interruptedException异常,可以通过在run()方法中捕获这个异常来让线程安全退出。
package com.TryFirst;
import java.util.Scanner;
public class TestMain implements Runnable{
public static void main(String[] args){
Thread thread = new Thread(new TestMain());
thread.start();
thread.interrupt();
}
@Override
public void run() {
System.out.println("thread go to sleep");
try{
//用休眠来模拟线程被阻塞
Thread.sleep(5000);
System.out.println("thread finish");
} catch (InterruptedException e){
System.out.println("thread is interrupted!");
}
}
}
如果I/0停滞,进入非运行状态,基本上要等到I/O完成才能离开这个状态;或通过出发异常,使用readLine()方法在等待网络上的发布信息”此时线程处于阻塞状态,让程序离开run()就出发close()方法来关闭流“,此时就会抛出IOException异常,通过捕获此异常离开run()。
interrupted()和isInterrupted()的主要区别是前者会将中断状态清除而后者不会。
Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有可能被其它线程调用中断来改变。
Java提供了两种线程:守护线程和用户线程。
守护线程又被称为“服务进程”、“精灵线程”、“后台线程”,是指在程序运行时在后台提供一种通用服务的线程,这种线程并不属于程序中不可或缺的部分,通俗点讲,每一个守护线程都是JVM中非守护线程的“保姆”。典型例子就是“垃圾回收器”。只要JVM启动,它始终在运行,实时监控和管理系统中可以被回收的资源。
用户线程和守护线程几乎一样,唯一的不同就在于如果用户线程已经全部退出运行,只剩下守护线程运行,JVM也就退出了。因为当所有非守护线程结束时,没有了守护者,守护线程就没有工作可做,也就没有继续运行程序的必要,程序也就终止了。同时会“杀死”所有的守护线程。也就是说,只要有任何非守护线程运行,程序就不会终止。
Java语言中,守护线程优先级都较低,它并非只有JVM内部提供,用户也可以自己设置守护线程,方法就是在调用线程的start()方法之前,设置setDaemon(true)方法,若将参数设置为false,则表示用户进程模式。需要注意的是,守护线程中产生的其它线程都是守护线程,用户线程也是如此。
需要在调用start()方法前调用setDaemon()这个方法,否则会抛出IllegalThreadStateException异常。
public class TestMain extends Thread{
@Override
public void run() {
for(int i = 0 ; i<15;i++) {
System.out.print(getName()+":"+i + ";");
}
}
}
/*
* public final void setDaemon(boolean on):是否设置为守护进程。true:是;false:否
*/
public class TestMain2 {
public static void main(String[] args){
TestMain t1 = new TestMain();
TestMain t2 = new TestMain();
t1.setName("A");
t2.setName("B");
//添加守护线程
t1.setDaemon(true);
t2.setDaemon(true);
t1.start();
t2.start();
Thread.currentThread().setName("C");
for(int i=0;i<2;i++) {
System.out.print(Thread.currentThread().getName()+":"+i + ";");
}
}
C:0;A:0;B:0;B:1;B:2;B:3;A:1;C:1;A:2;A:3;A:4;A:5;A:6;B:4;A:7;B:5;B:6;A:8;A:9;
A线程和B线程均设置为守护线程,C线程为用户进程。这三个线程均随机抢占CPU的使用权,当C抢占并且运行完毕之后,A和B这两个线程将在某一时间死亡,并不是立刻死亡而是继续执行一段时间,测试结果中会发现A和B在C结束后并没有马上停止并且也没有运行完全。
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
概括来说就是:当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换会这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。
一个线程池包括以下四个基本组成部分:
场景:假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。
线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1,T3的开销了。
线程池不仅调整T1,T3产生的时间段,而且它还显著减少了创建线程的数目,看一个例子:
假设一个服务器一天要处理50000个请求,并且每个请求需要一个单独的线程完成。在线程池中,线程数一般是固定的,所以产生线程总数不会超过线程池中线程的数目,而如果服务器不利用线程池来处理这些请求则线程总数为50000。一般线程池大小是远小于50000。所以利用线程池的服务器程序不会为了创建50000而在处理请求时浪费时间,从而提高效率。
两个方法都可以向线程池提交任务。
execute()方法的返回类型是void,它定义在Executor接口中
submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口。
参考博文
【1】:Java 并发基础常见面试题总结
【2】:JAVA多线程高并发面试题总结
【3】:Java多线程常用面试题
【4】:java线程知识点汇总