Java多线程Java面试题

Posted 蓝盒子itbluebox

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程Java面试题相关的知识,希望对你有一定的参考价值。

一、创建线程的方式?

1、继承Thread类创建

通过继承Thread并且重写其run(),run方法中即线程执行任务。创建后的子类通过调用 start() 方法即可执行线程方法。

通过继承Thread实现的线程类,多个线程间无法共享线程类的实例变量。(需要创建不同Thread对象,自然不共享)

**
 * 通过继承Thread实现线程
 */
public class ThreadTest extends Thread
  
  private int i = 0 ;

    @Override
    public void run() 
        for(;i<50;i++)
            System.out.println(Thread.currentThread().getName() + " is running " + i );
        
    
    public static void main(String[] args) 
        for(int j=0;j<50;j++)
        	if(j=20)
                new ThreadTest().start() ;
                new ThreadTest().start() ;
            
        
    

2、通过Runnable接口创建线程类

该方法需要先 定义一个类实现Runnable接口,
并重写该接口的 run() 方法,此run方法是线程执行体。
接着创建 Runnable实现类的对象,作为创建Thread对象的参数target,此Thread对象才是真正的线程对象。
通过实现Runnable接口的线程类,是互相共享资源的。

/**
 * 通过实现Runnable接口实现的线程类
 */
public class RunnableTest implements Runnable 
    private int i ;
    @Override
    public void run() 
        for(;i<50;i++)
            System.out.println(Thread.currentThread().getName() + " -- " + i);
        
    
    public static void main(String[] args) 
        for(int i=0;i<100;i++)
            System.out.println(Thread.currentThread().getName() + " -- " + i);
            if(i==20)
                RunnableTest runnableTest = new RunnableTest() ;
                new Thread(runnableTest,"线程1").start() ;
                new Thread(runnableTest,"线程2").start() ;
            
        
    

3、使用Callable和Future创建线程

从继承Thread类和实现Runnable接口可以看出,上述两种方法都不能有返回值,且不能声明抛出异常。

Callable接口则实现了此两点,Callable接口如同Runable接口的升级版,其提供的call()方法将作为线程的执行体,同时允许有返回值。

但是Callable对象不能直接作为Thread对象的target,因为Callable接口是 Java 5 新增的接口,不是Runnable接口的子接口。

对于这个问题的解决方案,就引入 Future接口,此接口可以接受call() 的返回值,RunnableFuture接口是Future接口和Runnable接口的子接口,可以作为Thread对象的target 。

并且, Future 接口提供了一个实现类:FutureTask

FutureTask实现了RunnableFuture接口,可以作为 Thread对象的target

public class CallableTest 
    public static void main(String[] args) 
        CallableTest callableTest = new CallableTest() ;
        //因为Callable接口是函数式接口,可以使用Lambda表达式
        FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->
           int i = 0 ;
           for(;i<100;i++)
               System.out.println(Thread.currentThread().getName() + "的循环变量i的值 :" + i);
           
           return i;
        );
       for(int i=0;i<100;i++)
           System.out.println(Thread.currentThread().getName()+" 的循环变量i : + i");
           if(i==20)
               new Thread(task,"有返回值的线程").start();
           
       
       try
           System.out.println("子线程返回值 : " + task.get());
        catch (Exception e)
           e.printStackTrace();
        
    

二、线程是状态

初始(NEW):新创建了一个线程对象,但还没有调用start()方法。

运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的成为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。
该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权,此时处于就绪状态(ready)。
就绪状态的线程在获得cpu 时间片后变为运行中状态(running)。

阻塞(BLOCKED):表线程阻塞于锁。

等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

超时等待(TIME_WAITING):该状态不同于WAITING,它可以在指定的时间内自行返回。

终止(TERMINATED):表示该线程已经执行完毕。

三、Java 多线程加锁的方式

  1. synchronized关键字

  2. Java.util.concurrent包中的lock接口和ReentrantLock实现类

这两种方式实现加锁。
1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。
Lock是一个类,通过这个类可以实现同步访问;

2)Locksynchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;
而Lock必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

总结来说,Locksynchronized有以下几点不同:

