引言:
Java中的多线程一直是java面试的高频热点,它的重要性不言而喻。
随着计算机的配置越来越高,我们需要将进程进一步优化,细分为线程,充分提高图形化界面的多线程的开发。
1.什么是进程和线程?
1.1 进程:进程是操作系统分配资源的基本单位,一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。它的本质是一个独立执行的程序。
1.2 线程:进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。线程是任务调度和执行的基本单位
一个进程中可以并发多个线程,每条线程执行不同的任务,切换受系统控制。
与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
2.线程和进程的区别是什么?
线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。
-
根本区别:进程是操作系统分配资源的基本单位,而线程是处理机调度和执行任务的基本单位
-
资源开销:每个进程都有自己独立的代码和数据空间,来回的切换有巨大的开销,而线程又被称为轻量级的进程,同一类的线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
-
包含关系:一个进程包含多个线程,执行过程是多条线程共同完成的,线程是进程的一部分,所以线程也被称为轻量级进程
-
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
-
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
-
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行
3.并发和并行是什么?
- 并发(Concurrent):两个或多个事件在同一时间间隔内执行,即交替执行,多线程是并发的一种写照,一个处理器同时处理多个任务,一个处理器每次只能执行一个任务,所以并发就需要在有限的共享资源的同时,来回切换执行
- 并行(Parallel):两个或者多个事件在同一时间同时进行,即同时做不同的事情,多核cpu,多条线程可以同时执行
4.java中创建多线程常用的有哪几种方式?
1.继承Thread
-
方法:继承Thread之后,重写run方法,创建实例,调用start方法(有一个小问题,为什么不去直接调用run方法呢,start最后也是调用的run方法)
因为run方法只是一个普通的方法,直接调用run不能开启线程,直接调用run方法,它会执行完方法继续往后执行,相当于是一个串行化的过程,而不是一个并发化的过程 -
优点:代码操作简单
-
缺点:java是单继承的,继承一个类之后没办法继承其他类,可扩展性太差
@Override
public void run() {
System.out.println("多线程"+Thread.currentThread().getName());
}
public static void main(String[] args) {
ThreadTest threadTest=new ThreadTest();
threadTest.setName("线程1");
threadTest.start();
System.out.println("主线程"+Thread.currentThread().getName());
}
}
2.实现runnable接口
- 方法:实现runnable接口,重写run方法,创建Thread类,将Runnable实现类对象传递给Thread对象,用Thread对象实例调用start方法
- 优点:可以再继承一个类,多实现几个接口,可扩展性强
- 缺点:不能直接调用start,需要构造一个Thread实例将参数传递进去再调用start
public class RunnableTest implements Runnable {
@Override
public void run() {
System.out.println("多线程--"+Thread.currentThread().getName());
}
public static void main(String[] args) {
RunnableTest runnableTest=new RunnableTest();
Thread thread=new Thread(runnableTest);
thread.setName("线程1");
thread.start();
System.out.println("主线程"+Thread.currentThread().getName());
}
}
3.实现callable接口方式
- 创建 Callable 接口的实现类,并实现call()方法,结合 FutureTask 类包装 Callable 对象,实现多线程。
- 优点:有返回值,拓展性也高
- 缺点:Jdk5以后才支持,需要重写call()方法,结合多个类比如 FutureTask 和 Thread 类
public class CallableTest implements Callable{
@Override
public Object call() throws Exception {
System.out.println("线程名称:"+Thread.currentThread().getName());
return "返回值";
}
public static void main(String[] args) {
FutureTask<Object> futureTask = new FutureTask<>(() -> {
System.out.println("线程名称:" +
Thread.currentThread().getName());
return "这是返回值";
});
Thread thread=new Thread(futureTask);
thread.setName("线程1");
thread.start();
System.out.println("主线程:"+Thread.currentThread().getName());
}
}
4 通过线程池创建线程
- 自定义 Runnable 接口,实现 run()方法,创建线程池,调用执行方法并传入对象。
- 优点:安全高性能,复用线程。
- 缺点: Jdk5后才支持,需要结合 Runnable 进行使用。
public class ThreadTest01 implements Runnable {
@Override
public void run() {
System.out.println("线程池:线程名称"+Thread.currentThread().getName());
}
public static void main(String[] args) {
//创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) {
executorService.execute(new ThreadTest01());
}
System.out.println("主线程:"+Thread.currentThread().getName());
//关闭线程池
executorService.shutdown();
}
}
5.Runnable、Thread、Callable三种创建多线程方法的区别?
- Runnable和Callable是接口,可以多实现,有利于程序的可扩展性,而Thread是抽象类只能继承,可扩展性差
- Thread和Runnable没有返回值,而Callable有返回值
- 继承Thread需要重写run方法,实现Runnable需要实现run方法,实现Callable接口需要实现call方法
- 继承Thread可以直接调用start方法,而实现Runnable接口需要构建Thread对象,将该实现类对象放入Thread中,通过Thread实例调用start方法,Callable需要新建的FutureTask实例放入Thread中,通过新建的Thread实例去调用start方法,获取返回值只需要借助 FutureTask 实例调用get()方法即可!
6.线程的几种状态?
线程的基本状态:新建、就绪、运行状态、阻塞状态、死亡状态
-
新建状态:利用NEW运算创建了线程对象,此时线程状态为新建状态,调用了新建状态线程的start()方法,将线程提交给操作系统,准备执行,线程将进入到就绪状态。
-
就绪状态:由操作系统调度的一个线程,没有被系统分配到处理器上执行,一旦处理器有空闲,操作系统会将它放入处理器中执行,此时线程从就绪状态切换到运行时状态。
-
运行状态:当 CPU 开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。。
-
阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
等待阻塞 :运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤 醒,wait()是 Object 类的方法。 同步阻塞:当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,Java虚拟机就会把这个线程放到这个对象的锁池中。 其他阻塞状态:当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了 I/O 请求时,就会进入这个状态。线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入就绪状态。
-
死亡状态:线程一旦脱离阻塞状态时,将重新回到就绪状态,重新向下执行,最终进入到死亡状态。一旦线程对象是死亡状态,就只能被GC回收,不能再被调用。
7. sleep() 和 wait() 有什么区别?
-
类的不同:sleep() 来自 Thread,wait() 来自 Object。
-
释放锁:sleep() 不释放锁;wait() 释放锁。
-
用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒
8.线程的 run() 和 start() 有什么区别?
- start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。
- run() 可以重复调用,而 start() 只能调用一次。
- 第二次调用start() 必然会抛出运行时异常
9.请问Java中可以有哪些方法来保证线程安全?
- 加锁:比如synchronize/ReentrantLock。
- 使用 volatile 声明变量,轻量级同步,不能保证原子性(需要解释)。
- 使用线程安全类,例如原子类 AtomicXXX等。
- 使用线程安全集合容器,例如:CopyOnWriteArrayList/ConcurrentHashMap等。
- ThreadLocal本地私有变量/信号量 Semaphore等。
10.请你说一下线程状态转换相关方法:sleep/yield/join wait/notify/notifyAll 的区别?
Thread下的方法:
- sleep():属于线程 Thread的方法,让线程暂缓执行,等待预计时间之后再恢复,交出CPU使用权,不会释放锁,抱着锁睡觉!进入超时等待状态TIME_WAITGING,睡眠结束变为就绪Runnable
- yield():属于线程 Thread的方法,暂停当前线程的对象,去执行其他线程,交出CPU使用权,不会释放锁,和sleep()类似,让相同优先级的线程轮流执行,但是不保证一定轮流,
注意:不会让线程进入阻塞状态 BLOCKED,直接变为就绪 Runnable,只需要重新获得CPU使用权。 - join():属于线程 Thread的方法,在主线程上运行调用该方法,会让主线程休眠,不会释放锁,让调用join()方法的线程先执行完毕,再执行其他线程。类似让救护车警车优先通过!!
Object下的方法:
- wait():属于 Object 的方法,当前线程调用对象的wait()方法,会释放锁,进入线程的等待队列,需要依靠notify()或者notifyAll()唤醒,或者wait(timeout)时间自动唤醒。
- notify():属于 Object 的方法,唤醒在对象监视器上等待的单个线程,随机唤醒。
- notifyAll():属于Object 的方法,唤醒在对象监视器上等待的全部线程,全部唤醒
11.线程池的核心属性有哪些?
1.使用线程池的好处:
重用存在的线程,减少对象创建销毁的开销,有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞,且可以定时定期执行、单线程、并发数控制,配置任务过多任务后的拒绝策略等功能。
2.类别:
- newFixedThreadPool :一个定长线程池,可控制线程最大并发数。
- newCachedThreadPool:一个可缓存线程池。
- newSingleThreadExecutor:一个单线程化的线程池,用唯一的工作线程来执行任务。
- newScheduledThreadPool:一个定长线程池,支持定时/周期性任务执行。
3.ThreadPoolExecutor构造函数里面的参数,能否解释下各个参数的作用?
- corePoolSize:核心线程数,线程池也会维护线程的最少数量,默认情况下核心线程会一直存活,即使没有任务也不会受存
keepAliveTime 控制! - 注意点:在刚创建线程池时线程不会立即启动,到有任务提交时才开始创建线程并逐步线程数目达到 corePoolSize。
- maximumPoolSize:线程池维护线程的最大数量,超过将被阻塞!
- 注意点:当核心线程满,且阻塞队列也满时,才会判断当前线程数是否小于最大线程数,才决定是否创建新线程
- keepAliveTime:非核心线程的闲置超时时间,超过这个时间就会被回收,直到线程数量等于 corePoolSize。
- unit:指定 keepAliveTime 的单位,如 TimeUnit.SECONDS、TimeUnit.MILLISECONDS
- workQueue:线程池中的任务队列,常用的是 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue。
- threadFactory:创建新线程时使用的工厂
- handler:RejectedExecutionHandler 是一个接口且只有一个方法,线程池中的数量大于
maximumPoolSize,对拒绝任务的处理策略,默认有 4 种策略:
1.AbortPolicy
2.CallerRunsPolicy
3.DiscardOldestPolicy
4.DiscardPolicy