Java并发编程:

 

1 Java多线程优势,实现方式:

        多线程优势:1.进程之间不能共享内存,线程之间共享内存更容易,多线程可协作完成进程工作;2.创建进程进行资源分配的代价较创建线程要大得多,多线程在高并发环境中效率更高。

        并发编程的挑战:上下文切换(任务从保存到再加载的过程--->如何减少:无锁并发编程、CAS算法、使用最少的线程、使用协程(在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换))、死锁、受限制于硬件和软件的限制

        并发编程的两个关键问题:线程之间如何同步、线程之间图和通信(共享内存、消息传递)

        四种方式:1.继承Thread类,重写run方法;2.实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target;3.通过Callable和FutureTask创建线程;4.通过线程池创建线程。

 

2 线程池?

       线程池的实现通过ThreadPoolExecutor类。参数主要有:

        --corePoolSize:核心线程数。--maxPoolSize:最大线程数。--keepAliveTime:线程空闲时间。--allowCoreThreadTimeout:允许核心线程超时。--queueCapacity:任务队列容量。

        在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。

        线程池是一种多线程处理方法,处理过程将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程,每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。

       在线程池创建一个线程的过程①当线程数小于核心线程数时,创建线程。②当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。③当线程数大于等于核心线程数,且任务队列已满。若线程数小于最大线程数,创建线程,若线程数等于最大线程数,抛出异常,拒绝任务。     

        线程池中队列的选择https://blog.csdn.net/langwang2/article/details/49745263

       1) 使用直接提交策略,也即SynchronousQueue:该队列是无界的,在某次添加元素后必须等待其他线程取走后才能继续添加,即此时如果有新的任务,请求会因为队列已满,创建新的线程(此策略可以避免在处理可能具有内部依赖性的请求集时出现锁:如果你的任务A1,A2有内部关联,A1需要先运行,那么先提交A1,再提交A2,当使用SynchronousQueue我们可以保证,A1必定先被执行,在A1么有被执行前,A2不可能添加入queue中。)

        2) 使用无界队列策略,即LinkedBlockingQueue:无界队列,永远也不会触发产生新的线程!将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize;注意添加任务速度远超处理任务速度,造成资源耗尽。

        3)有界队列,使用ArrayBlockingQueue:有助于防止资源耗尽,但是较难调整和控制;使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。

 

        四种常见的线程池

         newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。(同步队列为SynchronousQueue,核心线程数为0,最大线程数为integer.MAX_VALUE,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程,适用于负载较轻的服务器。)

        newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。(核心线程数为nThread,最大线程数也为nThread,队列为LinkedBlockingQueue,无界阻塞队列,当池中所有线程处于繁忙状态,新任务会添加到阻塞队列中,适用于执行长期的任务)

         newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。(核心线程数为nThread,最大线程数也为integer.MAX_VALUE,队列为DelayedWorkQueue() 一个按超时时间升序排序的队列,适用于周期性执行任务的场景)

         newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。(核心线程数为1,最大线程数也为1,队列为LinkedBlockingQueue,无界阻塞队列,当池中所有线程处于繁忙状态,新任务会添加到阻塞队列中,适用于任务一个一个执行的场景)

 

     线程池的作用:

      降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗

     提高响应速度。当任务到达时,可以不需要等到线程创建就能立即执行

     提高线程的可管理性。

 

      根据任务的类型来配置线程池大小:

  如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1 这样已经充分利用了处理器,也就是让它以最大火力不停进行计算。创建更多的线程对于程序性能反而是不利的,因为多个线程间频繁进行上下文切换对于程序性能损耗较大。

       如果是IO密集型任务,参考值可以设置为2*NCPU;当一个任务执行IO操作时,线程将被阻塞,于是处理器可以立即进行上下文切换以便处理其他就绪线程。如果我们只有处理器核心数那么多个线程的话,即使有待执行的任务也无法调度处理了。

        线程中的execute 和submit 方法区别,前者没有返回值,后者有返回值。

 