1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生,而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断; (I/OSynchronized都能相应中断,即不需要处理interruptionException异常)

4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

5)Lock可以提高多个线程进行读操作的效率。

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。
所以说,在具体使用时要根据适当情况选择。

1.可重入锁

如果锁具备可重入性,则称作为可重入锁。
synchronizedReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:
基于线程的分配,而不是基于方法调用的分配。

举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2

class  MyClass 
     public  synchronized  void  method1() 
         method2();
     
     public  synchronized  void  method2() 
     

上述代码中的两个方法method1method2都用synchronized修饰了,假如某一时刻,
线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。

但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。

2.可中断锁

可中断锁:顾名思义,就是可以相应中断的锁。
在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

在前面演示lockInterruptibly()的用法时已经体现了Lock的可中断性。

3.公平锁

公平锁即尽量以请求锁的顺序来获取锁。
比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。

非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。
这样就可能导致某个或者一些线程永远获取不到锁。

在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。

而对于ReentrantLockReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

另外在ReentrantLock类中定义了很多方法,比如:

isFair() //判断锁是否是公平锁

isLocked() //判断锁是否被任何线程获取了

isHeldByCurrentThread() //判断锁是否被当前线程获取了

hasQueuedThreads() //判断是否有线程在等待该锁

ReentrantReadWriteLock中也有类似的方法,同样也可以设置为公平锁和非公平锁。

不过要记住,ReentrantReadWriteLock并未实现Lock接口,它实现的是ReadWriteLock接口。

4.读写锁

读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。

正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。

ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。

可以通过readLock()获取读锁,通过writeLock()获取写锁。

ReadWriteLock, ReadWriteLock也是一个接口,在它里面只定义了两个方法:

四、DeplayQueue延时无界阻塞队列

在谈到DelayQueue的使用和原理的时候,我们首先介绍一下DelayQueue
DelayQueue是一个无界阻塞队列,只有在延迟期满时才能从中提取元素。
该队列的头部是延迟期满后保存时间最长的Delayed元素。

DelayQueue阻塞队列在我们系统开发中也常常会用到,
例如:缓存系统的设计,缓存中的对象,超过了空闲时间,需要从缓存中移出;
任务调度系统,能够准确的把握任务的执行时间。

我们可能需要通过线程处理很多时间上要求很严格的数据,如果使用普通的线程,
我们就需要遍历所有的对象,一个一个的检查看数据是否过期等,
首先这样在执行上的效率不会太高,其次就是这种设计的风格也大大的影响了数据的精度。

一个需要12:00点执行的任务可能12:01才执行,
这样对数据要求很高的系统有更大的弊端。由此我们可以使用DelayQueue。

下面将会对DelayQueue做一个介绍,然后举个例子。
并且提供一个Delayed接口的实现和Sample代码。
DelayQueue是一个BlockingQueue,其特化的参数是Delayed

(不了解BlockingQueue的同学,先去了解BlockingQueue再看本文)

Delayed扩展了Comparable接口,比较的基准为延时的时间值,Delayed接口的实现类getDelay的返回值应为固定值(final)

DelayQueue内部是使用PriorityQueue实现的。

DelayQueue=BlockingQueue+PriorityQueue+Delayed

DelayQueue的关键元素BlockingQueuePriorityQueueDelayed
可以这么说,DelayQueue是一个使用优先队列(PriorityQueue)实现的BlockingQueue,优先队列的比较基准值是时间。

他们的基本定义如下

public interface Comparable<T> 
    public int compareTo(T o);

public interface Delayed extends Comparable<Delayed> 
    long getDelay(TimeUnit unit);

public class DelayQueue<E extends Delayed> implements BlockingQueue<E> 
    private final PriorityQueue<E> q = new PriorityQueue<E>();

DelayQueue 内部的实现使用了一个优先队列。
当调用 DelayQueue 的 offer 方法时,把 Delayed 对象加入到优先队列 q 中。如下:

public boolean offer(E e) 
    final ReentrantLock lock = this.lock;
    lock.lock();
    try 
        E first = q.peek();
        q.offer(e);
        if (first == null || e.compareTo(first) < 0)
            available.signalAll();
        return true;
     finally 
        lock.unlock();
    

