多线程
线程和进程的区别?
进程是一个运行中的程序,操作系统会为他分配独立的内存,而线程的话,操作系统除了为他分配cpu之外,不会为他分配内存,他所使用的资源是所属线程的资源,一个进程至少包含一个线程,同时一个线程必须依附与一个进程
Java创建线程的方式
- 继承Threa类方式的多线程
优势:编写简单
劣势:无法继承其他类
2. 实现Runnable接口
优势:可以继承其他类,多线程可以共享一个Runnabel对象
劣势:编写复杂
3 实现Callable接口
可以有返回值,可以抛出一个异常
线程的生命周期 5个
新建 也就是new Thread()
就绪 执行start()方法之后
运行 线程获取到cpu之后
阻塞 线程挂起时候,比如线程自旋之后,未获取到锁,调用Thread.sleep()方法后
死亡 线程执行完run方法,或者线程抛出异常,用户中断线程
Thread类常用方法
currentThread():当前正在执行的线程
isAlive():判断当前线程是否处于活动状态
sleep():方法,让线程休眠指定的毫秒数,不释放对象的锁,与时间有关,比如隔多少秒打印一个数字
join():合并线程,让当前线程阻塞,去执行另外一个方法
yiled 让当前线程暂停一下,但是不是阻塞,只是让出了cpu的调度,执行该方法,线性就会进入就绪状态
wait(),调用后线程进入等待状态,并且释放对象的锁,并且只有调用noyify()方法或者notifyAll()方法才会唤起一个线程
getId():取得线程的唯一标识
如何停止一个线程?
1 run方法完成后线程自动终止
2 使用stop方法强行终止线程 强制停止有可能使一些清理性的工作得不到完成,还有就是对锁定的对象进行了解锁,导致数据得不到同步的处理,出现数据不一致
3 使用interrupt方法中断线程,仅仅打了一个停止的标记,并不是真的停止线程
4 通过在run方法中抛出一个异常也可以终止一个线程
判断线程是否是停止状态?
this.interrupted():测试当前线程是否已经中断,执行之后具有将线程标志状态清楚为fasle的功能,当前线程是指运行this.interrupted方法的线程
this.isInterrupted():测试线程Thread对象是否已经是中断状态,但是不清楚状态标志
线程的优先级?
在操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU资源较多,在Java中优先级分为1~10,如果小于1或者大于10,则JDK抛出异常
优先级是具有随机性的
守护线程
守护线程就是一种服务线程,主要是作用于用户线程,典型的守护线程就是垃圾回收线程,当用户线程不存在的时候,守护线程就自动结束
Synchronized的三种应用方式
修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。
需要注意的是如果一个线程A调用一个实例对象的非static 修饰的synchronized方法,而线程B需要调用这个实例对象所属类的的静态syschronized方法是允许的,不会发生互斥现象,因为锁的对象是不一样的,一个是类,一个是对象
Synchronized底层语义原理
Java中的同步是基于进入和退出管程(Monitor)对象实现,无论是显示同步(修饰代码块,既就是有明确的monitorenter和monitorexit指令)还是隐士同步(synchronized修饰方法)都是如此
理解java对象头与Monitor
在Jvm中,对象在内存中分为三块区域:对象头。实例数据和对齐填充
实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分按4字节对齐
填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,了解即可
而对于顶部,则是java头对象,他是实现synchronized对象的基础,一般而言,synchronized使用的锁对象是存储在java对象头里的,jvm采用2个字节来存储对象头(如果是数组会分配三个字节,多出来的一个存储数组的长度)其主要结构是由Mark Word和Class Metada Address组成,主要组成如下
其中Mark Word在默认情况下存储这对象的HashCode,分带年龄,锁标记位等
Synchronized修饰代码块的底层原理
同步语句块的使用的是monitorenter和monitorexit指令,其中monitorenter指向的是代码块的起始位置,monitorexit指向的是同步代码快的结束位置,当执行monitorenter指令的时候,当前线程试图去获取对象锁对应的monitor的持有权,当对象锁的monitor的计数器为0,那么线性就可以获取到monitor,并将计数器设置为1,取锁成功,如果当前线程已经拥有对象锁的monitor的持有权,那么他可以重入这个monitor,重入时计数器也会加1,如果其他线程已经拥有对象锁的monitor的持有权,那么当前线程将被阻塞,直到执行线程执行完毕,既就是monitorexit指令被执行,执行线程将会释放monitor锁,并将计数器设置为0,其他线程有机会持有monitor ,需要注意的是编译器会自动生成一个异常处理器,这个异常处理器可以处理所有的异常,目的就是用来执行monitorexit指令
字节码指令 public void syncTask(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=3, args_size=1 0: aload_0 1: dup 2: astore_1 3: monitorenter //注意此处,进入同步方法 4: aload_0 5: dup 6: getfield #2 // Field i:I 9: iconst_1 10: iadd 11: putfield #2 // Field i:I 14: aload_1 15: monitorexit //注意此处,退出同步方法 16: goto 24 19: astore_2 20: aload_1 21: monitorexit //注意此处,退出同步方法 22: aload_2 23: athrow 24: return Exception table: //省略其他字节码....... |
Synchronized方法底层原理
字节码指令 //省略没必要的字节码 //==================syncTask方法====================== public synchronized void syncTask(); descriptor: ()V //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法 flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=3, locals=1, args_size=1 0: aload_0 1: dup 2: getfield #2 // Field i:I 5: iconst_1 6: iadd 7: putfield #2 // Field i:I 10: return LineNumberTable: line 12: 0 line 13: 10 } |
方法级的同步是隐士的,既就是无需通过字节码指令来控制的,他实现在方法的调用和返回操作之中,JVM可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志判断一个方法时否是同步方法,当方法调用的时候,他会将先检查方法中的ACC_SYNCHRONIZED标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中叫管程一词),然后在执行方法,最后在方法完成(无论是正常完成还是非正常玩完成)释放monitor,在方法执行期间,执行线程持有了monitor,其他线程都无法再获得同一个monitor(管程的机制实现),如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步之外是自动释放!
Synchrnozied修饰的方法没有monitorenter指令和monitorexit指令,取而代之的是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的异步调用,这边是synchronized锁在同步代码块和同步方法上的实现原理
什么是管程?
管程就好比是一个建筑物,里面有一个特殊的房间,同一时间只能允许一个线程去访问,通常包含一些数据和代码
Java中锁的分类
乐观锁:认为读多写少,每次读取数据的时候都认为别人不会修改,所以不会加锁
但是在更新的时候,会先判断一下在此期间有没有人去更改数据,采取的是先读取当前版本号,然后进行加锁(比较现在的版本号和上一次的读取的版本号,如果一致则进行更新),如果失败,则需要重新进行重复读比较写的操作
悲观锁:读写数据都会加上锁,也就是排它锁
Java线程阻塞的代价
Java的线程是需要映射到操作系统的原生线程之上的,线程的阻塞唤醒都需要从用户态与核心态之间的切换,这种切换回消耗大量的系统资源,因为用户态和核心态都有自己的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
1 如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间
2 如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的
Java中有四种锁,分别是偏向锁,轻量级锁,自旋锁,重量级锁,不同的锁有不同的特点,应用场景也不一样
偏向锁
Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。
偏向锁的实现
偏向锁获取过程:
- 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。
- 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。
- 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4。
- 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)
- 执行同步代码。
注意:第四步中到达安全点safepoint会导致stop the word,时间很短。
偏向锁的释放:
偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
偏向锁的适用场景
始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作;
在有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用;
jvm开启/关闭偏向锁
- 开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
- 关闭偏向锁:-XX:-UseBiasedLocking
轻量级锁
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;
轻量级锁的加锁过程:
- 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图:
所示。 - 拷贝对象头中的Mark Word复制到锁记录中;
- 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。
- 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示。
- 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。
轻量级锁的释放
释放锁线程视角:由轻量锁切换到重量锁,是发生在轻量锁释放锁的期间,之前在获取锁的时候它拷贝了锁对象头的markword,在释放锁的时候如果它发现在它持有锁的期间有其他线程来尝试获取锁了,并且该线程对markword做了修改,两者比对发现不一致,则切换到重量锁。
因为重量级锁被修改了,所有display mark word和原来的markword不一样了。
怎么补救,就是进入mutex前,compare一下obj的markword状态。确认该markword是否被其他线程持有。
此时如果线程已经释放了markword,那么通过CAS后就可以直接进入线程,无需进入mutex,就这个作用。
尝试获取锁线程视角:如果线程尝试获取锁的时候,轻量锁正被其他线程占有,那么它就会修改markword,修改重量级锁,表示该进入重量锁了。
还有一个注意点:等待轻量锁的线程不会阻塞,它会一直自旋等待锁,并如上所说修改markword。
这就是自旋锁,尝试获取锁的线程,在没有获得锁的时候,不被挂起,而转而去执行一个空循环,即自旋。在若干个自旋后,如果还没有获得锁,则才被挂起,获得锁,则执行代码。
自旋锁
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
但是线程自旋是需要消耗cpu的,说白了就是让cpu在做无用功,如果一直获取不到锁,那线程也不能一直占用cpu自旋做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
自旋锁的优缺点
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,占着XX不XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup的线程又不能获取到cpu,造成cpu的浪费。所以这种情况下我们要关闭自旋锁;
自旋锁时间阈值
自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!
JVM对于自旋周期的选择,jdk1.5这个限度是一定的写死的,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM还针对当前CPU的负荷情况做了较多的优化
- 如果平均负载小于CPUs则一直自旋
- 如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞
- 如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞
- 如果CPU处于节电模式则停止自旋
- 自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差)
- 自旋时会适当放弃线程优先级之间的差异
自旋锁的开启
JDK1.6中-XX:+UseSpinning开启;
-XX:PreBlockSpin=10 为自旋次数;
JDK1.7后,去掉此参数,由jvm控制;
重量级锁
Synchronized的作用
在JDK1.5之前都是使用synchronized关键字保证同步的,Synchronized的作用相信大家都已经非常熟悉了;
它可以把任意一个非NULL的对象当作锁。
- 作用于方法时,锁住的是对象的实例(this);
- 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8则是metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
- synchronized作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。
Synchronized的实现
实现如下图所示;
它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
- Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
- Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;
- Wait Set:哪些调用wait方法被阻塞的线程被放置在这里;
- OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;
- Owner:当前已经获取到所资源的线程被称为Owner;
- !Owner:当前释放锁的线程。
JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。
OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中。
处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。
Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。
synchronized的执行过程:
1. 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁
2. 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1
3. 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
4. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁
5. 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
6. 如果自旋成功则依然处于轻量级状态。
7. 如果自旋失败,则升级为重量级锁。
上面几种锁都是JVM自己内部实现,当我们执行synchronized同步块的时候jvm会根据启用的锁和当前线程的争用情况,决定如何执行同步操作;
在所有的锁都启用的情况下线程进入临界区时会先去获取偏向锁,如果已经存在偏向锁了,则会尝试获取轻量级锁,启用自旋锁,如果自旋也没有获取到锁,则使用重量级锁,没有获取到锁的线程阻塞挂起,直到持有锁的线程执行完同步块唤醒他们;
偏向锁是在无锁争用的情况下使用的,也就是在当前线程没有执行完之前,没有其它线程会执行该同步块,一旦有了第二个线程的争用,偏向锁就会升级为轻量级锁,如果轻量级锁自旋到达阈值后,没有获取到锁,就会升级为重量级锁;
如果线程争用激烈,那么应该禁用偏向锁。