Java-线程篇-多线程并发下的锁

一、概念

1.线程、进程和程序

        ​线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。 ​

        进程(Process)是程序的一次动态执行,它对应着从代码加载,执行至执行完毕的一个完整的过程,是一个动态的实体,它有自己的生命周期。它因创建而产生,因调度而运行,因等待资源或事件而被处于等待状态,因完成任务而被撤消。反映了一个程序在一定的数据 集上运行的全部动态过程。通过进程控制块(PCB)唯一的标识某个进程。同时进程占据着相应的资源(例如包括cpu的使用 ,轮转时间以及一些其它设备的权限)。是系统进行资源分配和调度的一个独立单位。

        程序(Program)是一组计算机能识别和执行的指令,运行于电子计算机上,满足人们某种需求的信息化工具。它以某些程序设计语言编写,运行于某种目标结构体系上。打个比方,程序就如同以英语(程序设计语言)写作的文章,要让一个懂得英语的人(编译器)同时也会阅读这篇文章的人(结构体系)来阅读、理解、标记这篇文章。一般的,以英语文本为基础的计算机程序要经过编译、链接而成为人难以解读,但可轻易被计算机所解读的数字格式,然后放入运行。计算机程序,是指为了得到某种结果而可以由计算机等具有信息处理能力的装置执行的代码化指令序列,或者可以被自动转换成代码化指令序列的符号化指令序列或者符号化语句序列。

2.Java中的线程

        Java VM启动的时候会有一个进程java.exe。

        该进程中至少一个线程负责java程序的执行,而且这个线程运行的代码存在于main方法中,该线程称之为主线程。jvm启动不止一个线程,还有负责垃圾回收机制的线程。(主线程在执行其它对象时,无用对象在被垃圾回收)

a.进程的状态

        操作系统层面的五种状态:初始状态、就绪状态、运行状态、阻塞状态、终止状态。

        就绪状态:

        当进程已分配到除CPU以外的所有必要资源后,只要获得CPU,便可立即执行,进程这时的状态就称为就绪状态。在一个系统中处于就绪状态的进程可能有多个,通常将他们排成一个队列,称为就绪队列

        执行状态:

        进程已获得CPU,其程序正在执行。在单处理机系统中,只有一个进程处于执行状态;再多处理机系统中,则有多个进程处于执行状态

        阻塞状态:

        正在执行的进程由于发生某事件而暂时无法继续执行时,便放弃处理机而处于暂停状态,亦即程序的执行受到阻塞,把这种暂停状态称为阻塞状态,有时也称为等待状态或封锁状态。

b.线程的生命周期

        Java层面的六种状态:NEW(创建)、RUNNABLE(运行\就绪)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(定时等待)、TERMINATED(销毁)。

1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的成为“运行”。
        线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得cpu 时间片后变为运行中状态(running)。
        注意:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中
3. 阻塞(BLOCKED):线程在获取synchronized排他锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作线程显式地唤醒(通知或中断)。
5. 限期等待(TIMED_WAITING)状态:该状态不同于WAITING,处于这种状态的线程也不会被分配CPU执行时间,不过无须等待其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。
6. 销毁(TERMINATED)状态: 线程执行完了或者因异常退出了run()方法,该线程生命周期结束。

3.多线程

a.什么是多线程

        在计算机体系结构中,多线程是中央处理器 (CPU)(或多核处理器中的单核)在操作系统支持下并发提供多个执行线程的能力。 这种方法不同于多处理。 在多线程应用程序中,线程共享单个或多个内核的资源,包括计算单元、CPU 缓存和转换后备缓冲区 (TLB)。

b.创建线程

        实现方式和继承方式:

  • 实现方式好处:避免单继承的局限性。在定义线程时,建议使用实现方式。
  • 继承Thread:线程代码存放Thread子类run方法中。
  • 实现Runnable:线程代码存在接口的子类的run方法。
继承Thread类:
  • 定义类继承Thread
  • 子类覆盖父类Thread中的run方法。
    • 目的:将自定义线程运行的代码存储在run方法
  • 建立子类对象的同时线程也被创建。
  • 调用线程的start方法
    该方法两个作用:启动线程,调用run方法。通过调用start方法开启线程

          代码示例:

public class Test extends Thread {
    //private String name;
    Test(String name) {
        super(name); //this.name = name;
    }

    public void run() {
        for (int x = 0; x < 60; x++) {
            System.out.println((Thread.currentThread() == this) + "..." + this.getName() + " run..." + x); //当前运行线程即为调用对象线程
        }
    }
}

public class ThreadTest {
    public static void main(String[] args) {
        Test t1 = new Test("one---");
        Test t2 = new Test("two+++");
        t1.start();//开启线程并执行该线程的run方法。
        t2.start();
        for (int x = 0; x < 60; x++) {
            System.out.println("main....." + x);
        }
    }
}
实现Runnable接口:
  • 定义类实现Runnable接口

  • 覆盖Runnable接口中的run方法。将线程要运行的代码存放在该run方法中。
  • 通过Thread类创建线程
  • Runnable接口的子类对象作为实际参数传递给Thread类的构造函数
    因为,自定义的run方法所属的对象是Runnable接口的子类对象。
    所以要让线程去指定指定对象的run方法,就必须明确该run方法所属对象。
  • 用Thread类的start方法开启线程并调用Runnable接口子类的run方法。

  代码示例:

public class Ticket implements Runnable {
    private int tick = 100;

    @Override
    public void run() {
        while (tick > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
            }
            System.out.println(Thread.currentThread().getName() + "...remain tick:" + --tick);
        }
    }
}

public class RtJava {
    public static void main(String[] args){
        Ticket ticket = new Ticket();
        Thread t1 = new Thread(ticket);
        Thread t2= new Thread(ticket);
        Thread t3 = new Thread(ticket);
        Thread t4= new Thread(ticket);
        Thread t5 = new Thread(ticket);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}
实现Callable接口,重写它的call()方法:

        Callable接口其实是Executor框架中的功能类,它和Runnable接口类似,但是比Runnable接口功能强大,主要表现在以下三点:

  1. Callable可以在运行结束后提供一个返回值,Runnable没有返回值
  2. Callable中的call()方法可以主动抛出异常
  3. 运行Callable对象可以得到一个Future对象,Future表示异步计算的结果。由于线程属于异步计算模型,因此无法从别的线程中得到函数的返回结果,在这种情况下,就可以使用Future来监视目标线程调用call()的返回结果。当你调用Future的get()方法获取返回值的时候,当前线程会被阻塞,直到call()方法返回结果。

        操作步骤:

  1. 第一步编写一个类实现Callable接口,重写call()方法
  2. 启动线程
  3. 创建Callable接口实现类对象
  4. 创建一个FutureTask对象, 传递Callable接口实现类对象, FutureTask异步得到Callable执行结果, 提供get() FutureTask 实现Future接口( get()) 实现Runnable接口
  5. 创建一个Thread对象, 把FutureTask对象传递给Thread, 调用start()启动线程

        代码示例:

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
    Thread.sleep(3000);
    return "线程3";
    }
});
System.out.println("start");
System.out.println(future.get());
System.out.println("end");

c.常用API

        wait()、notify()和notifyAll()的区别:

  • wait(),notify(),notifyAll()方法都使用在同步中,因为这些方法要对持有监视器(锁)的线程操作(而只有同步才具有锁)
  • 使用这些方法时必须要标识所属的同步的锁。(因为这些方法在操作同步中线程时,都必须要标识它们所操作线程共有的锁,只有同一个锁上的被等待线程,可以被同一个锁上notify唤醒。不可以对不同锁中的线程进行唤醒。也就是说,等待和唤醒必须是同一个锁。)
  • 锁可以是任意对象,所以任意对象调用的方法一定定义Object类中。

        wait(),sleep()的区别:

  • wait():释放cpu执行权,释放锁。
  • sleep():释放cpu执行权,不释放锁。
方法名 功能说明 注意
start()启动一个新线程,在新的线程运行 run 方法中的代码start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。 每个线程对象的start方法只能调用一次 ,如果调用了多次,会出现IllegalThreadStateException
run()新线程启动后会调用的方法
yield() 提示线程调度器让出当前线程对CPU的使用,让出后可能该线程又继续被调度执行主要是为了测试和调试
sleep(long n) 让当前执行的线程休眠n毫秒,休眠时让出 cpu的时间片给其它线程
join()等待线程运行结束谁调用,等待谁执行结束
join(long n)等待线程运行结束,最多等待 n毫秒1、线程执行结束耗时小于n毫秒时,join方法会提前结束;
2、有多个线程都执行join时,总耗时会是最大的那个n值,并不是n累加
interrupt() 打断线程(理解为设置打断标记为true)1、如果被打断线程正在 sleep、wait、join ,会导致被打断的线程抛出 InterruptedException,并清除打断标记;
2、如果打断正在运行的线程,不会抛出异常、不会清除打断标记;
3、park 的线程被打断,会导致被打断的线程抛出 InterruptedException,但不会清除打断标记;
interrupted() 打断线程(理解为设置打断标记为true),是Thread类的静态方法会清除打断标记,即该方法被调用后,会设置线程打断标记为false
isInterrupted() 判断线程打断标记是否为true不会清除打断标记
currentThread() 获取当前正在执行的线程
getId() 获取线程长整型
的 id 
id 唯一
getName() 获取线程名
setName(String) 修改线程名
getPriority() 获取线程优先级
setPriority(int) 修改线程优先级 1、java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率;
2、仅是设置优先级,哪个线程被调度还是由操作系统控制
isAlive()判断线程有没有运行完毕
setDaemon()设置守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