DelayQueue 的 take 方法,把优先队列 q 的 first 拿出来(peek),如果没有达到延时阀值,则进行 await处理。如下:

public E take() throws InterruptedException 
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try 
        for (; ; ) 
            E first = q.peek();
            if (first == null) 
                available.await();
             else 
                long delay = first.getDelay(TimeUnit.NANOSECONDS);
                if (delay > 0) 
                    long tl = available.awaitNanos(delay);
                 else 
                    E x = q.poll();
                    assert x != null;
                    if (q.size() != 0)
                        available.signalAll(); //wake up other takers return x;
                
            
        
     finally 
        lock.unlock();
    

● DelayQueue 实例应用

Ps:为了具有调用行为,存放到 DelayDeque 的元素必须继承 Delayed 接口。Delayed 接口使对象成为延迟对象,它使存放在 DelayQueue 类中的对象具有了激活日期。

该接口强制执行下列两个方法。

一下将使用 Delay 做一个缓存的实现。

其中共包括三个类Pair、DelayItem、Cache

● Pair 类:

public class Pair<K, V> 
    public K first;
    public V second;
    public Pair() 
    
    public Pair(K first, V second) 
        this.first = first;
        this.second = second;
    

以下是对 Delay 接口的实现:

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
public class DelayItem<T> implements Delayed 
    /**
     * Base of nanosecond timings, to avoid wrapping
     */
    private static final long NANO_ORIGIN = System.nanoTime();
    /**
     * Returns nanosecond time offset by origin
     */
    final static long now() 
        return System.nanoTime() - NANO_ORIGIN;
    
    /**
     * Sequence number to break scheduling ties, and in turn to guarantee FIFO order among tied
     * entries.
     */
    private static final AtomicLong sequencer = new AtomicLong(0);

    /**
     * Sequence number to break ties FIFO
     */
    private final long sequenceNumber;

    /**
     * The time the task is enabled to execute in nanoTime units
     */
    private final long time;
    private final T item;
    public DelayItem(T submit, long timeout) 
        this.time = now() + timeout;
        this.item = submit;
        this.sequenceNumber = sequencer.getAndIncrement();
    
    public T getItem() 
        return this.item;
    
    public long getDelay(TimeUnit unit) 
        long d = unit.convert(time - now()TimeUnit.NANOSECONDS); return d;
    
    public int compareTo(Delayed other) 
        if (other == this) // compare zero ONLY if same object return 0;
            if (other instanceof DelayItem) 
                DelayItem x = (DelayItem) other;
                long diff = time - x.time;
                if (diff < 0) return -1;
                else if (diff > 0) return 1;
                else if (sequenceNumber < x.sequenceNumber) return -1;
                else
                    return 1;
            
        long d = (getDelay(TimeUnit.NANOSECONDS) - other.getDelay(TimeUnit.NANOSECONDS));
        return (d == 0) ?0 :((d < 0) ?-1 :1);
    

以下是 Cache 的实现,包括了 put 和 get 方法

import javafx.util.Pair;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Cache<K, V> 
    private static final Logger LOG = Logger.getLogger(Cache.class.getName());
    private ConcurrentMap<K, V> cacheObjMap = new ConcurrentHashMap<K, V>();
    private DelayQueue<DelayItem<Pair<K, V>>> q = new DelayQueue<DelayItem<Pair<K, V>>>();
    private Thread daemonThread;

    public Cache() 

        Runnable daemonTask = new Runnable() 
            public void run() 
                daemonCheck();
            
        ;
        daemonThread = new Thread(daemonTask);
        daemonThread.setDaemon(true);
        daemonThread.setName("Cache Daemon");
        daemonThread.start(

以上是关于Java多线程Java面试题的主要内容,如果未能解决你的问题,请参考以下文章

面试题分享15个顶级Java多线程面试题

java多线程面试题

Java多线程Java面试题

Java多线程Java面试题

史上最全Java多线程面试题及答案

JAVA多线程和并发基础面试题