3.volitile关键字的作用,原理

       Java语言提供了一种稍弱的同步机制,即volatile变量,常用于保持内存可见性和防止指令重排序

        Volatile如何保证内存可见性:①当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存.②当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。【只能保证可见性,并不能保证原子性,故而不能保证线程安全】

         Volatile防止指令重排序:为了使得处理器内部的运算单元能够尽量被充分利用,处理器会对输入代码进行乱序执行优化,保证该结果和顺序执行的结果是一致的,Volatile会在本地代码中插入内存屏障指令来保证处理器不发生乱序执行,防止指令重排序。

          重排序中需要遵守的原则:happens-before规则【具体包括:程序次序原则、管程锁定原则、线程启动原则、线程终止原则、线程中断原则、对象终结原则、传递性】

 

4.synchronized关键字的用法,优缺点

         用法:①修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;②修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;③修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;④修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

       优点:防止多个线程在更新共享状态时相互冲突(原子性);确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的(可见性)。

       缺点:不能够跨越多个对象;当在等待锁对象的时候,不能中途放弃,直到成功;等待没有超时限制;不能中断阻塞。

 

5.Volatile和Synchronized不同点:

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

     2 Synchronized阻塞,volatile线程不阻塞

     3 Synchronized保证原子性,volatile不保证原子性

     4 Synchronized编译器优化,volatile不优化 ; 

 

6. Lock和synchronized不同点:

       1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,底层使用系统调用,需要切换到内核态,而lock则使用aqs来实现。

    2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

 3)Lock可以让等待锁的线程响应中断,线程可以中断去干别的事务,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

 4)sync可以锁类,实例和实例方法,lock只能锁方法。

 

7.  ReenTrantLock可重入锁和synchronized的区别:

       可重入锁,也叫做递归锁,当前持有该锁的线程能够多次获取该锁,无需等待,在JAVA环境下 ReentrantLock 和synchronized 都是可重入锁。可重入锁最大的作用是避免死锁。

       实现原理:每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。线程结束,计数器会减1,当计数器的值减为0时,其他线程才有机会获取锁。

 

       1.Synchronized是依赖于JVM实现的,在代码执行时出现异常,JVM会自动释放锁定;而ReenTrantLock是JDK实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中

      2. ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。

      3. ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程,synchronized要么随机唤醒一个线程要么唤醒全部线程。

      4. ReenTrantLock 提供了一种等待可中断的特性,当前持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,对于处理执行时间非常长的同步块很有帮助。

 

8.常见锁:   

       读写锁:将对一个资源的访问分成了2个锁,一个读锁和一个写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。可以通过readLock()获取读锁,通过writeLock()获取写锁。

        自旋锁:使线程在没有取得锁的时候,不被挂起,而转去执行一个空循环,(即所谓的自旋,就是自己执行空循环),若在若干个空循环后,线程如果可以获得锁,则继续执行。若线程依然不能获得锁,才会被挂起。【尽可能的减少线程的阻塞,避免用户线程和内核的切换的消耗。】

        偏向锁:JDK1.6提出来的一种锁优化的机制。其核心的思想是,若某一锁被线程获取后,便进入偏向模式,当线程再次请求这个锁时,就无需再进行相关的同步操作了,从而节约了操作时间,如果在此之间有其他的线程进行了锁请求,则锁退出偏向模式。【消除数据在无竞争情况下的同步消耗】

        轻量级锁:相对于重量级锁而言,在获取锁和释放锁时更加高效,适用的场景是在线程交替获取某个锁执行同步代码块的场景,如果出现多个进程同时竞争同一个锁时,轻量级锁会膨胀成重量级锁。

 

9.悲观锁,乐观锁:

          乐观锁,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。CAS

          悲观锁,不管是否发生多线程冲突,只要存在这种可能,就每次访问都加锁,加锁就会导致锁之间的争夺,有争夺就会有输赢,输者等待。优缺点:悲观锁通过加锁独占数据,所以不会出现数据修改冲突,但正因为它的锁机制,导致并发效率不高,多线程时有较多等待。Synchronized

        CAS是乐观锁的一种实现方式,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

       CAS的缺陷及解决:①循环时间太长(使用处理器提供的pause指令,延迟流水线执行指令);②只能保证一个共享变量原子操作(解决方法:把多个共享变量合并成一个共享变量来操作);③ABA问题(解决方案:加上版本号,即在每个变量都加上一个版本号,每次改变时加1,即A —> B —> A,变成1A —> 2B —> 3A)。【硬件级别的原子性,如果要访问的内存区域在lock前缀指令执行期间已经在处理器内部的缓存中被锁定,并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。】

 

