java多线程常见面试题及答案

1.Java创建线程之后,直接调用start()方法和run()的区别

(1)调用start()会开启线程,让开启的线程去执行run()方法中的线程任务,此时无需等待run()方法执行完毕,即可继续执行下面的代码。
(2)调用run(),线程并未开启,去执行run()的只有主线程,还是要顺序指定,要等待run()方法体执行完毕后才可继续执行下面的代码。
(3)通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪状态并没有运行。
(4)调用start()方法后,一旦得到cpu时间片,就开始执行run()方法。

2.常用的线程池模式以及不同线程池的使用场景

CachedThreadPool 

这类线程池的特点就是里面没有核心线程,全是非核心线程,其maximumPoolSize设置为Integer.MAX_VALUE,线程可以无限创建,当线程池中的线程都处于活动状态的时候,线程池会创建新的线程来处理新任务,否则会用空闲的线程来处理新任务,这类线程池的空闲线程都是有超时机制的,keepAliveTime在这里是有效的,时长为60秒,超过60秒的空闲线程就会被回收,当线程池都处于闲置状态时,线程池中的线程都会因为超时而被回收,所以几乎不会占用什么系统资源。任务队列采用的是SynchronousQueue,这个队列是无法插入任务的,一有任务立即执行,所以CachedThreadPool比较适合任务量大但耗时少的任务。

FixedThreadPool

这类线程池的特点就是里面全是核心线程,没有非核心线程,也没有超时机制,任务大小也是没有限制的,数量固定,即使是空闲状态,线程不会被回收,除非线程池被关闭,从构造方法也可以看出来,只有两个参数,一个是指定的核心线程数,一个是线程工厂,keepAliveTime无效。任务队列采用了无界的阻塞队列LinkedBlockingQueue,执行execute方法的时候,运行的线程没有达到corePoolSize就创建核心线程执行任务,否则就阻塞在任务队列中,有空闲线程的时候去取任务执行。由于该线程池线程数固定,且不被回收,线程与线程池的生命周期同步,所以适用于任务量比较固定但耗时长的任务。

ScheduledThreadPool

这类线程池核心线程数量是固定的,好像和FixThreadPool有点像,但是它的非核心线程是没有限制的,并且非核心线程一闲置就会被回收,keepAliveTime同样无效,因为核心线程是不会回收的,当运行的线程数没有达到corePoolSize的时候,就新建线程去DelayedWorkQueue中取ScheduledFutureTask然后才去执行任务,否则就把任务添加到DelayedWorkQueue,DelayedWorkQueue会将任务排序,按新建一个非核心线程顺序执行,执行完线程就回收,然后循环。任务队列采用的DelayedWorkQueue是个无界的队列,延时执行队列任务。综合来说,这类线程池适用于执行定时任务和具体固定周期的重复任务。

SingleThreadPool
这类线程池顾名思义就是一个只有一个核心线程的线程池,从构造方法来看,它可以单独执行,也可以与周期线程池结合用。其任务队列是LinkedBlockingQueue,这是个无界的阻塞队列,因为线程池里只有一个线程,就确保所有的任务都在同一个线程中顺序执行,这样就不需要处理线程同步的问题。这类线程池适用于多个任务顺序执行的场景。

3.newFixedThreadPool此种线程池如果线程数达到最大值后会怎么办,底层原理。

创建一个可重用固定线程数线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。

4.同一个类不同方法都有synchronized锁,一个对象是否可以同时访问。或者一个类的static构造方法加上synchronized之后的锁的影响。

不可以;全局锁(该锁针对的是类,无论实例多少个对象,那么线程都共享该锁)。

5. 了解可重入锁的含义,以及ReentrantLock 和synchronized的区别

同一个线程再次进入同步代码的时候.可以使用自己已经获取到的锁,这就是可重入锁java里面内置锁(synchronize)和Lock(ReentrantLock)都是可重入的。

区别:

      这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。

1.Synchronized

    Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。

2.ReentrantLock

   由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项:

        1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。

        2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。

        3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象。

6. 同步的数据结构,例如concurrentHashMap的源码理解以及内部实现原理,为什么他是同步的且效率高

ConcurrentHashMap采用了非常精妙的"分段锁"策略,ConcurrentHashMap的主干是个Segment数组。

