JAVA面试题总结||——集合与线程

一.集合:

  1.1接口继承关系和实现:

        集合存放于Java.util包中,主要有3种: set无序集合、list有序列表集合、map键值对映射。

        1.Collection: Collection是集合List、Set、Queue的最基本的接口。

        2.Iterator:迭代器,可以通过迭代器遍历集合中的数据。

        3.Map:是映射表里的基础接口。

b3ff7a3864b9465fbb8acbb0fbdb6684.png

 

8fe584e3ca3f4287b34aade9ccee2c35.png

  1.2 List

       Java的List是非常常用的数据类型。List是有序的集合。List一共三个实现类:分别是ArrayList、Vector、LinkedList。

       1.2.1 ArrayList(数组)

           ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隙,当数组大小不满足时需要增加存储能力,就要将已有数组的数据复制到新的存储空间中。当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动,代价高。因此,它适合随机查找和遍历,不适合插入和删除。

        1.2.2 Vector

            Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起不一致,但是实现同步需要很高花费,因此访问它比访问ArrayList慢。

        1.2.3 LinkedList (链表)

            LinkedList是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,它还提供了List接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。

   1.3 Set            

       Set注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素,值不能重复。对象的相等性本质是对象hashcode值来判断的,如果想让两个不同的对象视为相等的,就必须覆盖对象的hashcode方法和equal方法。

       1.3.1 Hashset (Hash表)

           哈希表边存放的是哈希值。HashSet存储元素的顺序并不是按照存入时的顺序,而是按照哈希值来存的,所以取数据也是一样。元素的哈希值是提供元素的hashcode方法来获取的,HashSet首先判断两个元素的哈希值,如果值一样,接着会比较equal方法,如果equal方法结果为true,HashSet就视为同一个元素。如果结果为false则就不是同一个元素。

        1.3.2 TreeSet (二叉树)

           1.TreeSet是使用二叉树的原理对add()的对象按照指定顺序排序,每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。

           2.Integer和String对象都可以进行默认的TreeSet排序,而自定义类的对象是不可以的,自己定义的类必须实现Comparable接口,并且覆写相对应的compareTo函数,才可以正常使用。

           3.在覆写compare函数时,要返回相应的值才能使TreeSet按照一定的规则来排序。

           4.比较此对象与指定对象的顺序。如果该对象小于、等于或者大于指定对象,则分别返回负整数、零或正整数。

       1.3.3 LinkedHashSet (HashSet +LinkedHashMap)

          对于LinkedHashSet而言,它继承HashSet,又基于LinkedHashMap来实现的。它底层使用LinkedHashMap来保存所以元素,它继承与HashSet,其所有的方法操作上又与HashSet相同,因此它的实现上非常简单,只提供四个构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个LinkedHashMap来实现,在相关操作上与父类Hash Set的操作相同,直接调用父类HashSet的方法即可。

   1.4 Map

       1.4.1 HashMap (数组+链表+红黑树)

           HashMap根据键的hashCode值存储数据,大多数情况下可以直接定位到他的值,因而具有很快的访问速度,但遍历顺序却是不确定的。HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

           HashMap的存储流程:<k,v>键值对封装存储在链表的节点中,节点分为Entry和链接两部分,Entry存储键值对,链接存储下一个节点的引用。通过hashCode()求得键k的哈希值,通过哈希值计算得到数组中的下标,最终将存储键值对的节点链接到数组中,如果已存在相同键,则更改节点的值。(哈希值是整数int类型) 

ea5f7586d9144fce92a6a5b9ca126980.png 

           其中存在的问题是:

              1、不同的对象,通过hashCode()求得的哈希值可能相同。本质上hashCoed()是将输入转变为数字,按固定方式计算输出相应哈希值的函数。由于int类型有范围限制,输入没有范围限制,因此会存在多个输入对应一个输出的情况,也就是不同的输入有相同的哈希值。
总结就是,存在不同的输入有相同哈希值的情况。也就是,存在多个输入对一个哈希值的情况。
            2、在HashMap结构中,哈希值与数组的下标相对应。哈希值的范围远大于数组的范围,因此存在多个哈希值对一个数组下标的情况。

          解决问题的方法有:

拉链法/链地址法。拉链法是在数组中存储链表,将具有相同散列地址的键值对,通过一条同义词链表存储。散列地址与内存中的地址完全不同,散列地址是数组中的下标,内存中的地址是物理内存中的地址。

a592b1ced9cd4b8faf720ad694d94736.png 

         其中要用到两个重要的方法:hashCode()和equals()

hashCode()的作用是:求得哈希值。
equals()的作用是:比较两对象的值是否相等。(“==”比较的是地址是否相同,不是地址中存储的值是否相同。Objects类中定义的equals方法任然是通过“==”比较地址,在具体的类中通常需要重写equals方法)
通过hashCode()得到HashMap中内部数组的下标,在同义词链表中通过equals()方法找到键k存储的节点,如果没有已存在的节点,则增加新节点。
在键k所属的类中重写equals()方式时,需要保证该类中的hashCode()方法得到相同的哈希值。这样才能保证equals()判断相等的两个对象,找到同一个同义词链表。

         1.4.1.1 JAVA7 实现

             Java7 HashMap结构:大方向上,HashMap里面是一个数组,然后数组中每个每个元素都是一个单向链表。链表中结构会有一个Entry的实例,Entry包含四个属性:key,value,hash值和用于单向链表的next。

0c5035b975a74d5fa612f07b842c8349.png 

           1. capacity:当前数组容量,始终保持2^n,可以扩容,扩容后数组大小为当前的2倍。

           2. load Factor :负载因子,默认为0.75。

           3. threshold:扩容的阀值等于capacity*loadFactor。

        1.4.1.2 JAVA8 实现

             Java8对HashMap进行了一些修改,最大的不同就是利用红黑树,所以它由数组+链表+红黑树组成。

             根据Java7 HashMap的情况,在查找的时候,根据hash值可以快速定位到数组的具体下标,但是之后需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为O(n)。为了降低性能开销,在Java8中,当链表中的元素超过8个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为O(logN)。