10.理解同步/异步、阻塞/非阻塞的区别:

       同步:发起调用后,被调用者处理消息,必须等处理完才直接返回结果,没处理完之前是不返回的,调用者主动等待结果;

       异步:发起调用后,被调用者直接返回,但是并没有返回结果,等处理完消息后,通过状态、通知或者回调函数来通知调用者,调用者被动接收结果。

       阻塞:调用结果返回之前,该执行线程会被挂起,不释放CPU执行权,线程不能做其它事情,只能等待,只有等到调用结果返回了,才能接着往下执行;

       非阻塞:在没有获取调用结果时,不是一直等待,线程可以往下执行,如果是同步的,通过轮询的方式检查有没有调用结果返回,如果是异步的,会通知回调。
      

       例如:小明去买奶茶,点单交钱后啥都不干,等着拿奶茶,同步阻塞;在等的过程中翻出手机刷朋友圈,一会瞅瞅有没有做好,同步非阻塞;小明点单交钱后,店员给了小明小票,说奶茶做好了再来取,小明拿着票还是一直傻等,异步阻塞;小明拿着票,去坐着刷朋友圈,过会叫号后小明放下手机去拿奶茶,异步非阻塞。

 

        BIO:同步阻塞IO,服务器实现模式是一个连接一个线程,高并发下消耗大,适用于连接数目比较小且固定的架构,例如socket编程

        AIO: 异步非阻塞IO,服务器实现模式是一个有效请求一个线程。

        NIO:同步非阻塞IO,是一种基于通道和缓冲区的 I/O 方式 ,其中,同步的核心就是 Selector,Selector 代替了线程本身轮询 IO 事件,避免了阻塞同时减少了不必要的线程消耗;非阻塞的核心就是通道和缓冲区,当 IO 事件就绪时,可以通过写道缓冲区,保证 IO 的成功,而无需线程阻塞式地等待。

 

11.线程同步以及线程调度相关的方法:

         - wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;

        - sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;

        - notify():唤醒一个处于等待状态的线程,在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,由JVM确定唤醒哪个线程,与优先级无关;

        - notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

         notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放。如果notify方法后面的代码还有很多,需要这些代码执行完后才会释放锁),调用wait方法的线程就会解除wait状态和程序可以再次得到锁后继续向下运行。

线程的状态

1、初始状态(New):新创建了一个线程对象。

2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。

4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

      (一)、等待阻塞:运行的线程执行wait()方法,JVM把该线程放入等待池中。

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

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

5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

 

终止线程的三种方法:

1. 使用退出标志终止线程:当run方法执行完后,线程会退出,但有时候run方法永远不会结束,设置布尔类型的标志,并通过设置这个标志位true实现退出

2. 使用stop强行终止线程:已被舍弃,立即终止,会导致一些数据被处理到一部分就被终止,无法保证线程资源的正确释放,导致程序工作在不确定的状态

3. 使用interrupt()方法中断运行态和阻塞态线程。

 

12. sleep()和wait():

       共同点 :wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。 

      不同点 : 1.sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁;2.wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。3.wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用 ;4.sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常 。

      wait和await方法的区别:wait用于synchronized同步代码中,通过使用notify方法唤醒,await用于lock中,使用signal()唤醒。

 

13.调度算法:

        并行:同一时刻,多个执行流共同执行;

        并发:通过CPU的调度算法,使用户感觉就像是同时处理多个任务,但同一时刻只有一个执行流占用CPU执行,即使多核多CPU环境还是会使用并发,以提高处理效率。

CPU调度算法:

       轮询调度(RR):是每一次把来自用户的请求轮流分配给内部中的服务器,从1开始,直到N(内部服务器个数),然后重新开始循环。只有在当前任务主动放弃CPU控制权的情况下(比如任务挂起),才允许其他任务(包括高优先级的任务)控制CPU。其优点是其简洁性,它无需记录当前所有连接的状态,所以它是一种无状态调度,但不利于后面的请求及时得到响应。

        抢占式调度:允许高优先级的任务打断当前执行的任务,抢占CPU的控制权。这有利于后面的高优先级的任务也能及时得到响应。但实现相对较复杂且可能出现低优先级的任务长期得不到调度。

        先到先服务调度(FCFS):先到先服务,后到的线程不能抢占前面正在服务的线程。

        最短作业优先调度(SJF):CPU进程区间最短的先执行,如果两个进程区间具有同样的长度,那么按照FCFS来调度。

 