final Segment[] segments;

Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于不同Segment的数据进行操作是不用考虑锁竞争的。(就按默认的ConcurrentLeve为16来讲,理论上就允许16个线程并发执行,有木有很酷)

所以,对于同一个Segment的操作才需考虑线程同步,不同的Segment则无需考虑。

Segment类似于HashMap,一个Segment维护着一个HashEntry数组

transient volatile HashEntry[] table;

HashEntry是目前我们提到的最小的逻辑处理单元了。一个ConcurrentHashMap维护一个Segment数组,一个Segment维护一个HashEntry数组。

static final class HashEntry {

final int hash;

final K key;

volatile V value;

volatile HashEntry next;

//其他省略

}

我们说Segment类似哈希表,那么一些属性就跟我们之前提到的HashMap差不离,比如负载因子loadFactor,比如阈值threshold等等,如果用户不指定则会使用默认值,initialCapacity为16,loadFactor为0.75(负载因子,扩容时需要参考),concurrentLevel为16。

Segment数组的大小ssize是由concurrentLevel来决定的,但是却不一定等于concurrentLevel,ssize一定是大于或等于concurrentLevel的最小的2的次幂。比如:默认情况下concurrentLevel是16,则ssize为16;若concurrentLevel为14,ssize为16;若concurrentLevel为17,则ssize为32。为什么Segment的数组大小一定是2的次幂?其实主要是便于通过按位与的散列算法来定位Segment的index。

从源码看,put的主要逻辑也就两步:1.定位segment并确保定位的Segment已初始化 2.调用Segment的put方法。

get方法无需加锁,由于其中涉及到的共享变量都使用volatile修饰,volatile可以保证内存可见性,所以不会读取到过期数据。

Segment中的put方法是要加锁的。只不过是锁粒度细了而已。

7. atomicinteger和Volatile等线程安全操作的关键字的理解和使用

volatile关键字

  volatile是一个特殊的修饰符,只有成员变量才能使用它,与Synchronized及ReentrantLock等提供的互斥相比,Synchronized保证了Synchronized同步块中变量的可见性,而volatile则是保证了所修饰变量的可见性。可见性指的是在一个线程中修改变量的值以后,在其他线程中能够看到这个值(在Java并发程序缺少同步类的情况下,多线程对成员变量的操作对其它线程是透明的(不可见))。因为volatile只是保证了同一个变量在多线程中的可见性,所以它更多是用于修饰作为开关状态的变量。

  java关键字volatile,从表面意思上是说这个变量是易变的,不稳定的,事实上,确实如此,这个关键字的作用就是告诉编译器,凡是被该关键字声明的变量都是易变的、不稳定的。所以不要试图对该变量使用缓存等优化机制,而应当每次都从它的内存地址中去读值。使用volatile标记的变量在读取或写入时不需要使用锁,这将减少产生死锁的概率,使代码保持简洁。

  请注意,这里只是说每次读取volatile的变量时都要从它的内存地址中读取,并没有说每次修改完volatile的变量后都要立刻将它的值写回内存。也就是说volatile只提供了内存可见性,而没有提供原子性,操作互斥提供了操作整体的原子性,同一个变量多个线程间的可见性与多个线程中操作互斥是两件事情,所以说如果用这个关键字做高并发的安全机制的话是不可靠的。

volatile的用法如下:

public volatile static int count=0;//在声明的时候带上volatile关键字即可

什么时候使用volatile关键字?当我们知道了volatile的作用,我们也就知道了它应该用在哪些地方,很显然,最好是那种只有一个线程修改变量,多个线程读取变量的地方。也就是对内存可见性要求高,而对原子性要求低的地方。

从上面的描述中,我们可以看出volatile与加锁机制的主要区别是:加锁机制既可以确保可见性又可以确保原子性,而volatile变量只有确保可见性。

原子操作Atomic

  Volatile变量可以确保先行关系,保证下一个读取操作会在前一个写操作之后发生(即写操作会发生在后续的读操作之前),但它并不能保证原子性。例如用volatile修饰count变量,那么count++ 操作就不是原子性的。

在JDK5中增加了java.util.concurrent.atomic包,这个包中是一些以Atomic开头的类,这些类主要提供一些相关的原子操作。我们以AtomicInteger为例来看一个多线程计数器场景。场景很简单,让多个线程都对计数器进行加1操作。我们一般可能会这样做:

