JAVA problem sorting out the implementation principle of thread pool in Java

1. What is thread safety?

  When a class is accessed by multiple threads, the class behaves correctly regardless of how the runtime environment is scheduled or how the threads will execute alternately, and without requiring any additional synchronization or coordination in the calling code behavior, then the class is thread-safe. Correctness: A class behaves in accordance with its specification. (The specification I understand is the state result that we can predict when we write the class)

Race Condition: The correctness of a computation depends on the timing of alternate execution of multiple threads. (The timing of the threads is different, the results may be different)

(360 interview questions).

"Read-Modify-Write", which defines the transition of an object's state based on the object's previous state. Even if it is a volatile-modified variable, the auto-increment operation in a multi-threaded environment will also cause race conditions , so volatile cannot guarantee absolute thread safety.

locking mechanism

  In the definition of thread safety, no matter what execution sequence or alternate method is used for operations between multiple threads, the invariance condition must not be destroyed. When multiple variables are involved in the invariance condition, the variables are not independent of each other, and changes in one variable will constrain the values ​​of other variables. Therefore, when a variable changes, other related variables are also updated in the same atomic operation.

Built-in lock :

Every Java object can be used as a lock to achieve synchronization, which is called a built-in lock. The thread automatically acquires the lock before entering the synchronization code block, and the lock is released when the code block execution completes and exits normally or when an exception is thrown in the code block.

The built-in lock is a mutual exclusion lock, that is, after thread A acquires the lock, thread B blocks until thread A releases the lock, and thread B can acquire the same lock

Built-in locks are implemented using the synchronized keyword. The synchronized keyword can be used in two ways:

1. Modify the methods that need to be synchronized ( all methods that access state variables must be synchronized ), at this time, the object acting as the lock is the object that calls the synchronization method

2. Synchronized code block

It is the same as directly using synchronized to modify the method that needs to be synchronized, but the granularity of the lock can be finer, and the object acting as the lock is not necessarily this, but also other objects, so it is more flexible to use

Reentrant lock :

When a thread holding the lock requests to enter the lock it holds again, the request succeeds. "Reentrancy" means that the granularity of the operation to acquire the lock is "thread", not "call". An implementation of reentrancy that associates a counter and thread holder with each lock.

Protect state with locks

  Since locks enable serial access to the code paths they protect, some protocols can be constructed over locks to achieve exclusive access to shared state.

  There is no intrinsic connection between an object's built-in lock and its state. Although most classes use built-in locks as an effective locking mechanism, the object's domain does not necessarily have to be protected by built-in locks.

  For each immutability condition involving multiple variables, all variables involved are protected/synchronized with the same lock .

 

equals: Whether the same object instance . Note, "instance". For example String s = new String("test"); s.equals(s), this is the comparison of the same object instance;

Equal sign (==): Compare the memory address of the object instance (that is, the ID of the object instance) to determine whether it is the same object instance; it can also be said to determine whether the object instances are physically equal;

 

The use and principle of HashTable

Java Collection - Comparison of Similarities and Differences of HashMap, HashTable and ConCurrentHashMap

Detailed explanation of the difference between HashTable and HashMap

 

 2. What is the underlying implementation of Hash_table? Zipper method, array + linked list

拉链法解决冲突的做法是将所有通过哈希函数计算出来的哈希地址相同的结点存放在一个单链表之中。 

(1)Hashtable 是一个散列表,它存储的内容是键值对(key-value)映射。

(2)Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。

(3)Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。

 

3.HashMap与Hashtable对比

1.HashTable是同步的,HashMap是非同步的

HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。就是说任何线程要更新Hashtable时要首先获得同步锁,其它线程要等到同步锁被释放之后才能再次获得同步锁更新Hashtable。

(Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。)

2)HashTable与HashMap实现的接口一致,但HashTable继承自Dictionary,而HashMap继承自AbstractMap;

 

3)HashTable不允许null值(key和value都不可以) ,HashMap允许null值(key和value都可以)。

4)HashTable有一个contains(Object value)功能和containsValue(Object value)功能一样。

