精华内容
下载资源
问答
  • 线程,线程池,Java

    2019-08-06 16:33:37
    如果一致则可以直接使用此对象,如果不一致,则升级偏向为轻量级,通过自旋循环一定次数来获取,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把从轻量级升级为重量级,此过程就构成了 ...

    在了解线程的时候,首先来看一下什么是并发和并行
    并行和并发有什么区别?
    并行:多个处理器或多核处理器同时处理多个任务。
    并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。

    线程

    1.线程分为前台线程和后台线程。

    守护线程也叫后台线程,后台线程是依赖前台线程,如果前台线程都结束了,那么后台线程自动退出(JVM就退出),只要有一个前台线程活动,那么后台线程就活动。

    2.线程的创建

    2.1继承方式,就是继承Thead类,重写run方法,new thead;
    2.2实现方式,就是实现runnable接口,重写run方法 new thead(run)
    2.3开发的方式 常用匿名内部类的方式来使用线程 new thead(){ public void run(){}.start
    New Runnable(){public void run(){},new thead®
    New thead(new runnable){public void run(){}}.start
    2.4 实现 Callable 接口,有返回值

    3.线程的常用方法

    Public void start();手动启动线程(并不一定立刻执行,等JVM随机分配时间片)

    …isAlive()线程是否活着
    …static thread currentThread()返回当前正在执行的线程对象的引用
    …setName设置该线程名
    …getName返回线程名
    …setPriority(int newPriority);设置线程优先级
    …getPriority获取线程优先级
    …sleep(long millis);在指定的毫秒数内让当前正在执行的线程休眠
    线程等待(wait)
    调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。
    线程睡眠(sleep)
    sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态
    线程让步(yield)
    yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。
    线程中断(interrupt)
    中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。

    1. 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
    2. 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出InterruptedException,从而使线程提前结束 TIMED-WATING 状态。
    3. 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。
    4. 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程 thread 的时候,可以调用 thread.interrupt()方法,在线程的 run 方法内部可以根据 thread.isInterrupted()的值来优雅的终止线程。

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

    4线程生命周期(状态)
    当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。
    4.1新建状态(NEW)
    当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值
    4.2就绪状态(RUNNABLE):
    当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。
    4.3运行状态(RUNNING):
    如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。
    4.4阻塞状态(BLOCKED):
    阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
    等待阻塞(o.wait->等待对列):
    运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)中。
    同步阻塞(lock->锁池)
    运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。
    其他阻塞(sleep/join)
    运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O,处理完毕时,线程重新转入可运行(runnable)状态。
    4.5. 线程死亡(DEAD)
    线程会以下面三种方式结束,结束后就是死亡状态。
    正常结束
    6. run()或 call()方法执行完成,线程正常结束。
    异常结束
    7. 线程抛出一个未捕获的 Exception 或 Error。
    调用 stop
    8. 直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。
    在这里插入图片描述

    5 什么是线程同步
    线程同步是指多线程通过特定的东西(如互斥量)来控制线程之间的执行顺序(同步),也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有同步那线程之间是各自运行各自的。

    线程池

    1.为什么要使用线程池
    1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
    2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)

    2.线程池的四种创建方式
    1)FixThreadPool 正规线程
    我的理解这是一个有指定的线程数的线程池,有核心的线程,里面有固定的线程数量,响应的速度快。正规的并发线程,多用于服务器。固定的线程数由系统资源设置。创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程

    public static ExecutorService newFixedThreadPool(int threads)
        {
        return newFixedThreadPool(threads,threads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
        }
    

    核心线程是没有超时机制的,队列大小没有限制,除非线程池关闭了核心线程才会被回收。

    (2)CacheThreadPool 缓存线程池
    只有非核心线程,最大线程数很大(Int.Max(values)),它会为每一个任务添加一个新的线程,这边有一个超时机制,当空闲的线程超过60s内没有用到的话,就会被回收。缺点就是没有考虑到系统的实际内存大小。

       public static ExecutorService newCachedThreadPool(int threads)
        {
        return newFixedThreadPool(threads,Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
        }
    

    (3)SingleThreadPoll 单线程线程池

    看这个名字就知道这个家伙是只有一个核心线程,就是一个孤家寡人,通过指定的顺序将任务一个个丢到线程,都乖乖的排队等待执行,不处理并发的操作,不会被回收。确定就是一个人干活效率慢。

    (4)ScheduledThreadPoll
    这个线程池就厉害了,是唯一一个有延迟执行和周期重复执行的线程池。它的核心线程池固定,非核心线程的数量没有限制,但是闲置时会立即会被回收。
    3.线程池执行流程图
    线程池:一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体 性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务,这避免 了在处理短时间任务时创建与销毁线程的代价。线程池执行流程是每个开发必备的。
    在这里插入图片描述
    提交一个任务,线程池里存活的核心线程数小于线程数corePoolSize时, 线程池会创建一个核心线程去处理提交的任务。 如果线程池核心线程数已满,即线程数已经等于corePoolSize,一个新提 交的任务,会被放进任务队列workQueue排队等待执行。 当线程池里面存活的线程数已经等于corePoolSize了,并且任务队列 workQueue也满,判断线程数是否达到maximumPoolSize,即最大线程数是 否已满,如果没到达,创建一个非核心线程执行提交的任务。 如果当前的线程数达到了maximumPoolSize,还有新的任务过来的话, 直接采用拒绝策略处理。

    JAVA 锁
    1. 乐观锁
    乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。java 中的乐观锁基本都是通过 CAS 操作实现的,CAS( Compare-And-Swap,比较和替换)是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
    2. 悲观锁
    悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,
    才会转换为悲观锁,如 RetreenLock。
    .3. 自旋锁
    自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
    4.同步锁
    当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。 Java 中可以使用 synchronized 关键字来取得一个对象的同步锁。
    5.死锁
    何为死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。

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

    1. 作用于方法时,锁住的是对象的实例(this);
    2. 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen
      (jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,
      会锁所有调用该方法的线程;
    3. synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,
      当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

    4.1说一下 synchronized 底层实现原理?
    每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的, synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。所以同步操作是一个无差别的重量级操作,性能也很低。但在 Java 6 的时候,Java 虚拟机 对此进行了大刀阔斧地改进,提供了三种不同的 monitor 实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。

    4.2多线程中 synchronized 锁升级的原理是什么?
    synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

    5.volatile 关键字的作用(变量可见性、禁止重排序)
    Java 语言提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程。volatile 变量具备两种特性,volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取 volatile 类型的变量时总会返回最新写入的值。
    变量可见性
    其一是保证该变量对所有线程可见,这里的可见性指的是当一个线程修改了变量的值,那么新的值对于其他线程是可以立即获取的。
    禁止重排序
    volatile 禁止了指令重排。 比 sychronized 更轻量级的同步锁
    在访问 volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此 volatile 变量是一种比 sychronized 关键字更轻量级的同步机制。volatile 适合这种场景:一个变量被多个线程共享,线程直接给这个变量赋值。

    synchronized 和 volatile 的区别是什么?
    volatile 是变量修饰符;synchronized 是修饰类、方法、代码段。
    volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
    volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
    volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。

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

    1. void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁.
    2. boolean tryLock():如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false. 该方法和lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用, 当前线程仍然继续往下执行代码. 而 lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行.
    3. void unlock():执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程并不持有锁, 却执行该方法, 可能导致异常的发生.

    synchronized 和 Lock 有什么区别?
    synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
    synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
    通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

    synchronized 和 ReentrantLock 区别是什么?
    synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对synchronized 进行了非常多的改进。
    主要区别如下:
    ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
    ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
    ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。

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

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

    展开全文
  • 使用 Executors 工具类创建线程池 继承 Thread 类: public class MyThread extends Thread { @Override public void run() { System.out.println(Thread.currentThread().getName() + " run()方法正在执行...");...

    Java并发编程

    优缺点

    优点:

    • 充分利用多核CPU的计算能力:通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升

    • 方便进行业务拆分,提升系统并发能力和性能:在特殊的业务场景下,先天的就适合于并发编程。现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。

    缺点:

    • 并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,如:内存泄漏、上下文切换、线程安全、死锁等问题。

    并发编程三要素

    • 原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。

    • 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)

    • 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)

    线程安全

    • 线程切换带来的原子性问题
    • 缓存导致的可见性问题
    • 编译优化带来的有序性问题

    解决办法:

    • JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
    • synchronized、volatile、LOCK,可以解决可见性问题
    • Happens-Before 规则可以解决有序性问题

    多线程

    多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务。

    优点:

    可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。

    劣势:

    线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;

    多线程需要协调和管理,所以需要 CPU 时间跟踪线程;

    线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。

    线程和进程区别

    进程:

    一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程。

    线程:

    进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。

    • 线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。

    • 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位

    • 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

    • 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

    • 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

    • 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

    • 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行

    上下文切换

    线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

    概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

    上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

    Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。

    守护线程和用户线程

    用户 (User) 线程:

    运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程

    守护 (Daemon) 线程:

    运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作

    warning:

    • setDaemon(true)必须在start()方法前执行,否则会抛出 IllegalThreadStateException 异常
    • 在守护线程中产生的新线程也是守护线程
    • 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑
    • 守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作,所以守护 (Daemon) 线程中的 finally 语句块可能无法被执行。

    线程死锁

    死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。

    必要条件:

    • 互斥条件:线程(进程)对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放
    • 请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
    • 不剥夺条件:线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
    • 循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞

    避免线程死锁

    • 破坏互斥条件:这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。

    • 破坏请求与保持条件:一次性申请所有的资源。

    • 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

    • 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

    创建线程

    • 继承 Thread 类;
    • 实现 Runnable 接口;
    • 实现 Callable 接口;
    • 使用 Executors 工具类创建线程池

    继承 Thread 类:

    public class MyThread extends Thread {
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " run()方法正在执行...");
        }
    
    }
    public class TheadTest {
    
        public static void main(String[] args) {
            MyThread myThread = new MyThread(); 	
            myThread.start();
            System.out.println(Thread.currentThread().getName() + " main()方法执行结束");
        }
    
    }
    

    实现 Runnable 接口:

    public class MyRunnable implements Runnable {
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
        }
    
    }
    public class RunnableTest {
    
        public static void main(String[] args) {
            MyRunnable myRunnable = new MyRunnable();
            Thread thread = new Thread(myRunnable);
            thread.start();
            System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
        }
    
    }
    

    实现 Callable 接口:

    public class MyCallable implements Callable<Integer> {
    
        @Override
        public Integer call() {
            System.out.println(Thread.currentThread().getName() + " call()方法执行中...");
            return 1;
        }
    
    }
    public class CallableTest {
    
        public static void main(String[] args) {
            FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
            Thread thread = new Thread(futureTask);
            thread.start();
    
            try {
                Thread.sleep(1000);
                System.out.println("返回结果 " + futureTask.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
        }
    
    }
    

    使用 Executors 工具类创建线程池:

    public class MyRunnable implements Runnable {
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
        }
    
    }
    public class SingleThreadExecutorTest {
    
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newSingleThreadExecutor();
            MyRunnable runnableTest = new MyRunnable();
            for (int i = 0; i < 5; i++) {
                executorService.execute(runnableTest);
            }
    
            System.out.println("线程任务开始执行");
            executorService.shutdown();
        }
    
    }
    

    runnable 和 callable

    • Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
    • Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息
    • Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞

    run()和 start()

    每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。

    start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。

    start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。

    run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接掉用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。

    new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。

    而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

    总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。

    Callable 和 Future

    Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。

    Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说 Callable用于产生结果,Future 用于获取结果。

    FutureTask

    FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。

    线程的状态

    在这里插入图片描述

    阻塞(block):

    处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。

    阻塞的情况分三种:
    (一). 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;
    (二). 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
    (三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。

    线程调度

    按照特定机制为多个线程分配 CPU 的使用权

    调度模型:分时调度模型和抢占式调度模型

    分时调度模型:让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片

    Java虚拟机采用抢占式调度模型:优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。

    线程的调度策略

    线程调度器选择优先级最高的线程运行,发生以下情况,会终止线程的运行

    • 线程体中调用了 yield 方法让出了对 cpu 的占用权利
    • 线程体中调用了 sleep 方法使线程进入睡眠状态
    • 线程由于 IO 操作受到阻塞
    • 另外一个更高优先级线程出现

    在支持时间片的系统中,该线程的时间片用完

    线程调度器(Thread Scheduler)和时间分片(Time Slicing )

    线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。

    时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间可以基于线程优先级或者线程等待的时间。

    线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。

    线程同步以及线程调度

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

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

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

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

    sleep() 和 wait()

    • 类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
    • 是否释放锁:sleep() 不释放锁;wait() 释放锁。
    • 用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
    • 用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法
    • sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。

    wait, notify和 notifyAll被调用

    当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。

    sleep()和 yield()

    • sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;

    • 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;

    • sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;

    • sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。

    interrupt、interrupted 和 isInterrupted

    • interrupt:用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。

    • 注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。

    • interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。

    • isInterrupted:查看当前中断信号是true还是false

    线程间的同步

    线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。

    用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模式下的方法有:事件,信号量,互斥量。
    实现线程同步的方法

    同步代码方法:sychronized 关键字修饰的方法

    同步代码块:sychronized 关键字修饰的代码块

    使用特殊变量域volatile实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制

    使用重入锁实现线程同步:reentrantlock类是可重入、互斥、实现了lock接口的锁

    线程池队列已满

    (1)如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务

    (2)如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy

    多线程的运行安全

    • 方法一:使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
    • 方法二:使用自动锁 synchronized。
    • 方法三:使用手动锁 Lock。
    Lock lock = new ReentrantLock();
    lock. lock();
    try {
        System. out. println("获得锁");
    } catch (Exception e) {
        // TODO: handle exception
    } finally {
        System. out. println("释放锁");
        lock. unlock();
    }
    

    静态块、构造方法的线程调用

    线程类的构造方法、静态块是被 new这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用的。

    假设 Thread2 中 new 了Thread1,main 函数中 new 了 Thread2,那么:

    • Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run()方法是Thread2 自己调用的

    • Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run()方法是Thread1 自己调用的

    线程数过多

    • 线程的生命周期开销非常高
    • 消耗过多的 CPU
    • 降低JVM稳定性

    Java内存模型

    finalize()和析构函数(finalization)

    • 垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法;finalize是Object类的一个方法,该方法在Object类中的声明protected void finalize() throws Throwable { },在垃圾回收器执行时会调用被回收对象的finalize()方法,可以覆盖此方法来实现对其资源的回收。一旦垃圾回收器准备释放对象占用的内存,将首先调用该对象的finalize()方法,并且下一次垃圾回收动作发生时,才真正回收对象占用的内存空间

    • GC就是内存回收了,finalization 大部分时候,什么都不用做(也就是不需要重载)。只有在某些很特殊的情况下,调用了一些native的方法(一般是C写的),可以要在finaliztion里去调用C的释放函数。

    指令重排

    在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序

    条件:

    • 在单线程环境下不能改变程序运行的结果(会破坏多线程的执行语义);
    • 存在数据依赖关系的不允许重排序

    as-if-serial规则和happens-before规则

    • as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。

    • as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。

    • as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。

    并发关键字synchronized

    synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。synchronized 可以修饰类、方法、变量。

    使用:

    • 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁

    • 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

    • 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

    • 总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!

    双重校验锁实现对象单例:

    public class Singleton {
    
        private volatile static Singleton uniqueInstance;
    
        private Singleton() {
        }
    
        public static Singleton getUniqueInstance() {
           //先判断对象是否已经实例过,没有实例化过才进入加锁代码
            if (uniqueInstance == null) {
                //类对象加锁
                synchronized (Singleton.class) {
                    if (uniqueInstance == null) {
                        uniqueInstance = new Singleton();
                    }
                }
            }
            return uniqueInstance;
        }
    }
    

    uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

    • 为 uniqueInstance 分配内存空间
    • 初始化 uniqueInstance
    • 将 uniqueInstance 指向分配的内存地址

    但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行

    synchronized 底层实现:javap -c -v(反汇编指令)

    执行同步代码块之前之后都有monitor字样,其中前面的是monitorenter,后面的是离开monitorexit,一个线程也执行同步代码块,首先要获取锁,而获取锁的过程就是monitorenter ,在执行完代码块之后,要释放锁,释放锁就是执行monitorexit指令(还有一个monitorexit是发生异常的时候释放锁)。

    synchronized可重入:

    重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。

    自旋:

    synchronized 里面只是一些很简单的代码,执行时间非常快,等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。synchronized 里面的代码执行得非常快,等待锁的线程不要被阻塞,在 synchronized 的边界做忙循环,这就是自旋。如果做了多次循环发现还没有获得锁,再阻塞

    synchronized 锁升级(减低锁带来的性能消耗):

    在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

    synchronized 和 Lock

    • synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
    • synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
    • synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
    • Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

    synchronized 和 ReentrantLock

    • 都是可重入锁
    • ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
    • ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
    • ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
    • 二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word

    并发关键字volatile

    volatile 关键字来保证可见性和禁止指令重排,,volatile 的一个重要作用就是和 CAS 结合,保证了原子性。

    volatile无法保证原子性,但用volatile修饰long和double可以保证其操作原子性

    • 对于64位的long和double,如果没有被volatile修饰,那么对其操作可以不是原子的。在操作的时候,可以分成两步,每次对32位操作。
    • 使用volatile修饰long和double,那么其读写都是原子操作
    • 对于64位的引用地址的读写,都是原子操作
    • 在实现JVM时,可以自由选择是否把读写long和double作为原子操作
    • 推荐JVM实现为原子操作

    volatile 和 atomic

    • volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。

    • AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。

    synchronized 和 volatile

    synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。

    volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。

    • volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。

    • volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。

    • volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。

    • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

    • volatile关键字是线程同步的轻量级实现,volatile性能肯定比synchronized关键字要好。volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。

    lock interface

    Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。优点:

    • 可以使锁更公平
    • 可以使线程在等待锁的时候响应中断
    • 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
    • 可以在不同的范围,以不同的顺序获取和释放锁

    Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,在大部分情况下,非公平锁是高效的选择。

    乐观锁和悲观锁

    悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。

    乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

    乐观锁的实现方式:

    • 使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。

    • java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置值更新为新值 B。否则处理器不做任何操作

    CAS问题:

    • ABA 问题:比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。从 Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。
    • 循环时间长开销大:对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。
    • 只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。

    死锁

    产生死锁:

    • 互斥条件:所谓互斥就是进程在某一时间内独占资源。
    • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    • 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
    • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

    防止死锁:

    • 尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时退出防止死锁。
    • 尽量使用 Java. util. concurrent 并发类代替自己手写锁。
    • 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
    • 尽量减少同步的代码块。

    死锁与活锁,死锁与饥饿

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

    活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。

    饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。

    活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。

    Java 导致饥饿:

    • 高优先级线程吞噬所有的低优先级线程的 CPU 时间。

    • 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。

    • 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。

    AQS

    AQS的全称为AbstractQueuedSynchronizer,在java.util.concurrent.locks包下

    AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask皆是基于AQS
    AQS核心思想:

    如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

    CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。

    在这里插入图片描述

    AQS 对资源的共享方式

    • Exclusive(独占):只有一个线程能执行,如ReentrantLock。

    • 可分为公平锁和非公平锁:

      公平锁:按照线程在队列中的排队顺序,先到者先拿到锁

      非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的

    • Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。

    ReentrantReadWriteLock

    使用 ReentrantLock,可能本身是为了防止线程 A 在写数据、线程 B 在读数据造成的数据不一致,但这样,如果线程 C 在读数据、线程 D 也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。因此诞生了读写锁 ReadWriteLock。

    ReadWriteLock 是一个读写锁接口,读写锁是用来提升并发程序性能的锁分离技术,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

    而读写锁有以下三个重要的特性:

    • 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。

    • 重进入:读锁和写锁都支持线程重进入。

    • 锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。

    并发容器ConcurrentHashMap

    实现线程安全:

    JDK 1.6版本关键要素:

    • segment继承了ReentrantLock充当锁的角色,为每一个segment提供了线程安全的保障;

    • segment维护了哈希散列表的若干个桶,每个桶由HashEntry构成的链表。

    JDK1.8后,ConcurrentHashMap抛弃了原有的Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。

    SynchronizedMap 和 ConcurrentHashMap

    • SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为 map。

    • ConcurrentHashMap 使用分段锁来保证在多线程下的性能。

    • ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提升是显而易见的。

    • 另外 ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中,当iterator 被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时 new 新的数据从而不影响原有的数据,iterator 完成后再将头指针替换为新的数据 ,这样 iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。

    并发容器CopyOnWriteArrayList

    好处:

    • 多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。

    缺点:

    • 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 young gc 或者 full gc。
    • 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求。
    • 由于实际使用中可能没法保证 CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。

    设计思想:

    • 读写分离
    • 读和写分开最终一致性
    • 使用另外开辟空间的思路,来解决并发冲突

    并发容器之ThreadLocal

    ThreadLocal 是一个本地线程副本变量工具类,在每个线程中都创建了一个 ThreadLocalMap 对象,简单说 ThreadLocal 就是一种以空间换时间的做法,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。

    原理:线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。
    使用:

    public class TestThreadLocal {
        
        //线程本地存储变量
        private static final ThreadLocal<Integer> THREAD_LOCAL_NUM 
            = new ThreadLocal<Integer>() {
            @Override
            protected Integer initialValue() {
                return 0;
            }
        };
     
        public static void main(String[] args) {
            for (int i = 0; i <3; i++) {//启动三个线程
                Thread t = new Thread() {
                    @Override
                    public void run() {
                        add10ByThreadLocal();
                    }
                };
                t.start();
            }
        }
        
        /**
         * 线程本地存储变量加 5
         */
        private static void add10ByThreadLocal() {
            for (int i = 0; i <5; i++) {
                Integer n = THREAD_LOCAL_NUM.get();
                n += 1;
                THREAD_LOCAL_NUM.set(n);
                System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n);
            }
        }
        
    }
    

    ThreadLocal内存泄漏

    ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法

    解决方案:

    • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
    • 在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

    并发容器之BlockingQueue

    阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。

    这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。

    阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

    JDK7 提供了 7 个阻塞队列

    • ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。

    • LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。

    • PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。

    • DelayQueue:一个使用优先级队列实现的无界阻塞队列。

    • SynchronousQueue:一个不存储元素的阻塞队列。

    • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。

    • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

    线程池

    池化技术,线程池、数据库连接池、Http 连接池都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。

    Java 5+中的 Executor 接口定义一个执行线程的工具。它的子类型即线程池接口是 ExecutorService。工具类 Executors 提供了一些静态工厂方法,生成一些常用的线程池

    • newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

    • newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用 newFixedThreadPool方法来创建线程池,这样能获得更好的性能。

    • newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。

    • newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

    线程池优点:

    • 降低资源消耗:重用存在的线程,减少对象创建销毁的开销。

    • 提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

    • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

    • 附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。

    线程池状态:

    • RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
    • SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
    • STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
    • TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。
    • TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。

    Executors 框架

    Executors框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架。

    每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的,而且无限制的创建线程会引起应用程序内存溢出。

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

    弊端:

    • newFixedThreadPool 和 newSingleThreadExecutor:
      主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。

    • newCachedThreadPool 和 newScheduledThreadPool:
      主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。

    Executor 和 Executors

    • Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。

    • Executor 接口对象能执行我们的线程任务。

    • ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。

    • 使用 ThreadPoolExecutor 可以创建自定义线程池。

    • Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使用 get()方法获取计算的结果。

    submit() 和 execute()

    • 接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。

    • 返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有

    • 异常处理:submit()方便Exception处理

    线程池ThreadPoolExecutor

    ThreaPoolExecutor创建线程池方式只有一种,构造函数

    ThreadPoolExecutor 3 个最重要的参数:

    • corePoolSize :核心线程数,线程数定义了最小可以同时运行的线程数量。
    • maximumPoolSize :线程池中允许存在的工作线程的最大数量
    • workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中。

    ThreadPoolExecutor其他常见参数:

    • keepAliveTime:线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
    • unit :keepAliveTime 参数的时间单位。
    • threadFactory:为线程池提供创建新线程的线程工厂
    • handler :线程池任务队列超过 maxinumPoolSize 之后的拒绝策略

    ThreadPoolExecutor饱和策略

    如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略:

    • ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
    • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。不会丢失任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。这个策略喜欢增加队列容量。如果应用程序可以承受此延迟并且不能丢弃任何一个任务请求的话,可以选择这个策略。
    • ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
    • ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。

    线程池实现

    在这里插入图片描述

    import java.util.Date;
    
    /**
     * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。
     */
    public class MyRunnable implements Runnable {
    
        private String command;
    
        public MyRunnable(String s) {
            this.command = s;
        }
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
            processCommand();
            System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
        }
    
        private void processCommand() {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        @Override
        public String toString() {
            return this.command;
        }
    }
    
    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    
    public class ThreadPoolExecutorDemo {
    
        private static final int CORE_POOL_SIZE = 5;
        private static final int MAX_POOL_SIZE = 10;
        private static final int QUEUE_CAPACITY = 100;
        private static final Long KEEP_ALIVE_TIME = 1L;
        public static void main(String[] args) {
    
            //使用阿里巴巴推荐的创建线程池的方式
            //通过ThreadPoolExecutor构造函数自定义参数创建
            ThreadPoolExecutor executor = new ThreadPoolExecutor(
                    CORE_POOL_SIZE,
                    MAX_POOL_SIZE,
                    KEEP_ALIVE_TIME,
                    TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                    new ThreadPoolExecutor.CallerRunsPolicy());
    
            for (int i = 0; i < 10; i++) {
                //创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
                Runnable worker = new MyRunnable("" + i);
                //执行Runnable
                executor.execute(worker);
            }
            //终止线程池
            executor.shutdown();
            while (!executor.isTerminated()) {
            }
            System.out.println("Finished all threads");
        }
    }
    

    原子操作类

    原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。

    处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作。 CAS 操作——Compare & Set,或是 Compare & Swap,现在几乎所有的 CPU 指令都支持 CAS 的原子操作。

    java.util.concurrent 这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择另一个线程进入(这只是一种逻辑上的理解)

    • 原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

    • 原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

    • 原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

    ABA 问题的原子类:AtomicMarkableReference(通过引入一个 boolean来反映中间有没有变过),AtomicStampedReference(通过引入一个 int 来累加来反映中间有没有变过)

    atomic

    Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以像自旋锁一样,继续尝试,一直等到执行成功。

    AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

    CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。

    并发工具CountDownLatch、CyclicBarrier

    CountDownLatch与CyclicBarrier都是用于控制并发的工具类,都可以理解成维护的就是一个计数器,但是这两者还是各有不同侧重点的:

    • CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;CountDownLatch强调一个线程等多个线程完成某件事情。CyclicBarrier是多个线程互等,等大家都完成,再携手共进。

    • 调用CountDownLatch的countDown方法后,当前线程并不会阻塞,会继续往下执行;而调用CyclicBarrier的await方法,会阻塞当前线程,直到CyclicBarrier指定的线程全部都到达了指定点的时候,才能继续往下执行;

    • CountDownLatch方法比较少,操作比较简单,而CyclicBarrier提供的方法更多,比如能够通过getNumberWaiting(),isBroken()这些方法获取当前多个线程的状态,并且CyclicBarrier的构造方法可以传入barrierAction,指定当所有线程都到达时执行的业务功能;

    • CountDownLatch是不能复用的,而CyclicLatch是可以复用的。

    并发工具Semaphore、Exchanger

    Semaphore :

    Semaphore 就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个 int 型整数 n,表示某段代码最多只有 n 个线程可以访问,如果超出了 n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果 Semaphore 构造函数中传入的 int 型整数 n=1,相当于变成了一个 synchronized 了。

    Semaphore(信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。

    Exchanger:

    Exchanger是一个用于线程间协作的工具类,用于两个线程间交换数据。它提供了一个交换的同步点,在这个同步点两个线程能够交换数据。交换数据是通过exchange方法来实现的,如果一个线程先执行exchange方法,那么它会同步等待另一个线程也执行exchange方法,这个时候两个线程就都达到了同步点,两个线程就可以交换数据。

    锁升级

    偏向锁

    在这里插入图片描述
    在这里插入图片描述

    ​ 特点:适用于单个线程访问临界资源

    ​ 核心思想:一个线程获得锁后锁对象的对象头中Mark Word结构变为偏向锁结构(偏向锁标志位 置为1,锁标 志位为01,保存线程id),下次相同线程只需要比对线程id即可访问临界资源。

    轻量锁:

    在这里插入图片描述

    ​ 轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入 锁争用的时 候(检查锁对象中的线程id不是自己的,通过CAS修改失败),偏向锁就会升级为轻量级锁

    ​ 特点:适用多个线程交替访问临界资源

    ​ 核心思想:当锁升级为轻量锁后锁对象的对象头中Mark Word结构变为轻量锁结构(偏向锁标志 位置为0,锁 标志位为00),各个线程的栈帧中创建一个锁记录(Lock Record)空间,官方叫做 Displaced Mark Word 用于存储锁对象目前的Mark Word的拷贝。Lock Record 存储Mark Word完成后,各个线程通过CAS修 改锁对象Mark Word的指针指向Lock Record,Lock Record 中owner指针指向锁对象Mark Word,修改成功 的线程访问临界资源,未成的进行自旋转,但自 旋到一定的次数升级为重量级锁

    重锁:

    ​ 升级为重锁后锁对象标志位置为10,对象头Mark Word中的指针指向ObjectMonitor

    ​ 重锁采用Monitor实现依赖于操作系统的MutexLock(互斥锁)

    ​ ObjectMonitor中大致结构为

     ObjectMonitor() {
        ......
        _count        = 0; // 用来记录该线程获取锁的次数
        _recursions   = 0;     // 锁的重入次数
        _owner        = NULL;  // 指向持有 ObjectMonitor 锁的对象的线程
        _WaitSet      = NULL;   // 存放处于wait状态的线程队列
        _EntryList    = NULL ;    // 存放处于等待锁block状态的线程队列
        ......
      }
    
    
    • 当多个线程竞争锁时进入EntryList队列,某个线程获取到对象的Monitor后进入Owner区域并把Monitor中的Owner变量设置为当前线程,同时Monitor中的计数器 count自增1,表示获得对象锁
    • 持有Monitor的线程调用wait()方法,将释放当前持有的 Monitor , Owner变量恢复为 null, count自减1,同时该线程进入WaitSet集合中等待被唤醒(唤醒后进入EntryList)。若当前线程执行完毕会释放Monitor(锁)并复位变量的值,以便其他线程能够获取Monitor(锁)
    展开全文
  • 公平锁与非公平锁

    2021-04-19 16:22:31
    我们知道synchronized只能实现非公平锁,因此在需要使用公平锁的情况下会使用ReentrantLock,非公平锁公平锁的区别在于会不会在锁释放时,新的线程是否会和AQS队列中的队首线程竞争锁。但是以上八股文的背法,为了...

    ReentrantLock和synchronized的自JDK1.6以来主要的区别在于:1、等待可中断;2、公平锁与非公平锁;3、锁的多个条件。我们知道synchronized只能实现非公平锁,因此在需要使用公平锁的情况下会使用ReentrantLock,非公平锁和公平锁的区别在于会不会在锁释放时,新的线程是否会和AQS队列中的队首线程竞争锁。但是以上八股文的背法,为了更好的理解公平锁与非公平锁,笔者简单的模拟了公平锁和非公平锁的竞争的情况,详细的注释已经在代码里了。

    package JUC;import java.util.concurrent.*;import java.util.concurrent.atomic.AtomicInteger;import java.util.concurrent.locks.ReentrantLock;/** * 判断公平锁和非公平锁的依据是模拟了thread2和thread3会竞争锁资源,然后以sleep时间代表是thread2还是thread3 * thread2是sleep 250,thread3是sleep 200 * 公平锁的情况下,250一定在200前面;非公平锁的情况下会出现200在250的前面 * 非公平锁出现200在250之前的情况是因为thread3此时和AQS中thread2竞争,先拿到了锁,先输出了200 * 公平锁的情况下thread3不会和AQS中的thread2竞争,加在了thread2的后面,所以结果稳定是200在250的后面 * 使用sleep时间来表示thread2和thread3而不使用线程名的原因是:笔者使用的是线程池,线程池中的线程执行顺序和变量thread1、thread2、thread3没有关系,可能会出现线程3执行thread3,详细情况可以将第36行注释删除打印输出来看(可能需要多run几次或者修改59行的i的上限) * 第63行和第65行可以用于选择公平锁和非公平锁 * @author lvgang * @date 2021/4/15 15:51 */public class ReentrantLockTest {    static class TestThread implements Runnable {        ReentrantLock lock;        int sleepTime;        CountDownLatch latch3Thread;        TestThread(ReentrantLock lock, int sleepTime, CountDownLatch latch3Thread) {            this.lock = lock;            this.sleepTime = sleepTime;            this.latch3Thread = latch3Thread;        }        @Override        public void run() {            lock.lock();            try {                // System.out.println(Thread.currentThread().getName());                System.out.println("sleep时间:" + sleepTime);                Thread.sleep(sleepTime);            } catch (InterruptedException e) {                e.printStackTrace();            } finally {                lock.unlock();                latch3Thread.countDown();            }        }    }    public static void main(String[] args) throws InterruptedException {        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 6, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10), new ThreadFactory() {            private final AtomicInteger atomicInteger = new AtomicInteger(1);            @Override            public Thread newThread(Runnable r) {                return new Thread(r, "faceSchool线程_" + atomicInteger.getAndIncrement());            }        });        for (int i = 0; i < 20; i++) {            // latch3Thread的作用是让3个线程运行完之后再开始下一次公平锁或者非公平锁的结果的模拟,避免多次run,使得结果的对比不明显            CountDownLatch latch3Thread = new CountDownLatch(3);            // 非公平锁             ReentrantLock lock = new ReentrantLock();            // 公平锁            // ReentrantLock lock = new ReentrantLock(true);            TestThread thread1 = new TestThread(lock, 300,  latch3Thread);            threadPoolExecutor.execute(thread1);            // 此处的sleep是保证thread1获取了锁,避免和thread2竞争            Thread.sleep(100);            TestThread thread2 = new TestThread(lock, 250, latch3Thread);            threadPoolExecutor.execute(thread2);            // 此处的sleep是保证thread2进入了lock的AQS队列,顺便等待thread1运行完,模拟出thread2和thread3在非公平锁下的竞争情况            Thread.sleep(200);            TestThread thread3 = new TestThread(lock, 200, latch3Thread);            threadPoolExecutor.execute(thread3);            latch3Thread.await();            System.out.println("--------------------------------------------------------");        }        threadPoolExecutor.shutdown();    }}

        公平锁的结果:

        非公平锁的结果:

        笔者能力有限,有问题的地方,烦请指出。

       欢迎关注公众号:faceSchool。

    展开全文
  • 目录)1、什么是公平锁与非公平锁2、ReentrantLock如何实现公平与非公平3、公平锁与非公平锁性能对比 1、什么是公平锁与非公平锁 公平锁公平锁就是保障了多线程下各线程获取锁的顺序,先到的线程优先获取锁。 非...

    1、什么是公平锁与非公平锁

    公平锁:公平锁就是保障了多线程下各线程获取锁的顺序,先到的线程优先获取锁。
    非公平锁:非公平锁则无法提供这个保障(先到的线程优先获取锁)。



    2、ReentrantLock如何实现公平与非公平

    Java并发包下面的ReentrantLockReadWriteLock默认都是非公平模式

    下面我们就来一起看看ReentrantLock是如何实现公平与非公平的。

    ReentrantLock实现了Lock接口。提供了下面2个构造方法

    public ReentrantLock() {
        sync = new NonfairSync();
    }
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    

    默认构造的是非公平锁NonfairSync。
    NonfairSync和FairSync都是ReentrantLock的内部类,且继承ReentrantLock的内部抽象类Sync。

    // 公平锁
    protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
    	        // 主要区别:有hasQueuedPredecessors方法
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }
    
    // 非公平锁
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
          // 主要区别:没有hasQueuedPredecessors方法
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
    

    二者的主要区别在于是否有hasQueuedPredecessors方法,我们看下hasQueuedPredecessors的源码。该方法返回“队列中是否存在一个线程(先于当前线程)”,如果存在话,当前线程就要加入到队列的尾部。

    * @return {@code true} if there is a queued thread preceding the
     *         current thread, and {@code false} if the current thread
     *         is at the head of the queue or the queue is empty
     * @since 1.7
     */
    public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
    

    网上有这一张很棒的图:
    在这里插入图片描述

    结合这张图,二者的区别更明显。
    公平锁就是在获取锁之前会先判断等待队列是否为空或者自己是否位于队列头部,该条件通过才能继续获取锁。

    在结合兔子喝水的图分析,非公平锁获取所得顺序基本决定在9、10、11这三个事件发生的先后顺序,

    • 1、若在释放锁的时候总是没有新的兔子来打扰,则非公平锁等于公平锁;
    • 2、若释放锁的时候,正好一个兔子来喝水,而此时位于队列头的兔子还没有被唤醒(因为线程上下文切换是需要不少开销的),此时后来的兔子则优先获得锁,成功打破公平,成为非公平锁;

    其实对于非公平锁,只要线程进入了等待队列,队列里面依然是FIFO的原则,跟公平锁的顺序是一样的。因为公平锁与非公平锁的release()部分代码是共用AQS的代码。

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    
    /**
     * Wakes up node's successor, if one exists.
     */
    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
    
        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }
    
    



    3、公平锁与非公平锁性能对比

    非公平锁的效率高于公平锁:上文说到的线程切换的开销,其实就是非公平锁效率高于公平锁的原因,因为非公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起的开销。

    多线程连载(系列文章按顺序阅读哦):
    Java内存模型-volatile的应用(实例讲解)
    synchronized的三种应用方式(实例讲解)
    可重入锁-synchronized是可重入锁吗?
    大彻大悟synchronized原理,锁的升级
    一文弄懂Java的线程池

    在这里插入图片描述

    展开全文
  • 主要介绍了java 线程公平锁与非公平锁详解及实例代码的相关资料,需要的朋友可以参考下
  • 锁Lock分为"公平锁"和"非公平锁",公平锁表示线程获取锁的顺序是按照线程加锁的顺序分配的,即先来先得的FIFO先进先出的顺序,而非公平锁就是一个获取锁的抢占机制,是随机获得锁的,和公平锁不一样的就是先来的不一定先...
  • 线程:线程池

    2019-03-26 14:22:24
    Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处。第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和...
  • 的实现其实很简单,主要使用Java中synchronized关键字。 public class Lock { private volatile boolean isLocked = false; private Thread lockingThread = null; public synchronized void lock() throws ...
  • 2.什么是公平锁与非公平锁公平锁和非公平锁其实说的是获取锁的机会是不是对每个等待线程都公平。 这就好比一群学生都在竞争一个答题名额: 每次都是同一个学生抢到,这对其他学生不公平。 公平锁就是获取...
  • 线程池使用及优势

    2019-06-22 22:08:24
    线程池使用及优势 大厂面试题: 1、请你谈谈对volatile的理解? 2、CAS你知道吗?...5、公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解?请手写一个自旋锁。 6、CountDownLatch、Cyclic...
  • 公平锁与非公平锁  锁Lock分为L:公平锁和非公平锁。  公平锁:表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。  非公平锁:一种获取锁的抢占机制,是随机获得锁的,和公平锁...
  • java多线程 ReentrantLock.lock 公平锁公平锁
  • Java线程-线程池-(lock)

    千次阅读 2019-03-12 10:34:33
    在Java5当中,专门提供了对象,利用可以方便的实现对资源的封锁,用来控制对竞争资源并发访问控制,这些内容主要集中在java.util.concurrent.locks包下面,里面有三个重要的接口Condition、Lock、ReadWriteLock...
  • 公平锁与非公平锁:排队和竞争) package com.zhang.test; import java.util.concurrent.Semaphore; class ThradDemo001 extends Thread { private String name; private Semaphore wc; ...
  • 在Java并发场景中,会涉及到各种各样的锁,比如:高并发编程系列:4种常用Java线程锁的特点,性能比较、使用场景,这些锁有对应的种类:公平锁,乐观锁,悲观锁等等,这篇文章来详细介绍各种锁的分类: 公平锁/...
  • 如果一个代码块被synchronized修饰了,当一个线程获取了对应的,并执行该代码块时,其他线程便只能一直等待,等待获取的线程释放,而这里获取的线程释放只会有两种情况:  1)获取的线程执行完了该...
  • 线程池

    2019-11-18 14:32:39
    一、java线程池的参数 1. corePoolSize:指定了线程池中的线程数量。 2. maximumPoolSize:指定了线程池中的最大线程数量。 3. keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活...
  • 前言:刚学习了一段机器学习,最近需要重构一个java项目,又赶过来看java。大多是线程代码,没办法,那时候总觉得多线程是个很难的部分很少用到,所以一直没下决定去啃,那些...目标:线程,并发包(线程池,并发的数...
  • 线程池原理分析&的深度化 并发包 同步容器类 Vector与ArrayList区别 1.ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当...
  • 锁的深入 什么是重入锁 锁的种类 读写锁 悲观和乐观 CAS无锁 自旋锁 AQS 非公平锁 公平锁 互斥锁 排他锁 分布式锁:redis实现分布式锁。Zookeeper实现分布式锁 ...
  • 这里简单列下: 公平锁/非公平锁 可重入锁 独享锁/共享锁 互斥锁/读写锁 乐观锁/悲观锁 分段锁 偏向锁/轻量级锁/重量级锁 自旋锁 讲java的锁之前,需要先过几个基础。 volatile 细致入微系列:volatile原理 从修饰...
  • java 线程池

    2018-05-09 10:49:05
    值的构造函数要求使用公平锁。 锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而在 synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条 件,如果要和...
  • 为什么使用线程同步或使用锁能解决线程安全问题呢? 将可能会发生数据冲突问题(线程不安全问题),只能让当前一个线程进行执行。代码执行完成后释放,让后才能让其他线程进行执行。这样的话就可以解决线程不安全...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 15,217
精华内容 6,086
关键字:

线程池用公平锁