java并发包和类总结-JUC总结

多线程课程
JUC课程
实战Java高并发
Java并发编程的艺术

多线程

  1. 程序:是一个静态的概念,一般对应于操作系统中的一个可执行文件。一组指令的集合。
  2. 进程:是一个动态的概念,执行中的程序叫做进程。1.进程是程序的一次动态执行过程, 占用特定的地址空间。2.每个进程由3部分组成:cpu、data、code。每个进程都是独立的,保有自己的cpu时间,代码和数据,即便用同一份程序产生好几个进程,它们之间还是拥有自己的这3样东西,这样的缺点是:浪费内存,cpu的负担较重。3.多任务(Multitasking)操作系统将CPU时间动态地划分给每个进程,操作系统同时执行多个进程,每个进程独立运行。以进程的观点来看,它会以为自己独占CPU的使用权。其实这是并行执行。
  3. 线程:一个进程可以产生多个线程。同多个进程可以共享操作系统的某些资源一样,同一进程的多个线程也可以共享此进程的某些资源(比如:代码、数据),所以线程又被称为轻量级进程(lightweight process)。1.一个进程内部的一个执行单元,它是程序中的一个单一的顺序控制流程。2.一个进程可拥有多个并行的(concurrent)线程。3.一个进程中的多个线程共享相同的内存单元/内存地址空间,可以访问相同的变量和对象,而且它们从同一堆中分配对象并进行通信、数据交换和同步操作。线程都拥有各自的计数器、堆栈和局部变量。4.由于线程间的通信是在同一地址空间上进行的,所以不需要额外的通信机制,这就使得通信更简便而且信息传递的速度也更快。5.线程的启动、中断、消亡,消耗的资源非常少。
  4. 进程和线程的区别:1.每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销。
    2.线程可以看成是轻量级的进程,属于同一进程的线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小。
    3.线程和进程最根本的区别在于:进程是资源分配的单位,线程是调度和执行的单位。
    4.多进程: 在操作系统中能同时运行多个任务(程序)。
    5.多线程: 在同一应用程序中有多个顺序流同时执行。
    6.线程是进程的一部分,所以线程有的时候被称为轻量级进程。
    7.一个没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个线程,进程的执行过程不是一条线(线程)的,而是多条线(线程)共同完成的。
    8.系统在运行的时候会为每个进程分配不同的内存区域,但是不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组只能共享资源。那就是说,除了CPU之外(线程在运行的时候要占用CPU资源),计算机内部的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。
  5. 进程与程序的区别:一个进程肯定与一个程序相对应,并且只有一个,但是一个程序可以有多个进程,或者一个进程都没有。除此之外,进程还有并发性和交往性。简单地说,进程是程序的一部分,程序运行的时候会产生进程。
  6. 创建线程的三种方式:继承Thread类、实现Runnable接口、实现Callable接口。通过线程池创建。
  7. 创建:继承Thread+重写run,启动: 创建子类对象 + start
  8. 执行线程必须调用start(),加入到调度器中,不一定立即执行,系统安排调度分配执行;开启线程后,当方法执行时,调用run()代码将被执行。run()方法是线程体的入口点,线程执行的代码;直接调用run()方法不是开启多线程,是普通的方法调用,此时run()方法自己会去调用start()方法。
  9. 创建:实现Runnable+重写run启动: 创建实现类对象 +Thread代理类对象+ start(启动线程还是要借用Thread类,只有 thread才具有和cpu直接 打交道的能力,即启动start())。我们使用实现runnable接口重写run方法启动线程时,必须借用Thread对象,这个对象就是代理对象:就是真实角色和代理角色都实现同一个接口,代理角色中含有真实角色的引用,可以调用真实角色的操作。
  10. Thread类的构造方法
    Thread() Thread(Runnable target) Thread(Runnable target,String name) Thread(String name)
    Thread(ThreadGroup group,Runnable target) Thread(ThreadGroup group,Runnable target,String name)
    方法:start():导致此线程开始执行,Java虚拟机调用此线程的run()方法
    run():如果这个线程是使用单独的Runnable运行对象构造的,那么这个Runnable对象的run()方法被调用;否则此方法不执行任何操作并返回
  11. Runnale对同一个资源可以有多个代理,共享资源,并发(要保证线程安全,后期会讲)。
  12. 创建:实现Callable接口+重写call()方法,run()方法不能抛出异常和没有返回值,而call()方法可以抛出异常和有返回值。需要借助服务、线程池。创建目标对象:创建执行服务: 提交执行: 获取结果:关闭服务: 。
  13. Thread的构造方法要的是runnable,有的是callable,如何把它们之间加上关系:runnable实现了FutureTask,而FutureTask的构造方法中需要callable。FutureTask futuretask=new FutureTask<>(new MyThread());Thread t=new Thread(futuretask,”AA”);t.start();适配模式。多个线程都要使用callable,每次都要返回一个成功或者失败的返回值。futuretask.get();建议放在最后因为我们不会等这个线程,给它充足的时间去计算。如果把get放到前面,mian线程就被堵住了。以这里可以加一个循环的判断!等算完了,才往下做。两个线程都开始做同一个任务,只会执行一次!即复用。如果说非要进去!那么就要启动多个futuretask。
    在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成,
    当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。

一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。

仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,就不能再重新开始或取消计算。get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。

  1. 函数式编程:与面向对象的比较:
  2. Lamda表达式:函数式 编程。避免匿名内部类定义过多。
    拷贝中括号+写死右箭头+落地大括号。一个接口里面有且仅有一个方法的接口(才是函数式接口),才可以使用Lambda Express。
    (params)->expression (params)->statements (params)->{statements}
    如:new Thread({}->System.out.println(“多线程学习”)).start();
    用的线程比较少,只关注于线程体。
    Lambda表达式 简化线程(用一次)的使用。
    静态内部类,随着外部类的加载而加载;局部内部类,把类丢到方法内部来;匿名内部类 必须借助接口或者父类;jdk8 简化 lambda表达式 只需要关注线程体,删掉了接口名删掉了方法名,只需要关注你传什么参数,实现什么方法
  3. lambda推导,没有参数,没有返回值,lambda推导必须存在类型。类型(变量名)=()->{表达式};
  4. lambda推导 +参数,只要把方法 拷贝过来,不需要方法名;类型也可以拿掉;括号也省略;只有一行代码,花括号也省略。参数->表达式
  5. lambda推导 +参数+返回值,去掉类型,多个参数括号不能省略;假设只有一行代码,那么可以省掉return。(参数1,参数2)->表达式
  6. 线程状态:新生状态NEW、start()就绪状态、获得/失去执行权运行状态RUNNABLEyield()、run()方法结束死亡状态TERMINATED、阻塞状态(从运行状态wait()等待阻塞WAITING、TIME WAITINGsynchronized同步阻塞BLOCKED、sleep()/join()/IO流阻塞其他阻塞;等待阻塞notify()同步阻塞、同步阻塞锁可用【拿到对象的锁标记】就绪状态、其他阻塞sleep()休眠时间到/join()联合线程执行完毕/IO流阻塞结束就绪状态)

当线程执行wait()方法之后,线程进入等待状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时将会返回到运行状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态。
20. 新生状态(New):用new关键字建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,new 线程有了自己的工作空间,一个工作空间针对一个线程。
线程对象在构造的时候需要提供线程所需要的属性,如线程所属的线程组、线程优先级、是否是Daemon线程等信息。一个新构造的线程对象是由其parent线程来进行空间分配的,而child线程继承了parent是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程。至此,一个能够运行的线程对象就初始化好了,在堆内存中等待着运行。

  1. 就绪状态(Runnable):具备了运行条件,还没有分配到CPU,处于“线程就绪队列”,等待系统为其分配CPU。(有四种方法可以进入就绪状态1start()2阻塞解除3yield()4 JVM将CPU资源从本线程切换到其他线程。)
    线程start()方法的含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。
  2. 运行状态(Running):在运行状态的线程执行自己run方法中的代码。直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。
  3. 阻塞状态(Blocked):阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。(有四种方法可以进入阻塞状态1sleep(int millsecond)2wait()-notify()3join()-另一个线程执行完4read()write())
  4. 死亡状态(Terminated)1正常终止run()方法运行完毕2线程被强制终止stop() 或destory()当一个线程进入死亡状态以后,就不能再回到其它状态了。
  5. 线程方法:
    sleep()使线程停止运行一段时间,将处于阻塞状态。如果调用了sleep()方法后,没有其他等待执行的线程,这个时候当前线程不会马上恢复执行。
    join()阻塞指定线程等到另一个线程完成以后再继续执行。
    yield()让当前正在执行线程暂停,不是阻塞线程,而是将线程转入就绪状态;调用以后,如果没有其他等待执行的线程,此时当前线程马上就会恢复执行
    setDaemon()可以将指定的线程设置成后台线程,守护线程;创建用户线程的线程结束时,后台线程也将随之消亡;只能在线程启动之前把它设为后台线程
    setPriority(int newPriority) getPriority()线程的优先级代表的是概率 范围从1到10,默认为5
    stop()不推荐使用
  6. 终止线程的典型方式:终止线程我们一般不使用JDK提供的stop()/destroy()方法(它们本身也被JDK废弃了)。通常的做法是使用中断状态interrupt()或者提供一个boolean型的终止变量,当这个变量置为false,则终止线程的运行。1.线程类中定义线程体使用的标识2.线程体使用该标识3.对外提供方法改变标识

暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。这都是过期方法不建议使用,以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下。
27. 暂停线程执行:sleep(时间)指定当前线程阻塞的毫秒数;存在异常InterruptedException;时间达到后线程进入就绪状态;可以模拟网络延时、倒计时等;每个对象都有一个锁,sleep()不会释放锁:特点是抱着锁睡觉,站在马路中间谁都过不去。
sleep()方法与对象没关系,谁执行这个线程体,谁就去执行sleep()操作
28. 暂停线程:yield(),可以引起线程切换,但运行时没有明显延迟。礼让线程,让当前正在执行的线程暂停;不是阻塞线程,而是将线程从运行状态转入就绪状态,让CPU调度器重新调度,可能礼让成功,也可能又重回来调用自己了。即有时候礼让成功,有时候礼让不成功。
29. 暂停线程执行常用的方法有sleep()和yield()方法,这两个方法的区别是:
1.sleep()方法:可以让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态。
2.yield()方法:可以让正在运行的线程直接进入就绪状态,让出CPU的使用权。
30. 线程的联合:join(),合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞。又叫做插队线程,一个车插到别的车前面,别的车就得等插队车走后才能走,并且别的车进入阻塞状态。join用于让当前执行线程等待join线程执行结束。其实现原理是不停检查join线程是否存活,如果join线程存活则让当前线程永远等待。其中,wait(0)表示永远等待下去。直到join线程中止后,线程的this.notifyAll()方法会被调用,调用notifyAll()方法是在JVM里实现的。CountDownLatch也可以实现join的功能,并且比join的功能更多。join()写在谁A的run方法体中就阻塞谁,谁去调用join()谁B就去插队。
线程A在运行期间,可以线程B调用的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕后,才能继续执行。“爸爸线程A”要抽烟,于是联合了“儿子线程B”去买烟,必须等待“儿子线程B”买烟完毕,“爸爸线程A”才能继续抽烟。
如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(long millis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回。

  1. 线程优先级priority:Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程。线程调度器按照线程的优先级决定应该调度哪个线程来执行。线程优先级用数字来表示,范围从1到10。Thread.MIN_PRIORITY=1;Thread.MAX_PRIORITY =10;Thread.NORM_PRIORITY=5;默认为5。使用以下方法获得或设置线程对象的优先级:
    int getPriority()void setpriority(int newPriority)优先级的设定建议在start()调用前。优先级低只是意味着获得调度的概率低,并不是绝对先调用优先级高后调用优先级低的线程。线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java线程对于优先级的设定。

优先级高的线程分配时间片的数量要多于优先级低的线程。设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。
32. 获取线程基本信息的方法:isAlive()setName()getName()currentThread()
33. 线程组:ThreadGroup th=new ThreadGroup(“pg”);Thread t1=new Thread(th,new ThreadGroupName(),”T1”);使用构造函数指定线程所属的线程组th.activeCount()获得活动线程的总数 th.list()获得整个线程组中所有线程的信息
34. 守护线程:线程分为用户线程和守护线程;Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。虚拟机必须确保用户线程执行完毕;虚拟机不用等待守护线程执行完毕;如后台记录操作日志,监控内存使用等等。Thread.setDaemon(true)将线程设置为守护线程。Daemon属性需要在启动线程之前设置,不能在启动线程之后设置。

  1. Daemon线程被用作完成支持性工作,但是在Java虚拟机退出时Daemon线程中的finally块并不一定会执行,(main线程(非Daemon线程)在启动了线程DaemonRunner之后随着main方法执行完毕而终止,而此时Java虚拟机中已经没有非Daemon线程,虚拟机需要退出。Java虚拟机中的所有Daemon线程都需要立即终止,因此DaemonRunner立即终止,但是DaemonRunner中的finally块并没有执行。)在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。

  2. 线程中断:中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt()方法对其进行中断操作。线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返回false。
    从Java的API中可以看到,许多声明抛出InterruptedException的方法(例如Thread.sleep(long millis)方法)这些方法在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回false。

  3. 并发:同一个对象多个线程同时操作。(同时操作同一个账户;同时购买同一车次的票;操作容器)《比如说:多个代理同时去访问:有负数的情况、有相同的情况。分析:负数:临界值没有控制 相同的值:拷贝10的时候已经都拿到自己的工作台。》

  4. 线程同步:处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。这时候,我们就需要用到线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。

  5. 锁机制(synchronized):由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题。为了保证数据在方法中被访问时的正确性,在访问时加入了锁机制(synchronized),当一个线程获得对象的排他锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题:
    1.一个线程持有锁会导致其他所有需要此锁的线程挂起。
    2.在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
    3.如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。

  6. synchronized关键字:由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是
    synchronized关键字,它包括两种用法:synchronized方法和synchronized块。

  7. 同步方法:public synchonized void method(int args){}
    synchronized方法控制对"成员变量|类变量"对象的访问:每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞。方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方法方能获得该锁,重新进入可执行状态。缺陷:若将一个大的方法声明为synchonized将会大大影响效率。
    synchronized操作了什么,锁了对象的资源、而不是锁方法,锁是当前实例对象。而静态同步方法,锁是当前类的Class对象。

1 标准访问,请问先打印短信还是email?(两个线程同时访问同一个资源,谁先谁后是不一定的;在两个线程之间加上Thread.sleep(100);就能确定先后了)
也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
所有的非静态同步方法用的都是同一把锁——实例对象本身,
2 sendSMS()睡觉4秒钟,请问先打印短信还是email?(刚开始等了4秒,然后先短信后email)(同一个同学同一时刻只能使用手机的一个功能!)
也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
所有的非静态同步方法用的都是同一把锁——实例对象本身,
3 新增普通方法openPC,请问先打印短信还是openPhone?(先开机【因为这个没有同步方法啊】,等了4秒,然后打印短信)
加个普通方法后发现和同步锁无关
4 有两部手机,请问先打印短信还是email?(先打印邮件,等了4秒,然后打印短信)
可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。
换成两个对象后,不是同一把锁了,情况立刻变化。
所有的 非静态同步方法用的都是同一把锁——实例对象本身,
5 两个静态同步方法,同一部手机,请问先打印短信还是email?(等了4秒,先打印短信,然后打印邮件)
6 两个静态同步方法,2部手机,请问先打印短信还是email?(等了4秒,先打印短信,然后打印邮件)
但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!
所有的静态同步方法用的也是同一把锁——类对象本身,
7 1个静态同步方法,1个普通同步方法,同一部手机,请问先打印短信还是email?(先打印邮件,等了4秒,打印短信)
8 1个静态同步方法,1个普通同步方法,2部手机,请问先打印短信还是email?(先打印邮件,等了4秒,打印短信)
这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。

一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法
锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法

加个普通方法后发现和同步锁无关
换成两个对象后,不是同一把锁了,情况立刻变化。

都换成静态同步方法后,情况又变化
所有的非静态同步方法用的都是同一把锁——实例对象本身,

也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,
可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,
所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。

所有的静态同步方法用的也是同一把锁——类对象本身,
这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。
但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个实例对象的静态同步方法之间,
还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!
42. 同步块:synchronized(obj){},obj称之为同步监视器。
obj可以是任何对象,但是推荐使用共享资源作为同步监视器。
同步方法中无需执行同步监视器,因为同步方法的同步监视器是this即该对象本身,或class即类的模子。
同步监视器的执行过程:
第一个线程访问,锁定同步监视器,执行其中代码
第二个线程访问,发现同步监视器被锁定,无法访问
第一个线程访问完毕,解锁同步监视器
第二个线程访问,发现同步监视器未锁,锁定并访问。

方法里面的块:局部块
构造块:对象的信息
静态块:类的信息
同步块:
43. Synchronized的实现原理:JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。对于同步块的实现使用了monitorenter和monitorexit指令,而同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的。无论采用哪种方式,其本质是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态。

当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

锁释放与volatile写有相同的内存语义;锁获取与volatile读有相同的内存语义。

线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。•线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。

  1. 无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态
  2. 并发容器:CopyOnWriteArrayList
  3. 死锁:多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或多个线程都在等待对方释放资源,都停止执行的情形。某一个同步代码块同时拥有"两个以上对象的锁"时,就可能会发生死锁问题。

死锁: 过多的同步可能造成相互不释放资源。从而相互等待,一般发生于同步中持有多个对象的锁
避免: 不要在同一个代码块中,同时持有多个对象的锁解决方式:后一种 往外挪一下,不要锁套锁
47. 线程通信: volatile修饰成员变量,保证对线程的可见性。Synchronized修饰方法或块,保证多个线程在同一时刻只能有一个线程处于方法或块中,保证了线程对变量访问的可见性和排他性。
应用场景-生产者和消费者问题
假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中的产品取走消费。
如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止。
如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止。
分析:这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖互为条件。
对于生产者,没有生产产品之前,要通知消费者等待。而生产了产品之后,又要马上通知消费者消费。
对于消费者,在消费之后,要通知生产者已经消费结束,需要继续生产新的产品以供消费。
48. 在生产者消费者问题中,仅有synchronized是不够的:
synchronized可以阻止并发更新同一个共享资源,实现了同步。
synchronized不能用来实现不同线程之间的消息传递(通信)
49. 解决方法1:并发协作模型"生产者/消费者模式"–》管程法:生产者:负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)消费者:负责处理数据的模块(这里模块可能是:方法、对象、线程、进程) 缓冲区:消费者不能直接使用生产者的数据,他们都之间有个缓冲区。生产者将生产好的数据放入缓冲区,消费者从缓冲区中拿走要处理的数据。《根据容器进行交流的》缓冲区是实现并发的核心,缓冲区的设置有3个好处:
Ø 实现线程的并发协作
有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者生产的情况。 这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离。
Ø 解耦了生产者和消费者
生产者不需要和消费者直接打交道。
Ø 解决忙闲不均,提高效率
生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据 。
50. 解决方法2:并发协作模型"生产者/消费者模式"–》信号灯法:借助标志位
51. Java提供了三个方法解决线程之间的通信问题:等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
wait()表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁
wait(long timeout)指定等待的毫秒数
notify()唤醒一个处于等待状态的线程
notifyAll()唤醒同一个对象上所有调用
wait()方法的线程,优先级别高的线程优先调度
以上方法均是java.lang.Object类的方法;都只能在同步方法或者同步代码块中使用,否则会抛出异常

1) 使用wait()、notify()和notifyAll()时需要先对调用对象加锁。
2)调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列。
3)notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifAll()的线程释放锁之后,等待线程才有机会从wait()返回。
4)notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED。
5)从wait()方法返回的前提是获得了调用对象的锁。
从上述细节中可以看到,等待/通知机制依托于同步机制,其目的就是确保等待线程从wait()方法返回时能够感知到通知线程对变量做出的修改。WaitThread首先获取了对象的锁,然后调用对象的wait()方法,从而放弃了锁并进入了对象的等待队列WaitQueue中,进入等待状态。由于WaitThread释放了对象的锁,NotifyThread随后获取了对象的锁,并调用对象的notify()方法,将WaitThread从WaitQueue移到SynchronizedQueue中,此时WaitThread的状态变为阻塞状态。NotifyThread释放了锁之后,WaitThread再次获取到锁并从wait()方法返回继续执行。

2) 等待/通知的经典范式:
等待方(消费者)遵循如下原则。1)获取对象的锁。2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。3)条件满足则执行对应的逻辑。synchronized(对象) { while(条件不满足) { 对象.wait(); } 对应的处理逻辑 }
通知方遵循如下原则。1)获得对象的锁。2)改变条件。3)通知所有等待在对象上的线程。synchronized(对象) { 改变条件 对象.notifyAll(); }

3) 等待超时模式:假设超时时间段是T,那么可以推断出在当前时间now+T之后就会超时。这时仅需要wait(T)即可,在wait(T)返回之后会将执行T=FUTURE–now。如果T小于等于0,表示已经超时,直接退出,否则将继续执行wait(T)。可以看出,等待超时模式就是在等待/通知范式基础上增加了超时控制,这使得该模式相比原有范式更具有灵活性,因为即使方法执行时间过长,也不会“永久”阻塞调用者,而是会按照调用者的要求“按时”返回。

4) 使用等待超时模式来构造一个简单的数据库连接池,在示例中模拟从连接池中获取、使用和释放连接的过程,而客户端获取连接的过程被设定为等待超时的模式,也就是在1000毫秒内如果无法获取到可用连接,将会返回给客户端一个null。设定连接池的大小为10个,然后通过调节客户端的线程数来模拟无法获取连接的场景。

  1. java.util.Timer类:Timer类作用是类似闹钟的功能,也就是定时或者每隔一定时间触发一次线程。其实,Timer类本身实现的就是一个线程,只是这个线程是用来实现调用其它线程的。t1.schedule(task1,3000); //3秒后执行;t1.schedule(task1,5000,1000);//5秒以后每隔1秒执行一次!t1.schedule(task1,calendar1.getTime()); //指定时间定时执行;构造方法
    Timer() Timer(boolean isDaemon) Timer(String name) Timer(String name,boolean isDaemon)
    方法
    cancel() purge() schedule(TimerTask task,long delay) schedule(TimerTask task,long delay,long period)
    schedule(TimerTask task,Date time) schedule(TimerTask task,Date firsttime,long period)

  2. java.util.TimerTask类:TimerTask类是一个抽象类,该类实现了Runnable接口,所以该类具备多线程的能力。在这种实现方式中,通过继承TimerTask使该类获得多线程的能力,将需要多线程执行的代码书写在run方法内部,然后通过Timer类启动线程的执行。在实际使用时,一个Timer可以启动任意多个TimerTask实现的线程,但是多个线程之间会存在阻塞。所以如果多个线程之间需要完全独立的话,最好还是一个Timer启动一个TimerTask实现。构造方法
    TimeTask()
    方法
    cancel() run() scheduledExecutionTime()

  3. quanz:任务定时调度框架。Schdule调度器,控制所有的调度Trigger 触发条件,采用OSL模式JobDetail 需要处理的jobJob 执行逻辑
    DSL Domain-specific language领域特定语言,针对一个特定的领域,具有受限表达性的一种计算机程序语言,即领域专用语言,声明式编程:
    1Method Chaining方法链 Fluent Style流畅风格 builder模式构建器
    2Nested Functions 嵌套函数
    3Lambda Expressions/Closures
    4Functional Sequence

  4. happenbefore:在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。你写的代码可能根本没有按照你期望的顺序执行,因为编译器和CPU会尝试重排指令使得代码更快的运行
    第一步从内存中获取指令fetch将指令进行解码翻译
    第二步从寄存器中拿出对应的值、工作内存,需要拷贝
    第三步计算
    第四步同步到主存
    看到下一个指令与上一条无关,那么就提前执行了指令重排对我们的指令是有影响的
    执行代码的顺序可能与编写代码不一致,即虚拟机优化代码顺序,则为指令重排
    happen-before:即,编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段
    在虚拟机层面,为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则将程序编写顺序打乱–
    即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行–以即可能充分的利用CPU。

•对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。•对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求(JMM允许这种重排序)1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。•as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。•as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。
56. 虽然这里有两种情况:后面的代码先于前面的代码开始执行;前面的代码先开始执行,但当效率较慢的时候,后面的代码开始执行并先于前面的代码执行结束。
不管谁先开始,总之后面的代码在一些情况下存在先结束的可能。
在硬件层面,CPU会接收到的一些指令按照其规则重新排序,同样是基于CPU速度比缓存速度块的原因,和上面的一点的目的类似。只是虚拟机可以在更大层面更多指令范围内重新排序。

  1. 数据依赖:如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖。数据依赖分为以下三种类型:
    1:写后读
    2:写后写
    3:读后写
    以上三种情况,只要重新排序两个操作的执行顺序,程序的执行结果将会被改变,所以,编译器和处理器在重新排序时,会遵守数据依赖性,编译器和处理器不会改变数据依赖关系的两个操作的执行顺序。这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。

  2. 顺序一致性:一个线程中的所有操作必须按照程序的顺序来执行。(不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

  3. volitale:是Java虚拟机提供的轻量级的同步机制(轻量级的synchronized)。它能保证线程之间的变量的可见行,简单的说就是当一个线程对一个共享变量进行了修改后,另外一个线程能读到这个修改的值。比synchronized成本低因为它不会引起线程上下文切换和调度。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。它能保证可见性,不保证原子性,禁止指令重排。
    更详细的说是要符合以下两个规则:
    线程对变量进行修改之后,要立刻会写到主内存
    线程对变量读取的时候,要从主内存中读,而不是缓存(这样就形成了消息通信)
    Java线程—工作内存—save和load操作—主内存

  4. Volitale不保证原子性:对任意单个volatile变量的读写具有原子性,但类似于volatile++这种复合操作不具有原子性。原子性:不可分割、完整性,也就是某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败。拷贝回自己的内存空间,每个人都拿到0,写回到主内存时,线程1写回到的时候被挂起了,线程2的写回了。然后线程1恢复后又写回了一遍,把原来的1给覆盖了。如何解决:1.使用同步方法2.使用atomic类进行解决。

  5. Volitale禁止指令重排:从而避免多线程环境下程序出现乱序执行的现象。内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特定实现volatile的内存可见性)。

由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。

内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此在任何CPU上的线程都能读取到这些数据的最新版本。对volatile变量进行写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量值刷新回到主内存。对volatile变量进行读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。

《当第二个操作是volatile写时,不管第一个操作是什么都不能重排序。确保写之前的操作不会被编译重排序到写之后。
当第一个操作是volatile读时,不管第二个操作是什么都不能重排序。确保读之后的操作不会被编译重排序到读之前。
当第一个操作是volatile写,第二个操作是volatile读时,不能重排序,》

写:前StoreStore<禁止上面的普通写和下面的volitale写重排序>后StoreLoad<防止上面的volatile写和下面可能有的volitale读/写重排序> 读:后:LoadLoad<禁止下面的所有普通读操作和上面的volatile读重排序>后LoadStore<禁止下面所有的写操作和上面的volatile读重排序>

  1. JMM:Java内存模型(Java Memory Model),本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素—堆内存在线程之间共享)的访问方式。局部变量、方法定义参数、异常处理器参数不会在线程间共享,不会有内存可见性问题,不受内存模型的影响。

特性:可见性、原子性、有序性。

线程通信:线程之间以何种机制来交换信息。共享内存(线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信。)和消息传递(线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信。)。

线程同步:程序中用于控制不同线程间操作发生相对顺序的机制。共享内存(同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。)和消息传递(由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。)。

JMM是一个语言级的内存模型,处理器内存模型是硬件级的内存模型,顺序一致性内存模型是一个理论参考模型。

  1. JMM内存模型的可见性:由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(栈空间),工作内存是每个线程的私有数据区域。而JMM中规定所有的变量都存储在主内存,主内存时共享内存区域,所有的线程都可以访问。但线程对变量的操作必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。总结:各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的。《工作内存和主内存的同步延迟问题造成了可见性问题。》所以我们需要有一个机制:JMM内存模型的可见性,只要有一个线程改变数据后要写回到主内存中,其它的线程马上就会知道主内存中的数据已经改变了。

