Java高级工程师常见面试题(六)-多线程

1. Java创建线程之后,直接调用start()方法和run()的区别
调用run会在当前线程中执行方法,调用start会开启一条新线程来执行方法。
2. 常用的线程池模式以及不同线程池的使用场景
newCachedThreadPool:
底层:返回 ThreadPoolExecutor实例,corePoolSize为0;maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为60L;unit为TimeUnit.SECONDS;workQueue为 SynchronousQueue(同步队列)
通俗:当有新任务到来,则插入到SynchronousQueue中,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可以线程则执行,若没有可用线程则创建一个线程来执行该任务;若池中线程空闲时间超过指定大小,则该线程会被销毁。
适用执行很多短期异步的小程序或者负载较轻的服务器
newFixedThreadPool:
底层:返回 ThreadPoolExecutor实例,接收参数为所设定线程数量nThread,corePoolSize为nThread,maximumPoolSize为nThread;keepAliveTime为0L(不限时);unit为:TimeUnit.MILLISECONDS;WorkQueue为: new LinkedBlockingQueue<Runnable>() 无界阻塞队列
通俗:创建可容纳固定数量线程的池子,每个线程的存活时间是无限的,当池子满了就不在添加线程了;如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列)
适用执行长期的任务,性能好很多
newSingleThreadExecutor:
底层FinalizableDelegatedExecutorService包装的ThreadPoolExecutor实例,corePoolSize为1;maximumPoolSize为1;keepAliveTime为0L;unit为:TimeUnit.MILLISECONDS;workQueue为: new LinkedBlockingQueue<Runnable>() 无界阻塞队列
通俗:创建只有一个线程的线程池,且线程的存活时间是无限的;当该线程正繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列)
适用一个任务一个任务执行的场景
NewScheduledThreadPool:
底层:创建 ScheduledThreadPoolExecutor实例,corePoolSize为传递来的参数,maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为0;unit为:TimeUnit.NANOSECONDS;workQueue为: new DelayedWorkQueue() 一个按超时时间升序排序的队列
通俗:创建一个固定大小的线程池,线程池内线程存活时间无限制,线程池可以支持定时及周期性任务执行,如果所有线程均处于繁忙状态,对于新任务会进入DelayedWorkQueue队列中,这是一种按照超时时间排序的队列结构
适用周期性执行任务的场景
线程池任务执行流程:
当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。
当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行
当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务
当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理
当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭
备注:
一般如果线程池任务队列采用LinkedBlockingQueue队列的话,那么不会拒绝任何任务(因为队列大小没有限制),这种情况下,ThreadPoolExecutor最多仅会按照最小线程数来创建线程,也就是说线程池大小被忽略了。
如果线程池任务队列采用ArrayBlockingQueue队列的话,那么ThreadPoolExecutor将会采取一个非常负责的算法,比如假定线程池的最小线程数为4,最大为8所用的ArrayBlockingQueue最大为10。随着任务到达并被放到队列中,线程池中最多运行4个线程(即最小线程数)。即使队列完全填满,也就是说有10个处于等待状态的任务,ThreadPoolExecutor也只会利用4个线程。如果队列已满,而又有新任务进来,此时才会启动一个新线程,这里不会因为队列已满而拒接该任务,相反会启动一个新线程。新线程会运行队列中的第一个任务,为新来的任务腾出空间。
这个算法背后的理念是:该池大部分时间仅使用核心线程(4个),即使有适量的任务在队列中等待运行。这时线程池就可以用作节流阀。如果挤压的请求变得非常多,这时该池就会尝试运行更多的线程来清理;这时第二个节流阀—最大线程数就起作用了。
3. newFixedThreadPool此种线程池如果线程数达到最大值后会怎么办,底层原理。
参考:2
4. 多线程之间通信的同步问题,synchronized锁的是对象,衍伸出和synchronized相关很多的具体问题,例如同一个类不同方法都有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<K,V>[] segments;
Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于不同Segment的数据进行操作是不用考虑锁竞争的。(就按默认的ConcurrentLeve为16来讲,理论上就允许16个线程并发执行,有木有很酷)
所以,对于同一个Segment的操作才需考虑线程同步,不同的Segment则无需考虑。
Segment类似于HashMap,一个Segment维护着一个HashEntry数组
transient volatile HashEntry<K,V>[] table;
HashEntry是目前我们提到的最小的逻辑处理单元了。一个ConcurrentHashMap维护一个Segment数组,一个Segment维护一个HashEntry数组。
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> 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):表示该线程已经执行完毕。

14. 并发、同步的接口或方法
15. HashMap 是否线程安全,为何不安全。 ConcurrentHashMap,线程安全,为何安全。底层实现是怎么样的。
HashMap 不安全,没有加锁;
ConcurrentHashMap安全,segment分段锁;
16. J.U.C下的常见类的使用。 ThreadPool的深入考察; BlockingQueue的使用。(take,poll的区别,put,offer的区别);原子类的实现。

            

17. 简单介绍下多线程的情况,从建立一个线程开始。然后怎么控制同步过程,多线程常用的方法和结构
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.通过线程池创建线程
多线程同步怎么做:
1、synchronized同步方法
2、synchronized同步代码块
3、wait与notify
4、使用特殊域变量(volatile)实现线程同步
5、使用重入锁实现线程同步
6、使用局部变量实现线程同步(ThreadLocal)
7、使用阻塞队列实现线程同步
8、使用原子变量实现线程同步
说说几个线程里常用的方法:

猜你喜欢

转载自blog.csdn.net/u014378181/article/details/80940767