14.线程与进程: 

        进程:子进程是父进程的复制品。子进程获得父进程数据空间、堆和栈的复制品。相对与进程而言,线程是一个更加接近与执行体的概念,它可以与同进程的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。  

        两者都可以提高程序的并发度,提高程序运行效率和响应时间。  

        线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源管理和保护;而进程正相反。同时,线程适合于在SMP机器上运行,而进程则可以跨机器迁移。

         根本区别:进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径,线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉。

          有三个线程T1,T2,T3,可以确保它们按顺序执行的方法有:先启动最后一个(T3调用T2,T2调用T1);可以用线程类的join()方法在一个线程中启动另一个线程,另一个线程完成;

 

15.死锁与加锁:

        java并发框架支持锁:自旋锁、自旋锁的其他种类、阻塞锁、可重入锁、读写锁、互斥锁、悲观锁、乐观锁、公平锁、非公平锁、偏向锁、对象锁、线程锁、锁粗化、轻量级锁、锁消除、锁膨胀、信号量。

       当线程需要同时持有多个锁时,有可能产生死锁。假设有P1和P2两个进程,都需要A和B两个资源,现在P1持有A等待B资源,而P2持有B等待A资源,两个都等待另一个资源而不肯释放资源,就这样无限等待中,这就形成死锁。

产生死锁的原因主要是: 

(1) 因为系统资源不足。 (2) 进程运行推进的顺序不合适。 (3) 资源分配不当等。

产生死锁的四个必要条件

(1) 互斥条件:一个资源每次只能被一个进程使用。

(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。

(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

 

预防死锁:资源有序分配法;

检测死锁:资源分配图化简法;

 

避免死锁的常用方法:

1.避免一个线程同时获取多个锁;

2.避免一个锁在锁内同时占用多个资源,尽量保证每个锁只占用一个资源;

3.尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制;

4. 对于数据库锁,加锁和解锁必须在一个数据库链接里,否则会出现解锁失败的情况。

银行家算法:是指在分配资源之前先看清楚,资源分配后是否会导致系统死锁。如果会死锁,则不分配,否则就分配。

 

解决死锁:

1. 剥夺资源:从其他进程剥夺足够数量的资源给死锁进程,以解除死锁状态

2. 撤销进程:可以直接撤销死锁进程,直至有足够的资源可用。

 

16.java并发工具类:

       AbstractQueuedSynchronizer(AQS,抽象的队列式的同步器),AQS定义了一套多线程访问共享资源的同步器框架,基于FIFO等待队列实现,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch

       AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/ CountDownLatch)。https://blog.csdn.net/m_xiaoer/article/details/73459444

        AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,将暂时获取不到锁的线程加入到队列中。CLH队列中引入了头结点和尾节点,用于实现获取锁、入队列、释放锁等操作,每个节点只关心其前一个节点的状态,线程唤醒也只唤醒队头等待线程。

      1) CountDownLatch:允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。是通过内部计数器来实现的,每完成一个任务就对计数器进行减1。直到为0,闭锁等待的线程就可以恢复执行。可以用来实现线程等待和最大并行性。

       2) CyclicBarrier:可循环使用的屏障。它要做的事情,让一组线程到达屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会打开,所有被屏障拦截的线程才会继续运行。【应用于多线程计算数据,最后合并计算结果的场景】

       3) Semaphore:控制线程并发数的信号量。控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源【应用于流量监控】

      4) Exchanger:用于线程间协作的工具类,实现线程间的数据交换。【可应用与遗传算法、校对工作】

 

17.ThreadLocal变量:

         线程的局部变量,为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。 因为线程不安全 都是由全局变量引起的,  每个线程都有自己的副本变量相互之间不共享数据线程安全。

        Thread 在内部是通过ThreadLocalMap类型的成员变量threadLocals来维护ThreadLocal变量表,当前线程的threadlocalmap是在第一次调用set的时候创建map并且设置上相应的值。【可解决数据库连接、session管理等】

        与线程同步机制的对比:线程同步机制通过对象的锁机制保证同一时间只有一个线程去访问变量,该变量是多个线程共享的。ThreadLocal则为每一个线程提供了一个变量副本,从而隔离了多个线程访问数据的冲突,线程私有。同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

        ThreadLocalMap中解决Hash冲突的方式:线性探测。根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

       由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。---->避免内存泄漏:使用完ThreadLocal之后,调用remove方法。

 