JMM关于同步的规定:1线程解锁前,必须把共享变量的值刷新回主内存2线程加锁前,必须读取主内存的最新值到自己的工作内存3加锁解锁是同一把锁。

  1. JMM内存模型的原子性:number++在多线程下是非线程安全的,如何不加synchronized解决?被拆分为3个指令:执行getfield拿到原始的number,执行iadd进行加1操作,执行putfield写把累加后的值写回。三个线程都拿到1,都在各自的工作内存中加1,写回到的时候,没有拿到最新的值就又写了,写覆盖。使用atomic类解决。

原子操作是指不可中断的一个或一系列的操作,一般的处理器是利用总线加锁或者缓存加锁的方式实现多处理器之间的原子操作。

总线锁定:处理器提供一个LOCK # 信号,刚一个处理器在总线上输出一个信号时,其它的处理器请求就会被阻塞住,那么该处理器就可以独占共享内存了。

缓存锁定:内存区域如果被缓存在处理器的缓存行中,在Lock操作期间被锁定,在执行锁操作回写到内存时,处理器不在总线上声言LOCK #信号,而是修改内存地址,缓存的一致性机制会阻止同时修改由2个以上处理器缓存的内存区数据,其他的处理器成功回写被锁定的缓存行数据时,会使缓存行无效,其它的处理器就不能在使用这个缓存行了。

Java中是通过使用锁和循环CAS(atomic包)的方式来实现原子操作。如:AtomiclBoolean、AtomicInteger、AtomicLong.。

CAS实现原子操作的三大问题:1. ABA问题->使用版本号,AtomicStampReference
2. 循环时间长开销大
3. 只能保证一个共享变量的原子操作,可以考虑把多个变量合成一个共享变量,或者使用锁:偏向锁,轻量级锁和互斥锁。除了偏向锁,其他实现锁的方式都使用了循环CAS获得和释放锁

  1. JMM内存模型的有序性:计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令重排,编译器优化的重排、指令并行的重排、内存系统的重排。单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致,处理器在执行重排序时必须考虑指令之间的数据依赖性。多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。Memory Barrier实现禁止指令重排。加了volatile之后是禁止指令重排。

  2. 线程安全性获得保证:对于工作内存和主内存同步延迟线程导致的可见性问题,使用synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。对于指令重排导致的可见性问题和有序性问题,可以利用volatile关键字解决,因为它的另外一个作用就是禁止重排序优化。

  3. Volitale应用:DCL单例模式(Double Check Lock双端检锁机制): 懒汉式套路基础上加入并发控制,在加锁之前和之后都进行一次检测,保证在多线程环境下,对外存在一个对象。在多线程坏境下,单例模式出现了问题,如果加上synchronized,在多线程的环境控制住了,但是太重了,并发性下降了。
    1、构造器私有化 -->避免外部new构造器
    2、提供私有的静态属性 -->存储对象的地址)(没有volatile其他线程可能访问一个没有初始化的对象,保证同步更新。)
    3、提供公共的静态方法 --> 获取属性(避免创建两个对象,所以这里同步,锁定这个class;1、开辟空间 //2、初始化对象信息 //3、返回对象的地址给引用)

如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此,可以大幅降低synchronized带来的性能开销。上面代码表面上看起来,似乎两全其美。
双端检测机制不一定线程安全,原因是有指令重排的存在,加入volatile可以禁止指令重排。原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。Instance=new Singleton();可以分为以下3个步骤完成:memory=allocate();instance(memory);instance=memory;1.分配对象内存空间2.初始化对象3.设置instance指向刚分配的内存之地,此时instance!=null。步骤2和3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的,所以就会导致对象还没有完成初始化,但是instance就不为null了,造成了线程安全问题。地址不为空,但是内容为空,所以要在instance变量上面加上volatile。
另一个并发执行的线程B就有可能在第4行判断instance不为null。线程B接下来将访问instance所引用的对象,但此时这个对象可能还没有被A线程初始化!此时,线程B将会访问到一个还未初始化的对象。在知晓了问题发生的根源之后,我们可以想出两个办法来实现线程安全的延迟初始化。1)不允许2和3重排序。2)允许2和3重排序,但不允许其他线程“看到”这个重排序。
把instance声明为volatile型,就可以实现线程安全的延迟初始化。2和3之间的重排序,在多线程环境中将会被禁止。

还有一种方式是通过静态内部类的方式实现:JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。2和3重排序,但不允许非构造线程(这里指线程B)“看到”这个重排序。Java初始化一个类或接口的处理过程如下:
第1阶段:通过在Class对象上同步(即获取Class对象的初始化锁),来控制类或接口的初始化。这个获取锁的线程会一直等待,直到当前线程能够获取到这个初始化锁。第2阶段:线程A执行类的初始化,同时线程B在初始化锁对应的condition上等待。第3阶段:线程A设置state=initialized,然后唤醒在condition中等待的所有线程。第4阶段:线程B结束类的初始化处理。第5阶段:线程C执行类的初始化的处理。

通过对比基于volatile的双重检查锁定的方案和基于类初始化的方案,我们会发现基于类初始化的方案的实现代码更简洁。但基于volatile的双重检查锁定的方案有一个额外的优势:除了可以对静态字段实现延迟初始化外,还可以对实例字段实现延迟初始化。字段延迟初始化降低了初始化类或创建实例的开销,但增加了访问被延迟初始化的字段的开销。在大多数时候,正常的初始化要优于延迟初始化。如果确实需要对实例字段使用线程安全的延迟初始化,请使用上面介绍的基于volatile的延迟初始化的方案;如果确实需要对静态字段使用线程安全的延迟初始化,请使用上面介绍的基于类初始化的方案。

  1. ThreadLocal:即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。
    1.在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程。
    2.ThreadLocal能够放一个线程级别的变量,其本身能够被多个线程共享使用,并且又能够达到线程安全的目的。说白了,ThreadLocal就是想在多线程环境下去保证成员变量的安全,常用的方法,就是get/set/initiaValue方法。
    3.JDK建议ThreadLocal定义为private static
    4.ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的方法都可以非常方便的访问这些资源(Hibernate的Session工具类HibernateUti、通过不同的线程对象设置Bean属性,保证各个线程Bean对象的独立性)

  2. ThreadLocal:每个线程自身的存储本地、局部区域get/set/initialValue
    ThreadLocal:每个线程自身的数据,更改不会影响其他线程
    ThreadLocal:分析上下文 环境 起点1、构造器: 哪里调用 就属于哪里 找线程体2、run方法:本线程自身的
    InheritableThreadLocal:继承上下文 环境的数据 ,拷贝一份给子线程

  3. 可重入锁:锁作为并发共享数据保证一致性的工具,大多数内置锁都是可以重入的,也就是说,如果每个线程试图获取一个已经由它自己持有的锁时,那么这个请求会立刻成功,并且会将这个锁的计数值加1,而当线程退出同步代码块时,计数器会递减,当计数值等于0时,锁释放。如果没有可重入锁的支持,在第二次企图获得锁时会进入死锁状态。可重入锁随处可见。

  4. CAS:对于并发控制而言,锁是一种悲观的策略,它总是假设每一次的临界区操作会产生冲突,于是对每次操作都小心翼翼,而无锁是一种乐观策略,它会假设对资源的访问是没有冲突的,自然不需要等待,所以所有的线程都可以在不停顿的状态下持续执行,遇到冲突的话,使用CAS来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。compare and set ,它是一条CPU并发原语。比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止。这个过程是原子的。《多个线程去操作主内存中的数据。一个叫做期望值、一个叫做更新值。主内存的值,一个线程拷贝回去自己的工作内存,对它进行修改,然后写回到主内存的时候,会进行比较和交换,如果和拷贝的数据一样的话,就将改变后的数据写回去;否则的话,就不进行写回。》 CAS并发原语体现在Java语言中sun.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。原语属于操作系统用语,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。例子:使用atomicInteger的compareAndSet()方法

  5. CAS底层原理?atomicInteger.getAndIncrement()方法的源代码public final int getAndIncrement(){return unsafe.getAndInt(this,valueoffset,1)}《AtomicInteger 的getandincrement方法底层其实是CAS思想,套的是unsafe类的CPU原语来保证原子性,底层思想是比较并交换,真实值和期望值相等就交换成功,否则就失败,失败就再来,直到比较成功为止。》
    compareAndSwapInt(Object o,long offset,int expected,int x)
    1.UnSafe类:UnSafe类是CAS的核心类,由于Java方法无法直接访问底层系统,需要本地(native)方法来访问,UnSafe相当于一个后门,基于该类可以直接操作特定内存的数据。UnSafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于UnSafe类的方法。注意UnSafe类中的所有方法都是native修饰的,也就是说UnSafe类中的方法都直接调用操作系统底层资源执行相应任务。

2.变量valueOffset:表示该变量值在内存中的偏移地址,因为UnSafe就是根据内存偏移地址获取数据的。

3.变量value用volatile修饰,保证了多线程之间的内存可见性。

Unsafe.getAndAddInt():
public final int getAndAddInt(Object var1,long var2,int var4){int var5;do{var5=this.getIntVolatile(var1,var2);}while(!this.compareAndSwapInt(var1,var2,var5,var5+var4));return vat5;}
var1:AtomicInteger对象本身var2:该对象指的引用地址var4:需要变动的数量var5:是用var1var2找出的主内存中真实的值。用该对象当前的值与var5比较,如果相同,更新var5+var4并且返回true,如果不同,继续取值然后再比较,直到更新完成。

如果是多线程同时执行getAndAddInt()方法,假设线程A和线程B同时执行getAndAddInt操作:1、AtomicInteger里面的value原始值为3,即主内存中的value值为3,根据JMM模型,线程A和线程B各自持有一份值为3的副本分别到各自的工作内存。2、线程A通过getIntVolatile(var1,var2)拿到value值3,此时线程A被挂起。3、线程B通过getIntVolatile(var1,var2)拿到value值3,此时线程B没被挂起并执行compareAndSwapInt方法比较内存值也是3,成功被修改内存值为4,线程B收工。4、此时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值3和主内存中的额值4不一致,说明该值已经被其他线程抢先一步修改过了,那么线程A本次修改失败,只能重新读取重新来一遍了5、线程A重新获取value值,因为变量vlaue被volatile修饰,所以其他线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较并替换,直到成功。

  1. CAS缺点:1.循环时间长开销很大,使用dowhile语句,如果CAS失败会一直尝试,如果CAS长时间一直不成功,可能会被CPU带来很大的开销。2.只能保证一个共享变量的原子操作。对于多个共享变量操作,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。3.引出ABA问题。原子类AtomicInteger的ABA问题。
    CAS—UnSafe—CAS底层原理—ABA—原子引用更新—如何规避ABA问题
  2. 锁分为两种:悲观锁:synchronized是独占锁即悲观锁,会导致其他所有需要锁的线程挂起,等待持有锁的线程释放锁。乐观锁:每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止-CAS。
  3. 乐观锁的实现:
    有三个值:一个当前内存值V、旧的预期值A、将更新的值B。先获取到内存当中当前的内存值V,
    再将内存值V和原值A进行比较,要是相等就修改为要修改的值B并返回true,否则什么都不做,并返回false
    CAS是一组原子操作,不会被外部打断;
    属于硬件级别的操作(利用CPU的CAS指令,同时借助JNI来完成的非阻塞算法),效率比加锁操作高。
  4. ABA问题:如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了么?
    如果在这段期间曾经被改成B,然后又改回A,那么CAS操作就会误认为它从来没有被修改过。
  5. 原子类AtomicInteger的ABA问题:CAS会导致ABA问题,CAS算法实现一个重要前期需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。一个线程1从内存位置V中取出A,另一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B,然后线程2又将V位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,线程1操作成功。但整个过程是有问题的。
  6. AtomicReference原子引用:原子引用的泛型类AtomicReference a = new AtomicReference<>(); a.compareAndSet(z2,li4);
  7. AtomicStampedReference时间戳原子引用:–解决ABA问题,就是修改版本号,类似于时间戳。T1 100 1 T2 100 1 T2 101 2 T2 100 3 T1 1111 2AtomicStampedReference atomicStampedReference = new AtomicStampedReference<>(100,1);atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
  8. 13个原子操作类Atomic包里的类基本都是使用Unsafe实现的包装类。
    1 原子更新基本类型类
    •AtomicBoolean:原子更新布尔类型。
    •AtomicInteger:原子更新整型。
    •AtomicLong:原子更新长整型。
    int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。void lazySet(int newValue):最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。

2 原子更新数组
•AtomicIntegerArray:原子更新整型数组里的元素。
•AtomicLongArray:原子更新长整型数组里的元素。
•AtomicReferenceArray:原子更新引用类型数组里的元素。
int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加。boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。
3 原子更新引用
AtomicReference:原子更新引用类型。
AtomicReferenceFieldUpdater:原子更新引用类型里的字段
AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法:AtomicMarkableReference(V initialRef,Boolean initialMark)
4 原子更新字段类
•AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
•AtomicLongFieldUpdater:原子更新长整型字段的更新器。
AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。
要想原子地更新字段类需要两步。第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新类的字段(属性)必须使用public volatile修饰符。

  1. 并发集合:ConcurrentHashMap CopyOnWriteArrayList ConcurrentLinkedQueue BlockingQueue ConcurrentSkipListMap
  2. 跳表:是一种可以用来快速查找的数据结构,有点类似于平衡树。他们都可以对元素进行快速的查找,区别:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整。而对跳表的插入和删除只需要对整个数据结构的局部进行操作就可以。用跳表实现一个Map。

跳表的另一个特点是随机算法,本质是同时维护了多个链表,并且链表是分层的。
最低层的链表维护了跳表内所有的元素,没上面一层链表都是下面一层的子集,一个元素插入哪些层是完全随机的。跳表内的所有链表的元素都是排序的,查找时,可以从顶级链表开始找,一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续找。以空间换时间的算法。
ConcurrentSkipListMap Node(key,value,next) 对node的所有操作使用的CAS方法
Index(Node,down,right) HeadIndex(表示链表头部的第一个index)

  1. ArrayList是线程不安全的,故障现象:java.util.ConcurrentModificationException。导致原因:并发争夺修改导致。解决方案:1、new Vector() 2、Collections.synchronizedList(new ArrayList<>())3、new CopyOnWriteArrayList()
  2. 写时复制容器:读写分离思想,private transient volatile Object[] array;然后add()方法中使用了重用锁,public Boolean add(E e){final ReentrantLock lock=this.lock; lock.lock(); try{Object[] elements=getArray(); int len=elements.length; Object[] newElements=Arrays.copyOf(elements,len+1); newElements[len]=e; setArray(newElements); return true;} finally{ lock.unlock();}}往一个容器添加元素时,不直接往当前容器Object[]中添加,而是先将当前容器Object[]进行Copy,复制出一个新的容器Object[] newElements,然后新的容器中添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以是一种读写分离的思想,读和写不同的容器。