public class TestUtil {

private int counter = 0;

public int increase() {

synchronized (this) {

counter = counter + 1;

return counter;

}

}

public int decrease() {

synchronized (this) {

counter = counter - 1;

return counter;

}

}

}

而采用了AtomicInteger后,代码会变成下面的样子:

public class TestUtil {

private AtomicInteger counter = new AtomicInteger();

public int increase() {

return counter.incrementAndGet();

}

public int decrease() {

return counter.decrementAndGet();

}

}

采用AtomicInteger之后代码变得简洁了,更重要的是性能得到了提升,而且是比较明显的提升,有兴趣的读者可以再自己的机器上进行测试。性能提升的原因主要在于AtomicInteger内部通过JNI的方式使用了硬件支持的CAS指令。

而在java.util.concurrent.atomic包中,除了AtomicInteger外,还有很多实用的类。

8. 线程间通信,wait和notify

线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体,线程之间的通信就成为整体的必用方式之一。当线程存在通信指挥,系统间的交互性会更强大,在提高CPU利用率的同时还会对线程任务在处理过程中进行有效的把控与监督。

为了支持多线程之间的协作,JDK提供了两个非常重要的接口线程等待wait()方法和通知notify()方法。这两个方法并不是在Thread类中的,而是输出Object类。这也意味着任何对象都可以调用这2个方法。

1、wait() 和 notify()必须配合synchrozied关键字使用,无论是wait()还是notify()都需要首先获取目标对象的一个监听器。

2、wait()释放锁,而notify()不释放锁。

9. 定时线程的使用

实现定时任务线程有如下三种方式:

①普通线程死循环

