`
Everyday都不同
  • 浏览: 723428 次
  • 性别: Icon_minigender_1
  • 来自: 宇宙
社区版块
存档分类
最新评论

面试系列(六):多线程

阅读更多

差点把多线程给忘了。。。。  多线程基本上去每个公司面试都会问到……

 

 

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面试题合集-多线程与进程

    以下是对73道Java面试题合集——多线程与进程相关知识点的详细解释。 1. **进程与线程的概念**: - **进程**:是操作系统资源分配的基本单位,拥有独立的内存空间和系统资源,可以并发执行。 - **线程**:是程序...

    java多线程面试题59题集合

    在面试中,对Java多线程的理解和熟练运用往往成为衡量开发者技能水平的重要标准。以下是对Java多线程面试题59题集合中可能涉及的一些关键知识点的详细解析。 1. **线程的创建方式** - 继承Thread类:创建一个新的...

    多线程,多线程面试题,C#源码.zip

    对于C#开发者来说,理解和掌握多线程技术不仅能够优化代码性能,也是面试中常被问到的知识点。 首先,我们要理解什么是线程。线程是操作系统分配CPU时间的基本单元,一个进程可以包含一个或多个线程。主线程通常...

    面试题解惑系列(十)——话说多线程

    【标题】:“面试题解惑系列(十)——话说多线程” 【描述】:本篇文章主要探讨的是Java中的多线程概念及其在面试中常见的问题解析。 【标签】:“面试题解惑系列(十)——话说多线程” 【部分内容】:在Java中,多...

    Java程序员面试题全.zip

    并发+Netty+JVM、java筑基(基础)面试专题系列(一):Tomcat+Mysql+设计模式、JVM与性能优化知识点整理、MySQL性能优化的21个最佳实践、MYSQL、redis、spring、多线程、分布式、面试必备之乐观锁与悲观锁、面试必...

    java面试题_多线程(68题)

    Java多线程是Java编程中的核心概念,尤其在面试中,它是考察开发者高级技能和并发理解的关键领域。这里,我们将深入探讨与Java多线程相关的68个面试问题,涵盖基础知识、线程安全、同步机制、线程池、死锁等关键知识...

    线程编程 四个线程...

    "多线程编程基础知识" 多线程编程是指在一个程序中同时执行多个线程的技术。每个线程都是一个独立的执行路径,拥有自己的程序计数器、寄存器和堆栈空间。多线程编程可以提高程序的执行效率和响应速度,但也增加了...

    Java 多线程编程面试集锦20道问题解答Java多线程编程高难度面试题及解析

    15. **并发集合类(Concurrent Collection)**:Java提供了一系列线程安全的集合类,如`ConcurrentHashMap`、`CopyOnWriteArrayList`等,它们在多线程环境下保证操作的正确性和性能。 掌握这些知识点对于Java开发者...

    Java多线程与并发系列22道高频面试题(附思维导图和答案解析)

    本文总结了Java多线程与并发系列的22道高频面试题,涵盖了多线程的实现方法、线程停止、notify和notifyAll的区别、sleep和wait的区别、volatile关键字的作用等多方面的知识点。 一、Java中实现多线程的方法 Java中...

    分布式数据库面试专题系列:Memcached+Redis+MongoDB-06.rar

    支持多线程并发访问。 - **应用场景**:适合短期缓存,如会话存储,减少对数据库的频繁读写。 - **面试题**:如何解决Memcached的单点故障问题?Memcached如何进行数据备份? 2. **Redis**: - **简介**:Redis...

    多线程系列相关的技术要点

    1. Java多线程学习(一)Java多线程入门 2. Java多线程学习(二)synchronized...7. Java多线程学习(六)Lock锁的使用 8. Java多线程学习(七)并发编程中一些问题 9. Java多线程学习(八)线程池与Executor 框架

    多线程集合及IO面试

    ### 多线程集合及IO面试知识点概览 #### 逻辑思考题解析 1. **果冻颜色问题**: - 这是一个经典的抽屉原理(鸽巢原理)的应用问题。根据题意,桶里有黄色、绿色和红色三种颜色的果冻。如果随机抓取果冻,为了确保...

    Java面试专题-面试人员必看-微服务架构面试专题系列:BAT面试常问80题.rar

    - **多线程**:理解线程的创建方式,同步机制如synchronized和Lock,以及并发工具类。 2. **微服务架构**: - **微服务概念**:理解微服务架构的核心思想,如单一职责原则、服务自治和服务发现。 - **Spring ...

    多线程面试专题及答案

    在Java编程领域,多线程是面试中常见且重要的知识点,尤其对于系统设计和性能优化而言至关重要。本专题主要探讨了Java多线程的相关概念、技术及其在面试中的常见问题和解答。 一、多线程基础 1. 线程与进程:线程是...

    多线程面试题1

    在 IT 面试中,多线程是一个重要的考察点,尤其在 iOS 开发中。以下是对多线程面试题的详细解释: 一、进程与线程的理解 1. **进程**:进程是操作系统分配资源的基本单位,它可以看作是一个应用程序在运行时的实例...

    java多线程

    【Java多线程】是Java编程中不可或缺的一部分,尤其在面试场景中,了解和掌握多线程知识至关重要。以下是对标题和描述中涉及知识点的详细解释: 1. **进程与线程的区别**: - **进程**:是操作系统资源分配的基本...

    《.NET-C#面试手册》

    .NET/C#⾯试题汇总系列:多线程 .NET/C#⾯试题汇总系列:ASP.NET MVC .NET/C# ⾯试题汇总系列:ASP.NET Core .NET/C#⾯试题汇总系列:ADO.NET、XML、HTTP、AJAX、WebService .NET/C#⾯试题汇总系列:常⻅的算法 .NET...

    Java 高并发多线程编程系列案例代码

    Java 高并发多线程编程系列案例代码 & 教程 & 面试题集锦! !! 包括但不限于线程安全性, atomic包下相关类、CAS原理、Unsafe类、synchronized关键字等的使用及注意事项,

    java面试题_多线程(68题).zip

    Java多线程是Java编程中的核心概念,尤其在面试中,它是考察开发者高级技能和问题解决能力的重要领域。以下是对这个主题的一些详细知识点的解析: 1. **线程的定义**:线程是程序执行的最小单位,一个进程中可以有...

Global site tag (gtag.js) - Google Analytics