读:读写锁中读操作会受到写操作的阻碍。为了让读的性能发挥到极致。写时复制是的写时完全不用加锁的,写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。就是当列表需要修改时,我并不修改原有的内容而是对原有的数据进行一次复制,将修改的内容写入副本中,写完之后,再将修改完的副本替换原有的数据。这样可以保证写操作不会影响读了。
85. HashSet线程不安全,解决:1Collections.synchroniedSet(new HashSet<>())2CopyOnWriteArraySet,它的底层是CopyOnWriteArrayList
86. HashMap线程不安全,解决:ConcurrentHashMap<>()
在并发编程中使用HashMap可能导致程序死循环。而使用线程安全的HashTable效率又非常低下,基于以上两个原因,便有了ConcurrentHashMap的登场机会。ConcurrentHashMap的锁分段技术可有效提升并发访问率:HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。一个ConcurrentHashMap被进一步细分为16个段。

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

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁。

ConcurrentHashMap初始化方法是通过initialCapacity、loadFactor和concurrencyLevel等几个参数来初始化segment数组、段偏移量segmentShift、段掩码segmentMask和每个segment里的HashEntry数组来实现的。
1.初始化segments数组segments数组的长度ssize是通过concurrencyLevel计算得出的。为了能通过按位与的散列算法来定位segments数组的索引,必须保证segments数组的长度是2的N次方(power-of-two size),所以必须计算出一个大于或等于concurrencyLevel的最小的2的N次方值来作为segments数组的长度。
2.初始化segmentShift和segmentMask
3.初始化每个segment输入参数initialCapacity是ConcurrentHashMap的初始化容量,loadfactor是每个segment的负载因子,在构造方法里需要通过这两个参数来初始化数组中的每个segment。

定位Segment:既然ConcurrentHashMap使用分段锁Segment来保护不同段的数据,那么在插入和获取元素的时候,必须先通过散列算法定位到Segment。可以看到ConcurrentHashMap会首先使用Wang/Jenkins hash的变种算法对元素的hashCode进行一次再散列。之所以进行再散列,目的是减少散列冲突,使元素能够均匀地分布在不同的Segment上,从而提高容器的存取效率。假如散列的质量差到极点,那么所有的元素都在一个Segment中,不仅存取元素缓慢,分段锁也会失去意义。

ConcurrentHashMap的操作:get操作、put操作和size操作。
get操作:Segment的get操作实现非常简单和高效。先经过一次再散列,然后使用这个散列值通过散列运算定位到Segment,再通过散列算法定位到元素。get操作的高效之处在于整个get过程不需要加锁,除非读到的值是空才会加锁重读。我们知道HashTable容器的get方法是需要加锁的,那么ConcurrentHashMap的get操作是如何做到不加锁的呢?原因是它的get方法里将要使用的共享变量都定义成volatile类型,如用于统计当前Segement大小的count字段和用于存储值的HashEntry的value。定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写(有一种情况可以被多线程写,就是写入的值不依赖于原值),在get操作里只需要读不需要写共享变量count和value,所以可以不用加锁。之所以不会读到过期的值,是因为根据Java内存模型的happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。

put操作:由于put方法里需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须加锁。put方法首先定位到Segment,然后在Segment里进行插入操作。插入操作需要经历两个步骤,第一步判断是否需要对Segment里的HashEntry数组进行扩容,第二步定位添加元素的位置,然后将其放在HashEntry数组里。(1)是否需要扩容在插入元素前会先判断Segment里的HashEntry数组是否超过容量(threshold),如果超过阈值,则对数组进行扩容。值得一提的是,Segment的扩容判断比HashMap更恰当,因为HashMap是在插入元素后判断元素是否已经到达容量的,如果到达了就进行扩容,但是很有可能扩容之后没有新元素插入,这时HashMap就进行了一次无效的扩容。(2)如何扩容 在扩容的时候,首先会创建一个容量是原来容量两倍的数组,然后将原数组里的元素进行再散列后插入到新的数组里。为了高效,ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容。

size操作:如果要统计整个ConcurrentHashMap里元素的大小,就必须统计所有Segment里元素的大小后求和。Segment里的全局变量count是一个volatile变量,那么在多线程场景下,是不是直接把所有Segment的count相加就可以得到整个ConcurrentHashMap大小了呢?不是的,虽然相加时可以获取每个Segment的count的最新值,但是可能累加前使用的count发生了变化,那么统计结果就不准了。所以,最安全的做法是在统计size的时候把所有Segment的put、remove和clean方法全部锁住,但是这种做法显然非常低效。因为在累加count操作过程中,之前累加过的count发生变化的几率非常小,所以ConcurrentHashMap的做法是先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。那么ConcurrentHashMap是如何判断在统计的时候容器是否发生了变化呢?使用modCount变量,在put、remove和clean方法里操作元素前都会将变量modCount进行加1,那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。

  1. ConcurrentLinkedQueue: 如何使用非阻塞的方式来实现线程安全队列ConcurrentLinkedQueue。ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部;当我们获取一个元素时,它会返回队列头部的元素。它采用了“wait-free”算法(即CAS算法)来实现,该算法在Michael&Scott算法上进行了一些修改。

ConcurrentLinkedQueue由head节点和tail节点组成,每个节点(Node)由节点元素(item)和指向下一个节点(next)的引用组成,节点与节点之间就是通过这个next关联起来,从而组成一张链表结构的队列。默认情况下head节点存储的元素为空,tail节点等于head节点。

入队列就是将入队节点添加到队列的尾部。整个入队过程主要做两件事情:第一是定位出尾节点;第二是使用CAS算法将入队节点设置成尾节点的next节点,如不成功则重试。
一、定位尾节点:tail节点并不总是尾节点,所以每次入队都必须先通过tail节点来找到尾节点。尾节点可能是tail节点,也可能是tail节点的next节点。代码中循环体中的第一个if就是判断tail是否有next节点,有则表示next节点可能是尾节点。获取tail节点的next节点需要注意的是p节点等于p的next节点的情况,只有一种可能就是p节点和p的next节点都等于空,表示这个队列刚初始化,正准备添加节点,所以需要返回head节点。
二、设置入队节点为尾节点:p.casNext(null,n)方法用于将入队节点设置为当前队列尾节点的next节点,如果p是null,表示p是当前队列的尾节点,如果不为null,表示有其他线程更新了尾节点,则需要重新获取当前队列的尾节点。《底层还是unsafe.compareAndSwapObject()》

出队列:队列的就是从队列里返回一个节点元素,并清空该节点对元素的引用。首先获取头节点的元素,然后判断头节点元素是否为空,如果为空,表示另外一个线程已经进行了一次出队操作将该节点的元素取走,如果不为空,则使用CAS的方式将头节点的引用设置成null,如果CAS成功,则直接返回头节点的元素,如果不成功,表示另外一个线程已经进行了一次出队操作更新了head节点,导致元素发生了变化,需要重新获取头节点。

  1. 传值还是传引用问题:age属于main方法的,然后调用方法时复印了一份传给它,然后方法把复印件给改动了,我只是给你复印了一份值,原件根本没动,所以第一个age还是20;person是main的,传引用传内存地址给方法,两个引用指向了同一个地址,这时把这个地址的值改动了;str是属于main方法的,这个池子里有了abc这个池子里面没有xxx,那么就重新创建一个指向它。

  2. 公平锁:是指多个线程按照申请锁的顺序来获取锁,先来后到。

  3. 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象。

  4. 公平锁和非公平锁的区别:并发包ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是非公平锁,无参数:非公平锁,有参数true:公平锁。公平锁就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,此后会按照FIFO的规则从队列中取到自己。非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。ReentrantLock,非公平锁的优点在于吞吐量比公平锁大,而公平锁要求系统维护一个有序队列,成功高性能低。Synchronized,等同于锁,是一种非公平锁。

  5. 可重入锁(又称递归锁):ReentrantLock。指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。ReentrantLock与Synchronized都是典型的可重入锁。可重入锁的最大的作用是避免死锁。

  6. 自旋锁:SpinLock,Unsafe类+CAS思想。是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。getAndAddInt例子。通过CAS操作完成自旋锁例子:A线程先进来调用mylock方法自己持有锁5秒钟,B随后进来发现当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随即抢到。A线程进来,发现期望的是空的,那么while的条件就是false,于是不进入循环,直接拿到了锁。B线程进来,发现期望的值不是空,那么while的条件就是true,于是它进入锁中,一直会循环的判断,直到期望的值是空了,才能推出循环,获得锁。AtomicReference atomicReference = new AtomicReference<>();加锁:while (!atomicReference.compareAndSet(null,thread)){}解锁:atomicReference.compareAndSet(thread,null);

  7. 独占锁:指该锁一次只能被一个线程所持有。ReentrantLock和Synchronized而言都是独占锁。

  8. 共享锁:指该锁可以被多个线程所持有。对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。读锁的共享锁可保证并发读是非常高效的,读写、写读、写写的过程是互斥的。以前使用锁和synchronized读和写通通不能并发执行,数据一致量可以保证,但并发性急剧下降。锁不能进行细粒度的划分,只能把全部进行封杀。读写锁:多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时读,但是,如果有一个线程想去写共享资源来,就不应该再有其他线程可以对该资源进行读或写。private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();readWriteLock.writeLock().lock();readWriteLock.writeLock().unlock();readWriteLock.readLock().lock();readWriteLock.readLock().unlock();

  9. Java中的并发工具类:CountDownLatch、CyclicBarrier、Semaphore。工具类提供了一种并发流程控制的手段。Exchanger工具类则提供了在线程间交换数据的一种手段。

  10. CountDownLatch:让一个或一些线程阻塞直到另一些线程完成一系列操作之后才被唤醒。CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N。主要有两个方法,其他线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),当一个或多个线程调用await方法时,调用线程会被阻塞。当计数器的变为零时,因调用await方法被阻塞的线程会被唤醒,继续执行。《做减法、倒计时(教室关灯)》如果有某个解析sheet的线程处理得比较慢,我们不可能让主线程一直等待,所以可以使用另外一个带指定时间的await方法——await(long time,TimeUnit unit),这个方法等待特定时间后,就会不再阻塞当前线程。join也有类似的方法。

  11. CyclicBarrier:可循环使用的屏障。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。CyclicBarrier还提供一个更高级的构造函数CyclicBarrier(int parties,Runnable barrier-Action),用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景。线程进入屏障通过CyclicBarrier的await()方法。《做加法、累积(收集龙珠)》
    CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
    System.out.println(“召唤龙珠”);
    }); cyclicBarrier.await();
    CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。

  12. CyclicBarrier和CountDownLatch的区别:1.CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景。例如,如果计算发生错误,可以重置计数器,并让线程重新执行一次。2. CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得Cyclic-Barrier阻塞的线程数量。isBroken()方法用来了解阻塞的线程是否被中断。

  13. Semaphore:《信号量是对锁的扩展,无论是内部锁synchronized还是重入锁ReentreantLock,都是一次只允许一个线程访问一个资源,而信号量却可以指定多个线程同时访问某一个资源。》信号量是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
    引用:Semaphore可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。假如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有10个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,就可以使用Semaphore来做流量控制。在代码中,虽然有30个线程在执行,但是只允许10个并发执行。
    Semaphore的构造方法Semaphore(int permits)Semaphore(int permits,Boolean fair)接受一个整型的数字,表示可用的许可证数量。Semaphore(10)表示允许10个线程获取许可证,也就是最大并发数是10。
    Semaphore的用法也很简单,首先线程使用Semaphore的acquire()方法获取一个许可证,若无法获得则线程会等待,直到有线程释放一个许可或者当前线程被中断。使用完之后调用release()方法归还许可证。还可以用tryAcquire()方法尝试获取许可证,成功true失败false,它不会等待,而是立即返回。Semaphore还提供一些其他方法:acquireUninterruptibly()\tryAcquire(long,unit)。intavailablePermits():返回此信号量中当前可用的许可证数。intgetQueueLength():返回正在等待获取许可证的线程数。intgetQueueLength():返回正在等待获取许可证的线程数。booleanhasQueuedThreads():是否有线程正在等待获取许可证。void reducePermits(int reduction):减少reduction个许可证,是个protected方法。Collection getQueuedThreads():返回所有等待获取许可证的线程集合,是个protected方法。