f8a28c594800489c9b4a7a5526a75c69.png  

       1.4.2 ConcurrentHashMap

          JDK 1.7

            ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成。

            主要实现原理是实现了锁分离的思路,采用分段锁的机制,实现并发的更新操作。

            底层采用数组+链表的存储结构,包括两个核心静态内部类Segment和HashEntrySegment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术。每一个Segment元素存储的是HashEntry数组+链表(若干个桶),这个和HashMap的数据存储结构一样。

            HashEntry用来封装映射表的键值对,每个桶是由若干个HashEntry对象链接起来的链表。

           JDK 1.8

             取消Segment类,直接用table数组存储键值对。

             采用Node + CAS + Synchronized来保证并发安全。

             Node数据结构很简单,就是一个链表,但是只允许对数据进行查找,不允许进行修改

             CAS:关注于三个量,内存的地址 V 期望(你认为)的原值 A 新值B

             当HashEntry对象组成的链表长度超过8时,或数组长度小于64 就会扩容,否则链表转换为红黑树, 提升性能。底层变更为数组+链表+红黑树。

          1.4.2.1 Segment段:ConcurrentHashMap和HashMap的思路是差不多的,但是因为它支持并发操作,所以要复杂一些。整个Concurrent HashMap由一个个Segment组成,Segment代表“部分”或“一段”的意思,所以很多地方将其描述为段锁。

          1.4.2.2 线程安全 (Segment继承ReentrantLock加锁):ConcurrentHashMap是一个Segment数组,Segment通过继承ReentrantLock来进行加锁,所以每次需要加锁的操作锁住的是一个segment,这样只要保证每个Segment是线程安全的,也就实现了全局的线程安全。

    Java7 ConcurrentHashMap的结构

13d93eb0e4494d62b590b0a086089e19.png

         1.4.2.3 并行度(默认16) :ConcurrentHashMap有16个Segments,所以理论上最多可以同时支持16个线程并发写,只要它们的操作分别分布在不同的Segment上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,他是不可以扩容的。再具体到每个Segment内部,其实每个Segment很像之前介绍的HashMap,不过它要保证线程安全,所以处理起来要麻烦些。

         1.4.2.4 Java8 实现(引入红黑树):

            Java8 ConcurrentHashMap的结构

2e898e6cb5a34114b4a2b48ef5edaf30.png

      1.4.3 HashTable(线程安全) 

          HashTable是遗留类,很多映射的常用功能与HashMap类似,不同的的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为它引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场所可以用HashMap替换,需要线程安全的场所可以用ConcurrentHshMap替换。

        1.4.4 TreeMap (可排序)

            TreeMap实现sortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历Tree Map时,得到的记录时排过序的。

             如果使用排序的映射,建议使用TreeMap。

  

二.JAVA多线程并发:

       2.1.1 JAVA 并发知识库

