java面试题-多线程

来源:互联网 发布:淘宝 刷单 没有权重 编辑:程序博客网 时间:2024/06/05 23:39

1.如何在Java中实现线程

(1)继承Thread类,重写run()方法。

(2)实现Runnable接口,实现run()方法,然后创建Runnable实现类的实例,并以此实例作为Thread类的target来创建Thread对象。

(3)实现Callable接口,实现call()方法。并使用FutureTask类来包装Callable的实现类对象,并且以此FutureTask对象作为Thread对象的target来创建线程。FutureTask表示一个可以取消的异步运算,并可以返回结果。只有当运算完成才能取回结果,如果运算尚未完成get方法将会阻塞。一个FutureTask对象可以对调用了Callable和Runnable的对象进行包装。

 

2.Thread类中的start()和run()方法有什么区别

Thread类中的start()方法是通知“线程规划器”此线程已经准备就绪,等到调用线程的run()方法。run()方法就是直接运行run()中的代码。在程序中直接调用run()方法就是同步执行的,使用start()方法交由“线程规划器”来调用run()方法才能实现异步执行。

 

3.什么是竞态条件

竞态条件(race condition)是指两个或多个进程对共享的数据进行读或写的操作时,最终的结果取决于这些进程的执行顺序。当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。在临界区中使用适当的同步就可以避免竞态条件。

 

4.什么是死锁、活锁、饥饿

死锁:是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。死锁发生的四个条件:互斥条件、请求和保持条件、不剥夺条件、环路等待条件。

活锁:是指线程不断重复执行相同的操作,但每次操作的结果都是失败的。尽管这个问题不会阻塞线程,但是程序也无法继续执行。

饥饿:指的线程无法访问到它需要的资源而不能继续执行时。

 

5.介绍一下CountDownLatch和CyclicBarrier类

CountDownLatch类:在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。 CountDownLatch通过一个计数器来实现,计数器的初始值为等待完成线程的数量。每当一个线程完成后,计数器的值减1。当计数器值到达0时,表示所有的线程已经完成了任务,然后等待的线程就可以恢复执行任务。

相关方法:

CountDownLatch(int count);

参数count为计数值,表示等待的线程数量。

void await() throws InterruptedException;

调用await()方法的线程会被挂起,直到count值为0才继续执行。

boolean await(long timeout,TimeUnit unit) throws InterruptedException ;

和await()类似,只不过在等待一定的时间后count值还没变为0的话就会继续执行

void countDown();

将count值减1

在需要等待的地方调用await()方法,在一个线程执行完成后要记得调用countDown()方法。

CyclicBarrier类:允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。就是让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会打开,所有被屏障拦截的线程才会继续执行。与CountDownLatch类似,CountDownLatch阻塞主线程,CyclicBarrier阻塞子线程。但CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置,并且CyclicBarrier能够实现更为复杂的业务场景。

相关方法:

CyclicBarrier(int parties);

计数值parties,表示要屏障拦截的线程数量。

CyclicBarrier(int parties,Runnable barrierAction)

在线程到达屏障时,优先执行barrierAction。

int await();

调用await()方法的线程会被挂起,直到parties值达到指定值才继续执行。

int await(long timeout, TimeUnit unit);

和await()类似,在等待一定的时间后会继续执行。

int getNumberWaiting();

返回当前的障碍等待数。

int getParties();

返回parties的值。

boolean isBroken();

返回阻塞的线程是否被中断。

void reset();

重置为初始状态。

 

6.BlockingQueue和Semaphore类

Semaphore类:一个计数信号量,可以设定一个信号量阈值,多个线程竞争获取许可信号,线程完成后释放。线程申请许可信号如果超过阈值,线程将会被阻塞。相关方法:

Semaphore(int permits);

permits表示许可数量。

Semaphore(int permits, boolean fair);

参数fair表示创建公平锁与非公平锁信号量。ture为公平锁,false为非公平锁。

void acquire();

获取此信号量的许可,如果没有将会阻塞,直到有可用信号或线程被中断。

void acquire(int permits);

类似acquire(),申请获取permits个信号量许可。

int availablePermits();

返回此信号量中当前可用的许可数。

int getQueueLength();

返回正在等待的线程的估计数。

void release();

释放许可返回到信号量。

void release(int permits);

释放指定数量的许可。

BlockingQueue类:阻塞队列,当一个线程对一个已经满的队列进行入队操作时将会被阻塞,直到有另一个线程进行出队操作;当一个线程试图对一个空队列进行出队列操作时也会被阻塞,直到有另一个线程进行了入队操作。类似于生产者/消费者模式。相关方法:

boolean add(E e):将一个元素插入队列中。插入成功返回true,失败则抛出异常。

boolean offer(E e):将一个元素插入队列中。插入成功返回true,失败返回false。本方法不阻塞当前执行方法的线程。

boolean put(E e):将一个元素插入队列中。插入成功返回true,失败线程将会阻塞,直到有可用的空间。

boolean offer(E e,long timeout,TimeUnit unit):等待一段时间时间,如果在指定的时间内,还不能往队列中插入元素,则返回false。
E take():获取并移除队首的元素,如果队列为空则等待。

E poll(long timeout, TimeUnit unit):获取队首元素,如果队列为空则等待指定时间。

boolean remove(Object o):移除指定元素的单个实例。

JDK7提供7个阻塞队列

ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。

LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。

PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。

DelayQueue:一个使用优先级队列实现的无界阻塞队列。

SynchronousQueue:一个不存储元素的阻塞队列。

LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。

LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。


7.HashTable和ConcurrentHashMap类的不同

HashTable和HashMap的区别:

1.HashMap是非线程安全的,HashTable是线程安全的。HashMap效率比HashTable要高。

2.HashMap的key和value都允许null值,HashTable接收null将抛出NullPointerException。

ConcurrentHashMap是线程安全的HashMap的实现,HashTable使用synchronized机制进行同步,所以每次只能有一个线程进行操作,当Hashtable的大小增加到一定的时候,性能会急剧下降。ConcurrentHashMap的同步机制是基于Lock类,在读操作时不会加锁。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的Hashtable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。

ConcurrentHashMap采用了分段锁的设计,只有在同一个分段内才存在竞态关系,相比于对整个Map加锁分段锁大大的提高了高并发环境下的处理能力。但由于不是对整个Map加锁,导致一些需要扫描整个Map的方法(size(),containsValue())需要使用特殊的实现,另外一些方法(clear())甚至放弃了对一致性的要求,ConcurrentHashMap是弱一致性的。


8.volatile变量和atomic类

volatile是java提供的轻量的同步机制,用volatile修饰的成员变量Java内存模型不会对指令的操作进行重排序。volatile变量不会被缓存在寄存器中或者其他对CPU不可见的地方,每次总是从主存中读取volatile变量的结果。也就是说对于volatile变量的修改,其它线程总是可见的。volatile可以保证线程间操作的有序性和数据的可见性,不能保证原子性。

在JDK1.5中增加了以Atomic开头的一系列原子类,这些类可以保证多线程环境下,当某个线程在执行atomic的方法时,不会被其他线程打断,而别的线程一直等到该方法执行完成,才由JVM从等待队列中选择一个线程执行。原子类提供了保证原子性的操作。

使用volatile修饰的原子类可以提供级较好的线程安全操作。

 

9.什么是ThreadLocal类

ThreadLocal并不是一个Thread,而是Thread的局部变量,这些变量在多线程环境下访问时能保证各个线程里的变量相对独立于其他线程内的变量,就是变量只对当前线程可见。ThreadLocal不是为了解决多线程访问共享变量,而是为每个线程创建一个单独的变量副本,提供了保持对象的方法和避免参数传递的复杂性。ThreadLocal使用场景为一般用来解决数据库连接、Session管理等。

ThreadLocal可以看做是一个容器,容器里面存放着属于当前线程的变量。ThreadLocal类提供了四个对外开放的方法: 

设置当前线程的线程局部变量的值。

void set(Object value);

返回当前线程所对应的线程局部变量。
Object get();

将当前线程局部变量删除。在当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
void remove();

返回该线程局部变量的初始值。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次,ThreadLocal中的缺省实现直接返回一个null。
protected Object initialValue();


使用示例:

public class Service {private ThreadLocal<Integer> local = new ThreadLocal<>();public void setValue() {local.set((int) (Math.random() * 100));}public Integer getValue() {return local.get();}}// 线程调用类public class MyThread implements Runnable {private Service service;public MyThread(Service service) {this.service = service;}@Overridepublic void run() {service.setValue();try {Thread.sleep(2000);} catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + ": threadLocal=" + service.getValue());}public static void main(String[] args) {Service service = new Service();Thread thread = new Thread(new MyThread(service));Thread thread2 = new Thread(new MyThread(service));thread.start();thread2.start();}}

运行结果:

Thread-1: threadLocal=64

Thread-0: threadLocal=50

每个线程都有自己的值,不会受到其他线程的影响。


10.ThreadLocal类实现原理