信号量主要用于两个目的,一是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。《多个线程抢多个资源,信号灯,资源为1时就退化成synchronized。》acquire()方法用于:抢占信号量获得信号的允许,阻塞直到信号量可用releanse()方法用于:释放信号量。

  1. 线程间交换数据的Exchanger:Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。Exchanger可以用于遗传算法,遗传算法里需要选出两个人作为交配对象,这时候会交换两人的数据,并使用交叉规则得出2个交配结果。Exchanger也可以用于校对工作,比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水,为了避免错误,采用AB岗两人进行录入,录入到Excel之后,系统需要加载这两个Excel,并对两个Excel数据进行校对,看看是否录入一致。如果两个线程有一个没有执行exchange()方法,则会一直等待,如果担心有特殊情况发生,避免一直等待,可以使用exchange(V x,longtimeout,TimeUnit unit)设置最大等待时长。

  2. 阻塞队列:首先它是一个队列,而一个阻塞队列在数据结构中所起的作用应该是,线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素。当阻塞队列是空时,从队列中获取元素的操作将会被阻塞;当阻塞队列是满时,往队列里添加元素的操作将会被阻塞。试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素;试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程从队列中移除一个或者多个元素或者完全清空队列后使队列重新变得空闲起来并后续新增。阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。

  3. 阻塞队列的作用和好处:在多线程领域,所谓阻塞,就是在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒。好处是:我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切阻塞队列都给你一手包办了。在concurrent包发布之前,在多线程环境下,我们每个程序员都必须自己去控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。

  4. 阻塞队列架构:Iterable->Collection->Queue->BlockingQueue->LinkedBlockingDeque/PriorityBlockingQueue/SynchronousQueue/DelayQueue/ArrayblockingQueue/LinkedTransferQueue

  5. ArrayblockingQueue:由数组结构组成的有界阻塞队列。
    LinkedBlockingQueue :由链表结构组成的无界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。
    PriorityBlockingQueue :支持优先级排序的无界阻塞队列。默认情况下元素采取自然顺序升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。
    DelayQueue :使用优先级队列实现的延迟无界阻塞队列。
    SynchronousQueue :不存储元素的阻塞对垒,也即单个元素的对垒。
    LinkedTransferQueue :由链表结构组成的无界阻塞队列。
    LinkedBlockingDeque:由链表结构组成的双向阻塞队列。

  6. ArrayblockingQueue:ArrayBlockingQueue是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证线程公平的访问队列,所谓公平访问队列是指阻塞的线程,可以按照阻塞的先后顺序访问队列,即先阻塞线程先访问队列。非公平性是对先等待的线程是非公平的,当队列可用时,阻塞的线程都可以争夺访问队列的资格,有可能先阻塞的线程最后才访问队列。为了保证公平性,通常会降低吞吐量。我们可以使用以下代码创建一个公平的阻塞队列。ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true); 访问者的公平性是使用可重入锁实现的。

  7. LinkedBlockingQueue:锁分离的案例。Take()和put()分别实现了从队列中取数据和往队列中增加数据的功能。分别作用于队列的前端和尾端,理论上两者不冲突,如果使用独占锁在运行时就不是真正的并发。所以使用两把不同的锁。分裂这两个操作。takeLock putLock notEmpty notFull

  8. DelayQueue:DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。DelayQueue非常有用,可以将DelayQueue运用在以下应用场景。•缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。•定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,比如TimerQueue就是使用DelayQueue实现的。(1)如何实现Delayed接口。DelayQueue队列的元素必须实现Delayed接口。我们可以参考ScheduledThreadPoolExecutor里ScheduledFutureTask类的实现,一共有三步。第一步:在对象创建的时候,初始化基本数据。使用time记录当前对象延迟到什么时候可以使用,使用sequenceNumber来标识元素在队列中的先后顺序。第二步:实现getDelay方法,该方法返回当前元素还需要延时多长时间,单位是纳秒。第三步:实现compareTo方法来指定元素的顺序。例如,让延时时间最长的放在队列的末尾。(2)如何实现延时阻塞队列延时阻塞队列的实现很简单,当消费者从队列里获取元素时,如果元素没有达到延时时间,就阻塞当前线程。

  9. LinkedTransferQueue:LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法。(1)transfer方法:如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回。(2)tryTransfer方法:tryTransfer方法是用来试探生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回,而transfer方法是必须等到消费者消费了才返回。对于带有时间限制的tryTransfer(E e,long timeout,TimeUnit unit)方法,试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回false,如果在超时时间内消费了元素,则返回true。

  10. LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列。所谓双向队列指的是可以从队列的两端插入和移出元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其他的阻塞队列,LinkedBlockingDeque多了addFirst、addLast、offerFirst、offerLast、peekFirst和peekLast等方法,以First单词结尾的方法,表示插入、获取(peek)或移除双端队列的第一个元素。以Last单词结尾的方法,表示插入、获取或移除双端队列的最后一个元素。另外,插入方法add等同于addLast,移除方法remove等效于removeFirst。但是take方法却等同于takeFirst,不知道是不是JDK的bug,使用时还是用带有First和Last后缀的方法更清楚。在初始化LinkedBlockingDeque时可以设置容量防止其过度膨胀。另外,双向阻塞队列可以运用在“工作窃取”模式中。

  11. 阻塞队列的实现原理:使用通知模式实现。所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。通过查看JDK源码发现ArrayBlockingQueue使用了Condition来实现。

  12. BlockingQueue的核心方法:插入add(e)成功会返回true当阻塞队列满时再加会抛出illegalStateException:Queue full。 offer(e)插入成功返回true失败返回false。 put(e)当阻塞队列为满时生产者线程继续往队列里加,队列会一直阻塞生产线程直到中断退出。offer(e,time,unit)当阻塞队列满时,队列会阻塞生产者线程一定时间,超时后生产者线程会退出。
    移除remove()成功会返回对应元素当阻塞队列为空时再移除会抛出NoSuchElementException。 poll()移除成功返回出队列的元素,队列里没有就返回null。 take()当阻塞队列为空时,消费者线程试图从队列里取元素队列会一直阻塞消费者线程直到队列可用。 poll(time,unit)
    检查element()检查队列的首元素是谁,只是显示是谁并不是弹出,若队列为空则抛出NoSuchElementException peek()检查队列的首元素是谁,只是显示是谁并不是弹出,若队列为空则显示null

  13. SynchronousQueue:同步阻塞对列,没有容量,它是一个不存储元素的BlockingQueue,每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。它支持公平访问队列。默认情况下线程采用非公平性策略访问队列。使用以下构造方法可以创建公平性访问的SynchronousQueue,如果设置为true,则等待的线程会采用先进先出的顺序访问队列。SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合传递性场景。Transfer()_SynchronousQueue的吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue。

  14. 阻塞队列的用途:一、生产者消费者模式 二、线程池 三、消息中间件

  15. 生产者消费者模式传统版:synchronized wait notify 或 lock condition await singal例子:一个初始值为0的变量,两个线程对其交替操作,一个加1一个减1,来五轮。线程操作资源类、资源类判断、干活、通知。防止虚假唤醒机制。所以说,使用await()时必须放在循环中!

  16. Lock接口:Lock能够提供比使用synchronized更加广泛的锁操作。它允许更灵活的结构,可以有不同的属性,可以支持与多个Condition对象相关联。《锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。针对一个场景,手把手进行锁获取和释放,先获得锁A,然后再获取锁B,当锁B获得后,释放锁A同时获取锁C,当锁C获得后,再释放B同时获取锁D,以此类推。这种场景下,synchronized关键字就不那么容易实现了,而使用Lock却容易许多。》不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。

方法:lock() lockInteruptibly() tryLock() unlock() newCondition()
通过聚合了一个同步器的子类来完成线程访问控制的。

  1. 队列同步器:AbstractQueuedSynchronizer是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
    同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()获取当前同步状态、setState(int newState)设置当前同步状态和compareAndSetState(int expect,int update)使用CAS设置当前状态保证状态设置的原子性)来进行操作,因为它们能够保证状态的改变是安全的。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等 )。
    同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。

同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。同步器可重写的方法:tryAcquire(int) tryRelease(int) tryAcquireShared(int) tryReleaseShared(int) 同步器提供的模板方法:同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放同步状态和查询同步队列中的等待线程情况。自定义同步组件将使用同步器提供的模板方法来实现自己的同步语义。

同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点。同步器包含了两个节点类型的引用,一个指向头节点,而另一个指向尾节点。 同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node update),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。

独占式同步状态获取与释放:通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出,主要逻辑是:首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。总结:在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。

共享式同步状态获取与释放:共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。
独占式超时获取同步状态:

  1. ReentrantLock类:就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。实现Lock接口。Lock对象调用lock()方法上锁,调用unlock()方法解锁。
    方法:lock()获得锁,如果锁已经被占用则等待、lockInterruptibly()获得锁,但优先响应中断、tryLock()尝试获得锁如果成功返回true,失败false,该方法不等待立即返回、tryLock(long,unit)在给定时间内尝试获得锁、unlock()释放锁

ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer(本文简称之为AQS)。AQS使用一个整型的volatile变量(命名为state)来维护同步状态,马上我们会看到,这个volatile变量是ReentrantLock内存语义实现的关键。

使用公平锁时,加锁方法lock()调用轨迹如下。1)ReentrantLock:lock()。2)FairSync:lock()。3)AbstractQueuedSynchronizer:acquire(int arg)。4)ReentrantLock:tryAcquire(int acquires)。在第4步真正开始加锁,从上面源代码中我们可以看出,加锁方法首先读volatile变量state。
在使用公平锁时,解锁方法unlock()调用轨迹如下。1)ReentrantLock:unlock()。2)AbstractQueuedSynchronizer:release(int arg)。3)Sync:tryRelease(int releases)。在第3步真正开始释放锁,从上面的源代码可以看出,在释放锁的最后写volatile变量state。公平锁在释放锁的最后写volatile变量state,在获取锁时首先读这个volatile变量。根据volatile的happens-before规则,释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程读取同一个volatile变量后将立即变得对获取锁的线程可见。

非公平锁的释放和公平锁完全一样,所以这里仅仅分析非公平锁的获取。使用非公平锁时,加锁方法lock()调用轨迹如下。1)ReentrantLock:lock()。2)NonfairSync:lock()。3)AbstractQueuedSynchronizer:compareAndSetState(int expect,int update)。在第3步真正开始加锁。该方法以原子操作的方式更新state变量,本文把Java的compareAndSet()方法调用简称为CAS,如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。此操作具有volatile读和写的内存语义。编译器不会对volatile读与volatile读后面的任意内存操作重排序;编译器不会对volatile写与volatile写前面的任意内存操作重排序。组合这两个条件,意味着为了同时实现volatile读和volatile写的内存语义,编译器不能对CAS与CAS前面和后面的任意内存操作重排序。

总结:•公平锁和非公平锁释放时,最后都要写一个volatile变量state。•公平锁获取时,首先会去读volatile变量。•非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile写的内存语义。

原子状态:使用CAS存储当前锁的状态,判断锁是否已经被别的线程持有。
等待队列:所有没有请求到锁的线程,会进入等待队列进行等待。待有线程释放锁后,系统就从队列中唤醒一个线程,继续工作。
阻塞原语park()/unpark():用来挂起和恢复线程,没有得到锁的线程会被挂起。

  1. LockSupport工具:LockSupport定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也成为构建同步组件的基础工具。《与Thread.suspend()相比,它弥补了由于rersume()在前发生导致线程无法继续执行的情况;和Object.wait()相比,它不需要先获得某个对象的锁也不会抛出异常》LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark(Thread thread)方法来唤醒一个被阻塞的线程。

  2. ReentrantLock是如何实现重进入:重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决以下两个问题。1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。2)锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。ReentrantLock是通过组合自定义同步器来实现锁的获取与释放,该方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。成功获取锁的线程再次获取锁,只是增加了同步状态值,这也就要求ReentrantLock在释放同步状态时减少同步状态值如果该锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false,而只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。

  3. 公平性获取锁的特性:公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。tryAcquire方法与nonfairTryAcquire(int acquires)比较,唯一不同的位置为判断条件多了hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

  4. 读写锁ReentrantReadWriteLock:ReadWriteLock仅定义了获取读锁和写锁的两个方法,即readLock()方法和writeLock()方法。
    读读不互斥、读写互斥、写写互斥。如果系统中读操作次数远远大于写操作在,则读写锁就可以发挥最大的功效。

读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态。
写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。该方法除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。
读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。获取读锁的实现从Java 5到Java 6变得复杂许多,主要原因是新增了一些功能,例如getReadHoldCount()方法,作用是返回当前线程获取读锁的次数。读状态是所有线程获取读锁次数的总和,而个线程各自获取读锁的次数只能选择保存在ThreadLocal中,由线程自身维护,这使获取读锁的实现变得复杂。在tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是(1<<16)。
锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

  1. Condition接口:《任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式》
    通过lock对象调用newConditon()方法产生,利用condition对象,我们就可以让线程在合适的时间等待或者在某一个特定时间得到通知继续执行。condition对象调用await()等待当前线程会释放锁并在此等待,或者当前线程被中断也能跳出等待。condition对象调用signalAll()唤醒其他线程调用通知当前线程后,当前线程才从await方法返回并在返回前已经获取了锁。一个锁可以锁定多个Condition,实现分组唤醒,用谁去调用await()谁等待,用谁去调用signal()谁被精准唤醒。
    方法:await()、awaitUninterruptibly()、awaitNanos(long)、await(long,unit)、awaitUtil(Date)、signal()、signalAll()

ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个队列(以下称为等待队列),该队列是Condition对象实现等待/通知功能的关键。
等待队列:等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。事实上,节点的定义复用了同步器中节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类AbstractQueuedSynchronizer.Node。Condition拥有首尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter指向它,并且更新尾节点即可。上述节点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列。
等待:调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。
通知:调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。

应用:ArrayBlockingQueue的put()insert()take()extract(),有notEmpty和notFull两个Condition。
124. synchronized和lock有什么区别:
1.原始构成:synchronized是关键字属于JVM层面,monitorenter底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor方法只有在同步块或者方法中才能调用。Monitorexit。lock是具体的类(java.util.concurrent.locks.lovk)是API层面的锁。
2.使用方法:synchronized不需要用户去手动释放锁,当synchronized代码执行完后系统会自动释放锁。ReentrantLock则需要用户去手动释放锁,若没有主动释放锁,就有可能导致出现死锁现象,需要lock()和unlock()方法配合try/finally语句来完成。
3.等待是否可中断:synchronized不可中断,除非抛出异常或者正常运行完成。所以如果一个线程在等待锁,那么结果只有两种可能,要么它获得这把锁继续执行,要么它就保持等待。ReentrantLock可中断,一是可以通过设置超时方法if(lock.tryLock(long timeout,TimeUnit unit));二是lock.lockInterruotibly()放代码块中,后面线程调用interrupt()方法可中断。提供了这样一个机制,如果一个线程正在等待锁,那么它依然可以收到一个通知,被告知无需再等待,可以停止工作了,这种情况对于处理死锁是有一定帮助的。
4.加锁是否公平:synchronized为非公平锁。ReentrantLock两者都可以,默认为非公平锁,构造方法可以传入boolean值,true为公平锁。
5.锁绑定多个条件Condition:synchronized没有。ReentrantLock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

  1. 生产者而消费者模式:生产者和消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通信,而是通过阻塞队列来进行通信,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

