BAT常问问题总结以及回答(多线程回答一)

多线程

  1. 什么是线程?
        进程概念:进程是指运行中的应用程序,每个进程都有自己独立的地址空间(内存空间),比如用户点击桌面的IE浏览器,就启动了一个进程,操作系统就会为该进程分配独立的地址空间。当用户再次点击左面的IE浏览器,又启动了一个进程,操作系统将为新的进程分配新的独立的地址空间。目前操作系统都支持多进程。
         线程概念:是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。线程有就绪、阻塞和运行三种基本状态。

       1、线程是轻量级的进程

       2、线程没有独立的地址空间(内存空间)

       3、线程是由进程创建的(寄生在进程)

       4、一个进程可以拥有多个线程-->这就是我们常说的多线程编程

       5、线程有几种状态:

         a、新建状态(new)

         b、就绪状态(Runnable)

         c、运行状态(Running)

         d、阻塞状态(Blocked)

         e、死亡状态(Dead)

    线程--如何使用

    在java中一个类要当作线程来使用有两种方法

    1、继承Thread类,并重写run函数

    2、实现Runnable接口,并重写run函数

    因为java是单继承的,在某些情况下一个类可能已经继承了某个父类,这时在用继承Thread类方法来创建线程显然不可能java设计者们提供了另外一个方式创建线程,就是通过实现Runnable接口来创建线程。

     
  2. 什么是线程安全和线程不安全?

    线程安全就是在多线程环境下也不会出现数据不一致,而非线程安全就有可能出现数据不一致的情况。

    线程安全由于要确保数据的一致性,所以对资源的读写进行了控制,换句话说增加了系统开销。所以在单线程环境中效率比非线程安全的效率要低些,但是如果线程间数据相关,需要保证读写顺序,用线程安全模式

     
  3. 什么是自旋锁?sniplock
          互斥锁和他类似,但是互斥锁会让没获取到资源的其他线程睡眠。线程想要获取共享资源就要先获取锁定资源的锁。当线程无法获得锁的时候就会挂起或者阻塞,这个操作是在内核态进行的。我们想充分发挥cpu资源,所以jvm采取一个自旋锁机制,让获取不到资源的线程进行空循环,来一直等待锁释放。
          缺点:如果资源被某个线程长时间占用,就会使其他线程一直处于空循环状态,白白浪费cpu资源。
          使用场景:线程占用资源的时间不长的。
      优点:由于线程状态不会改变,获取资源速度会很快。
     
  4. 什么是Java内存模型?

    https://blog.csdn.net/javazejian/article/details/72772461

     
  5. 什么是CAS?
    java.util.concurrent包建立在CAS之上,没有CAS就没有并发包
    CAS:compare and swap  比较交换     java.util.concurrent包中借助CAS实现了区别于synchronise同步锁的一种乐观锁。
    CAS有三个操作数:V内存值,A预期的旧值,B要更新的新值;仅当内存值V和预期值A相等时,才会将内存值V更新为新值B,否则什么也不做。
    CAS原理:CAS是通过JNI实现的,java native interface,可以调用本地的C语言来控制计算机底层指令,
                  
    public final native boolean compareAndSwapInt(Object o, long offset,
                                                  int expected,
                                                  int x);

           这个就是被调用的本地方法,他会给cmpxchg底层指令加上lock锁,带有lock前缀的指令在运行期间会锁住主线,使得其他处理器暂时无法通过主线访问内存,但是代价昂贵;后来又做了优化,如果要访问的内存区域正好在处理器的缓存中被锁定的话就直接执行,其他的处理器无法读写该内存,能保证原子性,这个过程叫缓存锁定,

      虽然CAS很好的解决了线程操作的原子性问题,但是CAS仍然存在三大问题:ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作。
    AtomicInteger integer = new AtomicInteger(2);
    integer.compareAndSet(3, 4);
    System.out.println(integer.get());

     CAS缺点:

         1. ABA问题:

           比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但可能存在潜藏的问题。如下所示:

           

           现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:

              head.compareAndSet(A,B);

            在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A,此时堆栈结构如下图,而对象B此时处于游离状态:

           

           此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:

           

           其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。

           从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

    复制代码
    1 public boolean compareAndSet(
    2                V      expectedReference,//预期引用
    3 
    4                V      newReference,//更新后的引用
    5 
    6               int    expectedStamp, //预期标志
    7 
    8               int    newStamp //更新后的标志
    9 ) 
    复制代码

            实际应用代码:

    1 private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100, 0);
    2 
    3 ........
    4 
    5 atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);

     

     

         2. 循环时间长开销大:

          自旋CAS(不成功,就一直循环执行,直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

        

        3. 只能保证一个共享变量的原子操作

          当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

     

      CAS与Synchronized的使用情景:   

        1、对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。

        2、对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

       补充: synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。

     

      concurrent包的实现:

        由于java的CAS同时具有 volatile 读和volatile写的内存语义,因此Java线程之间的通信现在有了下面四种方式:

          1. A线程写volatile变量,随后B线程读这个volatile变量。

          2. A线程写volatile变量,随后B线程用CAS更新这个volatile变量。

          3. A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。

          4. A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。

        Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器中实现同步的关键(从本质上来说,能够支持原子性读-改-写指令的计算机器,是顺序计算图灵机的异步等价机器,因此任何现代的多处理器都会去支持某种能对内存执行原子性读-改-写操作的原子指令)。同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:

          1. 首先,声明共享变量为volatile;  

          2. 然后,使用CAS的原子条件更新来实现线程之间的同步;

          3. 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

        AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如下:

          

     

  6. 什么是乐观锁和悲观锁?
    悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。
    乐观锁:乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
     实现:悲观锁:可以在数据库中使用行锁表锁,或者在java代码中添加synchronized
              乐观锁:1、可以在数据库中添加一个版本号,通过每次对比版本号来确定该变量是否被其他变量更改过;2、使用cas原理,在java代码中使用concurrent包中的atomic 系列,例如:定义一个AtomicInteger,然后可以调用他的getAndIncrement方法来实现+1操作,也可以使用getAndSet()方法来实现数据更新,这些方法底层都使用了volatile限定变量 ,使用compareAndSwap方法来利用cas原理进行比较交换:
     
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class iplus {
    
        public static void main(String[] args) {
            AtomicInteger atomicInteger = new AtomicInteger(6);
            atomicInteger.getAndSet(10);
            atomicInteger.getAndIncrement();
            System.out.println(atomicInteger.get());
        }
    }
    

    使用场景:乐观锁:常使用在读取操作较多的地方。悲观锁:常用在写操作较多的地方,更严格的锁定可以防止并发写入出错。

  7. volatile关键字的理解
          a、背景:cpu执行指令,操作操作数向内存中进行读写,但是内存的读写速度很慢,所以每个cpu下都有一个高速缓存,当操作一个变量时候,先从内存中拷贝一份放到缓存中,然后再从缓存中进行读取。但是单线程的时候不会出问题,多线程的时候就可能由于缓存没有及时从已更改的内存中读取数据而出现差错,这就是经典的缓存一致性问题。过去的解决方案是对于硬件而言的:1、使用#LOCK信号锁对主线锁定,由于cpu调用其他组件是通过主线来调用的,所以锁定了主线就限定了只有一个CPU能够操作内存。(但是缺点是长时间锁主线会导致效率低下)2、使用缓存一致性协议,当cpu写数据时,发现写的是共享变量,就会通过协议通知其他缓存,将该共享变量的状态设为无效,这样其他cpu想要使用该共享变量的时候,就只能从内存中再次读取了。
         b、 并发的三个问题:1、原子性:一个或多个操作要么全部执行要么全部不执行。如果一个线程的两个操作之间有另外的线程进入,那么可能会出错(其实就是需要把关联性操作进行绑定操作)2、可见性:两个线程之间应该能立即看到修改后内存中的共享变量值;3、有序性:程序执行的顺序可能发生指令重排序(由于jvm要考虑效率),在单线程中虽然指令重排会影响到指令的顺序,但是还会有数据的依赖性保证指令重排不影响结果。但是多线程中不会存在数据依赖性,所以就会出现问题。只有解决这三个问题才能解决并发问题。
          c、java内存模型:java内存模型jmm为了效率并没有取消了指令重排和高速缓存,所以上面的三个问题依然存在,jmm定义了所有数据都在主存中,还定义了工作内存,相当于高速缓存。对于变量的操作都必须先在工作内存中操作,之后才能进入主存中。
          d、java语言中对三大问题提供的保证:1、原子性:只有取值和赋值操作是原子性操作,a=10是原子操作,而a=b;就不是个原子操作了,不是原子操作的需要使用sychonized或者lock锁定,他俩能保证同一时间只有一个线程;2、对于可见性java提供了volatile关键字,当一个共享变量被volatile修饰后,当他被修改后,会立即更新到主存,当被其他线程读取时,也会自动从主存中读取该值到工作内存中。同时lock和sychonized也能保证可见性,他们在释放锁之前只有单个线程操作共享变量,释放锁之后会自动将变量更新到主存中;3、有序性,sychonized和lock自然可以保证单线程,所以肯定是有序的,volatile也可以保证有序性,下面会具体说;jvm本身的happen-before原则也会保证有序性:
    • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作(只能保证单线程的)
    • 锁定规则:一个unLock操作先行发生于后面对同一个锁lock操作(lock释放了才能加锁)
    • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作(直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。)
    • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
         e、volatile剖析
               1、volatile的两层语义:第一层:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。第二层:禁止进行指令重排序。
                  对于可见性:类似于缓存一致性协议,当第一个线程操作后,将数据放到主存中,会使其他线程工作内存中的值无效。
    1.    对于有序性:在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。被volatile修饰的变量只能保证他自己的相对加载顺序,而不能保证这个变量之外的加载顺序(5条语句,第三条加了volatile,那么12条语句必定在3句之前,45句必定在3句之后,而12,45之间的次序无法保证。)
       
      2、volatile不保证原子性,一批操作中,当其中的一条操作完成后被阻塞,没有将共享变量放入主存,然后就被其他线程取得了,这样就无法保证正确性了。(解决原子性的方法是sychonized、lock、atomicInteger。)
    2. 下面这段话摘自《深入理解Java虚拟机》:

      “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

        1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

        2)它会强制将对缓存的修改操作立即写入主存;

        3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

      f、使用volatile的场景
      1. synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

          1)对变量的写操作不依赖于当前值

          2)该变量没有包含在具有其他变量的不变式中

        而atomic中使用volatile可以保证变量的可见性和有序性,原子性利用了cas的乐观锁机制(compareAndSwap),保证了原子性。
  8. 什么是AQS?

    AbstractQueuedSynchronizer简称AQS,是一个用于构建锁和同步容器的框架。事实上concurrent包内许多类都是基于AQS构建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解决了在实现同步容器时设计的大量细节问题。

    AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus。

     
  9. 什么是原子操作?在Java Concurrency API中有哪些原子类(atomic classes)?
    原子操作是不会被线程调度机制打断的操作,一旦开始就运行到结束,不会被其他线程从中抢断。
    API中的Atomic系列就是原子类,AtomicInt就是能够保证原子性的int值。
     
  10. 什么是Executors框架?

    Executor框架同java.util.concurrent.Executor 接口在Java 5中被引入。Executor框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。

    无限制的创建线程会引起应用程序内存溢出。所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用Executors框架可以非常方便的创建一个线程池。

    Executor是一个父接口,ExecutorService接口实现了Executor接口,而AbstractExecutor抽象类实现了ExecutorService接口,ThreadPoolExecutor继承了AbstractExecutor类。

    Executors是个工厂类,可以用来创建各种线程池,这些线程池所属的类的父类是Executor
    线程池类别:
    newFixedThreadPool:固定大小的线程池,队列无界
    newSingleThreadExecutor:单线程线程池,只用一个线程
    newCachedThreadPool:缓存线程池,无界
    newScheduledThreadPool:归属于ScheduledExecutorService类,这个类是ExecutorService接口的父类
     
  11. 什么是阻塞队列?如何使用阻塞队列来实现生产者-消费者模型?
    阻塞队列是附加了两个附加操作的队列;第一个附加操作是:队列满的时候,阻塞插入队列的线程;第二个附加操作是:队列空的时候,阻塞获取元素的队列,直到队列有值。

    阻塞队列的应用场景:

    阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。简而言之,阻塞队列是生产者用来存放元素、消费者获取元素的容器。(例如消息队列这样的)

    常见的阻塞队列:ArrayBlockingQueue:数组结构组成的有界阻塞队列(FIFO),队列满时不能保证线程竞技的公平性

                         LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列 (FIFO)

                            PropertyBlockingQueue:支持优先级的阻塞队列
    如何利用阻塞队列来实现生产者-消费者模式:当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。
    package Tencent;
    
    import java.util.concurrent.BlockingQueue;
    import java.util.concurrent.LinkedBlockingQueue;
    
    public class BlockingQueueTest {
    
        class Producer implements Runnable {
            protected BlockingQueue<Object> queue;
    
            Producer(BlockingQueue<Object> theQueue) {
                this.queue = theQueue;
            }
    
            public void run() {
                try {
                    while (true) {
                        Object justProduced = getResource();
                        queue.put(justProduced);
                        System.out.println("生产者资源队列大小= " + queue.size());
                    }
                } catch (InterruptedException ex) {
                    System.out.println("生产者 中断");
                }
            }
    
            Object getResource() {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException ex) {
                    System.out.println("生产者 读 中断");
                }
                return new Object();
            }
        }
    
        class Consumer implements Runnable {
            protected BlockingQueue<Object> queue;
    
            Consumer(BlockingQueue<Object> theQueue) {
                this.queue = theQueue;
            }
    
            public void run() {
                try {
                    while (true) {
                        Object obj = queue.take();
                        System.out.println("消费者 资源 队列大小 " + queue.size());
                        take(obj);
                    }
                } catch (InterruptedException ex) {
                    System.out.println("消费者 中断");
                }
            }
    
            void take(Object obj) {
                try {
                    Thread.sleep(100); // simulate time passing
                } catch (InterruptedException ex) {
                    System.out.println("消费者 读 中断");
                }
                System.out.println("消费对象 " + obj);
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
    
            int numProducers = 4;
            int numConsumers = 3;
    
            BlockingQueue<Object> myQueue = new LinkedBlockingQueue<Object>(5);
            BlockingQueueTest blockingQueueTest = new BlockingQueueTest();
            for (int i = 0; i < numProducers; i++) {
                new Thread(blockingQueueTest.new Producer(myQueue)).start();
            }
    
            for (int i = 0; i < numConsumers; i++) {
                new Thread(blockingQueueTest.new Consumer(myQueue)).start();
            }
    
            Thread.sleep(1000);
    
            System.exit(0);
        }
    }
     
  12. 什么是Callable和Future?

    创建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口。
    这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。
    如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。jdk1.5之后就使用了Callable和Future,通过他们来完成任务然后获取返回值。

    Callable接口代表一段可以调用并返回结果的代码;Future接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable用于产生结果,Future用于获取结果。

    Callable接口使用泛型去定义它的返回类型。Executors类提供了一些有用的方法在线程池中执行Callable内的任务。由于Callable任务是并行的(并行就是整体看上去是并行的,其实在某个时间点只有一个线程在执行),我们必须等待它返回的结果。
    java.util.concurrent.Future对象为我们解决了这个问题。在线程池提交Callable任务后返回了一个Future对象,使用它可以知道Callable任务的状态和得到Callable返回的执行结果。Future提供了get()方法让我们可以等待Callable结束并获取它的执行结果。(使用线程池的execute()方法来执行FutureTask对象(FutureTask构造器中需要传入一个task对象),Future是个接口,无法实例化为对象,所以使用FutureTask来创建Future对应的对象。
     
  13. 什么是FutureTask?
    FutureTask可用于异步获取执行结果或取消执行任务的场景。通过get()方法可以异步获取执行结果,不论FutureTask调用多少次run()或者call()方法,它都能确保只执行一次Runable或Callable任务。因此,FutureTask非常适合用于耗时高并发的计算,另外可以通过cancel()方法取消执行任务。
     
  14. 什么是同步容器和并发容器的实现?
    同步容器可以简单地理解为通过synchronized来实现同步的容器,比如Vector、Hashtable以及SynchronizedList等容器,如果有多个线程调用同步容器的方法,它们将会串行执行。
    同步容器由于不能保证多个操作之间的原子性,使用list自带的对象锁将一个同步容器当做一个对象进行锁定,降低了并发性,提高了安全性。
    并发容器在jdk1.5之后,通过并发容器来代替同步容器,可以极大地提高伸缩性并降低风险。比如使用ConcurrentHashMap,他使用分段锁技术(1.8之后不再采用分段锁技术了),这种情况下的任意数量线程访问map,执行写入或读取指令,可以并发的执行。
     
     
  15. 什么是多线程?优缺点?
    多个线程同时完成几件事情互相不干扰。
    优点:

    1.使用线程可以把占据时间长的程序中的任务放到后台去处理

    2.程序的运行效率可能会提高

    3.在一些等待的任务实现上如用户输入,文件读取和网络收发数据等,线程就比较有用了.

    缺点:

    1.如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换.

    2.更多的线程需要更多的内存空间

    3.线程中止需要考虑对程序运行的影响.

    4.通常块模型数据是在多个线程间共享的,需要防止线程死锁情况的发生
     
     
  16. 什么是多线程的上下文切换?
    单个cpu也能多线程运行,方法是cpu可以通过给不同的线程分配时间片时间来看成是多线程运行。每次从A线程切出,记录当前在A线程的状态后切到B线程,然后再从B线程切换回A线程的过程叫做多线程的上下文切换。
     
  17. ThreadLocal的设计理念与作用?
    ThreadLocal为了创建一个变量,这个变量只能被同一个线程使用,其他的线程无法使用;如果多个线程同时使用,互相不干扰。
    package test;
    
    import static java.lang.Thread.sleep;
    
    public class A {
      public static class myRunner implements Runnable{
    
          public ThreadLocal threadLocal = new ThreadLocal();
    
          @Override
          public void run() {
              threadLocal.set((int)(Math.random()*100D));
              try {
                  sleep(200);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println(threadLocal.get());
          }
      }
    
        public static void main(String[] args) {
            myRunner myRunner = new myRunner();
            Thread thread1 = new Thread(myRunner);
            Thread thread2 = new Thread(myRunner);
            thread1.start();
            thread2.start();
        }
    }

     

     
  18. ThreadPool(线程池)用法与优势?
    优势:合理利用线程池能够带来三个好处。第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。但是要做到合理的利用线程池,必须对其原理了如指掌。
    用法见:https://www.cnblogs.com/television/p/9402520.html
     
  19. Concurrent包里的其他东西:ArrayBlockingQueue、CountDownLatch等等。
    ArrayBlockingQueue:阻塞队列上面提到过,可以用作生产者-消费者模型
    CountDownLatch:是一个同步工具类,用来协调多个线程之间的同步,或者协调多个线程之间的通信。CountDownLatch原理:在CountDownLatch中放有需要等待执行的线程,然后设置一个计数器,初始值为正在执行的线程的数量,没执行完一个线程计数器就-1,直到为0的时候CountDownLatch里存的其他的线程才开始。
    CountDownLatch用法:

    CountDownLatch典型用法1:某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为n new CountDownLatch(n) ,每当一个任务线程执行完毕,就将计数器减1 countdownlatch.countDown(),当计数器的值变为0时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。

    CountDownLatch典型用法2:实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),将其计数器初始化为1,多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒。

    CountDownLatch缺点:CountDownLatch只能用一次,其中的初始值只能在初始化中被初始化一次,之后就没得了。
     
  20. synchronized和ReentrantLock的区别?
    相同点:两者都是加锁同步,而且都是阻塞式同步,进入线程阻塞和和唤醒的代价是较高的(需要在内核态和用户态之间进行切换,代价很大)
    不同点:synchronized是java语言本身的关键字,而lock是jdk1.5之后提供的API层面的互斥锁,需要lock()和unlock()配合try/catch语句来完成。
          ReentrantLock的优势:
            1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。(trylock()方法:无参数尝试获取锁,不能获取立即返回;有参数就是按参数时间来等待锁)。

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

     

    public static ReentrantLock lock = new ReentrantLock(true);设置为true则为公平锁;

     

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

    class MyThread implements Runnable {
     
        private Lock lock=new ReentrantLock();
        public void run() {
                lock.lock();
                try{
                    for(int i=0;i<5;i++)
                        System.out.println(Thread.currentThread().getName()+":"+i);
                }finally{
                    lock.unlock();
                }
        }

     

     
  21. Semaphore有什么作用?
    Semaphore代表的是并发操作中的信号量,控制并发的数量。初始化如下:
    private Semaphore semaphore = new Semaphore(1);

    其中有两个方法:acquire()方法和release()方法,前者用来请求线程,后者用来释放线程,构造器中的数量代表控制的线程数。

    类似于Synchronized,Syn限制只能有一个线程,而Semaphore限制多个线程。

     
  22. Java Concurrency API中的Lock接口(Lock interface)是什么?对比同步它有什么优势?
    原生的ReentrantLock是唯一实现Lock接口
    1、可以使锁更公平;
    2、可以使线程在等待锁的时候响应中断;
    3、可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间;
    4、可以在不同的范围,以不同的顺序获取和释放锁。
     
  23. Hashtable的size()方法中明明只有一条语句”return count”,为什么还要做同步?
    synchronized锁住的是整个Hashtable对象,避免在你取count的时候有其他线程修改这个Hashtable的count值。
     
  24. ConcurrentHashMap的并发度是什么?
    ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势,任何情况下,Hashtable能同时有两条线程获取Hashtable中的数据吗?当并发度满并且单个并发内也满的话就先给并发度*2,单个并发内容量超过8的时候,就会改为红黑树。
     
  25. ReentrantReadWriteLock读写锁的使用?
    对象上锁后,禁止本线程以外的线程读写该对象,当本线程写该对象时,防止其他线程读写是合理的,但是如果本线程仅仅是读,这个时候限制其他线程读取就不合理了,所以我们使用ReentrantReadWriteLock。
    ReadWriteLock解决了这个问题,当写操作时,其他线程无法读取或写入数据,而当读操作时,其它线程无法写入数据,但却可以读取数据 。(读写锁)
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    rwl.readLock().lock();//上读锁,其他线程只能读不能写

    rwl.readLock().unlock();
    
    
  26. CyclicBarrier和CountDownLatch的用法及区别?
    CountDownLatch见19题
    CycleBarrier 能做到让n个线程互相等待,当n个线程都做到某一步后,再继续下一步。
    final  CyclicBarrier cb = new CyclicBarrier(3);

    用以下语句控制在一个任务完成后进行等待

    cb.await();
     
  27. LockSupport工具?
    LockSupport可以用来控制线程阻塞和开放的,park方法用来消费许可让线程开放,unpark方法用来获取许可让线程阻塞。
     
  28. Condition接口及其实现原理?
    Condition接口提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式。
    Condition是通过lock创建出来的,Condition condition = lock.newCondition();
    condition.await(); condition.signal();
    当调用await()方法后,当前线程会释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。
     
  29. Fork/Join框架的理解?

     

    现将任务拆分成子任务(fork),然后再将任务的结果汇总(join)汇总成一个总的结果.

    需要实现大计算任务的类继承RecursiveAction或者是RecursiveTask,前者不会返回数据,后者会;具体适用方法如下:(需要重写compute方法)使用invoke调用这个方法。

    import java.util.concurrent.RecursiveTask;
    
    public class ForkJoinCalculator extends RecursiveTask<Long> {
    
        private long start;
        private long end;
        private static final long THRESHOLD = 10000;// 临界值
    
        public ForkJoinCalculator(long start, long end) {
            this.start = start;
            this.end = end;
        }
    
        @Override
        protected Long compute() {
    
            if (end - start <= 1000) {
                // 不大于临界值直接计算和
                long sum = 0;
                for (long i = start; i <= end; i++) {
                    sum += i;
                }
                return sum;
            } else {
                // 大于临界值继续进行拆分子任务
                long mid = (start + end) / 2;
    
                // 拆分子任务
                ForkJoinCalculator calculator1 = new ForkJoinCalculator(start, mid);
                calculator1.fork();
    
                // 拆分子任务
                ForkJoinCalculator calculator2 = new ForkJoinCalculator(mid + 1, end);
                calculator2.fork();
    
                //合并子任务结果
                return calculator1.join() + calculator2.join();
            }
        }
    }
    @Test
    public void test() {
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        Long sum = forkJoinPool.invoke(new ForkJoinCalculator(1, 1000000000L));
        System.out.println(sum);
    }

     

     
  30. wait()和sleep()的区别?
    wait()方法属于Thread类,而sleep属于Object类,使用wait()方法后,当前线程放弃对象锁,需要使用notify()方法来唤醒该线程才能让该线程进入就绪态。
    sleep()方法设定时间,在指定时间内等候,但是不会释放对象锁。
     
  31. 线程的五个状态(五种状态,创建、就绪、运行、阻塞和死亡)?
  32. start()方法和run()方法的区别?
    start()方法用来启动线程中的run方法的,run方法为重写方法,内容为线程任务。
    1.  
  33. Runnable接口和Callable接口的区别?
    Runnable和Callable的区别是,
    (1)Callable规定的方法是call(),Runnable规定的方法是run().
    (2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
    (3)call方法可以抛出异常,run方法不可以
    (4)运行Callable任务可以拿到一个Future对象,Future 表示异步计算的结果。
    1.  
  34. volatile关键字的作用?
    见7题
     
  35. Java中如何获取到线程dump文件?
  36. 线程和进程有什么区别?
    见第一题
    1.  
  37. 线程实现的方式有几种(四种)?

    1)继承Thread类创建线程

    2)实现Runnable接口创建线程

    3)使用Callable和Future创建线程

    4)使用线程池例如用Executor框架

     
     
  38. 高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
     
     (1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
    (2)并发不高、任务执行时间长的业务要区分开看:
    a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务
    b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
    (3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。
     
     
  39. 如果你提交任务时,线程池队列已满,这时会发生什么?
     
    许多程序员会认为该任务会阻塞直到线程池队列有空位。事实上如果一个任务不能被调度执行那么ThreadPoolExecutor’s submit()方法将会抛出一个RejectedExecutionException异常。
     
  40. 锁的等级:方法锁、对象锁、类锁?
    方法锁和对象锁是一个东西,所以只存在这两种锁:方法锁(对象锁)、类锁 方法锁(对象锁)只锁定当前对象和方法;而类锁可以锁定静态变量和静态方法,所有该类的实例对象都使用同一把锁。
      方法锁:
        public class object {
            public synchronized void method(){
                System.out.println("我是对象锁也是方法锁");
            }
        }

    对象锁:

        public class object {
            public void method(){
                synchronized(this){
                    System.out.println("我是对象锁");
                }
            }
        }

    类锁:

        public class object {
            public static synchronized void method(){
                System.out.println("我是第一种类锁");
            }
        }
    
    
        public class object {
            public void method(){
                synchronized (object.this) {
                    System.out.println("我是第二种类锁");
                }
            }
        }

     

     
     
  41. 如果同步块内的线程抛出异常会发生什么?
  42. 并发编程(concurrency)并行编程(parallellism)有什么区别?

    并发(concurrency)和并行(parallellism)是:

    解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
    解释二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
    解释三:在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群
     
  43. 如何保证多线程下 i++ 结果正确?
    说明要解决自增操作在多线程环境下线程不安全的问题,可以选择使用Java提供的原子类或者使用synchronized同步方法。
    而使用volatile关键字, 并不能解决非原子操作的线程安全性。

  44. 一个线程如果出现了运行时异常会怎么样?
    简单的说,如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。
     
  45. 如何在两个线程之间共享数据?
  46. 生产者消费者模型的作用是什么?
  47. 怎么唤醒一个阻塞的线程?
  48. Java中用到的线程调度算法是什么
  49. 单例模式的线程安全性?
  50. 线程类的构造方法、静态块是被哪个线程调用的?
  51. 同步方法和同步块,哪个是更好的选择?
  52. 如何检测死锁?怎么预防死锁?

    所谓死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁

    死锁产生的原因?

    1.因竞争资源发生死锁 现象:系统中供多个进程共享的资源的数目不足以满足全部进程的需要时,就会引起对诸资源的竞争而发生死锁现象

    2.进程推进顺序不当发生死锁

    检测死锁:

    首先为每个进程和每个资源指定一个唯一的号码;

    然后建立资源分配表和进程等待表。

    死锁预防:

     

    避免死锁(银行家算法):

     

    预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法

     

posted @ 2018-08-12 10:44  彩电  阅读(1989)  评论(0编辑  收藏  举报