18.阻塞队列:

       阻塞队列:在队列基础上支持了两个附加操作的队列。常用于生产者与消费者的场景中。当生产者线程试图向BlockingQueue放入元素时,入果队列已满,队列会阻塞插入元素的线程,直到队列不满;当消费者线程试图从中取出一个元素时,如果队列为空,获取元素的线程会等待队列变为非空,不在需要额外编写代码去唤醒。

实现类:

ArrayBlockingQueue: 一个基于数组实现的有界阻塞队列,必须设置容量

LinkedBlockingQueue: 基于链表实现的阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列

PriorityBlockingQueue: 一个无界的阻塞队列,使用的排序规则,和PriorityQueue类似并提供了阻塞操作

LinkedBlockingDeque: 一个基于双端链表的双端阻塞队列,容量可以选择进行设置

 

19.java原子类操作:

参考:http://www.cnblogs.com/Mainz/p/3556430.html#

       java.util.concurrent包下面有一个atomic子包,其中有几个以Atomic打头的类,例如AtomicInteger和AtomicLong。它们利用了现代处理器的特性,可以用非阻塞的方式完成原子操作。

       AtomicInteger 是一个支持原子操作的 Integer 类,保证对 AtomicInteger 类型变量的增加和减少操作是原子性的,不会出现多个线程下的数据不一致问题。(内部通过CAS算法来保证原子性)

 

20.forkJoin方法:

      forkJoin方法:ForkJoin是Java1.7提供的原生多线程并行处理框架,其基本思想是将大任务分割成小任务,最后将小任务聚合起来得到结果。

     Fork/Join使用两个类完成:

     1. ForkJoinTask: 我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务中执行fork()和join的操作机制,通常我们不直接继承ForkjoinTask类,只需要直接继承其子类。 RecursiveAction,用于没有返回结果的任务; RecursiveTask,用于有返回值的任务

      2. ForkJoinPool:task要通过ForkJoinPool来执行,分割的子任务也会添加到当前工作线程的双端队列中,进入队列的头部。当一个工作线程中没有任务时,会从其他工作线程的队列尾部获取一个任务。

      以多线程编写一个可以统计某个目录下所有文件大小总和的程序为例:其他方法可参见

         https://blog.csdn.net/zhongweijian/article/details/8209814

import java.io.File;
import java.util.LinkedList;
import java.util.Scanner;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
 
class DocumentSize extends RecursiveTask<Long>{
	LinkedList<File> documentList;	
	public DocumentSize(LinkedList<File> documentList){
		this.documentList=documentList;
	}
	@Override
	protected Long compute() {
		long total=0;
		for(File file:documentList){
			total+=file.length();
		}
		return total;
	}	
}
class FolderSize extends RecursiveTask<Long>{
	File file;
	public FolderSize(File file){
		this.file=file;
	}
	@Override
	protected Long compute() {
		long total=0;
		if(file.isFile()) 
			return file.length();
		else{
			if(file.isDirectory()){
				LinkedList<RecursiveTask<Long>> taskList=new LinkedList<RecursiveTask<Long>>();
				File []files=file.listFiles();
				LinkedList<File> folderList=new LinkedList<File>();
				LinkedList<File> documentList=new LinkedList<File>();
				for(File file:files){
					if(file.isFile()) 
						documentList.add(file);
					else if(file.isDirectory()) 
						folderList.add(file);
				}
				for(File file:folderList){
					FolderSize fs=new FolderSize(file);
					taskList.add(fs);
					fs.fork();
				}
				DocumentSize ds=new DocumentSize(documentList);
				taskList.add(ds);
				ds.fork();				
				for(RecursiveTask<Long> rs:taskList)
					total+=rs.join();				
			}
		}
		return total;
	}
}
 
public class TotalSize {
	public static void main(String [] args){
		Scanner scanner = new Scanner(System.in);
		File file=new File(scanner.nextLine());
		ForkJoinPool fjp=new ForkJoinPool();
		long size=0;
		size=fjp.invoke(new FolderSize(file));
		System.out.println("大小为"+size*1.0/1024/1024+"MB");	
	}
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值