阻塞队列版:资源类:volatile修饰flag表明生产消费动作是否执行;atomicInteger作为原子变量;阻塞队列作为容器;生产atomicInteger.incrementAndGet,blockingQueue.offer();消费blockingQueue.poll()当取得内容为空时再等两秒,然后消费退出;停止置flag为false。生产者线程生产、消费者线程消费、过几秒后叫停。

Java中的线程池类其实就是一种生产者和消费者模式的实现方式,但是我觉得其实现方式更加高明。生产者把任务丢给线程池,线程池创建线程并处理任务,如果将要运行的任务数大于线程池的基本线程数就把任务扔到阻塞队列里,这种做法比只使用一个阻塞队列来实现生产者和消费者模式显然要高明很多,因为消费者能够处理直接就处理掉了,这样速度更快,而生产者先存,消费者再取这种方式显然慢一些。

102.线程池:线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。线程池的特点:线程复用、控制最大并发数、管理线程。线程池的优点:第一降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。第二提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行。第三提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控。
103.线程池架构说明:线程池是通过Executor框架实现的,该框架中用到了Executor、Executors、ExecutorService(AbstractExecutorService/ ScheduledExecutorService)、ThreadPoolExecutor(ScheduledThreadPoolExecutor)这几个类。

104.线程池编码:Executors.newScheduledThreadPool() java8:Executors.newWorkStealingPoll(int)java8新增,使用目前机器上可用的处理器作为它的并行级别

ExecutorService threadPool=Executors.newFixedThreadPool(int)一池固定线程 适用于为了满足资源管理的要求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器。执行长期的任务,性能好很多。
threadPool.execute(()->){执行语句}); threadPool.shutdown();
public static ExecutorService newFixedThreadPool(int nThreads){return new ThreadPoolExecutor (nThreads, nThreads,0L,TimeUnit,new LinkedBlockingQueue());}
特点:1.创建一个定长的线程池,可控制线程最大并发数,超出的线程会在队列中等待
2.创建的线程池corePoolSize和maximumPoolSize时相等的,
3.它使用的是LinkedBlockingQueue(无界的)。
4.当线程池中的线程数大于corePoolSize时,keepAliveTime为多余的空闲线程等待新任务的最长时间,超过这个时间后多余的线程将被终止。这里把keepAliveTime设置为0L,意味着多余的空闲线程会被立即终止。
1)如果当前运行的线程数少于corePoolSize,则创建新线程来执行任务。
2)在线程池完成预热之后(当前运行的线程数等于corePoolSize),将任务加入LinkedBlockingQueue。
3)线程执行完1中的任务后,会在循环中反复从LinkedBlockingQueue获取任务来执行。FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)。使用无界队列作为工作队列会对线程池带来如下影响。
1)当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize。
2)由于1,使用无界队列时maximumPoolSize将是一个无效参数。
3)由于1和2,使用无界队列时keepAliveTime将是一个无效参数。4)由于使用无界队列,运行中的FixedThreadPool(未执行方法shutdown()或shutdownNow())不会拒绝任务(不会调用RejectedExecutionHandler.rejectedExecution方法)。

Executors.newSingleThreadExecutor()一池一线程 适用于需要保证顺序地执行各个任务,并且在任意时间点,不会有多个线程是活动的应用场景。一个任务一个任务执行的场景。
public static ExecutorService newSingleThreadExecutor(){return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor (1, 1,0L,TimeUnit,new LinkedBlockingQueue()));}
特点:
1.创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务都按照指定的顺序执行
2.创建的线程池corePoolSize和maximumPoolSize都设置为1,它使用的是LinkedBlockingQueue。
SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)。SingleThreadExecutor使用无界队列作为工作队列对线程池带来的影响与FixedThreadPool相同。
1)如果当前运行的线程数少于corePoolSize(即线程池中无运行的线程),则创建一个新线程来执行任务。
2)在线程池完成预热之后(当前线程池中有一个运行的线程),将任务加入Linked-BlockingQueue。
3)线程执行完1中的任务后,会在一个无限循环中反复从LinkedBlockingQueue获取任务来执行。

Executors.newCachedThreadPool()一池多线程 适用于执行很多的短期异步的小程序或者负载较轻的服务器。
public static ExecutorService newCachedThreadPool (){return new ThreadPoolExecutor (0, Integer.MAX_VALUE,60L,TimeUnit,new SynchronousQueue());}
特点:
1.创建一个可缓存线程池,如果线程池长度超过处理需要,可以灵活回收空闲线程,若无可回收,则新建线程
2.创建的线程池corePoolSize设置为0、maximumPoolSize设置为最大值
3.它使用的是SynchronousQueue,也就说来了任务就创建线程运行,当线程空闲时间超过60s就销毁线程。
CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但CachedThreadPool的maximumPool是无界的。这意味着,如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源。
1)首先执行SynchronousQueue.offer(Runnable task)。如果当前maximumPool中有空闲线程正在执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成;否则执行下面的步骤2)。
2)当初始maximumPool为空,或者maximumPool中当前没有空闲线程时,将没有线程执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤1)将失败。此时CachedThreadPool会创建一个新线程执行任务,execute()方法执行完成。3)在步骤2)中新创建的线程将任务执行完后,会执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这个poll操作会让空闲线程最多在SynchronousQueue中等待60秒钟。如果60秒钟内主线程提交了一个新任务(主线程执行步骤1)),那么这个空闲线程将执行主线程提交的新任务;否则,这个空闲线程将终止。由于空闲60秒的空闲线程会被终止,因此长时间保持空闲的CachedThreadPool不会使用任何资源。前面提到过,SynchronousQueue是一个没有容量的阻塞队列。每个插入操作必须等待另一个线程的对应移除操作,反之亦然。CachedThreadPool使用SynchronousQueue,把主线程提交的任务传递给空闲线程执行。

Executor框架:
Executor框架的两级调度模型:在HotSpot VM的线程模型中,Java线程(java.lang.Thread)被一对一映射为本地操作系统线程。Java线程启动时会创建一个本地操作系统线程;当该Java线程终止时,这个操作系统线程也会被回收。操作系统会调度所有线程并将它们分配给可用的CPU。在上层,Java多线程程序通常把应用分解为若干个任务,然后使用用户级的调度器(Executor框架)将这些任务映射为固定数量的线程;在底层,操作系统内核将这些线程映射到硬件处理器上。应用程序通过Executor框架控制上层的调度;而下层的调度由操作系统内核控制,下层的调度不受应用程序的控制。

Executor的结构:1.任务:包括被执行任务需要实现的接口,Runnable接口或Callable接口。2.任务的执行:包括任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口。Executor框架有两个关键类实现了ExecutorService接口(ThreadPoolExecutor和ScheduledThreadPoolExecutor)。3.异步计算的结果:包括接口Future和实现Future接口的FutureTask类。

Executors是一个接口,它是Executor框架的基础,它将任务的提交与任务的执行分离开来。扮演者线程池工厂的角色。通过Executors可以取得一个拥有特定功能的线程池。
ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务。
ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行命令,或者定期执行命令。ScheduledThreadPoolExecutor比Timer更灵活,功能更强大。
Future接口和实现Future接口的FutureTask类,代表异步计算的结果。
Runnable接口和Callable接口的实现类,都可以被ThreadPoolExecutor或Scheduled-ThreadPoolExecutor执行。

主线程首先要创建实现Runnable或者Callable接口的任务对象。工具类Executors可以把一个Runnable对象封装为一个Callable对象(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule))。然后可以把Runnable对象直接交给ExecutorService执行ExecutorService.execute(Runnable command));或者也可以把Runnable对象或Callable对象提交给ExecutorService执行(Executor-Service.submit(Runnable task)或ExecutorService.submit(Callabletask))。如果执行ExecutorService.submit(…),ExecutorService将返回一个实现Future接口的对象(到目前为止的JDK中,返回的是FutureTask对象)。由于FutureTask实现了Runnable,程序员也可以创建FutureTask,然后直接交给ExecutorService执行。最后,主线程可以执行FutureTask.get()方法来等待任务执行完成。主线程也可以执行FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。

Executor框架包含的成员组件:ThreadPoolExecutor、ScheduledThreadPoolExecutor、Future接口、Runnable接口、Callable接口和Executors。
ThreadPoolExecutor:通常使用工厂类Executors来创建。Executor框架最核心的类是ThreadPoolExecutor,它是线程池的实现类,主要由下列4个组件构成。corePool:核心线程池的大小。maximumPool:最大线程池的大小。BlockingQueue:用来暂时保存任务的工作队列。RejectedExecutionHandler:当ThreadPoolExecutor已经关闭或ThreadPoolExecutor已经饱和时(达到了最大线程池大小且工作队列已满),execute()方法将要调用的Handler。通过Executor框架的工具类Executors可以创建3种类型的ThreadPoolExecutor:SingleThreadPool、FixedThreadPool和CachedThreadPool。

ScheduledThreadPoolExecutor:通常使用工厂类Executors来创建。ScheduledThreadPoolExecutor继承自ThreadPoolExecutor。它主要用来在给定的延迟之后运行任务,或者定期执行任务。ScheduledThreadPoolExecutor的功能与Timer类似,但ScheduledThreadPoolExecutor功能更强大、更灵活。Timer对应的是单个后台线程,而ScheduledThreadPoolExecutor可以在构造函数中指定多个对应的后台线程数。
Executors可以创建2种类型的ScheduledThreadPoolExecutor。ScheduledThreadPoolExecutor,包含若干个线程的ScheduledThreadPoolExecutor,适用于需要多个后台线程执行周期任务,同时为了满足资源管理的需求而需要限制后台线程的数量的应用场景。SingleThreadScheduledExecutor,只包含一个线程的ScheduledThreadPoolExecutor,适用于需要单个后台线程执行周期任务,同时需要保证顺序地执行各个任务的应用场景。
ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(n)不一定会立即执行任务,是起到了计划任务的作用,它会在指定的时间对任务进行调度。
方法:schedule(Runnable command,long delay,unit)会对任务进行一次调度 scheduleAtFixedRate(Runnable command,long initialDelay,long period,unit) 会对任务进行周期性的调度,任务调度的频率是一定的,它是以上一个任务开始执行时间为起点,之后的period时间调度下一次任务。
scheduleWithFixedDelay(Runnable command,long initialDelay,long period,unit)会对任务进行周期性的调度,在上一个任务结束后,再经过delay时间进行任务调度。

ScheduledThreadPoolExecutor的运行机制:DelayQueue是一个无界队列,所以ThreadPoolExecutor的maximumPoolSize在Scheduled-ThreadPoolExecutor中没有什么意义(设置maximumPoolSize的大小没有什么效果)。
ScheduledThreadPoolExecutor的执行主要分为两大部分。
1)当调用ScheduledThreadPoolExecutor的scheduleAtFixedRate()方法或者scheduleWithFixedDelay()方法时,会向ScheduledThreadPoolExecutor的DelayQueue添加一个实现了RunnableScheduledFutur接口的ScheduledFutureTask。
2)线程池中的线程从DelayQueue中获取ScheduledFutureTask,然后执行任务。ScheduledThreadPoolExecutor为了实现周期性的执行任务,对ThreadPoolExecutor做了如下的修改。
•使用DelayQueue作为任务队列。
•获取任务的方式不同(后文会说明)。
•执行周期任务后,增加了额外的处理(后文会说明)。

ScheduledThreadPoolExecutor的实现:前面我们提到过,ScheduledThreadPoolExecutor会把待调度的任务(ScheduledFutureTask)放到一个DelayQueue中。ScheduledFutureTask主要包含3个成员变量,如下。
•long型成员变量time,表示这个任务将要被执行的具体时间。
•long型成员变量sequenceNumber,表示这个任务被添加到ScheduledThreadPoolExecutor中的序号。
•long型成员变量period,表示任务执行的间隔周期。
DelayQueue封装了一个PriorityQueue,这个PriorityQueue会对队列中的Scheduled-FutureTask进行排序。排序时,time小的排在前面(时间早的任务将被先执行)。如果两个ScheduledFutureTask的time相同,就比较sequenceNumber,sequenceNumber小的排在前面(也就是说,如果两个任务的执行时间相同,那么先提交的任务将被先执行)。

首先,让我们看看ScheduledThreadPoolExecutor中的线程执行周期任务的过程。ScheduledThreadPoolExecutor中的线程1执行某个周期任务的4个步骤。下面是对这4个步骤的说明。
1)线程1从DelayQueue中获取已到期的ScheduledFutureTask(DelayQueue.take())。到期任务是指ScheduledFutureTask的time大于等于当前时间。
2)线程1执行这个ScheduledFutureTask。
3)线程1修改ScheduledFutureTask的time变量为下次将要被执行的时间。
4)线程1把这个修改time之后的ScheduledFutureTask放回DelayQueue中(Delay-Queue.add())。

接下来,让我们看看上面的步骤1)获取任务的过程。下面是DelayQueue.take()方法的源代码实现。获取任务分为3大步骤。
1)获取Lock。
2)获取周期任务。
•如果PriorityQueue为空,当前线程到Condition中等待;否则执行下面的2.2。
•如果PriorityQueue的头元素的time时间比当前时间大,到Condition中等待到time时间;否则执行下面的2.3。
•获取PriorityQueue的头元素(2.3.1);如果PriorityQueue不为空,则唤醒在Condition中等待的所有线程(2.3.2)。
3)释放Lock。ScheduledThreadPoolExecutor在一个循环中执行步骤2,直到线程从PriorityQueue获取到一个元素之后(执行2.3.1之后),才会退出无限循环(结束步骤2)。

最后,让我们看看ScheduledThreadPoolExecutor中的线程执行任务的步骤4,把ScheduledFutureTask放入DelayQueue中的过程。下面是DelayQueue.add()的源代码实现。
添加任务分为3大步骤。
1)获取Lock。
2)添加任务。
•向PriorityQueue添加任务。
•如果在上面2.1中添加的任务是PriorityQueue的头元素,唤醒在Condition中等待的所有线程。
3)释放Lock。