/**

     * 普通thread

     * 这是最常见的,创建一个thread,然后让它在while循环里一直运行着,

     * 通过sleep方法来达到定时任务的效果,这样可以快速简单的实现

     */

    Thread thread = new Thread(new Runnable() {

         

    @Override

    public void run() {

        while(true) {

            System.out.println("普通线程执行中......");

            try {

                TimeUnit.SECONDS.sleep(1);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

        }

    }

});

    thread.start();

②使用定时器timer

/**

     * 与第一种方式相比:

     * 优势 1:当启动和取消任务时可以控制

     * 优势 2:第一次执行任务时可以指定你想要的delay时间

     * 在实现时,Timer类可以调度任务,TimerTask则是通过在run()方法里实现具体任务。

     * Timer实例可以调度多任务,它是线程安全的。

     *

     */

    TimerTask task = new TimerTask() {

         

        @Override

        public void run() {

            System.out.println("timer线程执行中......");

        }

    };

Timer timer = new Timer();

timer.scheduleAtFixedRate(task, 5000, 1000);

 ③使用定时调度线程池ScheduledExecutorService

Runnable runnable = new Runnable() {

    @Override

    public void run() {

    System.out.println("定时任务线程执行中......");

    }

};

ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();

service.scheduleWithFixedDelay(runnable, 5000, 500, TimeUnit.MILLISECONDS);

10. 场景:在一个主线程中,要求有大量(很多很多)子线程执行完之后,主线程才执行完成。多种方式,考虑效率。

join()

11. 进程和线程的区别

(1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。

(2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行。

(3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。

(4)系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。

12. 什么叫线程安全?举例说明

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,

就是线程安全的。 或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

存在竞争的线程不安全,不存在竞争的线程就是安全的

13. 线程的几种状态

Java中的线程的状态分为6种。

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

2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的成为“运行”。

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

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

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

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

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

参考:https://blog.csdn.net/pange1991/article/details/53860651/

14. 并发、同步的接口或方法

参考:https://blog.csdn.net/woshisap/article/details/43119569

15. HashMap 是否线程安全,为何不安全。 ConcurrentHashMap,线程安全,为何安全。底层实现是怎么样的。

HashMap 不安全,没有加锁;

ConcurrentHashMap安全,segment分段锁;

16. J.U.C下的常见类的使用。 ThreadPool的深入考察; BlockingQueue的使用。(take,poll的区别,put,offer的区别);原子类的实现。

           

参考:http://ifeve.com/j-u-c-framework/

17. 简单介绍下多线程的情况,从建立一个线程开始。然后怎么控制同步过程,多线程常用的方法和结构

为什么要线程同步

因为当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。举 个例子,如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块。假设账户原本有0块,如果取钱线程和存钱线程同时发生,会出现什么结果 呢?取钱不成功,账户余额是100.取钱成功了,账户余额是0.那到底是哪个呢?很难说清楚。因此多线程同步就是要解决这个问题。

(1)同步方法:

即有synchronized关键字修饰的方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

(2)同步代码块

即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步

(3)使用特殊域变量(Volatile)实现线程同步

a.volatile关键字为域变量的访问提供了一种免锁机制
b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新
c.因此每次使用该域就要重新计算,而不是使用寄存器中的值
d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量

(4)使用重入锁实现线程同步

在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用

ThreadLocal与同步机制

a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题
b.前者采用以”空间换时间”的方法,后者采用以”时间换空间”的方式

18. volatile的理解

(1)、保证此变量对所有线程的可见性,指一条线程修改了这个变量的值,新值对于其他线程来说是可见的,但并不是多线程安全的。

(2)、禁止指令重排序优化。

Volatile和Synchronized四个不同点:

1 粒度不同,后者锁对象和类,前者针对变量

2 syn阻塞,volatile线程不阻塞

3 syn保证三大特性,volatile不保证原子性

4 syn编译器优化,volatile不优化

19. 实现多线程有几种方式,多线程同步怎么做,说说几个线程里常用的方法

实现多线程有几种方式:

1.继承Thread类,重写run方法

2.实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target

3.通过Callable和FutureTask创建线程

4.通过线程池创建线程
 

20.说明类java.lang.ThreadLocal的作用和原理。列举在哪些程序中见过ThreadLocal的使用?

简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。

21.说说乐观锁与悲观锁

1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

2)悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

22.在Java中怎么实现多线程?描述线程状态的变化过程。

当多个线程访问同一个数据时,容易出现线程安全问题,需要某种方式来确保资源在某一时刻只被一个线程使用。需要让线程同步,保证数据安全线程同步的实现方案: 同步代码块和同步方法,均需要使用synchronized关键字

同步代码块:public void makeWithdrawal(int amt) {

synchronized (acct) { }

}

同步方法:public synchronized void makeWithdrawal(int amt) { }

线程同步的好处:解决了线程安全问题

线程同步的缺点:性能下降,可能会带来死锁

23.在多线程编程里,wait方法的调用方式是怎样的?

wait方法是线程通信的方法之一,必须用在 synchronized方法或者synchronized代码块中,否则会抛出异常,这就涉及到一个“锁”的概念,而wait方法必须使用上锁的对象来调用,从而持有该对象的锁进入线程等待状态,直到使用该上锁的对象调用notify或者notifyAll方法来唤醒之前进入等待的线程,以释放持有的锁。

24.sleep()和yield()有什么区别?

① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;

② 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;

③ sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;

④ sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。

25.当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法?

不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的synchronized修饰符要求执行方法时要获得对象的锁,如果已经进入A方法说明对象锁已经被取走,那么试图进入B方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。

26.说说关于同步锁的更多细节。

Java中每个对象都有一个内置锁。

当程序运行到非静态的synchronized同步方法上时,自动获得与正在执行代码类的当前实例(this实例)有关的锁。获得一个对象的锁也称为获取锁、锁定对象、在对象上锁定或在对象上同步。

当程序运行到synchronized同步方法或代码块时才该对象锁才起作用。

一个对象只有一个锁。所以,如果一个线程获得该锁,就没有其他线程可以获得锁,直到第一个线程释放(或返回)锁。这也意味着任何其他线程都不能进入该对象上的synchronized方法或代码块,直到该锁被释放。

释放锁是指持锁线程退出了synchronized同步方法或代码块。

关于锁和同步,有一下几个要点:

1)只能同步方法,而不能同步变量和类;

2)每个对象只有一个锁;当提到同步时,应该清楚在什么上同步?也就是说,在哪个对象上同步?

3)不必同步类中所有的方法,类可以同时拥有同步和非同步方法。

4)如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。

5)如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。

6)线程睡眠时,它所持的任何锁都不会释放。

7)线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。

8)同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块。

9)在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。

  关注公众号,获取免费软件、资料,笔记等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

心之所向...

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值