JVM启动后有哪些线程?
   -- main
   -- Finalizer:GC守护线程
    --RMI:Java自带的远程方法调用
    --Monitor :是一个守护线程,负责监听一些操作,也在main线程组中

getState() 获取线程状态Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING,TIMED_WAITING, TERMINATED
stop() 停止线程运行已过时,容易破坏同步代码块,造成线程死锁
suspend() 挂起(暂停)线程运行
resume() 恢复线程运行

4.线程池

a.概念

        线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

        任务调度以执行线程的常见方法是使用同步队列,称作任务队列。池中的线程等待队列中的任务,并把执行完的任务放入完成队列中。

        作用

        处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。

b.线程池模式:

        线程池模式一般分为两种:HS/HA半同步/半异步模式、L/F领导者与跟随者模式。

  • 半同步/半异步模式又称为生产者消费者模式,是比较常见的实现方式,比较简单。分为同步层、队列层、异步层三层。同步层的主线程处理工作任务并存入工作队列,工作线程从工作队列取出任务进行处理,如果工作队列为空,则取不到任务的工作线程进入挂起状态。由于线程间有数据通信,因此不适于大数据量交换的场合。

  • ​领导者跟随者模式,在线程池中的线程可处在3种状态之一:领导者leader、追随者follower或工作者processor。任何时刻线程池只有一个领导者线程。事件到达时,领导者线程负责消息分离,并从处于追随者线程中选出一个来当继任领导者,然后将自身设置为工作者状态去处置该事件。处理完毕后工作者线程将自身的状态置为追随者。这一模式实现复杂,但避免了线程间交换任务数据,提高了CPU cache相似性。在ACE(Adaptive Communication Environment)中,提供了领导者跟随者模式实现。 ​

c.获取线程池

        JAVA语言为我们提供了两种基础线程池的选择:ScheduledThreadPoolExecutorThreadPoolExecutor。它们都实现了ExecutorService接口(注意,ExecutorService接口本身和“线程池”并没有直接关系,它的定义更接近“执行器”,而“使用线程管理的方式进行实现”只是其中的一种实现方式)。

常见线程池:
ThreadPoolExecutor:

        ThreadPoolExecutor提供了一系列的参数用于配置线程池。

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                              TimeUnit unit, BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) { 
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler);
}
newCachedThreadPool:

        创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

        特点:

  • 工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
  • 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
  • 在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪

示例代码如下:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExecutorTest {
    public static void main(String[] args) {
        //Executors.newCachedThreadPool()
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            try {
                Thread.sleep(index * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            cachedThreadPool.execute(new Runnable() {
                public void run() {
                    System.out.println(index);
                }
            });
        }
    }
}
newFixedThreadPool:

        创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。

        FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源

        示例代码如下:因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。定长线程池的大小最好根据系统资源进行设置如Runtime.getRuntime().availableProcessors()。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExecutorTest {
    public static void main(String[] args) {
        //Executors.newFixedThreadPool
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            final int index = i;
            fixedThreadPool.execute(new Runnable() {
                public void run() {
                    try {
                        System.out.println(index);
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}
newSingleThreadExecutor:

        创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。
        单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

        代码示例:

public class ThreadPoolExecutorTest {
    public static void main(String[] args) {
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            singleThreadExecutor.execute(new Runnable() {
                public void run() {
                    try {
                        System.out.println(index);
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
} 
newScheduleThreadPool:

        创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。

        延迟3秒执行,延迟执行示例代码如下:

public class ThreadPoolExecutorTest {
    public static void main(String[] args) {
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
        //scheduledThreadPool.schedule
        scheduledThreadPool.schedule(new Runnable() {
            public void run() {
                System.out.println("delay seconds");
            }
        }, 3, TimeUnit.SECONDS);//延迟3秒执行
    }
}

        延迟1秒后每3秒执行一次,定期执行示例代码如下:

public class ThreadPoolExecutorTest {
    public static void main(String[] args) {
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
        //scheduledThreadPool.scheduleAtFixedRate
        scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
            public void run() {
                System.out.println("delay seconds, and excute every seconds");
            }
        }, 1, 3, TimeUnit.SECONDS);//延迟1秒后每3秒执行一次
    }
}

5.ThreadPoolExecutor

a.线程池核心参数

  • corePoolSize : 线程池核心线程数,默认情况下,核心线程会在线程池中一直存活,即使他们处于闲置状态。
    其有一个allowCoreThreadTimeOut属性如果设置为true,那么核心线程池会有超时策略。超时的时长为第三个参数 keepAliveTime 。如果超时,核心线程会被终结
  • maxmumPoolSize: 线程池所能容忍的最大线程数,当活动线程数达到这个数值后,后续的新任务会被阻塞
  • keepAliveTime: 非核心线程闲置时的超时时长超过这个时长非核心线程会被回收。这个参数如同第一个参数,如果设置相关属性后也会作用于核心线程。
  • unit: 指定keepAliveTime的参数时间单位。这是一个枚举,常用的有MILLISECONDS(毫秒)、SECONDS(秒)等
  • workQueue: 线程池的任务队列,通过execute()方法(执行方法)提交的Runable对象会存储在这个参数中。
  • threadFactory: 线程工厂,为线程池提供创建新线程的功能。
  • Handler:线程池满了以后执行的拒绝策略。
    • AbortPolicy:中止策略。直接抛出异常
    • DiscardPolicy:抛弃策略。丢弃任务继续执行
    • DiscardOldestPolicy:抛弃最旧的策略。抛弃存在任务队列中最久没有执行的任务
    • CallerRunsPolicy:调用者运行策略。谁发现的问题谁执行这个任务

b.执行流程

  • 首先可以通过线程池提供的submit()方法或者execute()方法,要求线程池执行某个任务。线程池收到这个要求执行的任务后,会有几种处理情况:
            1、如果当前线程池中运行的线程数量还没有达到corePoolSize大小时,线程池会创建一个线程运行你的任务,无论之前已经创建的线程是否处于空闲状态。
            2、如果当前线程池中运行的线程数量已经达到设置的corePoolSize大小,线程池会把你的这个任务加入到等待队列(workQueue)中。直到某一个的线程空闲了,线程池会根据设置的等待队列规则,从队列中取出一个新的任务执行。
            3、如果根据队列规则,这个任务无法加入等待队列。(当workQueue已满,且maximumPoolSize>corePoolSize时)这时线程池就会创建一个“非核心线程”直接运行这个任务。注意,如果这种情况下任务执行成功,那么当前线程池中的线程数量一定大于corePoolSize。
            4、如果这个任务,无法被“核心线程”直接执行,又无法加入等待队列,又无法创建“非核心线程”直接执行,且你没有为线程池设置RejectedExecutionHandler。(当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理)这时线程池会抛出RejectedExecutionException异常,即线程池拒绝接受这个任务。(实际上抛出RejectedExecutionException异常的操作,是ThreadPoolExecutor线程池中一个默认的RejectedExecutionHandler实现:AbortPolicy)
  • 一旦线程池中某个线程完成了任务的执行,它就会试图到任务等待队列中拿去下一个等待任务(所有的等待任务都实现了BlockingQueue接口,按照接口字面上的理解,这是一个可阻塞的队列接口),它会调用等待队列的poll()方法,并停留在哪里。
  • 当线程池中的线程超过你设置的corePoolSize参数,说明当前线程池中有所谓的“非核心线程”。那么当某个线程处理完任务后,如果等待keepAliveTime时间后仍然没有新的任务分配给它,那么这个线程将会被回收。线程池回收线程时,对所谓的“核心线程”和“非核心线程”是一视同仁的,直到线程池中线程的数量等于你设置的corePoolSize参数时,回收过程才会停止。(当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
    设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭

c.ThreadFactory的使用

        在ThreadPoolExecutor线程池中,创建线程的工作交给ThreadFactory来完成。要使用线程池,就必须要指定ThreadFactory。

        如果我们使用的构造函数时并没有指定使用的ThreadFactory,这个时候ThreadPoolExecutor会使用一个默认的ThreadFactory:DefaultThreadFactory。(这个类在Executors工具类中)

        在某些特殊业务场景下,通过实现ThreadFactory来创建一个自定义ThreadFactory线程工厂:

import java.util.concurrent.ThreadFactory;
/* 测试自定义的一个线程工厂 */
public class TestThreadFactory implements ThreadFactory {
    @Override
    public Thread newThread(Runnable r) {
        return new Thread(r);
    }
}

d.等待队列

        在使用ThreadPoolExecutor线程池的时候,需要指定一个实现了BlockingQueue接口的任务等待队列。在ThreadPoolExecutor线程池的API文档中,一共推荐了三种等待队列,它们是:

  • SynchronousQueue
  • LinkedBlockingQueue
  • ArrayBlockingQueue;
栈和队列
队列:

        队列是一种特殊的线性结构,允许在线性结构的前端进行删除/读取操作;允许在线性结构的后端进行插入操作;这种线性结构具有“先进先出”的操作特点,但是在实际应用中,队列中的元素有可能不是以“进入的顺序”为排序依据的。例如我们将要讲到的PriorityBlockingQueue队列。

        栈是一种线性结构,但是栈和队列相比只允许在线性结构的一端进行操作,入栈和出栈都是在一端完成。

有界队列
SynchronousQueue

         一种阻塞队列,其中每个 put 必须等待一个 take,反之亦然。同步队列没有任何内部容量。这是一个内部没有任何容量的阻塞队列,任何一次插入操作的元素都要等待相对的删除/读取操作,否则进行插入操作的线程就要一直等待,反之亦然。

        代码示例:

SynchronousQueue<Object> queue = new SynchronousQueue<Object>(); 
// 不要使用add,因为这个队列内部没有任何容量,所以会抛出异常“IllegalStateException”
// queue.add(new Object()); 
// 操作线程会在这里被阻塞,直到有其他操作线程取走这个对象
queue.put(new Object());
ArrayBlockingQueue

        一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。这是一个典型的“有界缓存区”,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素。一旦创建了这样的缓存区,就不能再增加其容量。试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致类似阻塞。

        代码示例:

// 我们创建了一个ArrayBlockingQueue,并且设置队列空间为2 
ArrayBlockingQueue<Object> arrayQueue = new ArrayBlockingQueue<Object>(2); 
// 插入第一个对象 
arrayQueue.put(new Object()); 
// 插入第二个对象 
arrayQueue.put(new Object()); 
// 插入第三个对象时,这个操作线程就会被阻塞。 
arrayQueue.put(new Object()); 
// 请不要使用add操作,和SynchronousQueue的add操作一样,它们都使用了AbstractQueue中的add实现
无界队列
LinkedBlockingQueue

        LinkedBlockingQueue是我们在ThreadPoolExecutor线程池中常用的等待队列。它可以指定容量也可以不指定容量。由于它具有“无限容量”的特性,所以我还是将它归入了无限队列的范畴(实际上任何无限容量的队列/栈都是有容量的,这个容量就是Integer.MAX_VALUE)。
LinkedBlockingQueue的实现是基于链表结构,而不是类似ArrayBlockingQueue那样的数组。但实际使用过程中,不需要关心它的内部实现,如果指定了LinkedBlockingQueue的容量大小,那么它反映出来的使用特性就和ArrayBlockingQueue类似了

        代码示例:

LinkedBlockingQueue<Object> linkedQueue = new LinkedBlockingQueue<Object>(2); 
linkedQueue.put(new Object()); 
// 插入第二个对象 
linkedQueue.put(new Object()); 
// 插入第三个对象时,这个操作线程就会被阻塞。 
linkedQueue.put(new Object());

// 或者如下使用: 
LinkedBlockingQueue<Object> linkedQueue = new LinkedBlockingQueue<Object>(); 
linkedQueue.put(new Object()); 
// 插入第二个对象 
linkedQueue.put(new Object()); 
// 插入第N个对象时,都不会阻塞 linkedQueue.put(new Object());
LinkedBlockingDeque

        LinkedBlockingDeque是一个基于链表的双端队列

        LinkedBlockingQueue的内部结构决定了它只能从队列尾部插入,从队列头部取出元素;

        但是LinkedBlockingDeque既可以从尾部插入/取出元素,还可以从头部插入元素/取出元素

        代码示例:

LinkedBlockingDeque linkedDeque = new LinkedBlockingDeque(); 
// push ,可以从队列的头部插入元素 
linkedDeque.push(new TempObject(1)); 
linkedDeque.push(new TempObject(2)); 
linkedDeque.push(new TempObject(3)); 

// poll , 可以从队列的头部取出元素 
TempObject tempObject = linkedDeque.poll(); 
// 这里会打印 tempObject.index = 3 
System.out.println("tempObject.index = " + tempObject.getIndex()); 

// put , 可以从队列的尾部插入元素 
linkedDeque.put(new TempObject(4)); 
linkedDeque.put(new TempObject(5)); 

// pollLast , 可以从队列尾部取出元素 
tempObject = linkedDeque.pollLast(); 
// 这里会打印 tempObject.index = 5 
System.out.println("tempObject.index = " + tempObject.getIndex());
PriorityBlockingQueue

        PriorityBlockingQueue是一个按照优先级进行内部元素排序的无限队列。

        存放在PriorityBlockingQueue中的元素必须实现Comparable接口,这样才能通过实现compareTo()方法进行排序。优先级最高的元素将始终排在队列的头部;PriorityBlockingQueue不会保证优先级一样的元素的排序,也不保证当前队列中除了优先级最高的元素以外的元素,随时处于正确排序的位置

        PriorityBlockingQueue并不保证除了队列头部以外的元素排序一定是正确的,代码示例:

PriorityBlockingQueue priorityQueue = new PriorityBlockingQueue();
priorityQueue.put(new TempObject(-5));
priorityQueue.put(new TempObject(5));
priorityQueue.put(new TempObject(-1));
priorityQueue.put(new TempObject(1));
// 第一个元素是5 
TempObject targetTempObject = priorityQueue.poll();
System.out.println("tempObject.index = " + targetTempObject.getIndex());
// 实际上在还没有执行priorityQueue.poll()语句的时候,队列中的第二个元素不一定是1 
// 第二个元素是1 
targetTempObject = priorityQueue.poll();
System.out.println("tempObject.index = " + targetTempObject.getIndex());
// 第三个元素是-1 
targetTempObject = priorityQueue.poll();
System.out.println("tempObject.index = " + targetTempObject.getIndex());
// 第四个元素是-5 
targetTempObject = priorityQueue.poll();
System.out.println("tempObject.index = " + targetTempObject.getIndex());


// 这个元素类,必须实现Comparable接口 
private static class TempObject implements Comparable<TempObject> {
    private int index;
    public TempObject(int index) {
        this.index = index;
    }
    public int getIndex() {
        return index;
    }
    @Override
    public int compareTo(TempObject o) {
        return o.getIndex() - this.index;
    }
}
LinkedTransferQueue

        LinkedTransferQueue也是一个无限队列,它除了具有一般队列的操作特性外(先进先出),还具有一个阻塞特性:LinkedTransferQueue可以由一对生产者/消费者线程进行操作,当消费者将一个新的元素插入队列后,消费者线程将会一直等待,直到某一个消费者线程将这个元素取走,反之亦然。

        LinkedTransferQueue的操作特性可以由下面这段代码提现。有两中类型的线程:生产者和消费者,这两类线程互相等待对方的操作:

/* 消费者线程 */
private static class ConsumerRunnable implements Runnable {
    private LinkedTransferQueue linkedQueue;
    public ConsumerRunnable(LinkedTransferQueue linkedQueue) {
        this.linkedQueue = linkedQueue;
    }

    @Override
    public void run() {
        Thread currentThread = Thread.currentThread();
        while (!currentThread.isInterrupted()) {
            try { // 等待,直到从LinkedTransferQueue队列中得到一个元素
                TempObject targetObject = this.linkedQueue.take();
                System.out.println("线程(" + currentThread.getId() + ")取得targetObject.index = " + targetObject.getIndex());
            } catch (InterruptedException e) {
                e.printStackTrace(System.out);
            }
        }
    }
}

//以下是启动代码:
LinkedTransferQueue<TempObject> linkedQueue = new LinkedTransferQueue<TempObject>(); 
// 这是一个生产者线程 
Thread producerThread = new Thread(new ProducerRunnable(linkedQueue)); 
// 这里有两个消费者线程 
Thread consumerRunnable1 = new Thread(new ConsumerRunnable(linkedQueue)); 
Thread consumerRunnable2 = new Thread(new ConsumerRunnable(linkedQueue)); 
// 开始运行 
producerThread.start(); 
consumerRunnable1.start(); 
consumerRunnable2.start(); 
// 这里只是为了main不退出,没有任何演示含义 
Thread currentThread = Thread.currentThread(); 
synchronized (currentThread) { currentThread.wait(); }

e.拒绝策略

        在ThreadPoolExecutor线程池中有一个重要的接口:RejectedExecutionHandler

        当提交给线程池的某一个新任务无法直接被线程池中“核心线程”直接处理,又无法加入等待队列,也无法创建新的线程执行;又或者线程池已经调用shutdown()方法停止了工作;又或者线程池不是处于正常的工作状态;这时候ThreadPoolExecutor线程池会拒绝处理这个任务,触发创建ThreadPoolExecutor线程池时定义的RejectedExecutionHandler接口的实现

        在创建ThreadPoolExecutor线程池时,一定会指定RejectedExecutionHandler接口的实现。如果调用的是不需要指定RejectedExecutionHandler接口的构造函数,如:

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

        那么ThreadPoolExecutor线程池在创建时,会使用一个默认的RejectedExecutionHandler接口实现,源代码片段如下:

public class ThreadPoolExecutor extends AbstractExecutorService { ......
    /**
     * The default rejected execution handler
     */
    private static final RejectedExecutionHandler defaultHandler = new AbortPolicy(); ......
// 可以看到,ThreadPoolExecutor中的两个没有指定RejectedExecutionHandler
// 接口的构造函数,都是使用了一个RejectedExecutionHandler接口的默认实现:AbortPolicy

    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) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler);
    } ......
}

        在ThreadPoolExecutor中已经提供了四种可以直接使用的RejectedExecutionHandler接口的实现:

  • CallerRunsPolicy:直接运行这个任务的run方法。

        注意:并不是在ThreadPoolExecutor线程池中的线程中运行,而是直接调用这个任务实现的run方法。

        源代码:

public static class CallerRunsPolicy implements RejectedExecutionHandler {
    /**
     * Creates a {@code CallerRunsPolicy}.
     */
    public CallerRunsPolicy() {
    }

    /**
     * Executes task r in the caller's thread, unless the executor * has been shut down, in which case the task is discarded. * * @param r the runnable task requested to be executed * @param e the executor attempting to execute this task
     */
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            r.run();
        }
    }
}
  • AbortPolicy:在任务被拒绝后会创建一个RejectedExecutionException异常并抛出。这个处理过程也是ThreadPoolExecutor线程池默认的RejectedExecutionHandler实现。
  • DiscardPolicy:默默丢弃这个被拒绝的任务,不会抛出异常,也不会通过其他方式执行这个任务的任何一个方法,更不会出现任何的日志提示。

  • DiscardOldestPolicy:检查当前ThreadPoolExecutor线程池的等待队列。并调用队列的poll()方法,将当前处于等待队列列头的等待任务强行取出,然后再试图将当前被拒绝的任务提交到线程池执行。

源代码:

public static class DiscardOldestPolicy implements RejectedExecutionHandler {
    ......
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            e.getQueue().poll();
            e.execute(r);
        }
    } 
    ......
}

二、多线程并发

1.通信与同步

        进程有自己的独立地址空间,因此进程之间重点关注通信,通信方式包括:管道Pipe、命名管道FIFO、消息队列MessageQueue、共享存储SharedMemory、信号量Semaphore、套接字Socket和信号Signal。线程除了线程栈外其他数据都是共享的,如果同时读写数据可能造成数据不一致甚至程序崩溃的后果,因此线程之间重点关注同步

        同步问题:多个线程在操作同一个资源,但是操作的动作不同。

        线程安全问题主要是是多线程的抢占式执行带来的随机性导致的,如果没有多线程,此时代码的执行顺序是固定的,因此程序的结果也就是固定的。如果有了多线程,此时抢占式执行下,代码的执行顺序就会有很多种情况,所以为了执行结果正确,就需要保证在这多种执行顺序的情况下,代码运行得到的结果都是一样的。

        其他原因:

        1.多个线程同时修改同一个变量

  • 一个线程修改一个变量,没问题
  • 多个线程读取同一个变量,没问题
  • 多个线程修改多个不同的变量,也没事

        2.修改操作不是原子的

        原子:不可拆分的基本单位.

  • 如果修改操作是原子的,不会出现问题
  • 如果修改操作是非原子的,出现问题的概率非常高.

        3.内存可见性问题

    一个线程针对一个变量进行读取操作,同时另一个线程针对这个变量进行修改,此时读到的值,不一定是修改之后的值.这个读线程没有感知到变量的变化,归根结底是编译器/jvm在多线程环境下优化时产生了误判.为了解决内存可见性问题:需要为变量加上volatile关键字

    当为变量加上volatile关键字时,告诉编译器,这个变量是’易变’的,需要每次都重新读取这个变量的内存内容。Volatile 不保证原子性,原子性是靠 synchronized 来保证的。Volatile 和 synchronized 都能保证线程安全。

        volatile关键字的作用主要有两个:

  • 解决内容可见性问题
  • 禁止指令重排序

        从JMM(java Memory Model  java内存模型)的角度表述该问题:

  • Java程序里,除了主内存,每个线程都有自己的工作内存(线程1和线程2的工作内存不是一个东西).
  • 线程1进行读取的时候,只是读取了工作内存的值.
  • 线程2修改的时候,先修改工作内存的值,然后再把工作内存的内容同步到主内存中.
  • 但是,由于编译器的优化,导致线程1没有重新从主内存同步数据到工作内存,读到的数据就是"修改之前"的结果.

       4.指令重排序

        本质上是编译器优化出bug了,可能是编译器觉得我们的代码有点差,在保持逻辑不变的情况下,进行调整(调整了代码的执行顺序),从而加快程序的执行效率.

2.并发与并行

        java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用, 从而保证了该变量的唯一性和准确性。

        什么是并发?什么是并行? 

  • 并行是指两个或者多个事件在同一时刻发生,而并发是指两个或多个事件在同一时间间隔发生。
  • 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。

        并发

        早期计算机的 CPU 都是单核的,一个 CPU 在同一时间只能执行一个进程/线程,当系统中有多个进程/线程等待执行时,CPU 只能执行完一个再执行下一个。

        计算机在运行过程中,有很多指令会涉及 I/O 操作,而 I/O 操作又是相当耗时的,速度远远低于 CPU,这导致 CPU 经常处于空闲状态,只能等待 I/O 操作完成后才能继续执行后面的指令。

        为了提高 CPU 利用率,减少等待时间,人们提出了一种 CPU 并发工作的理论。

        所谓并发,就是通过一种算法将 CPU 资源合理地分配给多个任务,当一个任务执行 I/O 操作时,CPU 可以转而执行其它的任务,等到 I/O 操作完成以后,或者新的任务遇到 I/O 操作时,CPU 再回到原来的任务继续执行。

        并发执行过程图

        虽然 CPU 在同一时刻只能执行一个任务,但是通过将 CPU 的使用权在恰当的时机分配给不同的任务,使得多个任务在视觉上看起来是一起执行的。

        并行

        并发是针对单核 CPU 提出的,而并行则是针对多核 CPU 提出的。和单核 CPU 不同,多核 CPU 真正实现了“同时执行多个任务”。

        多核 CPU 内部集成了多个计算核心(Core),每个核心相当于一个简单的 CPU,如果不计较细节,你可以认为给计算机安装了多个独立的 CPU。

        多核 CPU 的每个核心都可以独立地执行一个任务,而且多个核心之间不会相互干扰。在不同核心上执行的多个任务,是真正地同时运行,这种状态就叫做并行。

        并行执行图

3.线程并发问题

a.CPU缓存一致性

        计算机在执行程序的时候,每条指令都是在CPU中执行的,而执行的时候,又免不了要和数据打交道。而计算机上面的数据,是存放在主存当中的,也就是计算机的物理内存。

        刚开始,还相安无事的,但是随着CPU技术的发展,CPU的执行速度越来越快。而由于内存的技术并没有太大的变化,所以从内存中读取和写入数据的过程和CPU的执行速度比起来差距就会越来越大,这就导致CPU每次操作内存都要耗费很多等待时间。

        可是,不能因为内存的读写速度慢,就不发展CPU技术了吧,总不能让内存成为计算机处理的瓶颈吧。

        所以,人们想出来了一个好的办法,就是在CPU和内存之间增加高速缓存。缓存的概念大家都知道,就是保存一份数据拷贝。他的特点是速度快,内存小,并且昂贵。

        那么,程序的执行过程就变成了:

        当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

        而随着CPU能力的不断提升,一层缓存就慢慢的无法满足要求了,就逐渐的衍生出多级缓存。

        按照数据读取顺序和与CPU结合的紧密程度,CPU缓存可以分为一级缓存(L1),二级缓存(L3),部分高端CPU还具有三级缓存(L3),每一级缓存中所储存的全部数据都是下一级缓存的一部分。

        这三种缓存的技术难度和制造成本是相对递减的,所以其容量也是相对递增的。

        那么,在有了多级缓存之后,程序的执行就变成了:

        当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。

        单核CPU只含有一套L1,L2,L3缓存;

        如果CPU含有多个核心,即多核CPU,则每个核心都含有一套L1(甚至和L2)缓存,而共享L3(或者和L2)缓存。

下图为一个单CPU双核的缓存结构:

        随着计算机能力不断提升,开始支持多线程。那么问题就来了。我们分别来分析下单线程、多线程在单核CPU、多核CPU中的影响。

        单线程。cpu核心的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题。

        单核CPU,多线程。进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。

        多核CPU,多线程。每个核都至少有一个L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。

        在CPU和主存之间增加缓存,在多线程场景下就可能存在缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。

b.处理器优化和指令重排

      为了使处理器内部的运算单元能够尽量的被充分利用,处理器可能会对输入代码进行乱序执行处理。这就是处理器优化

        除了现在很多流行的处理器会对代码进行优化乱序处理,很多编程语言的编译器也会有类似的优化,比如Java虚拟机的即时编译器(JIT)也会做指令重排

        原子性问题,可见性问题和有序性问题都是抽象定义的。这个抽象的底层问题就是缓存一致性问题、处理器优化问题和指令重排问题等。

        原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。

        可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

        有序性即程序执行的顺序按照代码的先后顺序执行。

c.并发编程

        原子性,可见性和有序性是抽象定义的。而这个抽象的底层问题就是缓存一致性问题、处理器优化问题和指令重排问题等。

        在并发编程中,为了包装数据的安全性,需要满足以下三个特性:

  • 原子性:指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。
  • 可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  • 有序性:程序执行的顺序按照代码的先后顺序执行。

        缓存一致性问题其实就是可见性问题。而处理器优化是可以导致原子性问题的。指令重排即会导致有序性问题

d.内存模型

        为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。

        通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。他解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。

        内存模型解决并发问题主要采用两种方式:限制处理器优化使用内存屏障

什么是内存模型

        Java程序是需要运行在Java虚拟机上面的,Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。

        Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。

        而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。

        特别需要注意的是,主内存和工作内存与JVM内存结构中的Java堆、栈、方法区等并不是同一个层次的内存划分,无法直接类比。《深入理解Java虚拟机》中认为,如果一定要勉强对应起来的话,从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分。工作内存则对应于虚拟机栈中的部分区域。

        总结:

        JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。目的是保证并发编程场景中的原子性、可见性和有序性。

内存模型的实现

        在Java中提供了一系列和并发处理相关的关键字,比如volatilesynchronizedfinalconcurren包等。其实这些就是Java内存模型封装了底层的实现后提供给程序员使用的一些关键字。

        在开发多线程的代码的时候,我们可以直接使用synchronized等关键字来控制并发,从来就不需要关心底层的编译器优化、缓存一致性等问题。所以,Java内存模型,除了定义了一套规范,还提供了一系列原语,封装了底层实现后,供开发者直接使用。

原子性

        在Java中,为了保证原子性,提供了两个高级的字节码指令monitorenter和monitorexit。在synchronized的实现原理文章中,介绍过,这两个字节码,在Java中对应的关键字就是synchronized。

        因此,在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。

可见性

        Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。

        Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。

        除了volatile,Java中的synchronizedfinal两个关键字也可以实现可见性。只不过实现方式不同,这里不再展开了。

有序性

        在Java中,可以使用synchronizedvolatile来保证多线程之间操作的有序性。实现方式有所区别:

        volatile关键字会禁止指令重排。synchronized关键字保证同一时刻只允许一条线程操作。

三、线程安全(同步)

        Java中的同步是为了保证多线程访问共享资源的安全性和正确性而引入的机制。在Java中,每个对象都有一个内部锁(也称为监视器锁或互斥锁),在使用同步时,线程必须先获得该对象的锁才能够访问共享资源,如果没有获取到锁,则线程会阻塞等待。通过使用同步块或同步方法,来对共享数据进行加锁和解锁的操作。

        Java语言为了解决并发编程中存在的原子性、可见性和有序性问题,提供了一系列和并发处理相关的关键字,比如synchronized、volatile、final、concurren包等。

         Java中有两种加锁的方式:

  • synchronized关键字
    • synchronized是Java提供的一个并发控制的关键字。主要有两种用法,分别是同步方法和同步代码块。
  • Lock接口的实现类
    • ReentrantLock

1.Synchronized

         Synchronized是Java提供的一个并发控制的关键字。主要有两种用法,分别是同步方法和同步代码块。

        被Synchronized修饰的代码块及方法,在同一时间,只能被单个线程访问。

        代码示例:

public class SynchronizedDemo {
     //同步方法
    public synchronized void doSth(){
        System.out.println("Hello World");
    }

    //同步代码块
    public void doSth1(){
        synchronized (SynchronizedDemo.class){
            System.out.println("Hello World");
        }
    }
}

        Synchronized,是Java中用于解决并发情况下数据同步访问的一个很重要的关键字。当我们想要保证一个共享资源在同一时间只会被一个线程访问到时,我们可以在代码中使用Synchronized关键字对类或者对象加锁。

a.实现原理

       反编译

        使用Javap来反编译以上代码,结果如下:

 public synchronized void doSth();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return

  public void doSth1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #5                  // class com/hollis/SynchronizedTest
         2: dup
         3: astore_1
         4: monitorenter
         5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #3                  // String Hello World
        10: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: aload_1
        14: monitorexit
        15: goto          23
        18: astore_2
        19: aload_1
        20: monitorexit
        21: aload_2
        22: athrow
        23: return

        可以看到Java编译器为我们生成的字节码。在对于doSthdoSth1的处理上稍有不同。也就是说,JVM对于同步方法和同步代码块的处理方式不同。

        对于同步方法,JVM采用ACC_SYNCHRONIZED标记符来实现同步。 对于同步代码块。JVM采用monitorentermonitorexit两个指令来实现同步。

        关于这部分内容,在JVM规范中也可以找到相关的描述。

同步方法

        The Java® Virtual Machine Specification中有关于方法级同步的介绍:

        Method-level synchronization is performed implicitly, as part of method invocation and return. A synchronized method is distinguished in the run-time constant pool's methodinfo structure by the ACCSYNCHRONIZED flag, which is checked by the method invocation instructions. When invoking a method for which ACC_SYNCHRONIZED is set, the executing thread enters a monitor, invokes the method itself, and exits the monitor whether the method invocation completes normally or abruptly. During the time the executing thread owns the monitor, no other thread may enter it. If an exception is thrown during invocation of the synchronized method and the synchronized method does not handle the exception, the monitor for the method is automatically exited before the exception is rethrown out of the synchronized method.

        大概意思是:方法级的同步是隐式的。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。

同步代码块

        同步代码块使用monitorentermonitorexit两个指令实现。 The Java® Virtual Machine Specification 中有关于这两个指令的介绍:

        monitorenter:

        Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

  • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
  • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
  • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

        monitorexit:

        The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.

        The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

        大概意思指的是:可以把执行monitorenter指令理解为加锁,执行monitorexit理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行monitorenter)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。

总结:

        同步方法通过ACC_SYNCHRONIZED关键字隐式的对方法进行加锁。当线程要执行的方法被标注上ACC_SYNCHRONIZED时,需要先获得锁才能执行该方法。

        同步代码块通过monitorentermonitorexit执行来进行加锁。当线程执行到monitorenter的时候要先获得所锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。

        每个对象自身维护这一个被加锁次数的计数器,当计数器数字为0时表示可以被任意线程获得锁。当计数器不为0时,只有获得锁的线程才能再次获得锁。即可重入锁。

b.Java的对象模型

        带着问题来看:Monitor到底是什么?对象的锁的状态保存在哪里?

管程

        在操作系统中,管程(monitors)是很重要的概念。同样Monitor在java同步机制中也有使用。

        管程 (英语:Monitors,也称为监视器) 是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件设备或一群变量。管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。与那些通过修改数据结构实现互斥访问的并发程序设计相比,管程实现很大程度上简化了程序设计。 管程提供了一种机制,线程可以临时放弃互斥访问,等待某些条件得到满足后,重新获得执行权恢复它的互斥访问。

Java中的Monitor

        在多线程访问共享资源的时候,经常会带来可见性和原子性的安全问题。为了解决这类线程安全的问题,Java提供了同步机制、互斥锁机制,这个机制保证了在同一时刻只有一个线程能访问共享资源。这个机制的保障来源于监视锁Monitor,每个对象都拥有自己的监视锁Monitor。

        Monitor是一种同步工具,也可以说是一种同步机制,它通常被描述为一个对象,主要特点是:

        对象的所有方法都被“互斥”的执行。好比一个Monitor只有一个运行“许可”,任一个线程进入任何一个方法都需要获得这个“许可”,离开时把许可归还。

        通常提供singal机制:允许正持有“许可”的线程暂时放弃“许可”,等待某个谓词成真(条件变量),而条件成立后,当前进程可以“通知”正在等待这个条件变量的线程,让他可以重新去获得运行许可。

Monitor的结构

        在Java虚拟机(HotSpot)中,Monitor是基于C++实现的,由ObjectMonitor实现的,其主要数据结构如下:

  ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

        ObjectMonitor中有几个关键属性

  • _owner:指向持有ObjectMonitor对象的线程

  • _WaitSet:存放处于wait状态的线程队列

  • _EntryList:存放处于等待锁block状态的线程队列

  • _recursions:锁的重入次数

  • _count:用来记录该线程获取锁的次数

        当多个线程同时访问一段同步代码时,首先会进入_EntryList队列中,当某个线程获取到对象的monitor后进入_Owner区域并把monitor中的_owner变量设置为当前线程,同时monitor中的计数器_count加1。即获得对象锁。

        若持有monitor的线程调用wait()方法,将释放当前持有的monitor,_owner变量恢复为null_count自减1,同时该线程进入_WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。

        ObjectMonitor类中提供了几个方法

        获取锁:

void ATTR ObjectMonitor::enter(TRAPS){
	Thread * const Self = THREAD ;
	void * cur;
	//通过CAS尝试把monitor的'_owner'字段设置为当前线程
	cur = Atomic::cmpxchg_ptr (Self,&_owner, NULL) ;
	//获取锁失败
	if (cur == NULL) {	assert(_recursions == 0	,"invariant") ; 
		assert (_owner	== Self, "invariant") ;
		//CONSIDER: set or assert OwnerIsThread == 1
		return ;
	}

	//如果旧值和当前线程一样,说明当前线程已经持有锁,此次为重入, _recursions自增,并获得锁。
	if (cur == Self){
		// TODO-FIXME: check for interger overflow! BUGID 6557169.
		_recursions ++ ;
		return ;
	}

	//如果当前线程是第一次进入该monitor,设置_recursions为1,_owner为当前线程
	if (Self->is_lock_owner((address)cur)) {
		assert (_recursions == 0, "internal state error");
		_recursions = 1 ;
		// Commute owner from a thread-specific on-stack BasicLockObject address to
		// a full-fledged "Thread *".
		_owner = Self ;
		OwnerIsThread = 1;
		return ;
	}

	// 省略部分代码
	// 通过自旋执行ObjectMonitor::EnterI方法等待锁的释放
	for(;;){
		jt->setsuspend_equivalent();
		// cleared by handler_special_suspend_equivalent_condition()
		// or java_suspend_self()

	}
	EnterI(THREAD) ;

	if (!ExitSuspendEquivalent(jt)) break ;
	
	// We have acquired the contended monitor, but while we were
	// waiting another thread suspended us. We don't want to enter
	// the monitor while suspended because that would surprise the
	// thread that suspended us.

	_recursions = 0 ;
	_succ = NULL ;
	exit (Self);

	jt->java_suspend_self();
}

        释放锁:

void ATTR ObjectMonitor::enter(TRAPS){
	Thread * Self = THREAD ;
	// 如果当前线程不是Monitor的所有者
	if (THREAD !=  _owner){
		if (THREAD->is_lock_owned((address) _owner)){
			// Transmute _owner from a BasicLock point to a Thread address,
			// We don't need to hold _mutex for this transition.
			// Non-null to Non-null is safe as long as all readers can
			// tolerate either flavor.
			assert (_recursions == 0. "invariant") ;
			_owner = THREAD ;
			_recursions = 0 ;
			OwnerIsThread = 1 ;
		}else{
			// NOTE: we need to handle unbalanced monitor enter/exit
			// in native code by throwing an exception.
			// TODO: Throw an IllegalMonitorStateException ?
			TEVENT (Exit - Throw IMSX) ;
			assert(false, "Non-balanced monitor enter/exit!");
			if (false) {
				THROW(vmSymbols::java_lang_IllegalMonitorStateException());
			}
			return ;
		}
	}
	//如果_recursions次数不为0.自减
	if (_recursions != 0) {
	_recursions--;
	// this is simple recursive enterrecursions--;
	TEVENT (Inflated exit - recursive) ;
	return ;
}

// 省略部分代码,根据不同的策略 (由QMode指定),从cxa或EntryList中获取头节点
// 通过ObjectMonitor::ExitEpilog方法唤醒该节点封装的线程,唤醒操作最终由unpark完成。

 

         除了enter和exit方法以外,objectMonitor.cpp中还有以下方法:

  • void      wait(jlong millis, bool interruptable, TRAPS);
  • void      notify(TRAPS);
  • void      notifyAll(TRAPS);

         总结:

         通过对字节码文件和源码的阅读,我们了解到sychronized加锁的时候,会调用objectMonitor的enter方法,解锁的时候会调用exit方法。事实上,只有在JDK1.6之前,synchronized的实现才会直接调用ObjectMonitor的enterexit,这种锁被称之为重量级锁

        为什么说这种锁是重量级呢?

        因为Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到核心态,因此状态转换需要花费很多的处理器时间,对于代码简单的同步块(如被synchronized修饰的get 或set方法)状态转换消耗的时间有可能比用户代码执行的时间还要长,所以说synchronized是java语言中一个重量级的操纵。

        所以,在JDK1.6中出现对锁进行了很多的优化,进而出现轻量级锁偏向锁锁消除适应性自旋锁锁粗化(自旋锁在1.4就有 只不过默认的是关闭的,jdk1.6是默认开启的),这些操作都是为了在线程之间更高效的共享数据 ,解决竞争问题。

对象在JVM中的结构

        Java对象保存在堆内存中。在内存中,一个Java对象包含三部分:对象头(Head)、实例数据(Body)和对齐填充。

        其中对象头是一个很关键的部分,因为对象头中包含锁状态标志、线程持有的锁等标志。我们从Java对象模型入手,了解对象头以及对象头中和锁相关的运行时数据在JVM中是如何表示的。

        重新了解Java中的对象:

  • 1、在面向对象的软件中,对象(Object)是某一个类(Class)的实例。

  • 2、一切皆对象

        基于HotSpot虚拟机分析对象本身在JVM中的结构是什么样的:

oop-klass model

        HotSpot是基于c++实现,而c++是一门面向对象的语言,本身是具备面向对象基本特征的,所以Java中的对象表示,最简单的做法是为每个Java类生成一个c++类与之对应。但HotSpot JVM并没有这么做,而是设计了一个OOP-Klass Model

        OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象实例的具体类型。

        为什么HotSpot要设计一套oop-klass model呢?答案是:HotSopt JVM的设计者不想让每个对象中都含有一个vtable(虚函数表)

        这个解释似乎可以说得通。众所周知,C++和Java都是面向对象的语言,面向对象语言有一个很重要的特性就是多态。关于多态的实现,C++和Java有着本质的区别。

多态是面向对象的最主要的特性之一,是一种方法的动态绑定,实现运行时的类型决定对象的行为。多态的表现形式是父类指针或引用指向子类对象,在这个指针上调用的方法使用子类的实现版本。多态是IOC、模板模式实现的关键。

  • 在C++中通过虚函数表的方式实现多态,每个包含虚函数的类都具有一个虚函数表(virtual table),在这个类对象的地址空间的最靠前的位置存有指向虚函数表的指针。在虚函数表中,按照声明顺序依次排列所有的虚函数。由于C++在运行时并不维护类型信息,所以在编译时直接在子类的虚函数表中将被子类重写的方法替换掉。

  • 在Java中,在运行时会维持类型信息以及类的继承体系。每一个类会在方法区中对应一个数据结构用于存放类的信息,可以通过Class对象访问这个数据结构。其中,类型信息具有superclass属性指示了其超类,以及这个类对应的方法表(其中只包含这个类定义的方法,不包括从超类继承来的)。而每一个在堆上创建的对象,都具有一个指向方法区类型信息数据结构的指针,通过这个指针可以确定对象的类型。

        关于opp-klass模型的整体定义,在HotSpot的源码中可以找到。

        oops模块可以分成两个相对独立的部分:OOP框架和Klass框架。

        在oopsHierarchy.hpp里定义了oop和klass各自的体系。

oop体系:
//定义了oops共同基类
typedef class   oopDesc*                            oop;
//表示一个Java类型实例
typedef class   instanceOopDesc*            instanceOop;
//表示一个Java方法
typedef class   methodOopDesc*                    methodOop;
//表示一个Java方法中的不变信息
typedef class   constMethodOopDesc*            constMethodOop;
//记录性能信息的数据结构
typedef class   methodDataOopDesc*            methodDataOop;
//定义了数组OOPS的抽象基类
typedef class   arrayOopDesc*                    arrayOop;
//表示持有一个OOPS数组
typedef class   objArrayOopDesc*            objArrayOop;
//表示容纳基本类型的数组
typedef class   typeArrayOopDesc*            typeArrayOop;
//表示在Class文件中描述的常量池
typedef class   constantPoolOopDesc*            constantPoolOop;
//常量池告诉缓存
typedef class   constantPoolCacheOopDesc*   constantPoolCacheOop;
//描述一个与Java类对等的C++类
typedef class   klassOopDesc*                    klassOop;
//表示对象头
typedef class   markOopDesc*                    markOop;

        在Java程序运行过程中,每创建一个新的对象,在JVM内部就会相应地创建一个对应类型的OOP对象。在HotSpot中,根据JVM内部使用的对象业务类型,具有多种oopDesc的子类。除了oppDesc类型外,opp体系中还有很多instanceOopDescarrayOopDesc 等类型的实例,他们都是oopDesc的子类。

        这些OOPS在JVM内部有着不同的用途,例如instanceOopDesc表示类实例,arrayOopDesc表示数组。也就是说,当我们使用new创建一个Java对象实例的时候,JVM会创建一个instanceOopDesc对象来表示这个Java对象。同理,当我们使用new创建一个Java数组实例的时候,JVM会创建一个arrayOopDesc对象来表示这个数组对象。

        在HotSpot中,oopDesc类定义在oop.hpp中,instanceOopDesc定义在instanceOop.hpp中,arrayOopDesc定义在arrayOop.hpp中。

        简单看一下相关定义:

class instanceOopDesc : public oopDesc {
}

class arrayOopDesc : public oopDesc {
}

        通过上面的源码可以看到,instanceOopDesc实际上就是继承了oopDesc,并没有增加其他的数据结构,也就是说instanceOopDesc中包含两部分数据:markOop _markunion _metadata

        这里的markOop你可能又熟悉了,这不就是OOPS体系中的一部分吗,上面注释中已经说过,他表示对象头。 _metadata是一个联合体,这个字段被称为元数据指针。指向描述类型Klass对象的指针。

        HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头、实例数据和对齐填充。在虚拟机内部,一个Java对象对应一个instanceOopDesc的对象,该对象中有两个字段分别表示了对象头和实例数据。那就是_mark_metadata

        前面介绍到的_metadata是一个共用体,其中_klass是普通指针,_compressed_klass是压缩类指针。在深入介绍之前,就要来到oop-Klass中的另外一个主角klass了。

klass体系
//klassOop的一部分,用来描述语言层的类型
class  Klass;
//在虚拟机层面描述一个Java类
class   instanceKlass;
//专有instantKlass,表示java.lang.Class的Klass
class     instanceMirrorKlass;
//专有instantKlass,表示java.lang.ref.Reference的子类的Klass
class     instanceRefKlass;
//表示methodOop的Klass
class   methodKlass;
//表示constMethodOop的Klass
class   constMethodKlass;
//表示methodDataOop的Klass
class   methodDataKlass;
//最为klass链的端点,klassKlass的Klass就是它自身
class   klassKlass;
//表示instanceKlass的Klass
class     instanceKlassKlass;
//表示arrayKlass的Klass
class     arrayKlassKlass;
//表示objArrayKlass的Klass
class       objArrayKlassKlass;
//表示typeArrayKlass的Klass
class       typeArrayKlassKlass;
//表示array类型的抽象基类
class   arrayKlass;
//表示objArrayOop的Klass
class     objArrayKlass;
//表示typeArrayOop的Klass
class     typeArrayKlass;
//表示constantPoolOop的Klass
class   constantPoolKlass;
//表示constantPoolCacheOop的Klass
class   constantPoolCacheKlass;

oopDesc是其他oop类型的父类一样,Klass类是其他klass类型的父类。

         Klass向JVM提供两个功能:

  • 实现语言层面的Java类(在Klass基类中已经实现)

  • 实现Java对象的分发功能(由Klass的子类提供虚函数实现)

        之所以设计oop-klass模型,是因为HotSopt JVM的设计者不想让每个对象中都含有一个虚函数表。        

        HotSopt JVM的设计者把对象一拆为二,分为klassoop,其中oop的职能主要在于表示对象的实例数据,所以其中不含有任何虚函数。而klass为了实现虚函数多态,所以提供了虚函数表。所以,关于Java的多态,其实也有虚函数的影子在。

        _metadata是一个共用体,其中_klass是普通指针,_compressed_klass是压缩类指针。这两个指针都指向instanceKlass对象,它用来描述对象的具体类型。

instanceKlass

        JVM在运行时,需要一种用来标识Java内部类型的机制。在HotSpot中的解决方案是:为每一个已加载的Java类创建一个instanceKlass对象,用来在JVM层表示Java类。

        来看下instanceKlass的内部结构:

  //类拥有的方法列表
  objArrayOop     _methods;
  //描述方法顺序
  typeArrayOop    _method_ordering;
  //实现的接口
  objArrayOop     _local_interfaces;
  //继承的接口
  objArrayOop     _transitive_interfaces;
  //域
  typeArrayOop    _fields;
  //常量
  constantPoolOop _constants;
  //类加载器
  oop             _class_loader;
  //protected域
  oop             _protection_domain;
      ....

        可以看到,一个类该具有的东西,这里面基本都包含了。

        这里还有个点需要简单介绍一下:

        在JVM中,对象在内存中的基本存在形式就是oop。那么,对象所属的类,在JVM中也是一种对象,因此它们实际上也会被组织成一种oop,即klassOop。同样的,对于klassOop,也有对应的一个klass来描述,它就是klassKlass,也是klass的一个子类。klassKlass作为oop的klass链的端点。关于对象和数组的

        klass链大致如下图:

         在这种设计下,JVM对内存的分配和回收,都可以采用统一的方式来管理。

 内存存储

        对象的实例(instantOopDesc)保存在堆上,对象的元数据(instantKlass)保存在方法区,对象的引用保存在栈上。

        方法区用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 所谓加载的类信息,其实不就是给每一个被加载的类都创建了一个 instantKlass对象么。

class Model
{
    public static int a = 1;
    public int b;

    public Model(int b) {
        this.b = b;
    }
}

public static void main(String[] args) {
    int c = 10;
    Model modelA = new Model(2);
    Model modelB = new Model(3);
}

        存储结构如下:

        总结:

        每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了两部分信息,方法头以及元数据。对象头中有一些运行时数据,其中就包括和多线程相关的锁的信息。元数据其实维护的是指针,指向的是对象所属的类的instanceKlass

Java的对象头
class oopDesc {
  friend class VMStructs;
 private:
  volatile markOop  _mark;
  union _metadata {
    wideKlassOop    _klass;
    narrowOop       _compressed_klass;
  } _metadata;
}

        上面代码中的_mark_metadata其实就是对象头的定义。_mark ,即mark word。

         对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。

        对markword的设计方式上,非常像网络协议报文头:将mark word划分为多个比特位区间,并在不同的对象状态下赋予比特位不同的含义。下图描述了在32位虚拟机上,在对象不同状态时 mark word各个比特位区间的含义。

         同样,在HotSpot的源码中我们可以找到关于对象头对象的定义,会一一印证上图的描述。对应与markOop.hpp类。

enum { age_bits                 = 4,
      lock_bits                = 2,
      biased_lock_bits         = 1,
      max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
      hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits,
      cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
      epoch_bits               = 2
};

        从上面的枚举定义中可以看出,对象头中主要包含了GC分代年龄、锁状态标记、哈希码、epoch等信息。

        从上图中可以看出,对象的状态一共有五种,分别是无锁态、轻量级锁、重量级锁、GC标记和偏向锁。在32位的虚拟机中有两个Bits是用来存储锁的标记为的,但是我们都知道,两个bits最多只能表示四种状态:00、01、10、11,那么第五种状态如何表示呢 ,就要额外依赖1Bit的空间,使用0和1来区分。

在32位的HotSpot虚拟机 中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志位,1Bit固定为0,表示非偏向锁。

        markOop.hpp类中有关于对象状态的定义:

  enum { locked_value             = 0,
         unlocked_value           = 1,
         monitor_value            = 2,
         marked_value             = 3,
         biased_lock_pattern      = 5
  };

 c.锁优化

自旋锁

        自旋锁。所谓自旋,说白了就是一个 while(true) 无限循环。

可重入锁

        可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁

        Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。

锁升级:

        问题:偏向锁的一个特性是,持有锁的线程在执行完同步代码块时不会释放锁。那么当第二个线程执行到这个synchronized代码块时是否一定会发生锁竞争然后升级为轻量级锁呢?


        线程A第一次执行完同步代码块后,当线程B尝试获取锁的时候,发现是偏向锁,会判断线程A是否仍然存活。如果线程A仍然存活,将线程A暂停,此时偏向锁升级为轻量级锁,之后线程A继续执行,线程B自旋。但是如果判断结果是线程A不存在了,则线程B持有此偏向锁,锁不升级。

        Synchronized锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁

         锁的升级流程图:

        synchronized关键字就像是汽车的自动档,现在详细讲这个过程。一脚油门踩下去,synchronized会从无锁升级为偏向锁,再升级为轻量级锁,最后升级为重量级锁,就像自动换挡一样。那么自旋锁在哪里呢?这里的轻量级锁就是一种自旋锁

初次执行到Synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。这里要明确一下什么是锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。

在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。

长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么Synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。

显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。

2.ReentrantLock

        java提供了两种方式来加锁,一种是关键字:synchronized,一种是concurrent包下的lock锁。synchronized是java底层支持的,而concurrent包则是jdk实现。

        ReentrantLock、ReadLock、WriteLock 是Lock接口最重要的三个实现类。对应了“可重入锁”、“读锁”和“写锁”,后面会讲它们的用途。

ReadWriteLock其实是一个工厂接口,而ReentrantReadWriteLock是ReadWriteLock的实现类,它包含两个静态内部类ReadLock和WriteLock。这两个静态内部类又分别实现了Lock接口。

 lock实现过程中的几个关键词:计数值、双向链表、CAS+自旋

        代码示例:,开50个线程同时更新counter,分成三块来看看源码(初始化、获取锁、释放锁)

import java.util.concurrent.locks.ReentrantLock;

public class LockTest {

    public static void main(String[] args) throws Exception {
        final int[] counter = {0};

        ReentrantLock lock = new ReentrantLock();

        for (int i= 0; i < 50; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    lock.lock();
                    try {
                        int a = counter[0];
                        counter[0] = a + 1;
                    }finally {
                        lock.unlock();
                    }
                }
            }).start();
        }

        // 主线程休眠,等待结果
        Thread.sleep(5000);
        System.out.println(counter[0]);
    }
}