95b0bea3f2e64ac4861f3d30ac0b7489.png

          2.1.2 JAVA线程实现/创建方式

                 2.1.2.1 继承Thread类

                     Thread类本质上是实现Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。start方法是一个native方法,它将启动一个新线程,并执行run()方法。

                  2.1.2.2 实现Runnable接口

                       如果自己的类已经继承了其他类,就无法继承Thread类,可以实现Runnable接口。

                       1)首先需实例化一个Thread类,并传入自己的实例对象

                       2)通过thread.start方法启动线程

                       3)当传入目标参数后,Thread的run方法就会内部的run方法执行线程运行

                   2.1.2.3 ExecutorService、Callable<Class>、Future有返回值线程

                        有返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了,再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程。

                  //创建一个线程池

                  ExecutorService pool = Excutors.newFixed ThreadPool(tasksize);

                  //创建多个有返回值的任务

                  List<Future> list = newArrayList<Future>();

                  for(int i=0; i <tasksize;i++;){

                        Callable c= new MyCallable(i+ “”);

                        //执行任务并获取Future对象

                        Future f = pool.submit(c);

                        list.add(f);
                  }

                        //关闭线程池

                       pool.shutdown();

                       //获取所有并发任务的运行结果

                       for(Future f: list){

                           syso(“res" +f.get().tostring ());

                        }
       
                2.1.2.4 基于线程池的方式

                     线程和数据库连接,这些资源都是非常宝贵的资源。那么每次在需要的时候才创建,不需要的时候销毁,是非常浪费资源的。这个时候就可以使用缓存的策略,也就是使用线程池。

                       1)通过Excutors.newFixedThreadPool创建线程

                       2)执行ThreadPool.execute方法

          2.1.3 四种线程池

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

               2.1.3.1 newCachedThreadPool

                    创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用execute将重用以前构造的线程。如果现有线程没有可用的,则创建一个新线程添加到线程池中。终止并从缓存中移除那些已有60秒钟未被使用的线程。因此长时间保持空闲的线程池不会使用任何资源。

                2.1.3.2  newFixThreadPool

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

                2.1.3.3 newScheduledThreadPool

                    创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

                    ScheduledExecutorService   scheduledThreadPool = Executors.newScheduledThead Pool(3);

                    执行schedule方法或者scheduleAtFixedRate方法

                2.1.3.4  newSingleThreadExecutor

                     Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可用以在线程死后或者发生异常时,重新启用一个线程来替代原来的线程继续执行下去。

           2.1.4  线程生命周期

                 当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。当线程的生命周期中,它要经过新建、就绪、运行、阻塞和死亡5种状态。尤其是当线程启动以后,它不可能一直霸占着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。

               2.1.4.1 新建状态

                    当程序使用new关键字创建一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值

                2.1.4.2 就绪状态

                     当线程对象调用了star()方法之后,该线程处于就绪状态。Java虚拟机为其创建方法调用栈和程序计数器,等待调度运行。

                 2.1.4.3 运行状态

                     如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。

                  2.1.4.4 阻塞状态

                       阻塞状态是指线程因为某种原因放弃了cpu使用权,即让出了cpu timeslice,暂时停止运行。直到线程进入可运行状态,才有机会再次获得cpu timeslice转到运行状态。

                       阻塞情况分三种:

                       1)等待阻塞(o.wait—>等待队列) :运行的线程执行o.wait()方法,JVM会把该线程放入等待队列中。

                       2)同步阻塞(lock—>锁池):运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。

                       3)其他阻塞:运行的线程执行Thread.sleep方法或者t.join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、jion()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行状态。

                  2.1.4.5 线程死亡

                       线程会以下面三种方式结束,结束后就是死亡状态。

                            正常结束: run()或者call()方法执行完成,线程正常结束。

                            异常结束: 线程抛出一个未捕获的Exception或者Error。

                            调用stop: 直接调用该线程的stop()方法来结束该线程—该方法通常容易导致死锁,慎用。

         ed39245405874c379931856d949b2657.png

          2.1.5 终止线程4种方式

                 2.1.5.1 正常运行结束

                      程序运行结束,线程自动结束。

                 2.1.5.2 使用退出标志退出线程

                      一般run方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。他们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如最直接的方法就是设一个bolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出。

                   2.1.5.3 Interrupt 方法结束线程

                        使用interrupt方法来中断线程有两种情况:

                         1. 线程处于阻塞状态:如果使用sleep,同步锁的wait,socket中的receiver,accept等方法时,会使线程处于阻塞状态。当调用线程的interrupt方法时,会抛出InerrruputException异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后break跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用interrupt方法线程就会结束,实际上是错的,一定要先捕获InterruptException异常之后通过break来跳出循环,才能正常结束run方法。

                         2. 线程未处于阻塞状态:使用isInterruput判断线程的中断标志来退出循环。当使用interrupt方法时,中断标志就会设置true,和使用自定义标志来控制循环是一样的。

                    2.1.5.4 stop方法终止线程(线程不安全)

                          程序中可以直接使用thread.stop()方法来强行终止线程,但是stop方法是很危险的,就像突然关掉计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是thread.stop方法调用后,创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop后导致了该线程所持有的所有锁的突然释放,那么被保护数据就有可能呈现不一致,其他线程在使用这些被破坏的数据时,有可能导致导致一些很奇怪的应用程序错误。因此,不推荐使用stop方法来终止线程。

                2.1.6 sleep与wait方法的区别

                      1.对于sleep方法,我们首先要知道该方法是属于Thread类中的,而wait方法是属于Object类中。

                      2.sleep方法导致了程序暂停执行指定的时间,让出CPU执行其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态。

                      3.在调用sleep方法的过程中,线程不会释放对象锁。

                      4.而当调用wait方法的时候,线程会放弃对象锁,进入等待此对象的等待线程池,只有只有针对此对象调用notify方法后本线程才进入对象锁定池准备获取对象锁进行运行状态。

                2.1.7 start与run方法的区别

                       1.start方法是用来启动线程,真正实现了多线程的运行。这时无需等待run方法体代码执行完闭,可以直接继续执行下面的代码。

                       2. 通过调用Thread类的start方法来启动一个线程,这时此线程是处于就绪状态,并没有运行。

                       3.方法run成为线程体,,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行run函数当中的代码。Run方法运行结束,此线程终止。然后CPU再调度其他线程。

                  2.1.8 JAVA后台线程

                         1.定义:守护线程--也称“服务线程”,他是后台线程,它有一个特性,即为用户线程 提供 公共服务,在没有用户线程可服务时会自动离开。
                         2.优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
                         3.设置:通过setDaemon(true)来设置线程为“守护线程”;将一个用户线程设置为守护线程的方式是在线程对象创建之前用线程对象的setDaemon方法。
                         4.在Daemon线程中产生的新线程也是Daemon的。
                         5.线程则是JVM级别的,以Tomcat为例,如果你在Web应用中启动一个线程,这个线程的生命周期并不会和Web应用程序保持同步。也就是说,即使你停止了Web应用,这个线程依旧是活跃的。
                         6.example:垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
                          7.生命周期:守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;如果还有一个或以上的非守护线程则JVM不会退出。

                   2.1.9.JAVA锁

                          2.1.9.1.乐观锁
                            乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
                              java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
                          2.1.9.2.悲观锁
                              悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。

                           2.1.9.3 自旋锁

                               自旋锁原理非常简单,如果持有锁的线程能在很短的时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户之间的切换进入阻塞挂起状态,它们只需要等一等,等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

                               线程自旋是需要消耗cup的,说白了就是让cup在做无用功,如果一直获取不到锁,那线程也不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。

                               如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,就会导致其他争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

                         自旋锁的优缺点

                             自旋锁尽可能的减少线程阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!

                             但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时侯就不适合使用自旋锁了,因为自旋锁在获取锁前一直占用CPU做无用功,占着锁而不使用,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其他需要cup的线程又不能获取到CPU,造成CPU的浪费,所以这种情况我们要关闭自旋锁。

                         自旋锁时间阀值

                             自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢,如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能,因此自旋的周期选的额外重要!

                      2.1.9.4  Synchronized 同步锁

                           synchronized 它可以把任意一个非NULL的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁。

                       Synchronized 作用范围

                         1. 作用于方法时,锁住的是对象的实例;

                         2. 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen,永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;

                         3. synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

                       Synchronized 核心组件

                          1)Wait Set:那些调用wait方法被阻塞的线程放置在这里;

                          2)Contention List:竞争队列,所有请求锁的线程首先被放在竞争队列中;

                          3)Entry List:Contenion List中那些有资格成为候选资源的线程被移动到Entry List中;

                          4)OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被称为OnDeck;

                          5)Owner:当前已经获取到锁资源的线程被称为Owner;

                          6)!Owner: 当前释放锁的线程。

                      Synchronized 实现

                         1. JVM 每次从队列的尾部取出一个数据用于锁竞争候选者,但是并发..情况下,ContenionList会被大量的并发线程进行CAS访问,,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。

                         2. Owner 线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。

                         3. Owner 线程不不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,它需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。

                         4. OnDeck 线程获取到锁资源后会变成Owner线程,而没有等到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中。

                         5. 处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的。

                         6. Synchronized 是非公平锁。Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋锁获取锁的线程还可能直接抢占OnDeck线程的锁资源。

                          7. 每个对象都有个monitor对象,加锁就是在竞争monitor对象,代码块加锁是在前后分别加上monitorenter和monitorexit指令来实现的,方法加锁是通过一个标记位来判断的。

                         8. synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。

                         9. 锁可以从偏向锁升级到轻量级锁,再升级到重量级锁,这种升级过程叫做锁膨胀;

               2.1.9.5 ReentrantLock 

                       ReentantLock继承接口Lock并实现了接口中定义的方法,他是yz一种可重入锁,除了能完成synchronized所能完成的所有工作外,还提供了诸如可响应中断锁,可轮询锁请求,定时锁等避免多线程死锁的方法。

                   Lock 接口的主要方法

                       1.void lock0:执行此方法时,如果锁处于空闲状态,当前线程将获取到锁.相反,如果锁被其他线程持有,将禁用当前线程,直到当前线程获取到锁.
                       2. boolean tryLock():用,则获取锁,并立即返回true,否则返回false.该方法lock0的区别在于,tryLock()只是“试图”获取锁,如果锁不可用,不会导致当前线程被禁当前线程仍然继续往下执行代码.而lock()方法则是一定要获取到锁,如果锁不可用,就一直等待,在未获得锁之前,当前线程并不继续向下执行.
                        3. void unlock():执行此方法时,当前的锁.锁只能由持有者释放,如果线并不持有锁,却执行该方法,可能导致异常的发生.
                        4. Condition newCondition():条件对象,获取等待通知组件。该组件和当前的锁绑定当前线程只有获取了锁,才能调用该组件的 await()方法,而调用后,当前线程将缩放锁。
                        5.getHoldCount():查询当前线程保持此锁的次数,也就是执行此线程执行lock方法的次数。
                        6. getQueueLength():返回正等待获取此锁的线程估计数,比如启动10个线程,1个线程获得锁,此时返回的是9
                        7. getWaitQueueLength: (Condition condition)返回等待与此锁相关的给定条件的线程估计数。比如10个线程,用同一个condition 对象,并且此时这10个线程都执condition 对象的await方法,那么此时执行此方法返回10
                        8. hasWaiters(Condition condition):查询是否有线程等待与此锁有关的给定条件(condition),对于指定contidion对象,有多少线程执行了condition.await方法
                        9. hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁
                        10.  hasQueuedThreads():是否有线程等待此锁
                        11.   isFair()该锁是否公平锁

                        12.   isHeldByCurrentThread()当前线程是否保持锁锁定,线程的执行lock方法的前后分别是false和true

                        13.   isLock()此锁是否有任意线程占用

                        14.   lock Interruptibly()如果当前线程未被中断,获取锁

                        15.   tryLock()尝试获得锁,仅在调用时锁未被线程占用,获得锁

                        16.   tryLock(long timeout TimeUntie unit)如果锁在给定等待时间内没有被另一个线程保持,则获取该锁。

          非公平锁

               JVM按随机,就近原则分配锁的机制则称为不公平锁,ReentrantLock在构造函数中提供了是否公平锁的初始化方式,默认为非公平锁。非公平锁实际执行的效率要远超公平锁,除非程序有特殊需要,否则最常用非公平锁的分配机制。

          公平锁

               公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁,ReentrantLock在构造函数中提供了是否公平锁的初始化方式来定义公平锁。

          ReentrantLock 和 synchronized

              1. ReentrantLock通过方法lock和unlock来进行加锁与解锁操作,与synchronized会被JVM自动解锁机制不同,ReentrantLock加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用ReentrantLock必须在finally控制块中进行解锁操作。

              2. ReentrantLock相比synchronized的优势是可中断,公平锁,多个锁。

    ReentrantLock 实现