Future接口和实现Future接口的FutureTask类:用来表示异步计算的结果。当我们把Runnable接口或Callable接口的实现类提交(submit)给ThreadPoolExecutor或ScheduledThreadPoolExecutor时,ThreadPoolExecutor或ScheduledThreadPoolExecutor会向我们返回一个FutureTask对象。FutureTask除了实现Future接口外,还实现了Runnable接口。因此,FutureTask可以交给Executor执行,也可以由调用线程直接执行(FutureTask.run())。根据FutureTask.run()方法被执行的时机,FutureTask可以处于下面3种状态。1)未启动。FutureTask.run()方法还没有被执行之前,FutureTask处于未启动状态。当创建一个FutureTask,且没有执行FutureTask.run()方法之前,这个FutureTask处于未启动状态。2)已启动。FutureTask.run()方法被执行的过程中,FutureTask处于已启动状态。3)已完成。FutureTask.run()方法执行完后正常结束,或被取消(FutureTask.cancel(…)),或执行FutureTask.run()方法时抛出异常而异常结束,FutureTask处于已完成状态。当FutureTask处于未启动或已启动状态时,执行FutureTask.get()方法将导致调用线程阻塞;当FutureTask处于已完成状态时,执行FutureTask.get()方法将导致调用线程立即返回结果或抛出异常。当FutureTask处于未启动状态时,执行FutureTask.cancel()方法将导致此任务永远不会被执行;当FutureTask处于已启动状态时,执行FutureTask.cancel(true)方法将以中断执行此任务线程的方式来试图停止任务;当FutureTask处于已启动状态时,执行FutureTask.cancel(false)方法将不会对正在执行此任务的线程产生影响(让正在执行的任务运行完成);当FutureTask处于已完成状态时,执行FutureTask.cancel(…)方法将返回false。

FutureTask的使用:可以把FutureTask交给Executor执行;也可以通过ExecutorService.submit(…)方法返回一个FutureTask,然后执行FutureTask.get()方法或FutureTask.cancel(…)方法。除此以外,还可以单独使用FutureTask。当一个线程需要等待另一个线程把某个任务执行完后它才能继续执行,此时可以使用FutureTask。假设有多个线程执行若干任务,每个任务最多只能被执行一次。当多个线程试图同时执行同一个任务时,只允许一个线程执行任务,其他线程需要等待这个任务执行完后才能继续执行。

FutureTask的实现:基于AbstractQueuedSynchronizer(以下简称为AQS)。java.util.concurrent中的很多可阻塞类(比如ReentrantLock)都是基于AQS来实现的。AQS是一个同步框架,它提供通用机制来原子性管理同步状态、阻塞和唤醒线程,以及维护被阻塞线程的队列。JDK 6中AQS被广泛使用,基于AQS实现的同步器包括:ReentrantLock、Semaphore、ReentrantReadWriteLock、CountDownLatch和FutureTask。每一个基于AQS实现的同步器都会包含两种类型的操作,如下。•至少一个acquire操作。这个操作阻塞调用线程,除非/直到AQS的状态允许这个线程继续执行。FutureTask的acquire操作为get()/get(long timeout,TimeUnit unit)方法调用。•至少一个release操作。这个操作改变AQS的状态,改变后的状态可允许一个或多个阻塞线程被解除阻塞。FutureTask的release操作包括run()方法和cancel(…)方法。基于“复合优先于继承”的原则,FutureTask声明了一个内部私有的继承于AQS的子类Sync,对FutureTask所有公有方法的调用都会委托给这个内部子类。AQS被作为“模板方法模式”的基础类提供给FutureTask的内部子类Sync,这个内部子类只需要实现状态检查和状态更新的方法即可,这些方法将控制FutureTask的获取和释放操作。具体来说,Sync实现了AQS的tryAcquireShared(int)方法和tryReleaseShared(int)方法,Sync通过这两个方法来检查和更新同步状态。Sync是FutureTask的内部私有类,它继承自AQS。创建FutureTask时会创建内部私有的成员对象Sync,FutureTask所有的的公有方法都直接委托给了内部私有的Sync。FutureTask.get()方法会调用AQS.acquireSharedInterruptibly(int arg)方法,这个方法的执行过程如下。1)调用AQS.acquireSharedInterruptibly(int arg)方法,这个方法首先会回调在子类Sync中实现的tryAcquireShared()方法来判断acquire操作是否可以成功。acquire操作可以成功的条件为:state为执行完成状态RAN或已取消状态CANCELLED,且runner不为null。2)如果成功则get()方法立即返回。如果失败则到线程等待队列中去等待其他线程执行release操作。3)当其他线程执行release操作(比如FutureTask.run()或FutureTask.cancel(…))唤醒当前线程后,当前线程再次执行tryAcquireShared()将返回正值1,当前线程将离开线程等待队列并唤醒它的后继线程(这里会产生级联唤醒的效果,后面会介绍)。4)最后返回计算的结果或抛出异常。FutureTask.run()的执行过程如下。1)执行在构造函数中指定的任务(Callable.call())。2)以原子方式来更新同步状态(调用AQS.compareAndSetState(int expect,int update),设置state为执行完成状态RAN)。如果这个原子操作成功,就设置代表计算结果的变量result的值为Callable.call()的返回值,然后调用AQS.releaseShared(int arg)。3)AQS.releaseShared(int arg)首先会回调在子类Sync中实现的tryReleaseShared(arg)来执行release操作(设置运行任务的线程runner为null,然会返回true);AQS.releaseShared(int arg),然后唤醒线程等待队列中的第一个线程。4)调用FutureTask.done()。当执行FutureTask.get()方法时,如果FutureTask不是处于执行完成状态RAN或已取消状态CANCELLED,当前执行线程将到AQS的线程等待队列中等待(见下图的线程A、B、C和D)。当某个线程执行FutureTask.run()方法或FutureTask.cancel(…)方法时,会唤醒线程等待队列的第一个线程(见图10-16所示的线程E唤醒线程A)。假设开始时FutureTask处于未启动状态或已启动状态,等待队列中已经有3个线程(A、B和C)在等待。此时,线程D执行get()方法将导致线程D也到等待队列中去等待。当线程E执行run()方法时,会唤醒队列中的第一个线程A。线程A被唤醒后,首先把自己从队列中删除,然后唤醒它的后继线程B,最后线程A从get()方法返回。线程B、C和D重复A线程的处理流程。最终,在队列中等待的所有线程都被级联唤醒并从get()方法返回。

Runnable接口和Callable接口的实现类:都可以被ThreadPoolExecutor或Scheduled-ThreadPoolExecutor执行。它们之间的区别是Runnable不会返回结果,而Callable可以返回结果。除了可以自己创建实现Callable接口的对象外,还可以使用工厂类Executors来把一个Runnable包装成一个Callable。public static Callable callable(Runnable task)或public static Callable callable(Runnable task, T result)。当我们把一个Callable对象(比如上面的Callable1或Callable2)提交给ThreadPoolExecutor或ScheduledThreadPoolExecutor执行时,submit(…)会向我们返回一个FutureTask对象。我们可以执行FutureTask.get()方法来等待任务执行完成。当任务成功完成后FutureTask.get()将返回该任务的结果。例如,如果提交的是对象Callable1,FutureTask.get()方法将返回null;如果提交的是对象Callable2,FutureTask.get()方法将返回result对象。

  1. 我们可以通过ThreadPoolExecutor来创建一个线程池。ThreadPoolExecutor:public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue workQueue){this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(),defaultHandler);}

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler){ }

corePoolSize,线程池中的常驻核心线程数。1.在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程;2.当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列中;3. 当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止。默认情况下,只有当线程池中的线程数大于corePoolSize时keepAliveTime才会起作用,直到线程池中的线程不大于corePoolSize。

maximumPoolSize,线程池中能够容纳同时执行的最大线程数,此值必须大于等于1。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。

keepAliveTime,多余空闲线程的存活时间,当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止
unit, keepAliveTime的单位。所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。

TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)。

workQueue 任务队列,被提交但尚未被执行的任务,它是一个BlockingQueue接口的对象,仅用于存放Runnable对象,可以选择以下几个阻塞队列:
1.有界的任务队列ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
2.无界的任务队列LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。不存在任务入队失败的情况除非资源耗尽。
3.直接提交的队列SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。提交的任务不会被真实的保存,而总是将新任务提交给线程执行,如果没有空闲的线程则尝试创建新的线程,若已达最大值执行拒绝策略。所以通常要设置很大的最大值否则会很容易执行拒绝策略。
4.优先任务队列PriorityBlockingQueue:一个具有优先级的无限阻塞队列。可以根据任务自身的优先级顺序先后执行。

threadFactory表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认线程工厂。可以通过线程工厂给每个创建出来的线程设置更有意义的名字。使用开源框架guava提供的ThreadFactoryBuilder可以快速给线程池里的线程设置有意义的名字new ThreadFactoryBuilder().setNameFormat(“XX-task-%d”).build();
是一个接口,它只有一个方法用来创建线程Thread newThread(Runnable r);自定义线程池可以跟踪何时创建线程、自定义线程名称、组、优先级、守护线程。

handler拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。

106.线程池底层工作原理:1.在创建了线程池后,等待提交过来的任务请求。2.当调用ThreadPoolExecutor执行execute()方法添加一个请求任务时,线程池会做如下判断:2.1如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;《执行这一步骤需要获取全局锁》2.2如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列BlockQueue;2.3如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;《执行这一步骤需要获取全局锁》2.4如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行RejectedExecutionHandler.rejectedExecution()。(核心线程池是否已满:没满->创建线程执行任务,满了->队列是否已经满了:没满->将任务存储在队列里,满了->线程池是否已满:没满->创建线程执行任务,满了->按照策略处理无法执行的任务)3.当一个线程完成任务时,它会从队列中取下一个任务来执行。4.当一个线程无事可做超过一定的时间keepAliveTime,线程池会判断:如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小。

工作线程:线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后,还会循环获取工作队列里的任务来执行。我们可以从Worker类的run()方法里看到这点。

线程池中的线程执行任务分两种情况,如下。1)在execute()方法中创建一个线程时,会让这个线程执行当前任务。2)这个线程执行完上图中1的任务后,会反复从BlockingQueue获取任务来执行。

ThreadPoolExecutor核心代码:
Public void execute(Runnable command){
if(command==null)throw new NullPointerException();
int c=ctl.get();
if(workerCountOf©<corePoolSize){//当前线程池的线程总数小于核心线程数
if(addWorker(command,true)) return //会将任务调度执行
c=cti.get()
}
if(isRunning©&&workQueue.offer(command){//否则进入等待队列
int recheck=ct1.get();
if(!isRunning(recheck)&&remove(command) ) reject(command);
else if(workerCountOf(recheck))==0 addWorker(null,false);
}
else if(!addWorker(command,false)) reject(command))//进入等待队列失败就将任务直接提交给线程池,否则提交失败
}

107.线程池的拒绝策略:等待队列已经排满了再也塞不下新任务了,同时,线程池中的max线程也达到了,无法继续为新任务服务,这时候我们就需要拒绝策略机制合理的处理。AbortPolicy:默认,直接抛出RejectedExecutionException异常阻止系统正常运行。 CallerRunsPolicy:调用者运行,一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低任务流量,比如说返回给main线程。 DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。
以上内置拒绝策略均实现了RejectedExecutionHandler接口。当然,也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略,public interface RejectedExecutionHandler{void rejectedExecution(Runnable r,ThreadPoolExecutor executor)}。
实例:如记录日志或持久化存储不能处理的任务。

108.线程池的方法,你用哪个?一个都不用,生产上只能使用自定义的。自定义过线程池使用:线程池不允许使用Execuyors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源消耗的风险。

  1. 可以使用两个方法向线程池提交任务,分别为execute()和submit()方法。
    线程池对象.execute():execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。通过以下代码可知execute()方法输入的任务是一个Runnable类的实例。threadsPool.execute(new Runnable() { @Override public void run() { } });

submit()方法:用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。Future future = executor.submit(harReturnValuetask); try { Object s = future.get(); } catch (InterruptedException e) { // 处理中断异常 } catch (ExecutionException e) { // 处理无法执行任务异常 } finally { // 关闭线程池 executor.shutdown(); }

可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。

109.扩展线程池:ThreadPoolExecutor也是一个可扩展的线程池,它提供了beforeExecute()、afterExecutr()、terminated()三个接口对线程池进行控制。ThreadPoolExecutor.Worker是内部类,它是一个实现了Runnable接口的类,是工作线程。Worker.runTask()方法会被线程池以多线程模式异步调用,即Worker.runTask()会同时被多个线程访问。可以输出一些有用的信息用于错误排查等等。

110.线程池配置合理线程数:
1.任务的性质:CPU密集型任务、IO密集型任务和混合型任务。性质不同的任务可以用不同规模的线程池分开处理。
CPU密集型:CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才可能得到加速,通过多线程。而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速。CPU密集型任务配置尽可能少的线程数量,CPU核数+1个线程的线程池。

IO密集型:即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运行能力,所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。IO密集型大部分线程都阻塞,所以需要多配置线程数,由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。
混合型的任务:如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。可以通过Runtime.getRuntime ().availableProcessors()方法获得当前设备的CPU个数。

2.任务的优先级:高、中和低。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先执行。
3.任务的执行时间:长、中和短。
执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。
4.任务的依赖性:是否依赖其他系统资源,如数据库连接。
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU。建议使用有界队列。有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点儿,比如几千。有一次,我们系统里后台任务线程池的队列和线程池全满了,不断抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞,任务积压在线程池里。如果当时我们设置成无界队列,那么线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。当然,我们的系统所有的任务是用单独的服务器部署的,我们使用不同规模的线程池完成不同类型的任务,但是出现这样问题时也会影响到其他任务。

111.线程池的监控:如果在系统中大量使用线程池,则有必要对线程池进行监控,方便在出现问题时,可以根据线程池的使用状况快速定位问题。可以通过线程池提供的参数进行监控,在监控线程池的时候可以使用以下属性。taskCount:线程池需要执行的任务数量。completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过。如该数值等于线程池的最大大小,则表示线程池曾经满过。getPoolSize:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减。getActiveCount:获取活动的线程数。通过扩展线程池进行监控。可以通过继承线程池来自定义线程池,重写线程池的beforeExecute、afterExecute和terminated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。例如,监控任务的平均执行时间、最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。protected void beforeExecute(Thread t, Runnable r) { }

110.死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那他们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。产生死锁的原因:系统资源不足、进程运行推进的顺序不合适、资源分配不当。
111.如何解决死锁:jps -l(java ps)命令定位进程号、jstack得到线程堆栈找到死锁查看。Linux中是通过ps –ef|grep xxxx ls –l。如何避免死锁,除了使用无锁的函数以外,另外一种有效的做法是使用重入锁,通过重入锁的中断或者限时等待可以有效规避死锁带来的问题。

112.线上问题定位:有很多问题只有在线上或者预发环境才能发现,而线上又不能调试代码,所以线上问题定位就只能看日志、系统状态和dump线程。1)在Linux命令行下使用TOP命令查看每个进程的情况2)再使用top的交互命令数字1查看每个CPU的性能数据。3)使用top的交互命令H查看每个线程的性能信息。•第一种情况,某个线程CPU利用率一直100%,则说明是这个线程有可能有死循环,那么请记住这个PID。
•第二种情况,某个线程一直在TOP 10的位置,这说明这个线程可能有性能问题。
•第三种情况,CPU利用率高的几个线程在不停变化,说明并不是由某一个线程导致CPU偏高。如果是第一种情况,也有可能是GC造成,可以用jstat命令看一下GC情况,看看是不是因为持久代或年老代满了,产生Full GC,导致CPU利用率持续飙高,命令和回显如下。还可以把线程dump下来,看看究竟是哪个线程、执行什么代码造成的CPU利用率高。执行以下命令,把线程dump到文件dump17里。执行如下命令。dump出来的线程ID(nid)是十六进制的,而我们用TOP命令看到的线程ID是十进制的,所以要用printf命令转换一下进制。然后用十六进制的ID去dump里找到对应的线程。