a.实现原理

        加锁流程图:

        在Lock的构造方法中,我们可以了解到它给sync赋了一个初始值。

 /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

         在lock的构造函数中,定义了一个NonFairSync:

static final class NonfairSync extends Sync 

 NonfairSync 又是继承于Sync

abstract static class Sync extends AbstractQueuedSynchronizer

        这个Sync继承于AbstractQueuedSynchronizer(简称AQS),最后继承于AbstractOwnableSynchronizer(AOS),AOS主要是保存获取当前锁的线程对象。

        关系继承结构图:

        FairSync 与 NonfairSync的区别在于,是不是保证获取锁的公平性,因为默认是NonfairSync,我们以这个为例了解其背后的原理。

        其他几个类代码不多,最后的主要代码都是在AQS中,我们先看看这个类的主体结构。

AbstractQueuedSynchronizer

         这个Node内部结构如下:

         可以看出,锁的存储结构就两个东西:"双向链表" + "int类型状态"

注意:他们的变量都被transientvolatile修饰。

lock.lock()获取锁
/**
 * Acquires the lock.
 */
public void lock() {
    sync.lock();
}

 可以看到调用的是,NonfairSync.lock().

         在AQS中有个int类型的state值,这里就是通过CAS(乐观锁)去修改state的值。lock的基本操作还是通过乐观锁来实现的

        获取锁通过CAS,那么没有获取到锁,等待获取锁是如何实现的?我们可以看一下else分支的逻辑,acquire方法:

         这里干了三件事情:

  • tryAcquire:会尝试再次通过CAS获取一次锁。
  • addWaiter:将当前线程加入上面锁的双向链表(等待队列)中
  • acquireQueued:通过自旋,判断当前队列节点是否可以获取锁。