public class MyService {

    private Lock lock = new ReentrantLock();

    private Condition condition = lock.newCondition();

    private boolean hasValue = false;

    public void set() {
        try {
            lock.lock();
            while (hasValue == true) {
                condition.await();
            }
            System.out.println("打印 *");
            hasValue = true;
            condition.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void get() {
        try {
            lock.lock();
            while (hasValue == false) {
                condition.await();
            }
            System.out.println("打印 o");
            hasValue = false;
            condition.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

}

public class ThreadA extends Thread {

    private MyService myService;

    public ThreadA(MyService myService) {
        super();
        this.myService = myService;
    }

    @Override
    public void run() {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            myService.set();
        }
    }
}

public class ThreadB extends Thread {

    private MyService myService;

    public ThreadB(MyService myService) {
        super();
        this.myService = myService;
    }

    @Override
    public void run() {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            myService.get();
        }
    }
}

public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyService myService = new MyService();
        ThreadA a = new ThreadA(myService);
        a.setName("A");
        a.start();
        ThreadB b = new ThreadB(myService);
        b.setName("B");
        b.start();
    }
}

     Condition类和Object类锁方法区别:

            1. Condition类的await方法和Object类的wait方法等效。

            2. Condition类的signal方法和Object类的notify方法等效。

            3. Condition类的signalAll方法和Object类的notify All方法等效。

            4. ReentrantLock类可以唤醒指定条件的线程,而object的唤醒是随机的。

      tryLock 和 lock 和 lock Interruptibly 的区别

            1. tryLock 能获得锁就返回true,不能就立刻返回false,tryLock方法可以增加时间限制,如果超过该时间段还没获得锁,返回false

            2. lock能获得锁就返回true,不能的话一直等待获得锁

            3. lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程lock不会抛出异常,而lock Interruptibly会抛出异常。

      4.1.9.6  Semaphore 信号量

           Semaphore 是一种基于计数的信号量。它可以设定一个阀值,基于此,多个线程竞争获取许可信号,做完自己的申请后归还,超过阀值后,线程申请许可信号将会被阻塞。Semaphore 可以用来构建一些对象池,资源池之类的,比如数据库连接池。

       实现互斥锁 (计数器为1)

       我们也可以创建计数为1的Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。

        Semaphore 与ReentrantLock

           Semaphore 基本能完成ReentrantLock 的所有工作,使用方法也与之类似,通过acquire与release方法来获得和释放临界资源。经实测,Semaphore.acquire方法默认为可响应中断锁,与ReentrantLock.lock Interruptibly作用效果一致,也就是说在等待临界资源的过程中可以被Thread.interrupt方法中断。

           此外,Semaphore也实现了可轮询的锁请求与定时锁的功能,除了方法名try Acquire与tryLock不同,其使用方法与ReentrantLock几乎一致。Semaphore也提供了公平与非公平锁的机制,也可在构造函数中进行设定。

           Semaphore 的锁释放操作也由手动进行,因此与ReentrantLock一样,为了避免线程因抛出异常而无法正常释放锁的情况发生,释放锁的操作也必须在finally代码块中完成。

       4.1.9.7 AtomicInteger

            首先说明,此处AtomicInteger,一个提供原子操作的Interger的类,常见的还有AtomicBoolean,AtomicInterger,等他们的实现原理相同,区别在与运算对象类型的不同。令人兴奋地,还可以通过Atomic Reference将一个对象的所有操作转化成原子操作。

        4.1.9.8 可重入锁(递归锁)

             本文讲的是广义上的可重入锁,而不是单指JAVA下的ReentrantLock。可重入锁,也叫递归锁,指的是同一线程,外层函数获得锁之后,内层递归函数仍然有获取该锁的代码,但不受影响。在JAVA环境下ReentrantLock和synchronized都是可重入锁。

         4.1.9.9 公平锁与非公平锁

              公平锁:加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得

              非公平锁:加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待

               1. 非公平锁性能比公平锁高5~10倍,因为公平锁需要在多核的情况下维护一个队列

               2. Java中的synchronized是非公平锁,ReentrantLock默认的lock方法采用的是非公平锁

          4.1.9.10 ReadWriteLock 读写锁

              为了提高性能,Java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,你只要上好相应的锁即可。

              读锁:如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁

               写锁:如果你的代码修改数据,只能有一个人在写,且不能同时在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!

              4.1.9.11  共享锁和独占锁

                  Java并发包提供的加锁模式分为独占锁和共享锁

                  独占锁:独占锁模式下,每次只能有一个线程持有锁,ReentrantLock 就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/写冲突,如果莫个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。

                  共享锁:共享锁则允许多个线程同时获取锁,并发访问共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。

                  1. AQS的内部类Node定义了两个常量SHARED和EXCLUSIVE,他们分别标识AQS队列中等待线程的锁获取模式。

                  2. Java的并发包中提供了ReadWriteLock,读写锁。它允许一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。

               4.1.9.12  重量级锁

                   Synchronized 是通过对象内部的一个叫做监视器锁来实现的。但是监视器锁本质又是依赖于底层的操作系统Mutex Lock来实现的。而操作系统实现线程之间的切换就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized的效率低。因此,这种依赖于操作系统所实现的锁我们称之为“重量级锁”。

                4.1.9.13  轻量级锁

                     锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。

                     锁升级:随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

                   4.1.9.14 偏向锁

                       大多情况下锁的不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在莫个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了便护。引入偏向锁的目的是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖CAS原子指令(由于多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。

                    4.1.9.15  分段锁

                         分段锁也并非一种实际的锁,而是一种思想ConcurrentHashMap 是学习分段锁的最好实践。

                     4.1.9.16 锁优化

                          减少锁持有的时间:只用在有线程安全要求的程序上加锁

                          减少锁粒度:将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低了锁竞争,偏向锁,轻量级锁成功率才会提高。最典型的减小锁粒度的案例就是ConcurrentHashMap。

                          锁分离:最常见的锁分离就是读写锁Read WriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能。读写分离思想可以延伸,只要操作互不影响,锁就可以分离。比如Linked BlockingQueue从头部取出,从尾部放数据。

                          锁粗化:通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用公共资源后,应该立即释放锁。但是,凡事都有一个度,如果对同一个锁不停的进行请求,同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化。

                           锁消除:锁消除是在编译器级别的事情。在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。

              4.1.10  线程的基本方法

                      线程相关的基本方法有wait,notify,notifyAll,sleep,join,yield等

2bcc6e1b05cb40b491018fcc4d81f84b.png

              4.1.10.1  线程等待(wait) 

                   调用该方法的线程进入WAITING状态,只有等待另外线程的通知或者被中断才会返回,需要注意的是调用wait()方法后,会释放对象的锁。因此,wait方法一般用在同步方法或者同步代码块中。

              4.1.10.2  线程睡眠(sleep)

                    sleep导致当前线程休眠,与wait方法不同的是sleep不会释放当前占有的锁,sleep会导致线程进入TIMED- WATING状态,而wait()方法会导致当前线程进入WATING状态。

               4.1.10.3   线程让步(yield)

                     yield会使当前线程让出CPU执行时间片,与其他线程一起重新竞争CPU时间片。一般情况下,优先级高的线程会有更大的可能性成功竞争得到CPU时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。

                4.1.10.4  线程中断(interrupt)

                      中断一个线程,基本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。

                     1. 调用interrupt方法并不会中断一个正在运行的线程。也就是说处于Running状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。

                     2. 若调用sleep而使线程处于TIMED- WATING状态,这时调用interrupt方法,会抛出Interruputed Exception,从而使线程提前结束TIMED- WATING状态。

                     3. 许多声明抛出InterruptedException的方法,抛出异常前,都会清除中断标识位,所以抛出异常后,调用isInterruptd方法将会返回false。

                     4. 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程thread的时候,可以调用thread.interrupt方法,在线程的run方法内部可以根据thread.isInterrupted方法的值来优雅的终止线程。

                 4.1.10.5 Join等待其他线程终止

                       join方法,等待其他线程终止,在当前线程中调用一个线程的join方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待cpu的宠幸。

                 4.1.10.6 为什么要用join方法

                      很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要在子线程结束后再结束,这个时候就要用到join方法。

                  4.1.10.7 线程唤醒 (notify)

                       Object类中的notify方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个wait方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有notifyAll,唤醒再次监视器上等待的所有线程。

                   4.1.10.8 其他方法

                        1. sleep(): 强迫一个线程睡眠N豪秒。

                        2. isAlive(): 判断一个线程是否存活。

                        3. join(): 等待线程终止。

                        4.activeCount(): 程序中活跃的线程数。

                        5. enumerate(): 枚举程序中的线程。

                        6. currentThread(): 得到当前线程。

                        7. isDaemon(): 一个线程是否为守护线程。

                        8. setDaemon(): 设置一个线程为守护线程。

                        9. setName(): 为线程设置一个名称。

                        10. wait(): 强迫一个线程等待。

                        11. notify(): 通知一个线程继续运行。

                        12. setPriority(): 设置一个线程的优先级。

                        13. get Priority(): 获得一个线程的优先级。

              4.1.11. 线程上下文切换

                      巧妙地利用了时间片轮转的方式,CPU给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一个任务的状态后,继续服务下一个任务,任务的状态保存及再加载,这段过程就叫做上下文切换。时间片轮转的方式使多个任务在同一颗CPU上执行变成了可能。

                    4.1.11.1 进程

                         是指一个程序运行的实例。在Linux系统中,线程就是能并行运行并且与他们的父进程共享同一个地址空间和其他资源的轻量级的进程。

                    4.1.11.2 上下文

                         是指某一时间点CPU寄存器和程序计数器的内容。

                    4.1.11.3 寄存器

                         是CPU内部的数量较少但是速度很快的内存(与之对应的是CPU外部相对较慢的RAM主内存)寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。

                    4.1.11.4 程序计数器

                        是一个专用的寄存器,用于表明指令序列中CPU正在执行的的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。

                    4.1.11.5 PCB—“切换帧”

                         上下文切换可以认为是内核在CPU上对于进程(包括线程)进行切换,上下文切换过程中的信息是保存在进程控制块中的。PCB还经常被称作“切换帧”。信息会一直保存到CPU的内存中,直到他们被再次使用。

                     4.1.11.6 上下文切换的活动

                          1.挂起一个进程,将这个进程在CPU中的状态存储于内存中的某处。

                          2.在内存中检索下一个进程的上下文并将其在CPU的寄存器中恢复。

                          3.跳转到程序计数器所指向的位置,以恢复该进程在程序中。

                      4.1.11.7 引起线程上下文切换的原因

                           1. 当前执行任务的时间片用完之后,系统CPU正常调度下一个任务;

                           2.当前执行任务碰到IO阻塞,调度器将此任务挂起,继续下一个任务;

                           3.多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一个任务;

                           4. 用户代码挂起当前任务,让出CPU时间;

                           5. 硬件中断;

                4.1.12 同步锁与死锁 

                        4.1.12.1 同步锁

                             当多个线程同时访问同一个数据时,很容易出现问题,为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。Java中可以使用synchronized关键字来取得一个对象的同步锁。

                        4.1.12.2 死锁

                            何为死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。

                 4.1.13 线程池原理

                         线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。他的主要特点为:线程复用;控制最大并发数;管理线程。

                        4.1.13.1 线程复用

                            每一个thread类都有一个start方法。当调用start启动线程时Java虚拟机会调用该类的run方法。那么该类的run方法中就是调用了Runable对象的run方法。我们可以继承重写Thread类,在其start方法中添加不断循环调用传递过来的Runnable对象。这就是线程池的实现原理。循环方法中不断获取Runable是用Queue实现的,在获取下一个Runable之前可以是阻塞的。

                         4.1.13.2 线程池的组成

                             一般的线程池主要分为以下四个组成部分:

                                 1. 线程池管理器:用于创建并管理线程池

                                 2. 工作线程:线程池中的线程

                                 3. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行

                                 4. 任务队列:用于存放待处理的任务,提供一种缓冲机制

                            Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor,Callable和Future,FutureTask这几类。

                             1. corePoolSize:指定了线程池中的线程数量。

                             2. maximumPoolSize:指定了线程池中的最大线程数量。

                             3. keepAliveTime:当前线程池数量超过corePoolSize时,多余的空闲线程的存活时间,即多次时间内会被销毁。

                             4. unit:keepAliveTime的单位。

                             5. workQueue:任务队列,被提交但尚未被执行的任务。

                             6. threadFactory:线程工厂,用于创建线程,一般用默认的即可。

                             7. handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。

                 4.1.13.3 拒绝策略

                      线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。

                      JDK内置的拒绝策略如下:

                       1. AbortPolicy:直接抛出异常,阻止系统正常运行。

                       2. CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。

                       3. DiscardOldestPolicy:丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。

                        4. DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。

                  4.1.13.4 Java线程池工作过程

                      1. 线程刚创建时,里面没有一个线程。任务队列是作为参数传过来的。不过,就算队列里面有任务,线程池也不会马上执行它们。

                     2. 当调用execute方法添加一个任务时,线程池会做如下判断:

                         a)如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务。

                         b)如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列。

                         c)如果这个时候队列满了,而且正在运行的线程数量小于或等于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;

                         d)如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会抛出异常RejectExecutionException。

                      3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。

                      4. 当一个线程无事可做,超过一定的时间时,线程池会判断,如果当前运行的线程大于core PoolSize,那么这个线程就会被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

             4.1.14 Java阻塞队列原理

                    阻塞队列,关键字是阻塞,先理解阻塞的含义,在阻塞队列中,线程阻塞有这样的两种情况:

                        1. 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞,直到有数据放入队列。

                        2. 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞,直到队列中有空的位置,线程被自动唤醒。

                    4.1.14.1 阻塞队列的主要方法

                        1. 抛出异常:add,remove,element

                        2. 返回一个结果但抛出异常:offer,poll,peek

                        3. 阻塞:put,take

                        4. 超时:offer(e time unit),poll(time unit)

                     插入操作:

                        1. public abstract boolean add(E param E): 将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功时返回true,如果当前没有可用的空间,则抛出illegalStateException。如果该元素是NULL,则抛出空指针异常。

                        2. public abstract boolean offer(E param E): 将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功时返回true,如果当前没有可用的空间,则返回false。

                        3.  public abstract boolean offer(E param E) throws InterruputedException将指定元素插入此队列中,将等待可用的空间。

                        4. offer(E o, long timeout, Timeunit unit ): 可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。

                     获取数据操作:

                         1. poll(time): 取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null;

                         2. poll(Long timeout, TimeUnit unit): 从BlockingQueue 取出一个队首的对象,如果在指定的时间内,队列一旦有数据可取,则立即返回队列中的数据。否则直到时间超时还没有数据可取,返回失败。

                         3. take(): 取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新数据被加入。

                         4. drainTo(): 一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据的效率;不需要多次分批加锁或释放锁。

                   4.1.14.2  Java中的阻塞队列

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

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

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

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

                       5. SynchronousQueue:不存储元素的阻塞队列。

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

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

                   4.1.14.3 ArraryBlockingQueue (公平、非公平)

                      用数组实现的有界阻塞队列。此队列按照先进先出的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或者消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里面插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。

                    4.1.14.4 LinkedBlockingQueue (两个独立锁提高并发)

                         基于链表的阻塞队列,同ArrayListBlockingQueue类似,此队列按照先进先出的原则对元素进行排序。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行操作队列中的数据,以此来提高整个队列的并发性能。

                    4.1.14.5 Linked Transfer Queue

                         是一个由链表结构组成的无界阻塞队列TransferQueue队列。相对于其他阻塞队列,Linked TransferQueue多了tryTransfer和transfer方法。

                         1. transfer方法:如果当前有消费者正在等待接收元素,transfer方法可以把生产者传入的元素立刻transfer给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回。

                         2. tryTransfer方法。则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素。则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回。而transfer方法是必须等到消费者消费了才返回。

                   对于带有时间限制的tryTransfer()方法,则是试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没有消费元素,则返回false,如果在超时时间内消费了元素,则返回true。

             4.1.15 volatile 关键字的作用(变量可见性、禁止重排序)

                 Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。volatile变量具备两种特性,volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

                 变量可见性:其一是保证该变量对所有线程可见,这里的可见性指的是当一个线程修改了变量的值,那么新的值对于其他线程是可以立刻获取的。

                 禁止重排序:volatile禁止了指令重排。

                 比synchronized更轻量级的同步锁

                  在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。volatile适合这种场景:一个变量被多个线程共享,线程直接给这个变量赋值。

                   当对非volatile变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的CPU cache中。而声明变量是volatile的,JVM保证了每次读写变量都从内存中读,跳过CPU cache这一步。

                   适应场景

                   值得说明的是对volatile变量的单次读/写操作可以保证原子性的,如long和double类型的变量,但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。在某些场景下可以代替synchronized。但是volatile不能完全取代synchronized的位置,只有在一些特殊的场景下,才能适用volatile。总的来说,必须同时满足下面两个条件才能保证在并发的情况下线程安全:

                   (1)对变量的写操作不依赖于当前值(比如i++),或者说是单纯的变量赋值。

                   (2)该变量没有包含在具有其他变量的不变式中,也就是说,不同的volatile变量之间,不能互相依赖。只有在状态真正独立于程序内其他内容时才能使用volatile。

             4.1.17 如何在两个线程之间共享数据

                    Java里面进行多线程通信的主要方式就是共享内存的方式,共享内存主要的关注点有两个:可见性和有序性原子性。Java内存模型JMM解决了可见性和有序性的问题,而锁解决了原子性的问题,理想情况下我们希望做到“同步”和“互斥”。有以下常规实现方法:

               将数据抽象成一个类,并将数据的操作作为这个类的方法

              1.将数据抽象成一个类,并将对这个数据的操作作为这个类的方法,这个设计可以很容易做到同步,只要在方法上加“synchronized”。

               Run对象作为一个类的内部类

               2.将Runable对象作为一个类的内部类,共享数据作为这个类的成员变量,每个线程对共享数据的操作也封装在外部类,以便实现对数据的各个操作的同步和互斥,作为内部类的各个Runable对象调用外部类的这些方法。

            4.1.18 ThreadLocal

                ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

                ThreadLocalMap(线程的一个属性)

                 1.每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。

                 2.将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocal Map中,然后在线程执行的各处通过这个静态ThreadLocal实例的get方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。

                 3. ThreadLocalMap其实就是线程里面的一个属性,它在Thread类中定义

                         ThreadeLocal.ThreadLocalMap thraedLocals = null;

                       常用的场景为用来解决数据库连接、Session管理等;

              4.1.19 synchronized和ReentrantLocal的区别

                    4.1.19.1 两者的共同点:

                       1. 都是用来协调多线程对共享对象、变量的访问

                       2. 都是可重入锁,同一个线程可以多次获得同一个锁

                       3. 都保证了可见性和互斥性

                    4.1.19.2 两者的不同点:

                        1. ReentrantLock显示的获得、释放锁,synchronized隐式获得释放锁

                        2. ReentrantLock可响应中断、可轮回,synchronized是不可以响应中断的,为处理锁的不可用性提供了更高的灵活性

                        3. ReentrantLock是API级别的,synchronized是JVM级别的

                        4. ReentrantLock可以实现公平锁

                        5. ReentrantLock通过Condition可以绑定多个条件

                        6. 底层实现不一样,synchronized是同步阻塞,使用的是悲观并发策略,lock是同步非阻塞,采用的是乐观并发策略

                        7. Lock是一个接口,而synchronized是Java的关键字,synchronized是内置的语言实现

                        8. synchronized在发生异常时,会自动释放线程占有的锁,因此不会出现导致死锁发生;