contains方法其实做的事情就是containsValue, 里面将value值使用equals进行对比, 所以在HashTable中直接取消了contains方法而是使用containsValue代替.

5)HashTable使用Enumeration进行遍历,HashMap使用Iterator进行遍历。

HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。

6)HashTable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。

7)哈希值的使用不同

使用策略

由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。 HashMap不能保证随着时间的推移Map中的元素次序是不变的。

 

我们能否让HashMap同步?

HashMap可以通过下面的语句进行同步: 
Map m = Collections.synchronizeMap(hashMap); 

 

快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?

链接:https://www.nowcoder.com/questionTerminal/95e4f9fa513c4ef5bd6344cc3819d3f7?pos=101&mutiTagIds=570&orderByHotValue=1

一:快速失败(fail—fast)

          在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除),则会抛出Concurrent Modification Exception。

          原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

      注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。

      场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。

    二:安全失败(fail—safe)

      采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历

      原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。

      缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

          场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

 

HashMap原理

HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用 hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象。

HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象 的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的 键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。

当两个对象的hashcode相同会发生什么 碰撞探测(collision detection)以及碰撞的解决

因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。

如果两个键的hashcode相同,你如何获取值对象

找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。

如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置

为什么String, Interger这样的wrapper类适合作为键? 

String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final 的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算 hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不 可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象 的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的 hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。

我们可以使用自定义的对象作为键吗?

当然你可能使用任何对象作为键,只要它遵守了equals()和hashCode()方法的定义规则,并且当对象插入到Map中之 后将不会再改变了。如果这个自定义对象时不可变的,那么它已经满足了作为键的条件,因为当它创建之后就已经不能改变了。

我们可以使用CocurrentHashMap来代替Hashtable吗?

我们知道Hashtable是synchronized的,但是 ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替 HashTable,但是HashTable提供更强的线程安全性。看看这篇博客查看Hashtable和ConcurrentHashMap的区别。

 

线程池

 

1、什么是线程池:

jdk1.5引入Executor线程池框架,通过它把任务的提交和执行进行解耦,只需要定义好任务,然后提交给线程池,而不用关心该任务是如何执行、被哪个线程执行,以及什么时候执行。

线程池的作用:

线程池作用就是限制系统中执行线程的数量

为什么要用线程池:

1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

一个线程池包括以下四个基本组成部分:
1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。

线程池的工作过程如下:

  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
    • 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    • 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
    • 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    • 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  4. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

Java线程池的工厂类:Executors类,

Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。

2.常见线程池

newFixedThreadPool()
说明:初始化一个指定线程数的线程池,其中corePoolSize == maxiPoolSize,使用LinkedBlockingQuene作为阻塞队列
特点:即使当线程池没有可执行任务时,也不会释放线程。
newCachedThreadPool()
说明:初始化一个可以缓存线程的线程池,默认缓存60s,线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作为阻塞队列;
特点:在没有任务执行时,当线程的空闲时间超过keepAliveTime,会自动释放线程资源;当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销;
因此,使用时要注意控制并发的任务数,防止因创建大量的线程导致而降低性能。
newSingleThreadExecutor()
说明:初始化只有一个线程的线程池,内部使用LinkedBlockingQueue作为阻塞队列。
特点:如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行
newScheduledThreadPool()
特定:初始化的线程池可以在指定的时间内周期性的执行所提交的任务,在实际的业务场景中可以使用该线程池定期的同步数据。

总结:除了newScheduledThreadPool的内部实现特殊一点之外,其它线程池内部都是基于ThreadPoolExecutor类(Executor的子类)实现的。

 

“线程池中的coreNum和maxNum有什么不同”

corePoolSize:

核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;

maximumPoolSize:

线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;

keepAliveTime:

表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;

 

corePoolSize,maximumPoolSize,workQueue之间关系

1.当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程

2.当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行

3.当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务

4.当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理

5.当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程

6.当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭

Java中线程池的实现原理-求职必备

深入分析java线程池的实现原理

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324866681&siteId=291194637