addWaiter

        添加当前线程到等待链表中

        可以看到,通过CAS确保能够在线程安全的情况下,将当前线程加入到链表的尾部。

        enq方法是个自旋+上述逻辑,有兴趣的可以翻翻源码。

acquireQueued

        可以看到,当当前线程到头部的时候,尝试CAS更新锁状态,如果更新成功表示该等待线程获取成功。从头部移除

         简要概括一下,获取锁的一个流程:

 lock.unlock()释放锁
public void unlock() {
    sync.release(1);
}

        调用的是,NonfairSync.release()

        可以确认,释放锁就是对AQS中的状态值State进行修改。同时更新下一个链表中的线程等待节点。

总结
  • lock的存储结构:一个int类型状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程)
  • lock获取锁的过程:本质上是通过CAS来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。
  • lock释放锁的过程:修改状态值,调整等待链表。
  • 可以看到在整个实现过程中,lock大量使用CAS+自旋。因此根据CAS特性,lock建议使用在低锁冲突的情况下。目前java1.6以后,官方对synchronized做了大量的锁优化(偏向锁、自旋、轻量级锁)。因此在非必要的情况下,建议使用synchronized做同步操作。

b.条件变量实现原理

        每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject。

        await 流程:

        开始 Thread-0 持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程,创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部。