1 标准访问,请问先打印短信还是email?(两个线程同时访问同一个资源,谁先谁后是不一定的;在两个线程之间加上Thread.sleep(100);就能确定先后了)
也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,
锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
所有的非静态同步方法用的都是同一把锁——实例对象本身,
2 sendSMS()睡觉4秒钟,请问先打印短信还是email?(刚开始等了4秒,然后先短信后email)(同一个同学同一时刻只能使用手机的一个功能!)
也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,
锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
所有的非静态同步方法用的都是同一把锁——实例对象本身,
3 新增普通方法openPC,请问先打印短信还是openPhone?(先开机【因为这个没有同步方法啊】,等了4秒,然后打印短信)
加个普通方法后发现和同步锁无关
4 有两部手机,请问先打印短信还是email?(先打印邮件,等了4秒,然后打印短信)
可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。
换成两个对象后,不是同一把锁了,情况立刻变化。
所有的 非静态同步方法用的都是同一把锁——实例对象本身,
5 两个静态同步方法,同一部手机,请问先打印短信还是email?(等了4秒,先打印短信,然后打印邮件)
6 两个静态同步方法,2部手机,请问先打印短信还是email?(等了4秒,先打印短信,然后打印邮件)
但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!
所有的静态同步方法用的也是同一把锁——类对象本身,
7 1个静态同步方法,1个普通同步方法,同一部手机,请问先打印短信还是email?(先打印邮件,等了4秒,打印短信)
8 1个静态同步方法,1个普通同步方法,2部手机,请问先打印短信还是email?(先打印邮件,等了4秒,打印短信)
这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。

一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法
锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法

加个普通方法后发现和同步锁无关
换成两个对象后,不是同一把锁了,情况立刻变化。

都换成静态同步方法后,情况又变化
所有的非静态同步方法用的都是同一把锁——实例对象本身,

也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,
可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,
所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。

所有的静态同步方法用的也是同一把锁——类对象本身,
这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。
但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个实例对象的静态同步方法之间,
还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!

120.有助于提高锁的性能的几点建议:1.减小锁持有时间2.减小锁粒度3.读写分离锁来替换独占锁4.锁分离5.锁粗化
121.Java虚拟机对锁优化所做的努力:1.锁偏向2.轻量级锁3.自旋锁4.锁消除
122.悲观锁—sychronized\lock乐观锁—无锁CAS
123.生产着消费者模式;
125.Future模式:是多线程开发中非常常见的一种设计模式,它的核心思想是异步调用。当我们需要调用一个函数方法时,如果这个函数执行很慢,那么我们就要进行等待。但有时候,我们可能并不急着要结果。因此,我们可以让被调者立即返回,让它在后台慢慢处理这个请求。对于调用者来说,则可以先处理一些其他任务,在真正需要数据的场合再去尝试获得需要的数据。对于Future模式,虽然它无法立即给出你需要的数据,但是它会返回你一个契约,将来你可以凭借这个契约去重新获取你需要的信息。
主要角色:main:系统启动调用client发出请求client:返回Data对象,立即返回futureData,并开启clentThread线程装配realData data:返回数据的接口 futureData:future数据,构造很快,但是是一个虚拟的数据,需要装配realData realData:真实数据,其构造是比较慢的
实现:核心接口data就是客户端需要获取的数据。两个实现,realData真实数据futureData是提取realData的一个订单可以立即返回得到的
Data接口:public interface Data{public String getResult();}
futureData:实现了一个快速返回的realData包装,当使用其getResult方法时,如果实际的数据没有准备好那么程序就会阻塞,等待realdata准备好并注入到futuredata中,才最终返回数据。是realdata的代理,封装了获取realdata的等待过程。
realData:实现了Data接口。
Client:实现了获取futureData,并开启构造realdata的线程。并在接收请求后,很快的返回futuredata。
Main:负责调用client发起七个请求,并消费返回的数据。
126. Future模式实例:Callable创建线程时,构造了FutureTask对象,表示这个任务是有返回值的,使用Callable接口,告诉FutureTask我们需要的数据该如何产生。将FutureTask提交给线程池,会有返回值。接下来我们不用关心数据是如何产生的,可以去做一些额外的事情,在需要的时候通过Future.get()得到实际的数据。
Future接口:契约、订单,通过它你可以得到真实的拘束
RunnableFuture:继承了Future和Runnable两个接口,run()用于构造真实的数据
FutureTask类:实现了RunnableFutur,有一个内部类sync一些实质性的工作会委托给sync类实现。而sync类最终会调用callable接口,完成实际数据的组装工作。
Callable接口:只有一个方法call(),它会返回需要构造的实际数据。《是future框架和应用程序之间的重要接口》如果我们要实现自己的业务系统,通常需要实现自己的callable对象。此外,futureTask类也与应用密切相关,通常,我们会使用callable实例构造一个futureTask实例,并将它提交给线程池。
127. Java NIO:是new IO的简称,是一种可以代替Java IO的一套新的IO机制。与并发并无直接的关系,但是可以大大提高线程的使用效率。
基础内容:通道、缓冲区、文件IO、网络IO
网络IO:标准的网络IO是基于Socket的服务端的多线程模式。为了让服务器可以支持更多的客户端连接,通常的做法是为每一个客户端连接开启一个线程。缺点:服务线程在等待IO。想办法将网络的等待时间从线程中分离出来。
NIO:通道:类似于流,一个通道可以和文件或者网络Socket对应。如果通道对应着一个Socket,那么往这个通道中写数据,就等于向Socket中写入数据。缓冲区:一个内存区域或者byte数据,数据需要包装成这样的形式才能和通道交互。选择器:

并发编程的艺术:
1.并发编程的挑战:1.上下文切换的问题2.死锁的问题3.硬件和软件的资源限制的问题,
软件资源限制:有数据库的链接数和socket连接数等,硬件的资源限制有带宽的上传/下载速度、硬盘读写速度和CPU处理速度。
2.如何减少上下文切换
1、无锁并发编程。多线程竞争锁时,会引起上下文切换,没有竞争到锁的线程,一直处于等待状态,所以上下文切换的时间会比较长。所以可以使用一些方法避免使用锁。例如数据的ID利用Hash算法取模,不同的线程取不同的数据段。

2、CAS 算法。Java 的Atomic包就是使用的CAS算法更新数据,而不加锁。

3、使用最少线程。避免创建不必要的线程。

4、协程。在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
3.常见避免死锁的方式。
1、避免一个线程同时获得多个锁
2、保证一个锁只占用一个资源(避免一个线程在锁内同时占用多个资源)
3、尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
4、数据裤锁,加锁和解锁必须在同一个数据库连接中
4.资源限制的挑战
硬件资源的限制:可以用集群执行程序
软件资源的限制:使用连接池资源的复用

  1. concurrent包的实现
    由于Java的CAS同时具有volatile读和volatile写的内存语义,因此Java线程之间的通信现在有了下面4种方式。
    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包的源代码实现,会发现一个通用化的实现模式。
    首先,声明共享变量为volatile。
    然后,使用CAS的原子条件更新来实现线程之间的同步。
    同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。
    AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。
  2. final
    与锁和volatile相比,对final域的读写更像是普通变量的访问,下面介绍final域的内存语义:
    对于final域,编译器和处理器要遵循两个重排序规则:
    1、在构造函数内对一个final域的写入,与随后把这个被构造函数对象的引用赋值给一个引用变量,这两个操作之间不能重排序;
    这样可以保证对象引用为任意线程可见之前,保证对象的final域已经被正确的初始化了,而普通的域没有这个保障。

2、初次读一个包含final域的对象的引用,与随后初次读这个fina域,这两个操作不能重排序。
这样可以保证在读一个对象的final域之前,一定会先读包含这个对象的引用,防止在读普通的域时,这个域还没有被写线程写入。

java并发包和类总结-JUC总结相关推荐

  1. Java多线程并发编程--Java并发包(JUC)

    Java多线程并发–Java并发包(JUC) 前言 前一篇文章中,笔者已经介绍了Java多线程的一些基础知识,但是想要成为一名中高级Java程序员还必须懂得Java并发包(JUC)的知识点,而且JUC ...

  2. juc是个什么鬼(一) Java并发包详情,CAS分析,解决ABA问题

    JUC就是java.util.concurrent包,俗称java并发包 通过看JDK的API,我们发现JUC下有俩子包,分别是atomic和locks包,这篇文章重点就是看这两个包下的内容 Atom ...

  3. Java并发包中常用类

    Java并发包中常用类小结(一) 从JDK1.5以后,Java为我们引入了一个并发包,用于解决实际开发中经常用到的并发问题,那我们今天就来简单看一下相关的一些常见类的使用情况. 1.Concurren ...

  4. Java并发包中常用类小结(一)

                                   Java并发包中常用类小结(一) 从JDK1.5以后,Java为我们引入了一个并发包,用于解决实际开发中经常用到的并发问题,那我们今天就来 ...

  5. Java并发包下的辅助工具类

    Java并发包下为我们提供了一些辅助工具类,来简单看看如何使用 一.CountDownLatch 减法器,必须等到减法器的值为0了,才能继续往下执行.也可以将它理解为栅栏 让一些线程堵塞直到另一个线程 ...

  6. 深入java并发包源码(三)AQS独占方法源码分析

    深入java并发包源码(一)简介 深入java并发包源码(二)AQS的介绍与使用 深入java并发包源码(三)AQS独占方法源码分析 AQS 的实现原理 学完用 AQS 自定义一个锁以后,我们可以来看 ...

  7. 第 5-6 课:Java 并发包中的高级同步工具 + 面试题

    Java 中的并发包指的是 java.util.concurrent(简称 JUC)包和其子包下的类和接口,它为 Java 的并发提供了各种功能支持,比如: 提供了线程池的创建类 ThreadPool ...

  8. Java并发包基石-AQS详解

    目录 1 基本实现原理 1.1 如何使用 1.2 设计思想 2 自定义同步器 2.1 同步器代码实现 2.2 同步器代码测试 3 源码分析 3.1 Node结点 3.2 独占式 3.3 共享式 4 总 ...

  9. 并发编程入门(五):Java并发包和Java8并发

    目录 前言 JUC(Java.util.concurrent) 1.Java并发包之原子类 1.1.AtomicInteger 1.2.AtomicReference 1.3.AtomicStampe ...

  10. java并发包详解(jdk7)

    在此对java并发包做一个大致总结,如有错误,请指正. juc包的总体结构大致如下 外层框架主要有Lock(ReentrantLock.ReadWriteLock等).同步器(semaphores等) ...

最新文章

  1. 您最喜欢的C#扩展方法是什么? (codeplex.com/extensionoverflow)
  2. 获取小程序用户信息+java_java获取微信小程序用户信息
  3. Linux(内核和用户态的)动态内存管理
  4. r语言做绘制精美pcoa图_R语言统计与绘图:绘制QQ图
  5. html 手机楼层布局,纯html、css、javascript实现楼层跳跃式的页面布局
  6. 语言常用c100单词,英语口语练习_夏普新款PW-C100-G电子词典测评_沪江英语
  7. php变量的几种写法
  8. 软件设计师19-系统开发和运行02
  9. eclipse中maven工程添加本地库至Maven Dependencies
  10. 逆水寒7.25服务器维护,逆水寒7月26日更新维护公告 更新内容汇总
  11. bzoj 1228 [SDOI2009]ED
  12. 辛辛苦苦做了几天白忙活,错在哪里?
  13. paip.java c++得到当前类,方法名称以及行号
  14. 原码一位乘法c语言程序,原码一位乘法与补码一位乘法
  15. Photoshop定义画笔选区为空的原因
  16. web平台微信扫码登录
  17. 【起航计划ObjC 002】印第安老斑鸠ObjC的幻想 ---- Ubuntu下安装并使用MAC虚拟机
  18. UVA 11909 Soya Milk(简单数学三角函数计算)
  19. 刷题 BFS 广度优先算法 : 大胖子走迷宫 (python, java)
  20. 移动宽带frp内网穿透访问家中网络(by quqi99)

热门文章

  1. php编程基础与实例教程第二版课后,PHP编程基础与实例教程(第2版)
  2. memcached介绍与作用和它的工作原理
  3. android逻辑分辨率,移动端web页面知识小结之手机分辨率与手机像素
  4. CSS背景图片代码示例
  5. 【云周刊】第121期:图管够!灌篮高手、女儿国…阿里日,这帮程序员太会玩了!...
  6. 一个让人不得不转的故事-《通宵达旦工资只有3200 博客网架构师艰难浪迹于北京》...
  7. 分享一下最近微信域名防封的一些心得和经验,怎么才能做到域名防封呢
  8. 使用cmd查看电脑显卡的信息
  9. html5快捷键保存,保存的快捷键是什么?有了快捷键保存太方便了
  10. 海康威视摄像头rtsp推流至H5总结