`
Everyday都不同
  • 浏览: 714150 次
  • 性别: 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()方法。

分享到:
评论

相关推荐

    Java程序员面试题全.zip

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

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

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

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

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

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

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

    Java入门面试题集锦:全面覆盖30道经典问题,助你准备顺利通过面试!

    为你带来了一系列Java入门面试题,涵盖了30道经典问题,从Java基础知识到面向对象编程、异常处理、多线程、集合框架等各个方面,旨在帮助你全面复习和巩固Java的基础概念和核心特性。无论是准备面试还是提升自己的...

    JAVA面试题解惑系列合集

    作者:臧圩人 JAVA面试题解惑系列博客文章精选 1. JAVA面试题解惑系列 1.1 JAVA面试题解惑系列(一)...1.10 JAVA面试题解惑系列(十)——话说多线程 1.11 JAVA面试题解惑系列(十一)——这些运算符你是否还记得?

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

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

    Java面试题-面向对象、多线程.pdf

    1、面向对象的特征有哪些方面? 答:面向对象的特征主要有以下几个方面: ...总共90多道题目,包含面向对象、算法、多线程等面试题及详解 大厂面试题集,纯人工手写,分享不易,有问题敬请谅解 。。。。。。。。

    java多线程

    JAVA多线程和并发基础面试问答,主要针对面授的问题

    多线程通信和等待机制.docx

    多线程的面试题,从基础到深入系列,收集的其它同仁的,实用性强

    秒杀多线程-MoreWindows

    系列中不但会详细讲解多线程同步互斥的各种“招式”,而且会进一步的讲解多线程同步互斥的“内功心法”。有了“招式”和“内功心法”,相信你也能对多线程挥洒自如,在笔试面试中顺利的秒杀多线程试题。

    Java面试题系列.zip

    Java核心技术:如多线程、网络编程、序列化等都有详细的解释和示例。 常用框架:如Spring、MyBatis等框架的使用方法和内部原理都有涉及。 数据库相关:包括关系型数据库和非关系型数据库的使用,以及JDBC、MyBatis等...

    Java面试题解惑系列

    来自网络,主要包括以下内容:1、类初始化的顺序;2、到底创建了几个String对象;3、变量(属性)的覆盖;4、final,finally,finalize;...10.多线程;11.运算符总结。 适合将要笔试面试Java的朋友参考。

    知识星球JavaGuide面试大全

    知识星球JavaGuide面试大全,包括大厂面试题,万字总结java面试题和答案(1),Java多线程,Java基础,java虚拟机,北京-百度-Java中级,北京-京东-Java中级,操作系统,杭州-阿里云-Java中级,计算机网络,数据结构与算法,数据库...

    北京百度java面试题大全

    多线程编程:涉及线程的创建、同步与互斥、线程池、线程间通信等多线程编程的知识。 IO操作:包括文件读写、字符流和字节流、序列化与反序列化等IO操作的相关知识。 JDBC和数据库:涉及JDBC的使用、连接数据库、...

    JAVA多线程精讲下

    本课程是《零基础学Java》系列课程中的"多线程"相关课程.授课形式延续了之前课程的授课风格。内容细腻、详尽,由浅入深!通过本门课程的学习你可以深刻理解Java多线程的原理及实现运行机制,深刻学习多线程的生命...

    BAT面试官有点懵系列,Java多线程你真的理解透彻了吗?带你玩转一次多线程!Let’s go!别再out了!

    文章目录神标题引入线程和进程多线程的优势线程创建方式继承Thread类来创建和启动实现Runnable接口重写run方法创建线程类使用 Callable 和 Future 创建线程三种创建线程方式做出对比线程生命周期线程控制join线程...

    JAVA面试汇总(精选系列).rar

    3、多线程&并发篇 4、Spring篇 5、MyBatis篇 6、SpringBoot篇 7、MySQL篇 8、Redis篇 9、SpringCloud篇 10、Nginx篇 11、zookeeper篇 12、kafka篇 13、MQ篇 14、Elasticsearch篇 15、Linux篇 16、数据结构与算法篇 ...

Global site tag (gtag.js) - Google Analytics