c.可中断锁

        可以响应中断的锁。

        什么是中断。Java并没有提供任何直接中断某线程的方法,只提供了中断机制。何谓“中断机制”?线程A向线程B发出“请你停止运行”的请求(线程B也可以自己给自己发送此请求),但线程B并不会立刻停止运行,而是自行选择合适的时机以自己的方式响应中断,也可以直接忽略此中断。也就是说,Java的中断不能直接终止线程,而是需要被中断的线程自己决定怎么处理。

        如果线程A持有锁,线程B等待获取该锁。由于线程A持有锁的时间过长,线程B不想继续等待了,我们可以让线程B中断自己或者在别的线程里中断它,这种就是可中断锁

        在Java中,synchronized就是不可中断锁,而Lock的实现类都是可中断锁,可以简单看下Lock接口。

        API:

/* Lock接口 */
public interface Lock {

    void lock(); // 拿不到锁就一直等,拿到马上返回。

    void lockInterruptibly() throws InterruptedException; // 拿不到锁就一直等,如果等待时收到中断请求,则需要处理InterruptedException。

    boolean tryLock(); // 无论拿不拿得到锁,都马上返回。拿到返回true,拿不到返回false。

    boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 同上,可以自定义等待的时间。

    void unlock();

    Condition newCondition();
}

