官方定义:线程是CPU调度和分配的基本单位,一定要和进程是操作系统进行资源分配(包括cpu、内存、磁盘IO等)的最小单位区别清楚。注意,一个是cpu的,一个是系统的资源(这里的资源表示除了CPU 之外的一切东西,也叫上下文) CPU进程无法同时刻共享,但是出现一定要共享CPU的需求呢?此时线程的概念就出现了。线程被包含在进程当中,进程的不同线程间共享CPU和程序上下文。(共享进程分配到的资源)。 单CPU进行进程调度的时候,需要读取上下文+执行程序+保存上下文,即进程切换。如果这个CPU是单核的话,那么在进程中的不同线程为了使用CPU核心,则会进行线程切换,但是由于共享了程序执行环境,这个线程切换比进程切换开销少了很多。在这里依然是并发,唯一核心同时刻只能执行一个线程。 如果这个CPU是多核的话,那么进程中的不同线程可以使用不同核心,真正的并行出现了。 线程的出现是为了降低上下文切换的消耗,提高系统的并发性,并突破一个进程只能干一样事的缺陷,使到进程内并发成为可能。例如一个程序里面,如果有很多的功能,但是他是单线程的,也就是一个功能一个功能的串行执行,这样其实对cpu 的利用率不是很高的,因为如果此时还会发生一些阻塞的时候,这时候分配给这个程序的时间其实就浪费了。为此,又将进程的各部分功能采用线程的方式来实现,这样,同样是这个程序在运行的时候,由于是多线程的方式执行的,此时可以并发地(单核)甚至并行(多核)地执行。这样,就可以减少一个进程的运行时间,进程的上下文切换就会减少。进程层面:用户进程在请求Redis、Mysql数据等网络IO阻塞掉的时候,或者在进程时间片到了,都会引发上下文切换。 一个应用程序会有多个进程,如果此时的进程是最小的CPU调度单位的话,那么对于一个共享的资源的读写都是需要进行进程的切换的,相反的,如果此时有一种模式,可以针对共享的资源,使得上下文的切换的开销最小,那么此时就是线程了。 引起上下文切换的情况 1. 不同进程间的线程的切换。 2. 不同的进程切换。 在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。 复制代码
进程是允许某个并发的程序在某个数据集上的运行过程
一般来说,进程由正文段,用户数据和进程控制块共同组成。其中,正文段主要是机器指令,用户数据主要是进行可以直接操作的用户数据。进程控制块是一种数据结构,用于描述和控制进程运行时候的各种状态。
并发性。 多个进程实体能在一段时间间隔内同时运行。并发性是进程和现代操作系统的重要特征。
动态性。 进程是进程实体的执行过程。进程的动态性表现在因执行程序而创建进程、因获得CPU而执行进程的指令、因运行终止而被撤销的动态变化过程。此外,进程在创建后还有进程状态的变化。
独立性。 在没有引入线程概念的操作系统中,进程是独立运行和资源调度的基本单位。
异步性。 是指进程的执行时断时续,进程什么时候执行、什么时候暂停都无法预知,呈现一种随机的特性。
进程是程序的一次执行,而进程总是对应至少一个特定的程序。一个程序可以对应多个进程,同一个程序可以在不同的数据集合上运行,因而构成若干个不同的进程。几个进程能并发地执行相同的程序代码,而同一个进程能顺序地执行几个程序。
是进程执行活动全过程的静态描述。包括计算机系统中与执行该进程有关的各种寄存器的值、程序段在经过编译之后形成的机器指令代码集、数据集及各种堆栈值和PCB结构。可按一定的执行层次组合,如用户级上下文、系统级上下文等。
Unix System Ⅴ 的进程上下文组成:由用户级上下文、寄存器上下文、系统级上下文组成。
用户级上下文:由进程的用户程序段部分编译而成的用户正文段、用户数据和用户栈等组成。
寄存器上下文:由程序计数器(PC)、处理机状态字(PS)、栈指针和通用寄存器组成。 PC给出CPU将要执行的下一条指令的虚地址;PS给出机器与该进程相关联时的硬件状态;栈指针指向下一项的当前地址;通用寄存器则用于不同执行模式之间的参数传递。
系统级上下文又分为静态部分和动态部分。 这里的动态部分是指进入和退出不同的上下文层次时,系统为各层上下文中相关联的寄存器值所保存和恢复的记录。 系统级上下文静态部分包括PCB结构、将进程虚地址空间映射到物理空间的有关表格、核心栈等。 这里,核心栈主要用来装载进程中所使用的系统调用的调用序列。
系统级上下文的动态部分是与寄存器上下文相关联的。进程上下文的层次概念主要体现在动态部分中,即系统级上下文的动态部分可看成是由一些数量变化的层次组成,其变化规则符合许先进后出的堆栈方式。
进程上下文切换发生在不同的进程之间而不是同一个进程内。 进城上下文切换分成三个步骤: (1) 把被切换进程的相关信息保存到有关存储区,例如该进程的PCB中。 (2) 操作系统中的调度和资源分配程序执行,选取新的进程。 (3) 将被选中进程的原来保存的正文部分从有关存储区中取出,并送至寄存器与堆栈中,激活被选中进程执行。
注意,线程任何时候都会面临CPU时间结束或者被剥夺的时候,也就是线程在执行完一些指令之后(还没完成所有的指令),会被切换出CPU的使用。这里就会发生线程的上下文切换。但是!!!!这时候是不会释放锁的,也就是即使你在某个地方无法获得CPU继续执行,但是这部分的代码只有你能在未来获得CPU之后继续执行,其他的线程是无法执行的(因为没有释放锁!!!)。这个就是原子性。
指的是程序执行的时候按照代码编写的先后顺序。 有序性的保证
as-if-searies 语义可以保证,不管是否发生指令重排,单线程的程序执行的结果必须是一致的。为了保证这个,但发生数据的依赖的时候,有依赖的数据操作指令一般不会重排。
happens-before 的程序规则
程序规则:一段代码在单线程中执行的结果是有序的。注意是执行结果,因为虚拟机、处理器会对指令进行重排序(重排序后面会详细介绍)。虽然重排序了,但是并不会影响程序的执行结果,所以程序最终执行的结果与顺序执行的结果是一致的。故而这个规则只对单线程有效,在多线程环境下无法保证正确性。 也就是as-if-searies其实是保证单线程的安全性的。
锁定规则:这个规则比较好理解,无论是在单线程环境还是多线程环境,一个锁处于被锁定状态,那么必须先执行unlock操作后面才能进行lock操作。
volatile变量规则:这是一条比较重要的规则,它标志着volatile保证了线程可见性。通俗点讲就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作一定是happens-before读操作的。
由于CPU是流水线作业的,因此,单条的CPU指令,其实也是分成很多的步骤,例如取指,译码,执行,访问存储器,写寄存器
一条指令,按时序,一次经过各个流水线完成。例如绿色的指令,先经历了取指令,译码,运行,写回寄存器等四个流水线。
当有多个线程并发访问同一个资源的时候,一个线程的修改,其他的线程可以立刻知道。 这里涉及到两个:
可见性并不能保证线程安全。i=0;因为对于非原子操作i++,仍然可能会存在修改覆盖的情况。i++其实会分成:
由于单纯的可见性,没有原子性,那么此时会发生另外一个线程重入,例如在3执行之前,另外的一个线程已经将i的值修改完成。此时i=1,那么线程更新i的值,但是赋值的时候回i=1,因此这次的修改逻辑是是错误的。
volatile 其实最大的用处就是
通过定义自己的类并继承Thread类,同时重写run()方法,可以直接获得一个线程实例。 复制代码
public class ThreadDemo01 extends Thread{ public ThreadDemo01(){ //编写子类的构造方法,可缺省 } public void run(){ //编写自己的线程代码 System.out.println(Thread.currentThread().getName()); } public static void main(String[] args){ ThreadDemo01 threadDemo01 = new ThreadDemo01(); threadDemo01.setName("我是自定义的线程1"); threadDemo01.start(); System.out.println(Thread.currentThread().toString()); } } 复制代码
需要注意的一个点就是需要使用start()方法来启动一个线程,直接调用run方法是无法实现线程的效果的。start()方法会调用start0(),这是一个native方法。而且,start方法是一个加了synchronize 的方法,可以同步的创建线程。如果直接调用run方法的话,就相当于直接调用一个普通方法。 继承的缺点: 复制代码
通过Tread 创建一个线程类的实例的时候,构造方法有一种为 Thread(Runnnable arg);因此,可以传进入一个实现Runnable接口的实例来获得一个线程类实例,而且此时可以做到资源共享。多个线程实例可以共享同一个runnable 实例的资源。但是这里会出现多线程共享资源时候的同步问题,需要自己做同步操作。 复制代码
public class ThreadDemo02 { public static void main(String[] args){ System.out.println(Thread.currentThread().getName()); Thread t1 = new Thread(new MyThread()); t1.start(); } } class MyThread implements Runnable{ @Override public void run() { // TODO Auto-generated method stub System.out.println(Thread.currentThread().getName()+"-->我是通过实现接口的线程实现方式!"); } } 复制代码
通过同一个Runnable 实例,可以实现资源共享的多线程并发。因为传入 Thread的Runnable实例是同一个实例,共享该实例的所有资源。
创建线程池主要有三个静态方法供我们使用,由Executors来进行创建相应的线程池:
public static ExecutorSevice newFixedThreadPool(int nThreads) public static ExecutorSevice newCachedThreadPool() public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 复制代码
我们只需要把实现了Runnable的类的对象实例放入线程池,那么线程池就自动维护线程的启动、运行、销毁。我们不需要自行调用start()方法来开启这个线程。线程放入线程池之后会处于等待状态直到有足够空间时会唤醒这个线程。我们只需要将实现了runnable接口的实例给到线程池就行。
private ExecutorService threadPool = Executors.newFixedThreadPool(5); threadPool.execute(socketThread); 复制代码
// Java线程池的完整构造函数 public ThreadPoolExecutor( int corePoolSize, // 线程池长期维持的线程数,即使线程处于Idle状态,也不会回收。 int maximumPoolSize, // 线程数的上限 long keepAliveTime, TimeUnit unit, // 超过corePoolSize的线程的idle时长, // 超过这个时间,多余的线程会被回收。 BlockingQueue<Runnable> workQueue, // 任务的排队队列 ThreadFactory threadFactory, // 新线程的产生方式 RejectedExecutionHandler handler) // 拒绝策略 复制代码
线程池默认的拒绝行为是AbortPolicy,也就是抛出RejectedExecutionHandler异常,该异常是非受检异常,很容易忘记捕获。如果不关心任务被拒绝的事件,可以将拒绝策略设置成DiscardPolicy,这样多余的任务会悄悄的被忽略。
execute 方法执行逻辑有这样几种情况:
1. 如果当前运行的线程少于 corePoolSize,则会创建新的线程来执行新的任务; 2. 如果运行的线程个数等于或者大于 corePoolSize,则会将提交的任务存放到阻塞队列 workQueue 中; 3. 如果当前 workQueue 队列已满的话,则会创建新的线程来执行任务; 4. 如果线程个数已经超过了 maximumPoolSize,则会使用饱和策略 RejectedExecutionHandler 来进行处理。 复制代码
需要注意的是,线程池的设计思想就是使用了核心线程池 corePoolSize,阻塞队列 workQueue 和线程池 maximumPoolSize,这样的缓存策略来处理任务,实际上这样的设计思想在需要框架中都会使用。
线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过 新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直"霸占"着CPU独自运行,所以CPU需要在多条线程之间切换,于是 线程状态也会多次在运行、阻塞之间切换。
当程序使用new关键字创建了一个线程之后,该线程就处于 新建状态,此时的线程情况如下:
线程对象调用了start()方法之后,该线程处于 就绪状态。此时的线程情况如下:
注意:
当CPU开始调度处于 就绪状态 的线程时,此时线程获得了CPU时间片才得以真正开始执行run()方法的线程执行体,则该线程处于 运行状态。
处于运行状态的线程最为复杂,它 不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就执行结束了),线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。线程状态可能会变为 阻塞状态、就绪状态和死亡状态。比如:对于采用 抢占式策略 的系统而言,系统会给每个可执行的线程分配一个时间片来处理任务;当该时间片用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。线程就会又 从运行状态变为就绪状态,重新等待系统分配资源;
处于运行状态的线程在某些情况下,让出CPU并暂时停止自己的运行,进入 阻塞状态。 发生如下情况时,线程将会进入阻塞状态:
join方法可以看做是线程间协作的一种方式,很多时候,一个线程的输入可能非常依赖于另一个线程的输出,这就像两个好基友,一个基友先走在前面突然看见另一个基友落在后面了,这个时候他就会在原处等一等这个基友,等基友赶上来后,就两人携手并进。其实线程间的这种协作方式也符合现实生活。在软件开发的过程中,从客户那里获取需求后,需要经过需求分析师进行需求分解后,这个时候产品,开发才会继续跟进。如果一个线程实例A执行了threadB.join(),其含义是:当前线程A会等待threadB线程终止后threadA才会继续执行。关于join方法一共提供如下这些方法: 主动调用的线程会先执行,执行完之后在继续执行当前的线程。例如 在A中 使用 B.join 此时会先执行B 执行完成之后再执行 A 复制代码
public static native void sleep(long millis)方法显然是Thread的静态方法,很显然它是让当前线程按照指定的时间休眠,其休眠时间的精度取决于处理器的计时器和调度器。需要注意的是如果当前线程获得了锁,sleep方法并不会失去锁。sleep方法经常拿来与Object.wait()方法进行比价,这也是面试经常被问的地方。 sleep() VS wait() - sleep()方法是Thread的静态方法,而wait是Object实例方法 - wait()方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而sleep()方法没有这个限制可以在任何地方种使用。另外,wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()方法只是会让出CPU并不会释放掉对象锁; - sleep()方法在休眠时间达到后如果再次获得CPU时间片就会继续执行,而wait()方法必须等待Object.notift/Object.notifyAll通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行。 复制代码
public static native void yield();这是一个静态方法,一旦执行,它会是当前线程让出CPU,但是,需要注意的是,让出的CPU并不是代表当前线程不再运行了,如果在下一次竞争中,又获得了CPU时间片当前线程依然会继续运行。另外,让出的时间片只会分配给当前线程相同优先级的线程。什么是线程优先级了?下面就来具体聊一聊。 现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当前时间片用完后就会发生线程调度,并等待这下次分配。线程分配到的时间多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要或多或少分配一些处理器资源的线程属性。
另外需要注意的是,sleep()和yield()方法,同样都是当前线程会交出处理器资源,而它们不同的是,sleep()交出来的时间片其他线程都可以去竞争,也就是说都有机会获得当前线程让出的时间片。而yield()方法只允许与当前线程具有相同优先级的线程能够获得释放出来的CPU时间片。
深入分析synchronize
CAS :并发编程中,锁是消耗性能的操作,同一时间只能有一个线程进入同步块修改变量的值。如果不加 synchronized 的话,多线程修改 a 的值就会导致结果不正确,出现线程安全问题。但锁又是要给耗费性能的操作。不论是拿锁,解锁,还是等待锁,阻塞,都是非常耗费性能的。那么能不能不加锁呢?
CAS详解:操作系统里面实现了cas 其中的 (expect==old){old=new;}
是原子操作。但是即使如此,它还是无法解决ABA问题,因为在读取完old的数值之后,old的数值可能会先发生A-B-A的变换,此时进行到原子操作 cas,这是也是会成功的。但是old的值其实是改变过了。
CAS可以保证数值在高并发的时候的正确性,但是付出了多次尝试,也就是自旋的代价,此时是会浪费一定的CPU性能的。
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) UnsafeWrapper("Unsafe_CompareAndSwapInt"); oop p = JNIHandles::resolve(obj); jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); return (jint)(Atomic::cmpxchg(x, addr, e)) == e; UNSAFE_END 复制代码
cas底层是通过cmpxch(x,addr,e)来实现的。这个函数会对多核的处理器进行锁操作,使得每次都只有一个线程来执行这两个指令,从而实现原子性。
锁机制有如下两种特性:
互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块(复合操作)进行访问。互斥性我们也往往称为操作的原子性。
可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作从而引起不一致。
在 Java 中,每个对象都会有一个 monitor 对象,这个对象其实就是 Java 对象的锁,通常会被称为“内置锁”或“对象锁”。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。
在 Java 中,针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的 Class 对象锁。每个类只有一个 Class 对象,所以每个类只有一个类锁。
synchronize修饰的是普通代码块和非静态方法的时候是对象锁
synchronize修饰的是静态方法和类的class 实例的时候(类.class),是类锁
synchronize 中可能使用到的有关锁的字段
如果一个对象处于偏向锁的状态,这就表示这个偏向锁科能是当前线程的,也可能是其他线程的。
总结就是,一开始使用偏向锁之后,如果没有新的线程出现的话,那么锁可以不断的重入,但是不用释放,而且只有在第一次使用的时候使用了CAS。之后不用进行任何操作。
但是如果出现了新的线程执行CAS的话,由于偏向锁不会主动的释放锁,需要等待竞争的到来才会释放锁。因此,竞争的线程进行CAS的时候,会发现Thread_ID已经不为初始值 null 了,此时一定会失败,触发其撤销的机制。撤销的过程中会判断线程是否存活,存活的话是否会继续竞争等。再对对应的转态标志位进行修改。
如果在撤销阶段发现竞争是存在的,那么此时会执行锁膨胀,由偏向锁转变为轻量级锁。全局安全点就是没有执行指令的时候。处理完成之后,会恢复被暂停的线程继续执行。在升级为轻量级锁之前,持有偏向锁的线程(线程 A)是暂停的,JVM 首先会在原持有偏向锁的线程(线程 A)的栈中创建一个名为锁记录的空间(Lock Record),用于存放锁对象目前的 Mark Word 的拷贝,然后拷贝对象头中的 Mark Word 到原持有偏向锁的线程(线程 A)的锁记录中(官方称之为 Displaced Mark Word ),这时线程 A 获取轻量级锁,此时 Mark Word 的锁标志位为 00,word lock(word Mark 里面的一个字段??)指向线程 A 的锁记录地址,如下图:
当原持有偏向锁的线程(线程 A)获取轻量级锁后,JVM 唤醒线程 A,线程 A 执行同步代码块,执行完成后,开始轻量级锁的释放过程。 对于其他线程而言,也会在栈帧中建立锁记录,存储锁对象目前的 Mark Word 的拷贝。JVM 利用 CAS 操作尝试将锁对象的 Mark Word 更正指向当前线程的 Lock Record,如果成功,表明竞争到锁,则执行同步代码块,如果失败,那么线程尝试使用自旋的方式来等待持有轻量级锁的线程释放锁。当然,它不会一直自旋下去,因为自旋的过程也会消耗 CPU,而是自旋一定的次数,如果自旋了一定次数后还是失败,则升级为重量级锁,阻塞所有未获取锁的线程,等待释放锁后唤醒。 轻量级锁的释放,会使用 CAS 操作将 Displaced Mark Word 替换会对象头中,成功,则表示没有发生竞争,直接释放。如果失败,表明锁对象存在竞争关系,这时会轻量级锁会升级为重量级锁,然后释放锁,唤醒被挂起的线程,开始新一轮锁竞争,注意这个时候的锁是重量级锁。
自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定: 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。 相反的,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能减少自旋时间甚至省略自旋过程,以避免浪费处理器资源。 自适应自旋解决的是“锁竞争时间不确定”的问题。JVM很难感知到确切的锁竞争时间,而交给用户分析就违反了JVM的设计初衷。自适应自旋假定不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定,因此,可以根据上一次自旋的时间与结果调整下一次自旋的时间。 自旋锁的目标是降低线程切换的成本。如果锁竞争激烈,我们不得不依赖于重量级锁,让竞争失败的线程阻塞;如果完全没有实际的锁竞争,那么申请重量级锁都是浪费的。轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。 顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅_将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功_,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。当然,由于轻量级锁天然瞄准不存在锁竞争的场景,如果存在锁竞争但不激烈,仍然可以用自旋锁优化,自旋失败后再膨胀为重量级锁。 轻量级锁的CAS 也是无状态的时候执行将word mark 更新为指向本线程中Lock Recoder 的指针,如果更新成功,那么此时获得锁,将锁的标志设置为轻量级锁。 如果CAS失败,那么表示此时已经被别的线程获取了锁,先比较是否是自身获取了锁。如果是指向自身Lock Recoder 的指针,那么就是一次重入。如果不是,那么就自旋几次。如果自旋几次都失败了,那么就改变了Word Mark 的标志位为重量级锁(word Mark 会指向一个monitor对象。此时锁的管理已经已交给了monitor了。注意,一个对象会关联一个监视器来进行同步管理),并且自己会进入阻塞队列中。 此时,获得锁的线程会执行完毕,完成锁的释放。也就是将锁设置为无锁状态,但是此时CAS的时候回发现,此时的锁已经是重量级锁,由于不一致,因此表明有线程已经阻塞了。这时候就执行重量级锁的释放即可。释放锁,并唤醒阻塞的线程即可。 复制代码
偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。 一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。 轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。 两个CAS 的预期值是已知的,偏向锁是:偏向=1|线程ID=null|01 。撤销锁的时候可以实现word Mark重置 轻量级锁的CAS 的预期值是 偏向=0|锁标志位=00|锁记录指针=null CAS的目的就是将所记录的指针指向自己的线程中锁记录地址。轻量级锁会自动的释放锁,释放的流程就是将预期值cas设置到 word Mark中。 复制代码
锁原理:偏向锁、轻量锁、重量锁1.加锁2.撤销偏向锁1.加锁2.解锁3.膨胀为重量级锁
Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)
JVM-锁消除+锁粗化 自旋锁、偏向锁、轻量级锁 逃逸分析-30
CAS操作、Java对象头、偏向锁的获取与撤销、轻量级锁的获取与撤销、锁粗化、锁消除
从偏向锁是如何升级到重量级锁的
啃碎并发(七):深入分析Synchronized原理
注意,synchronized 内置锁 是一种 对象锁(锁的是对象而非引用变量),作用粒度是对象 ,可以用来实现对 临界资源的同步互斥访问 ,是 可重入 的。其可重入最大的作用是避免死锁,如:
synchronized 使用的时候,代码块和方法还是有点区别的。 当使用修饰的是方法的时候,只有一个标志位 ACC_SYNCHRONIZED。这个标志位其实就是是否需要获取锁对象monitor。而monitor 获取之后执行同步方法的步骤其实就和修饰代码块的流程开始一样了。
修饰的是代码块的时候,发编译后得到的内容如下:
任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是 基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。 1. MonitorEnter指令:插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁; 3. MonitorExit指令:插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit; 那什么是Monitor?可以把它理解为 一个同步工具,也可以描述为 一种同步机制,它通常被 描述为一个对象。 与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。 也就是通常说Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的): 复制代码
ObjectMonitor() { _header = NULL; _count = 0; // 记录个数 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; } 复制代码cxq,EntryList and WaitSet三者之间的区别
cxq(ContentionList),EntryList ,WaitSet。
注意,新来的竞争线程是加入到的cxq中的,而EntryList是每次唤醒的时候从cxq中搬运过来的线程。
整个唤醒和等待的流程
当多个线程执行到同步代码块时就会产生竞争,synchronized会执行monitorenter指令,最终会调用C++的ObjectMonitor::enter方法。 1.通过CAS尝试把monitor的owner字段设置为当前线程。 2.如果设置之前的owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行recursions++,记录锁重入的次数。 3.如果当前线程是第一次进入该monitor,设置recursions为1,_owner为当前线程,该线程成功获得锁并返回。 4.如果获取锁失效,则进入等待队列,等待锁的释放。
等待流程概括小结 1.当前线程被封装成ObjectWaiter对象的node,状态设置为ObjectWaiter::TS_CXQ。 2.在for循环中,通过CAS把node节点push到_cxq列表中,同一时刻可能有多个线程把自己的node节点push到cxq列表中。 3.node节点push到_cxq列表之后,通过自旋尝试获取锁,如果没有获取到锁,则通过park将当前线程挂起,等待被唤醒。 4.当线程被唤醒时,会从挂起的点继续执行,通过ObjectMonitor::TryLock尝试获取锁。
recursions对应线程的重入次数,可重入锁,当减为0时说明线程完全出了同步代码块并释放锁,同时根据不同的策略(QMode指定)去唤醒正在等待阻塞的线程,从等待链表_cxq和_EntryList中获取头结点,通过ExitEpilog方法唤醒该节点封装的线程,唤醒操作最终由UNpark操作。
原理弄清楚了,顺便总结了几点Synchronized和ReentrantLock的区别:
Synchronized是JVM层次的锁实现,ReentrantLock是JDK层次的锁实现; Synchronized的锁状态是无法在代码中直接判断的,但是ReentrantLock可以通过ReentrantLock#isLocked判断; Synchronized是非公平锁,ReentrantLock是可以是公平也可以是非公平的; Synchronized是不可以被中断的,而ReentrantLock#lockInterruptibly方法是可以被中断的; 在发生异常时Synchronized会自动释放锁(由javac编译时自动实现),而ReentrantLock需要开发者在finally块中显示释放锁; ReentrantLock获取锁的形式有多种:如立即返回是否成功的tryLock(),以及等待指定时长的获取,更加灵活; Synchronized在特定的情况下对于已经在等待的线程是后来的线程先获得锁(上文有说),而ReentrantLock对于已经在等待的线程一定是先来的线程先获得锁;
首先,非公平锁的性能比较好,但是可能会出现线程饥饿等情况。公平锁可以保证获取锁的顺序,但是需要付出额外的维护开销。
ReentrantLock 底层是实现了AQS,这个类里面实现了一些公用的方法,例如
公平锁和非公平锁都是实现了 Sync这个类。这个类主要有两个方法,一个是lock()函数用于获取锁,另外一个是尝试获取锁tryLock();
其中,在Sync中已经实现了非公平锁的nonfairTryAcquire()。实现ReentrantLock的时候,默认创建的是非公平锁。
注意,这个方法是会被acquire()函数调用的。
公平锁类的lock不会CAS去尝试获取锁。而是直接使用acquire()来获取锁。下面我们看看acquire()函数如何获取锁。acquire()函数是AQS中的一个函数,如下图:
这个函数里面主要有一个被公平和非公平锁重写了的tryAcquire(); 其中非公平锁会检测一下是否存在阻塞队列hasQueuedPredecessors(),这里是唯二区别。后面就是判断是否获取锁,是否为重入锁等。如果获取失败,听过acquire()函数可以知道,此时会执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg))函数。
从上面的AQS可以知道,首先会把线程包装成一个Node对象,然后加入一个阻塞队列的链表尾中。采用循环和CAS来保证入队成功和线程安全。 当入队成功之后,执行上图的此时会执行acquireQueued()函数,这里会先判断是否为队列的首位,如果成功了,那么就会直接CAS获取锁,否则,执行一个park函数。进行阻塞。
公平和非公平锁--ReentrantLock
为了不会阻塞的访问同步资源,引入了乐观锁机制。乐观锁机制其实就是一种简单的思想,具体实现是,表中有一个版本字段,第一次读的时候,获取到这个字段。处理完业务逻辑开始更新的时候,需要再次查看该字段的值是否和第一次的一样。如果一样更新,反之循环继续尝试。乐观锁的一种实现就是CAS + 版本号。 复制代码
总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加(悲观)锁。一旦加锁,不同线程同时执行时,只能有一个线程执行,其他的线程在入口处等待,直到锁被释放。频繁的阻塞和唤醒可能会带来很大的开销。相反的,乐观锁不会加锁。 复制代码
PV操作:首先要保证P操作和V操作是原子性的。其次,要明确PV 操作的意义是对资源的访问控制,也就是基于资源S 的情况来执行响应的线程阻塞和唤醒操作。
对于生产者和消费者模式,也叫公共缓冲区的并发问题,我们一般使用两个变量来标志该缓存区剩余的空间以及被占用的空间。被占用的空间,称为 producer,表示已经存放了商品;buffer 表示缓存区剩余的空间。 互斥量为 S=1。表示的是只有一个资源。由于一开始我们的缓存区为12个,因此,我们设置S=12 。表示的是当该资源使用完之后,就会使得线程进行阻塞。具体的P V 操作如下:
V 操作表示对某个资源的释放,注意,资源释放之后才做判断,如果还有S<=0。有线程使用了资源,因此需要唤醒该线程。
具体的实现:
注意,虽然对通过资源的 PV 是需要成对的,但是可以在不同的函数中。
首先,S 表示的是资源的可以使用情况,S=0表示资源已经被使用完,那么此时一定有线程占用了该资源。
当一个线程申请完资源 此时申请 S : S--
,如果此时S《0,那么就表示没有资源可以使用了,此时需要进行阻塞。如果S >=0,那么就表示申请资源是成功的。
当一个线程是否了该资源: 此时执行 S++
,如果此时S<=0,那么就表示还有其他线程占用了资源还没有释放。由于每次只有一个线程可以持有该同步代码块的使用权(因为其他的线程被阻塞了)。因此,需要唤醒阻塞的线程。因为我就释放了一个资源,因此,只能唤醒一个线程。
这个其实就是PV 操作的官方的版本感觉。不再自己实现PV 操作,不再自己进行 资源使用情况和对应情况下的判断,操作等。
对于Condition,JDK API中是这样解释的:
Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。
条件(也称为条件队列 或条件变量)为线程提供了一个含义,以便在某个状态条件现在可能为 true 的另一个线程通知它之前,一直挂起该线程(即让其“等待”)。因为访问此共享状态信息发生在不同的线程中,所以它必须受保护,因此要将某种形式的锁与该条件相关联。等待提供一个条件的主要属性是:以原子方式 释放相关的锁,并挂起当前线程,就像 Object.wait 做的那样。
Condition 实例实质上被绑定到一个锁上。要为特定 Lock 实例获得 Condition 实例,请使用其newCondition() 方法。
当我们创建完一个lock 之后,可以为这个lock 绑定多个阻塞队列。例如这里我们可以使用两个队列来,一个阻塞生产者,另外一个阻塞消费者。当产品满了的时候,生产线程挂到生产者阻塞队列中;当商品空了的时候,消费消除阻塞到消费者阻塞队列中。但生产完一件商品时候,唤醒的是一个消费阻塞队列中的线程。同样的,当消费一件商品之后,唤醒生产阻塞队列中的一个生产线程。注意这里和synchronize 不一样,synchronize 默认每次都会唤醒所有被阻塞的线程(不论是生产还是消费的线程,最大的原因还是只有一个阻塞队列。)
因此,从这一点来说,其实和 PV 操作是有点一一样的。
可以看到,一个lock ,但是注册了两个condition 也就是两个的阻塞队列。
synchronize就是基本操作了。
首先,equals()函数一般用于比较两个变量是否相等。这里的相等是逻辑是的相等。equals()一般会先试用==来确定这两个变量的地址是否一致,如果不一致,那么就进行内容的比较。因此,equals()就是一个全方面的比较方法 最简答的就是Object中的equals()直接返回的是==,两个变量的地址值。
== 主要用于比较变量的值的。如果是基本的数据类型,则直接比较值,如果是引用类型,则比较的是引用的地址值。对象是放在堆中的,引用是放栈中的。
hashcode 是一种映射编码函数。用于标志一个对象的编码。规定,相同的对象(逻辑上相等),其hashcode是一定相等的。不相等的对象其hashcode也可能相等。因此,hashcode也是用来比较对象是否相等的。它只是想布隆过滤器一样,只能判断不相等的hashcode一定不相等。
最常用的方法:
这样做的效率很高,因为不用每次一开始就使用equals()来进行比较。
但是,一般我们的equals 逻辑是自己写的,因此,我们还要自己确保equals的两个对象的hashcode是一样的。因此,重写了equals 一定要重写hashcode。
看似简单的hashCode和equals面试题,竟然有这么多坑!
这里面的hashcode 仅仅是用来定位数组的。tables是一个Entry数组,其中的Entry是一个节点的数据格式。带有指针。
公共部分:
if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } //如果遍历之后没有找到,表明桶为空。 modCount++; //addEntry实现了空桶和非空桶两种增加模式。扩容也是在addEntry中实现的。 addEntry(hash, key, value, i); return null; } 复制代码
在1.7中 当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)。 因此 1.8 中引入红黑树,重点优化了这个查询效率。
图中可知,使用了链表和红黑树的结构,但链表的长度大于8的时候,自动进行树化。反树化则是6
1.8中的put的详细解释
www.jianshu.com/p/c0642afe0…
www.javazhiyin.com/37729.html
blog.csdn.net/u010842515/…
juejin.im/post/5db943…
可以的,锁被别人获取了,但是我还是可以访问该对象的。而且可以执行所有的没有加锁的函数。当你要去执行加锁的代码块的时候,就会被阻塞。
不是的。其实是对一个共享的资源的读写操作加锁。例如我们常说的读写,读读,写写等操作,针对的是一个共享的资源的修改。我们只是对这些修改的指令进行加锁。其实你可以只对其中的一个方面加锁,例如读的时候加锁,例如写的时候加锁,例如读写都加锁等。concurrentHashMap 中只对put 进行了加锁,此时其实是一个写锁。锁住的是一整个的链表或者树根。但是此时依旧可以访问这个对象中的所有的属性和没有加锁的代码块。注意是所有的变量。。。。即使是同步代码看重点关注的变量。。。。这一点很神奇。
当一个变量被 volatile 修饰时,任何线程对它的写操作都会立即刷新到主内存中,并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据。通过这两步可以实时的保证每个线程得到的数据都最新的。
首先,了解一下类的加载时机:
也就是,即使外部类被使用到了,但是此时是不会同时加载内部类的,不管是不是静态的内部类。除非此时你显式的调用该类的一些静态的属性或者代码块。
public class Singleton { //无论是否使用,都会先创建,可能会有性能的缺陷 private static Singleton instance = new Singleton(); private Singleton (){ } public static Singleton getInstance() { return instance; } } 复制代码
//线程不安全的模式 public class Singleton { private static Singleton instance; private Singleton (){ } public static Singleton getInstance() { if (instance == null) { //由于线程不安全,可能会多次创建对象 instance = new Singleton(); } return instance; } } 复制代码
public class Singleton { //线程安全模式 private static Singleton instance; private Singleton (){ } public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } 复制代码
public class Singleton { //volatile防止指令重排。 private volatile static Singleton instance; private Singleton (){ } public static Singleton getInstance() { if (instance== null) { synchronized (Singleton.class) { if (instance== null) { //这个赋值语句不是原子操作,包括了一个对象创建和赋值的过程。 //这里其实有3个步骤,分配内存地址,初始化对象,将地址赋值给单例。 //但是这里的顺序是不一定的,也就是可能出现先赋值再初始化的过程。 //而在初始化的时候,如果有新的线程如果得到单例已经赋值,那么可能得到 //一个空的单例。但使用了volatile时候,会保证执行的顺序为 //单例的赋值一定是在最后的。(代码顺序性) instance= new Singleton(); } } } return singleton; } } 复制代码
public class Singleton { private Singleton() {} //静态内部类不会被初始化,除非触发初始化条件 static class SingletonHolder { private static final Singleton instance = new Singleton(); } public static Singleton getInstance() { //触发初始化条件。调用了静态内部类的一个静态的属性。 //之所以要是静态的内部类,是因为内部类不能有静态属性,因此要静态内部类。 //简单就是静态内部类才能有静态的属性。 return SingletonHolder.instance; } } 复制代码
注:本系列博文主要是做的信息整合方面的工作,很感谢收集到的各位的博文,很多地方还没有注明出处,主要是在看的时候有想加入自己的一些想法等。因此,对于参考到的,没有标出reference 的博文 ,我后续会标注上,并对此表示真诚地道歉!!