在ThreadLocal类中有一个静态内部类ThreadLocalMap,用键值对的形式存储每一个线程的变量副本,ThreadLocalMap中元素的key为当前ThreadLocal对象,而value对应线程的变量副本。ThreadLocal的set和createMap方法源码:

 public void set(Tvalue) {        Thread t = Thread.currentThread();        ThreadLocalMap map = getMap(t);        if (map !=null)            map.set(this,value);        else            createMap(t, value);    }void createMap(Threadt, T firstValue) {        t.threadLocals =new ThreadLocalMap(this,firstValue);}

在ThreadLocal类中的几个主要的方法,都是对其内部类ThreadLocalMap进行操作。

ThreadLocalMap部分源代码:

 static class ThreadLocalMap {

//map使用Entry类来存储数据,key是ThreadLocal的弱引用        static class Entryextends WeakReference<ThreadLocal> {            Object value;            Entry(ThreadLocal k, Objectv) {                super(k);                value = v;            }        }//createMap调用的构造方法。ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {            table = new Entry[INITIAL_CAPACITY];            int i =firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);            table[i] =new Entry(firstKey,firstValue);            size = 1;            setThreshold(INITIAL_CAPACITY);        }………………}

ThreadLocal的实现思想,就是每个线程维护一个ThreadLocalMap的映射表,映射表的 key 是ThreadLocal实例本身,value是要存储的副本变量。ThreadLocalMap采用散列表(Hash)来实现的,但是实现方式和HashMap不太一样。HashMap 解决hash冲突采用分离链表法(separate chaining),ThreadLocalMap采用开放定址法(open addressing)。

ThreadLocal实例本身并不存储值,它只是提供一个在当前线程中找到副本值的key。


11.使用ThreadLocal可能存在什么问题

由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就有可能会导致内存泄漏。

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统GC的时候这个ThreadLocal就会被回收。这样在ThreadLocalMap中就会出现key为null的Entry而无法访问这些Entry的value,如果当前线程不结束,这些Entry的value就会一直存在一条强引用链Thread->ThreaLocalMap->Entry->value而永远无法回收,造成内存泄漏。

虽然在ThreadLocalMap的设计中已经加上了一些防护措施:在调用get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。但是这些被动的预防措施并不能保证不会内存泄漏。如果分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么也会导致内存泄漏。如果声明了static的ThreadLocal使ThreadLocal拥有超长生命周期也可能导致的内存泄漏。

为什么使用弱引用?key如果使用强引用,引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致内存泄漏。

因此,使用完ThreadLocal都要调用它的remove()方法清除数据。

 

12.Java线程池

创建线程要花费一定的资源和时间,如果每个任务都需要创建一个线程将会消耗大量的资源。在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。从JDK1.5开始,Java API提供了Executor框架让你可以创建不同的线程池。

Executor:定义了一个接收Runnable对象的方法executor。

ExecutorService:ExecutorService接口继承自Executor,提供了更丰富的实现多线程的方法。

ThreadPoolExecutor:ExecutorService的直接实现类。

通过Executors工具类可以创建不同的线程池:

FixedThreadPool:适用于为了满足管理资源的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器。

SingleThreadExecutor:适用于需要保证顺序地执行各个任务,并且在任意时间点不会有多个线程在活动的场景。

CachedThreadPool:大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者负载比较轻的服务器。

ScheduledThreadPoolExecutor:创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。ScheduledThreadPoolExecutor适用于需要在多个后台线程执行周期任务,同时为了满足资源管理需求需要限制后台线程数量的应用场景。

ThreadPoolExecutor参数:

corePoolSize:核心线程数,如果运行的线程少于corePoolSize,则创建新线程来执行新任务,即使线程池中的其他线程是空闲的。

maximumPoolSize:最大线程数,可允许创建的线程数。 

keepAliveTime:如果线程数多于corePoolSize,则这些多余的线程的空闲时间超过keepAliveTime时将被终止。

unit:keepAliveTime参数的时间单位。

workQueue:保存任务的阻塞队列,当运行的线程数少于corePoolSize时,在有新任务时直接创建新线程来执行任务而无需再进队列;当运行的线程数等于或多于corePoolSize,在有新任务添加时则选加入队列,不直接创建线程;当队列满时,在有新任务时就创建新线程。

threadFactory:使用ThreadFactory创建新线程,默认使用defaultThreadFactory创建线程。

handle:定义处理被拒绝任务的策略,默认使用ThreadPoolExecutor.AbortPolicy,任务被拒绝时将抛出RejectExecutorException。

13.线程池中submit() 和 execute()方法有什么区别?

两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中, 而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。