d.公平锁、非公平锁

        如果多个线程申请一把公平锁,那么当锁释放的时候,先申请的先得到,非常公平。显然如果是非公平锁,后申请的线程可能先获取到锁,是随机或者按照其他优先级排序的。

        对ReentrantLock类而言,通过构造函数传参可以指定该锁是否是公平锁,默认是非公平锁。一般情况下,非公平锁的吞吐量比公平锁大,如果没有特殊要求,优先使用非公平锁。

 /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

e.读写锁、共享锁、互斥锁

        读写锁其实是一对锁,一个读锁(共享锁)和一个写锁(互斥锁、排他锁)。

        看下Java里的ReadWriteLock接口,它只规定了两个方法,一个返回读锁,一个返回写锁。

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

        读写锁其实做的事情是一样的,但是策略稍有不同。很多情况下,线程知道自己读取数据后,是否是为了更新它。那么何不在加锁的时候直接明确这一点呢?如果我读取值是为了更新它(SQL的for update就是这个意思),那么加锁的时候就直接加写锁,我持有写锁的时候别的线程无论读还是写都需要等待;如果我读取数据仅为了前端展示,那么加锁时就明确地加一个读锁,其他线程如果也要加读锁,不需要等待,可以直接获取(读锁计数器+1)。

        虽然读写锁感觉与乐观锁有点像,但是读写锁是悲观锁策略。因为读写锁并没有在更新前判断值有没有被修改过,而是在加锁前决定应该用读锁还是写锁。乐观锁特指无锁编程,如果仍有疑惑可以再回到第一、二小节,看一下什么是“乐观锁”。

        JDK提供的唯一一个ReadWriteLock接口实现类是ReentrantReadWriteLock。看名字就知道,它不仅提供了读写锁,而是都是可重入锁。

