差点把多线程给忘了。。。。 多线程基本上去每个公司面试都会问到……
1、谈谈你对多线程的理解
线程:表示程序的执行流程,是CPU调度执行的基本单位
多线程:指的是一个程序(一个进程)运行时产生了不止一个线程,使用多线程的好处,在于并行的执行多任务,彼此独立,可以提高执行效率。
2、实现多线程的方式
在java中实现多线程有多种途径:继承Thread类,实现Runnable接口,实现Callable接口,线程池负责创建。
一个线程对象只能启动一个线程,无论你调用多少遍start()方法,结果只有一个线程。
Thread.start()方法(native)启动线程,使之进入就绪状态,当cpu分配时间该线程时,由JVM调度执行run()方法。 (调用start时不一定立即执行)
比较推荐实现Runnable接口的方式,原因如下:
(1)适合多个相同程序代码的线程去处理同一资源的情况,把虚拟CPU(线程)同程序的代码,数据有效的分离,较好地体现了面向对象的设计思想。 (可联想到模拟火车站卖票的例子)
(2)可以避免由于Java的单继承特性带来的局限。我们经常碰到这样一种情况,即当我们要将已经继承了某一个类的子类放入多线程中,由于一个类不能同时有两个父类,所以不能用继承Thread类的方式,那么,这个类就只能采用实现Runnable接口的方式了。 (单继承多实现)
(3)有利于程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。当多个线程的执行代码来自同一个类的实例时,即称它们共享相同的代码。多个线程操作相同的数据,与它们的代码无关。当共享访问相同的对象时,即它们共享相同的数据。当线程被构造时,需要的代码和数据通过一个对象作为构造函数实参传递进去,这个对象就是一个实现了Runnable接口的类的实例。
3、线程的状态
1)6种状态
新建(New)---使用new来新建一个线程
可运行(Runnable)----调用start()方法,线程处于运行或可运行状态
阻塞(Blocked)---线程需要获得内置锁,当该锁被其他线程使用时,此线程处于阻塞状态
等待(Waiting)---当线程等待其他线程通知调度表可以运行时,此时线程处于等待状态
计时等待(Timed Waiting)---当线程调用含有时间参数的方法(如sleep())时,线程可进入计时等待状态
终止(Terminated)--当线程的run()方法结束或者出现异常时,线程处于终止状态
2)sleep和wait的区别?
sleep()方法是属于Thread类中的; 而wait()方法,则是属于Object类中的。
sleep()方法导致了程序暂停执行指定的时间,占着cpu去睡觉,其他线程不能占用cpu,os认为该线程正在工作,不会让出系统资源,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,wait是进入等待池等待,让出系统资源,其他线程可以占用cpu,一般wait不会加时间限制,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
4、线程的安全
1)synchronized关键字是多线程并发环境的执行有序性的方式之一,当一段代码会修改共享变量,这一段代码成为互斥区或临界区,为了保证共享变量的正确性,synchronized标示了临界区。
2)成员(全局)变量的类用于多线程时是不安全的,不安全体现在这个成员变量可能发生非原子性的操作,而变量定义在方法内也就是局部变量是线程安全的。
3)生产--消费者模式
其实是一种很经典的线程同步模型,很多时候,并不是光保证多个线程对某共享资源操作的互斥性就够了,往往多个线程之间都是有协作的。
class Plate { List<Object> eggs = new ArrayList<Object>(); public synchronized Object getEgg() { while(eggs.size() == 0) { try { wait(); } catch (InterruptedException e) { } } Object egg = eggs.get(0); eggs.clear();// 清空盘子 notify();// 唤醒阻塞队列的某线程到就绪队列 System.out.println("拿到鸡蛋"); return egg; } public synchronized void putEgg(Object egg) { while(eggs.size() > 0) { try { wait(); } catch (InterruptedException e) { } } eggs.add(egg);// 往盘子里放鸡蛋 notify();// 唤醒阻塞队列的某线程到就绪队列 System.out.println("放入鸡蛋"); } } class AddThread extends Thread{ private Plate plate; private Object egg=new Object(); public AddThread(Plate plate){ this.plate=plate; } public void run(){ for(int i=0;i<5;i++){ plate.putEgg(egg); } } } class GetThread extends Thread{ private Plate plate; public GetThread(Plate plate){ this.plate=plate; } public void run(){ for(int i=0;i<5;i++){ plate.getEgg(); } } }
测试下:
public static void main(String args[]){ try { Plate plate=new Plate(); Thread add=new Thread(new AddThread(plate)); Thread get=new Thread(new GetThread(plate)); add.start(); get.start(); add.join(); get.join();//等到取和拿线程执行完毕后再继续往下执行System.out.println("测试结束"); } catch (Exception e) { e.printStackTrace(); } System.out.println("测试结束"); }
打印结果:
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
测试结束
4)显示地调用Lock(实现类比如ReentrantLock)
Lock bankLock = new ReentrantLock();
bankLock.lock();
//....
bankLock.unlock();//通常在finally里释放锁
5)ThreadLocal,顾名思义,它不是一个线程,而是线程的一个本地化对象。当工作于多线程中的对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。
从线程的角度看,这个变量就像是线程的本地变量。
public class ThreadLocalTest { public static void main(String [] args) { SequenceNumber sn = new SequenceNumber(); // ③ 3个线程共享sn,各自产生序列号 TestClient tc1 = new TestClient(sn); TestClient tc2 = new TestClient(sn); TestClient tc3 = new TestClient(sn); tc1.start(); tc2.start(); tc3.start(); } } class SequenceNumber { // ①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值 private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() { public Integer initialValue() { return 0; } }; public int getNextNum() { seqNum.set(seqNum.get()+1); return seqNum.get(); } } class TestClient extends Thread { private SequenceNumber sn; public TestClient(SequenceNumber sn) { this.sn = sn; } public void run() { // ④每个线程打出3个序列值 for(int i = 0; i<3; i++) { System.out.println("thread[" + Thread.currentThread().getName() + "] sn[" + sn.getNextNum() +"]"); } } }
结果:
thread[Thread-1] sn[1]
thread[Thread-0] sn[1]
thread[Thread-2] sn[1]
thread[Thread-0] sn[2]
thread[Thread-1] sn[2]
thread[Thread-0] sn[3]
thread[Thread-2] sn[2]
thread[Thread-1] sn[3]
thread[Thread-2] sn[3]
5、高并发
注:以下部分不要求全部答到,可以选择一个点答就行~
1)数据结构
java.util.concurrent包中提供了一些适合多线程程序使用的高性能数据结构,包括队列和集合类对象等。
1、队列
a、BlockingQueue接口:线程安全的阻塞式队列;当队列已满时,向队列添加会阻塞;当队列空时,取数据会阻塞。(非常适合消费者-生产者模式)
阻塞方式:put()、take()。
非阻塞方式:offer()、poll()。
实现类:基于数组的固定元素个数的ArrayBolockingQueue和基于链表结构的不固定元素个数的LinkedBlockQueue类。
b、BlockingDeque接口: 与BlockingQueue相似,但可以对头尾进行添加和删除操作的双向队列;方法分为两类,分别在队首和对尾进行操作。
实现类:标准库值提供了一个基于链表的实现,LinkedBlockgingDeque。
2、集合类
在多线程程序中,如果共享变量是集合类的对象,则不适合直接使用java.util包中的集合类。这些类要么不是线程安全,要么在多线程下性能比较差。
应该使用java.util.concurrent包中的集合类。
a、ConcurrentMap接口: 继承自java.util.Map接口
putIfAbsent():只有在散列表不包含给定键时,才会把给定的值放入。
remove():删除条目。
replace(key,value):把value 替换到给定的key上。
replace(key, oldvalue, newvalue):CAS的实现。
实现类:ConcurrentHashMap(若干个segements,每个segement都有自己的锁,常见的HashMap可以看作只有一个segement的ConcurrentHashMap):
创建时,如果可以预估可能包含的条目个数,可以优化性能。(因为动态调整所能包含的数目操作比较耗时,这个HashMap也一样,只是多线程下更耗时)。
创建时,预估进行更新操作的线程数,这样实现中会根据这个数把内部空间划分为对应数量的部分。(默认是16,如果只有一个线程进行写操作,其他都是读取,那么把值设为1 可以提高性能)。
注:当从集合中创建出迭代器遍历Map元素时,不一定能看到正在添加的数据,只能和集合保证弱一致性。(当然使用迭代器不会因为查看正在改变的Map,而抛出java.util.ConcurrentModifycationException)
b、CopyOnWriteArrayList接口:继承自java.util.List接口。
是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略;
顾名思义,在CopyOnWriteArrayList的实现类,所有对列表的更新操作都会新创建一个底层数组的副本,并使用副本来存储数据;对列表更新操作加锁,读取操作不加锁。
适合多读取少修改的场景,如果更新操作多,那么不适合用,同样迭代器只能表示创建时列表的状态,更新后使用了新的底层数组,迭代器还是引用旧的底层数组。
2)多线程任务的执行
过去线程的执行,是先创建Thread类,再调用start方法启动,这种做法要求开发人员对线程进行维护,在线程较多时,一般创建一个线程池同一管理,同时降低重复创建线程的开销
在J2SE5.0中,java.util.concurrent包提供了丰富的用来管理线程和执行任务的实现。
1、基本接口(描述任务)
a、Callable接口:
Runnable接口受限于run方法的类型签名,而Callable只有一个方法call(),可以有返回值,可以抛出受检异常。
b、Future接口:
过去,需要异步线程的任务执行结果,要求主线程和任务执行线程之间进行同步和数据传递。
Future简化了任务的异步执行,作为异步操作的一个抽象。调用get()方法可以获取异步的执行结果,如果任务没有执行完,会等待,直到任务完成或被取消,cancel()可以取消。
——Callable(一个产生结果)和Future(一个拿到结果)。
FutureTask实现了两个接口,Runnable和Future,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值:
public class CallableAndFuture { public static void main(String[] args) { Callable<Integer> callable = new Callable<Integer>() { public Integer call() throws Exception { return new Random().nextInt(100); } }; FutureTask<Integer> future = new FutureTask<Integer>(callable); new Thread(future).start(); try { Thread.sleep(5000);// 可能做一些事情 System.out.println(future.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
下面来看另一种方式使用Callable和Future,通过ExecutorService的submit方法执行Callable,并返回Future,代码如下
public class CallableAndFuture { public static void main(String[] args) { ExecutorService threadPool = Executors.newSingleThreadExecutor(); Future<Integer> future = threadPool.submit(new Callable<Integer>() { public Integer call() throws Exception { return new Random().nextInt(100); } }); try { Thread.sleep(5000);// 可能做一些事情 System.out.println(future.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
c、Delayed接口:
延迟执行任务,getDelay()返回当前剩余的延迟时间,如果不大于0,说明延迟时间已经过去,应该调度并执行该任务。
2、组合接口(描述任务)
a、RunnableFuture接口:继承自Runnable接口和Future接口。
当来自Runnalbe接口中的run方法成功执行之后,相当于Future接口表示的异步任务已经完成,可以通过get()获取运行结果。
b、ScheduledFuture接口:继承Future接口和Delayed接口,表示一个可以调用的异步操作。
c、RunnableScheduledFuture接口:继承自Runnable、Delayed和Future,接口中包含isPeriodic,表明该异步操作是否可以被重复执行。
3、Executor接口、ExcutorServer接口、ScheduleExecutorService接口和CompletionService接口(描述任务执行)
a、executor接口,execute()用来执行一个Runnable接口的实现对象,不同的Executor实现采取不同执行策略,但提供的任务执行功能比较弱。
b、excutorServer接口,继承自executor;
提供了对任务的管理:submit(),可以吧Callable和Runnable作为任务提交,得到一个Future作为返回,可以获取任务结果或取消任务。
提供批量执行:invokeAll()和invokeAny(),同时提交多个Callable;invokeAll(),会等待所有任务都执行完成,返回一个包含每个任务对应Future的列表;invokeAny(),任何一个任务成功完成,即返回该任务结果。
提供任务关闭:shutdown()、shutdownNow()来关闭服务,前者不允许新的任务提交,后者试图终止正在运行和等待的任务,并返回已经提交单没有被运行的任务列表。(两个方法都不会等待服务真正关闭,只是发出关闭请求。)。shutdownDow,通常做法是向线程发出中断请求,所以确保提交的任务实现了正确的中断处理逻辑。
c、ScheduleExecutorService接口,继承自excutorServer接口:支持任务的延迟执行和定期执行,可以执行Callable或Runnable。
schedule(),调度一个任务在延迟若干时间之后执行;
scheduleAtFixedRate():在初始延迟后,每隔一段时间循环执行;在下一次执行开始时,上一次执行可能还未结束。(同一时间,可能有多个)
scheduleWithFixedDelay:同上,只是在上一次任务执行完后,经过给定的间隔时间再开始下一次执行。(同一时间,只有一个)
以上三个方法都返回ScheduledFuture接口的实现对象。
d、CompletionService接口,共享任务执行结果。
通常在使用ExecutorService接口,通过submit提交任务,并得到一个Future接口来获取任务结果,如果任务提交者和执行结果的使用者是程序的不同部分,那就要把Future在不同部分进行传递;而CompletionService就是解决这个问题,程序不同部分可以共享CompletionService,任务提交后,执行结果可以通过take(阻塞),poll(非阻塞)来获取。
标准库提供的实现是 ExecutorCompletionService,在创建时,需要提供一个Executor接口的实现作为参数,用来实际执行任务。
6、线程池
Java通过Executors提供四种线程池,分别为:
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
用法举例:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); for (int i = 0; i < 10; i++) { final int index = i; try { Thread.sleep(index * 1000); } catch (InterruptedException e) { e.printStackTrace(); } cachedThreadPool.execute(new Runnable() { public void run() { System.out.println(index); } }); }
ps: Servlet是线程安全的么?
Servlet是单实例多线程的。
Servlet不是线程安全的。
要解释为什么Servlet为什么不是线程安全的,需要了解Servlet容器(即Tomcat)是如何响应HTTP请求的。
当Tomcat接收到Client的HTTP请求时,Tomcat从线程池中取出一个线程,之后找到该请求对应的Servlet对象并进行初始化,然后调用service()方法。要注意的是每一个Servlet对象在Tomcat容器中只有一个实例对象,即是单例模式。如果多个HTTP请求请求的是同一个Servlet,那么这两个HTTP请求对应的线程将并发调用Servlet的service()方法。
相关推荐
以下是对73道Java面试题合集——多线程与进程相关知识点的详细解释。 1. **进程与线程的概念**: - **进程**:是操作系统资源分配的基本单位,拥有独立的内存空间和系统资源,可以并发执行。 - **线程**:是程序...
在面试中,对Java多线程的理解和熟练运用往往成为衡量开发者技能水平的重要标准。以下是对Java多线程面试题59题集合中可能涉及的一些关键知识点的详细解析。 1. **线程的创建方式** - 继承Thread类:创建一个新的...
对于C#开发者来说,理解和掌握多线程技术不仅能够优化代码性能,也是面试中常被问到的知识点。 首先,我们要理解什么是线程。线程是操作系统分配CPU时间的基本单元,一个进程可以包含一个或多个线程。主线程通常...
【标题】:“面试题解惑系列(十)——话说多线程” 【描述】:本篇文章主要探讨的是Java中的多线程概念及其在面试中常见的问题解析。 【标签】:“面试题解惑系列(十)——话说多线程” 【部分内容】:在Java中,多...
并发+Netty+JVM、java筑基(基础)面试专题系列(一):Tomcat+Mysql+设计模式、JVM与性能优化知识点整理、MySQL性能优化的21个最佳实践、MYSQL、redis、spring、多线程、分布式、面试必备之乐观锁与悲观锁、面试必...
Java多线程是Java编程中的核心概念,尤其在面试中,它是考察开发者高级技能和并发理解的关键领域。这里,我们将深入探讨与Java多线程相关的68个面试问题,涵盖基础知识、线程安全、同步机制、线程池、死锁等关键知识...
"多线程编程基础知识" 多线程编程是指在一个程序中同时执行多个线程的技术。每个线程都是一个独立的执行路径,拥有自己的程序计数器、寄存器和堆栈空间。多线程编程可以提高程序的执行效率和响应速度,但也增加了...
15. **并发集合类(Concurrent Collection)**:Java提供了一系列线程安全的集合类,如`ConcurrentHashMap`、`CopyOnWriteArrayList`等,它们在多线程环境下保证操作的正确性和性能。 掌握这些知识点对于Java开发者...
本文总结了Java多线程与并发系列的22道高频面试题,涵盖了多线程的实现方法、线程停止、notify和notifyAll的区别、sleep和wait的区别、volatile关键字的作用等多方面的知识点。 一、Java中实现多线程的方法 Java中...
支持多线程并发访问。 - **应用场景**:适合短期缓存,如会话存储,减少对数据库的频繁读写。 - **面试题**:如何解决Memcached的单点故障问题?Memcached如何进行数据备份? 2. **Redis**: - **简介**:Redis...
1. Java多线程学习(一)Java多线程入门 2. Java多线程学习(二)synchronized...7. Java多线程学习(六)Lock锁的使用 8. Java多线程学习(七)并发编程中一些问题 9. Java多线程学习(八)线程池与Executor 框架
### 多线程集合及IO面试知识点概览 #### 逻辑思考题解析 1. **果冻颜色问题**: - 这是一个经典的抽屉原理(鸽巢原理)的应用问题。根据题意,桶里有黄色、绿色和红色三种颜色的果冻。如果随机抓取果冻,为了确保...
- **多线程**:理解线程的创建方式,同步机制如synchronized和Lock,以及并发工具类。 2. **微服务架构**: - **微服务概念**:理解微服务架构的核心思想,如单一职责原则、服务自治和服务发现。 - **Spring ...
在Java编程领域,多线程是面试中常见且重要的知识点,尤其对于系统设计和性能优化而言至关重要。本专题主要探讨了Java多线程的相关概念、技术及其在面试中的常见问题和解答。 一、多线程基础 1. 线程与进程:线程是...
在 IT 面试中,多线程是一个重要的考察点,尤其在 iOS 开发中。以下是对多线程面试题的详细解释: 一、进程与线程的理解 1. **进程**:进程是操作系统分配资源的基本单位,它可以看作是一个应用程序在运行时的实例...
【Java多线程】是Java编程中不可或缺的一部分,尤其在面试场景中,了解和掌握多线程知识至关重要。以下是对标题和描述中涉及知识点的详细解释: 1. **进程与线程的区别**: - **进程**:是操作系统资源分配的基本...
.NET/C#⾯试题汇总系列:多线程 .NET/C#⾯试题汇总系列:ASP.NET MVC .NET/C# ⾯试题汇总系列:ASP.NET Core .NET/C#⾯试题汇总系列:ADO.NET、XML、HTTP、AJAX、WebService .NET/C#⾯试题汇总系列:常⻅的算法 .NET...
Java 高并发多线程编程系列案例代码 & 教程 & 面试题集锦! !! 包括但不限于线程安全性, atomic包下相关类、CAS原理、Unsafe类、synchronized关键字等的使用及注意事项,
Java多线程是Java编程中的核心概念,尤其在面试中,它是考察开发者高级技能和问题解决能力的重要领域。以下是对这个主题的一些详细知识点的解析: 1. **线程的定义**:线程是程序执行的最小单位,一个进程中可以有...