而Lock在发生异常时,如果没有主动通过unlock方法去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁。

                        9. Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断

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

                        11. Lock可以提高多个线程进行读操作的效率,既就是实现读写锁等。

                4.1.20 ConcurrentHashMap 并发

                       4.1.20.1 减小颗粒度

                        减小颗粒度是指缩小锁定对象的范围,从而减小锁冲突的可能性,从而提高系统的并发能力。减少颗粒度是一种消弱多线程锁竞争的有效手段,这种技术典型的应用是ConcurrentHashMap类的实现。对于HashMap而言,最重要的两个方法是get与set方法,如果对整个HashMap加锁,可以得到线程安全的对象,但是加锁粒度太大。Segment的大小也被称为ConcurrentHashMap的并发度。

                       4.1.20.2 ConcurrentHashMap 分段锁

                         ConcurrentHashMap,它内部细分了若干个小的hashmap,称之为段。默认一个ConcurrentHashMap被进一步细分为16个段,既就是锁的并发度。

                         如果需要在ConcurrentHashMap中添加一个新的表项,并不是将整个HashMap加锁,而是首先根据hashCode得到该表项应该存放在哪个段中,然后对该段进行加锁,并完成put操作。在多线程环境中,如果多个线程同时进行put操作,只要被加入的表项不存放在同一个段中,线程间可以做到真正的并行。

                          ConcurrentHashMap是由Segment数组结构和hashEntry数组结构组成

                 4.1.21 Java中用到的线程调度

                        4.1.21.1 抢占式调度:

                         抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。

                        4.1.21.2 协同式调度:

                          协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的路线就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就堵塞,那么可能导致整个系统崩溃。

                         4.1.21.3 线程让出CPU的情况:

                           1.当前运行线程主动放弃CPU,JVM暂时放弃CPU操作(基于时间片轮转调度的JVM操作系统不会让线程永久放弃CPU,或者说放弃本次时间片的执行权),例如调用yield方法。

                           2. 当前运行线程因为某些原因进入阻塞状态,例如阻塞在I/O上。

                           3. 当前运行线程结束,即运行完run方法里面的任务。

                  4.1.22 什么是CAS(比较并交换—乐观锁机制—锁自旋)

                         4.1.22.1 概念及特性

                            CAS比较并交换,CAS算法的过程是这样的:它包含3个参数CAS(V,E,N)。V表示更新的变量(内存值),E表示预期值(旧的),N表示新值。当且仅当V值等于E值时,才会将V值设置为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不处理。最后CAS返回当前V的真实值。

                             CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。

                          4.1.22.2 ABA问题

                              CAS会导致“ABA”问题。CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。

                              比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

                               部分乐观锁的实现是通过版本号的方式来解决ABA问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。

                   4.1.23 什么是AQS(抽象的队列同步器)

                           AbstractQueuedSynchronizer类如其名,抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock。

                           它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具体volatile的语义。state的访问方式有三种:

  getState(),setState(),compareAndSetState()

                          AQS定义两种资源共享方式:

                          Exclusive独占资源—ReentrantLock

                          share共享资源—semaphore/CountDownLatch

                          AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现,AQS这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过state的get/set/CAS)之所以没有定义成abstract,是因为独占模式下只用实现tryAcquire—tryRelese,而共享模式下只用实现tryAcquireShared—tryReleaseShared。如果都定义成abstract,那么每个模式下也要去实现另一模式下的接口。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护,AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

                          1. isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。

                          2. tryAcquire(int): 独占方式。尝试获取资源,成功则返回true,失败则返回false。

                          3. tryRelease(int): 独占方式。尝试释放资源,成功则返回true,失败则返回false。

                          4. tryAcquireShared(int): 共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。

                          5. tryReleaseShared(int): 共享方式。尝试释放资源,如果释放后允许唤醒后续等待节点返回true,否则返回false。

                 同步器的实现是ABS核心(state 资源状态计数)

                 同步器的实现是ABS核心,以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock时,会调用try Acquire()独占该锁并将state+1。此后,其他线程再try Acquire时就会失败,直到A线程unlock到state=0(即释放锁)为止,其他线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的,这就是可重入的概念。但要注意获取多少次就要释放多少次,这样才能保证state回到零状态。

                 一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需要实现try Acquire- try Release、tryAcquireShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

 

 

 

 

 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值