ReentrantReadWriteLock读写锁

        读写锁指一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。

        ReentrantReadWriteLock中有两个静态内部类:ReadLock读锁和WriteLock写锁,这两个锁实现了Lock接口ReentrantReadWriteLock支持可重入,同步功能依赖自定义同步器(AbstractQueuedSynchronizer)实现,读写状态就是其同步器的同步状态

        写锁的获取和释放:

        写锁WriteLock是支持重进入的排他锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取读锁时,读锁已经被获取或者该线程不是已获取写锁的线程,则当前线程进入等待状态。读写锁确保写锁的操作对读锁可见。写锁释放每次减少写状态,当前写状态为0时表示写锁已背释放。

        读锁的获取与释放

        读锁ReadLock是支持重进入的共享锁(共享锁为shared节点,对于shared节点会进行一连串的唤醒,知道遇到一个读节点),它能够被多个线程同时获取,在没有其他写线程访问(写状态为0)时,读锁总是能够被成功地获取,而所做的也只是增加读状态(线程安全)。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已经被获取,则进入等待状态。

CyclicBarrier和CountDownLatch

        CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;(具体业务场景:我们在玩LOL英雄联盟时会出现十个人不同加载状态,但是最后一个人由于各种原因始终加载不了100%,于是游戏系统自动等待所有玩家的状态都准备好,才展现游戏画面)

        CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行。而且可以重用

        CountDownLatch是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而CyclicBarrier更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。

f.同步方案对比

        wait/notify:依托于synchronized,基于VM底层对于阻塞的实现,使用waitSet作为等待机制,set结构的问题,要么是随机一个(set的提取算法),要么是全部提出来唤醒

        await/signal:依赖于ReentrantLock条件变量,已经用条件变量与AQS体系作为唤醒机制,本质上底层是park/unpark实现阻塞

        park/unpark:以thread为操作对象,操作更精准,可以准确地唤醒某一个线程(notify随机唤醒一个线程,notifyAll唤醒所有等待的线程),增加了灵活性。

        其实park/unpark的设计原理核心是“许可”:park是等待一个许可,unpark是为某线程提供一个许可

        但是这个“许可”是不能叠加的,“许可”是一次性的。

        比如线程B连续调用了三次unpark函数,当线程A调用park函数就使用掉这个“许可”,如果线程A再次调用park,则进入等待状态。

3.AQS

a.什么是AQS

        AQS,全程AbstractQueuedSynchronizer,位于java.util.concurrent.locks包下。

        是JDK1.5提供的一套用于实现阻塞锁和一系列依赖FIFO等待队列的同步器(First Input First Output先进先出)的框架实现。是除了java自带的synchronized 关键字之外的锁机制。 可以将AQS作为一个队列来理解。

        核心思想:

        如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。

        如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现,他将请求共享资源的线程封装成队列的节点(Node),通过CAS、自旋以及LockSupport.park()维护state变量的状态,使并发达到同步的效果。

b.AQS应用

        我们常用的ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等并发类均是基于AQS来实现的。具体用法是通过继承AQS,并实现其模板方法,来达到同步状态的管理。

        AQS设计是基于模板方法模式的,一般的使用方式是:

  1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
  2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。

        AQS定义的可重写的方法:

  1. protected boolean tryAcquire(int arg): 独占式获取同步状态,试着获取,成功返回true,反之为false
  2. protected boolean tryRelease(int arg) :独占式释放同步状态,等待中的其他线程此时将有机会获取到同步状态;
  3. protected int tryAcquireShared(int arg) :共享式获取同步状态,返回值大于等于0,代表获取成功;反之获取失败;
  4. protected boolean tryReleaseShared(int arg) :共享式释放同步状态,成功为true,失败为false
  5. protected boolean isHeldExclusively(): 是否在独占模式下被线程占用。
AQS模板方法

独占锁:

  • void acquire(int arg);// 独占式获取同步状态,如果获取失败则插入同步队列进行等待;
  • void acquireInterruptibly(int arg);// 与acquire方法相同,但在同步队列中进行等待的时候可以检测中断;
  • boolean tryAcquireNanos(int arg, long nanosTimeout);// 在acquireInterruptibly基础上增加了超时等待功能,在超时时间内没有获得同步状态返回false;
  • boolean release(int arg);// 释放同步状态,该方法会唤醒在同步队列中的下一个节点

共享锁:

  • void acquireShared(int arg);// 共享式获取同步状态,与独占式的区别在于同一时刻有多个线程获取同步状态;
  • void acquireSharedInterruptibly(int arg);// 在acquireShared方法基础上增加了能响应中断的功能;
  • boolean tryAcquireSharedNanos(int arg, long nanosTimeout);// 在acquireSharedInterruptibly基础上增加了超时等待的功能;
  • boolean releaseShared(int arg);// 共享式释放同步状态
AQS自定义方法
  1. 首先,我们需要去继承AbstractQueuedSynchronizer这个类,然后我们根据我们的需求去重写相应的方法,比如要实现一个独占锁,那就去重写tryAcquire,tryRelease方法,要实现共享锁,就去重写tryAcquireShared,tryReleaseShared;
  2. 然后,在我们的组件中调用AQS中的模板方法就可以了,而这些模板方法是会调用到我们之前重写的那些方法的。也就是说,我们只需要很小的工作量就可以实现自己的同步组件,重写的那些方法,仅仅是一些简单的对于共享资源state的获取和释放操作,至于像是获取资源失败,线程需要阻塞之类的操作,自然是AQS帮我们完成了

c.源码分析

        AQS实现:

        AQS维护一个共享资源state,通过内置的FIFO来完成获取资源线程的排队工作。(这个内置的同步队列称为"CLH"队列)。该队列由一个一个的Node结点组成,每个Node结点维护一个prev引用和next引用,分别指向自己的前驱和后继结点。AQS维护两个指针,分别指向队列头部head和尾部tail。

        当线程获取资源失败(比如tryAcquire时试图设置state状态失败),会被构造成一个结点加入CLH队列中,同时当前线程会被阻塞在队列中(通过LockSupport.park实现,其实是等待态)。当持有同步状态的线程释放同步状态时,会唤醒后继结点,然后此结点线程继续加入到对同步状态的争夺中。

 d.乐观锁

        总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。实现方式 : 版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

        乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。

e.悲观锁

        总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

写多的场景下用悲观锁就比较合适。

f.CAS算法

        即compare and swap(比较并替换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

  • 需要读写的内存值 V
  • 进行比较的值 A
  • 拟写入的新值 B

        当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

ABA问题

        如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

        解决方案:

        一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

循环时间长开销大

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

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

CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

CAS与synchronized的使用情景

        简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多读场景,冲突一般较多)

  1. 对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
  2. 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值