目录
一、初识线程(内存模型、三个特性、重排序)
线程相关概念
说到线程就必须说进程,进程是系统分配应用的基本单位,进程可以理解为一个可以独立运行的程序单位,就好比开一个QQ就是一个进程(同时可以打开多个QQ,允许多个进程?),那么一个进程可以包含多个线程,比如打开一个qq聊天窗口就是一个线程(如果是聊天合并窗口,也只有一个线程)。使用单线程,在执行耗时较长的操作的时候,就会存在等待的情况。在多核CPU的条件下,为了提高CPU的利用率,可以使用多个线程去执行其他操作,减少耗时。
注意点:
1.使用多线程很消耗系统资源,因为多线程需要开辟内存,而且线程切换也是需要时间的。(多线程处理导致生产内存使用到80%告警)
2.线程的终止会对程序有影响
3.多个线程之间存在共享数据,容易出现线程死锁的情况
主线程与子线程的概念
主线程就比如main线程,子线程就是main方法中开启的线程。
多线程运行的随机性
由于CPU时间片的问题,因此多个线程之间,谁先抢到CPU资源,谁就先执行。
上下文切换
处理器从一个线程切换到另一个线程需要消耗资源。
线程安全
就是多个线程对同一个对象中的同一个变量的值进行修改时,有可能出现混乱,导致值不同步的情况。
原子性
要么成功要么失败,不能出现成功一半的情况。实现方案:使用synchronized锁 或者CAS
可见性
一个线程对一个对象中的变量进行修改,对另一个线程是不是可见。 volatile解决可见性和有序性问题
有序性
在多核处理器条件下,内存的操作顺序和代码顺序可能不一致。(不是必然出现的)。volatile解决可见性和有序性问题,以及synchronized
重排序
编译器在编译的时候,可能会出现代码顺序与编译后的顺序不一致。(不是必然出现的)。
指令重排序:由JIT编译器和处理器引起的,代码顺序与执行顺序不一致。
存储系统重排序:由高速缓存和写缓冲器引起的,感知顺序与执行顺序不一致。
内存操作
load 读内存。从内存读取数据到寄存器。
store 写内存。把数据写到指定地址的内存单元中。
loadload(读读)重排序、loadstore(读写)重排序等等,就是说他可能是后面的先执行(重排序)
貌似串行语义
保证重排序不影响单线程的执行顺序。
内存模型
1)每个线程都有独立的栈空间
2)每个线程都可以访问堆内存
3)CPU不直接读取主内存中的数据,CPU读取数据时,先把主内存数据读到cache缓存中,再把cache中的数据读到CPU寄存器中。
4)JVM中的共享数据可能被分配到不同的CPU寄存器中,就会产生线程之间的不可见性。
Java内存模型可以抽象为:工作内存和主内存。
二、线程的创建
实现runnable,继承Thread,实现calllable
//方式一
public class MyThread implements Runnable{
@Override
public void run() {
System.out.println("this is run");
}
public static void main(String[] args) {
new Thread(new MyThread()).start();
}
}
//方式二:
public class MyThread extends Thread{
@Override
public void run() {
System.out.println("this is run");
}
public static void main(String[] args) {
new Thread(new MyThread()).start();
}
}
//方式三:
public class ExcutorsCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "test";
}
public static void main(String[] args) {
Thread t = new Thread(new MyThread());
t.start();
}
}
执行线程start和run的区别?
start方法是开启一个线程,run只是执行一个普通方法。
线程的生命周期
包含5个阶段,包括:新建、就绪、运行、阻塞、销毁。
新建:就是刚使用new方法,new出来的线程;
就绪:就是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;
阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态;
销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;
线程什么时候结束?
1.run方法正常执行完毕
2.run方法抛出异常
推荐实现Runnable接口方式开发多线程,因为Java单继承但是可以实现多个接口。
然而这两种实现方式,在实际开发中却很少用到,最多只能做简单的测试,调试。更多的是使用线程池。
《阿里巴巴Java开发手册》在第一章第六节并发处理这一部分也强调到“线程资源必须通过线程池提供,不允许在应用中自行显示创建线程”
三、锁
锁的概念
悲观锁:保证每次操作都锁住资源。synchronized
乐观锁:不直接锁资源,通过CAS(compare and swap)+自旋
内部锁(内置锁):每个java对象都可以用做一个实现同步的锁,这些锁成为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。
内置锁是互斥锁。
synchronized,它是JVM内置的关键字。java中每个对象都一个锁,也叫监视器,它是一种排他锁。
synchronized原理是JVM指令monitorenter和moniterexit来加锁和释放
排他锁:只能被一个线程拿到。
synchronized(lock){
}
lock被称为锁对象,注意,如果lock是this,那么如果两个线程操作的是不同的锁对象,那么就不会同步。因为她们持有的是不同的锁。
可以使用常量作为锁对象。
对象锁:每一个类的实例对应一把锁,用来当作锁的对象称为对象锁。当一个对象中有synchronized修饰的方法或者代码块的时候,要想执行这段代码,就必须先获得这个对象锁
公平锁:多个线程申请获取同一资源时,必须按照申请顺序,依次获取资源。
非公平锁:资源释放时,任何线程都有机会获得资源,而不管其申请顺序。
互斥锁(同步):当使用synchroinzed锁住多段不同的代码片段, 但是这些同步块使用的同步监视器对象是同一个时,那么这些代码片段之间就是互斥的。多个线程不能同时执行他们。
同步锁:同步就是ABCD这些线程要约定一个执行的协调顺序。比如D要执行,B和C必须都得做完,而B和C要开始,A必须先得做完。
独占锁(写锁):指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁。
共享锁(读锁):指该锁可被多个线程所持有。
对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁,读锁的共享可保证并发读是非常高效的,读写,写读,写写的过程是互斥的
共享锁、排它锁、独占锁是悲观锁的一种实现
自旋锁:自旋锁可以使线程在没有取得锁的时候,不被挂起,而转去执行一个空循环,(即所谓的自旋,就是自己执行空循环),若在若干个空循环后,线程如果可以获得锁,则继续执行。若线程依然不能获得锁,才会被挂起。
使用自旋锁后,线程被挂起的几率相对减少,线程执行的连贯性相对加强。因此,对于那些锁竞争不是很激烈,锁占用时间很短的并发线程,具有一定的积极意义,但对于锁竞争激烈,单线程锁占用很长时间的并发程序,自旋锁在自旋等待后,往往毅然无法获得对应的锁,不仅仅白白浪费了CPU时间,最终还是免不了被挂起的操作 ,反而浪费了系统的资源。
阻塞锁:让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。。
JAVA中,能够进入\退出、阻塞状态或包含阻塞锁的方法有 ,synchronized 关键字(其中的重量锁),ReentrantLock,Object.wait()\notify()
可重入锁(避免死锁):可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁
锁的升级
对象头
对象=对象头+对象体+对齐字节(填充字节)
对象头=Mark Word +Kclass Word+数组长度
对象的几个部分的作用:
1.对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode;
2.Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例;
每个对象实例里,都有对象头,对象头里有MarkWord和Kclass,MarkWord里面有2位标识位来标识锁的状态,分别是:无锁状态-》偏向锁-》轻量级锁-》重量级锁
对象状态有5种:正常、偏向锁、轻量级锁、重量级锁、GC标记
对象由对象头、对象体和填充字节组成。
对象头里面有Mark Word和Kclass、数组长度组成。
MarkWord放的是线程锁状态以及Gc需要的信息(hashcode)。
Kclass是一个指向方法区实例的指针。
JFK1.6以前用的是阻塞的重量级锁,Hotspot作者发现不是每次都有线程竞争,于是引入了偏向锁。
锁升级的过程:
1.假设刚开始A线程获取锁的时候第一次使用CAS操作来获取偏向锁;并且会把线程ID存到对象头的MarkWord和栈帧里面。
2.第二个B线程来了检验对象头MarkWord是不是自己拥有这把偏向锁,也就是查看是否有当前线程ID,如果是直接使用。
如果不是,会去检查MarkWord锁标识位是不是1(1表示是偏向锁),如果不是1,说明前一个线程释放了,则通过CAS操作来获取锁。
如果锁标识位是1,再进行一次CAS加锁尝试把偏向锁指向自己,如果失败(因为A线程正在使用当中并且A程还存活,发生锁竞争)则将锁升级成为轻量级锁;
3.升级成为轻量级锁之后,线程B会使用自旋的方式来获取这个锁,自旋也就是循环尝试,如果B通过自旋的方式也一直获取不到锁,那么锁将会升级成为重量级锁,线程通过阻塞等待获取资源。
4.重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁。
锁消除:锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。
悲观锁synchronized
synchronized锁的重入
锁的重入:同一个线程持有同一个锁对象,可以重复进入同步代码块。
//1.递归
public class SynchronizedTest {
public synchronized void test1() throws InterruptedException {
System.out.println("this is test1");
TimeUnit.SECONDS.sleep(3);
test1();
}
public static void main(String[] args) throws InterruptedException {
SynchronizedTest synchronizedTest = new SynchronizedTest();
synchronizedTest.test1();
}
}
//不同方法
public class SynchronizedTest {
public synchronized void test1() throws InterruptedException {
System.out.println("this is test1");
test2();
TimeUnit.SECONDS.sleep(3);
}
public synchronized void test2() throws InterruptedException {
System.out.println("this is test2");
TimeUnit.SECONDS.sleep(3);
}
public static void main(String[] args) throws InterruptedException {
SynchronizedTest synchronizedTest = new SynchronizedTest();
synchronizedTest.test1();
}
}
synchronized作用在方法上 相当于持有当前对象的锁。因此在调用test1时,锁住了当前对象,但仍然可以进入到test2.
可重入原理:加锁次数计数器
1、JVM负责跟踪对象被加锁的次数;
2、有个monitor计数器,线程第一次给对象加锁的时候,计数变为1.每当这个相同的线程在此对象上再次获得锁时,计数会递增。
3、任务结束离开,则会执行monitorexit,计数递减,直到完全释放
synchronized不可中断
一旦这个锁已经被别的线程获得了,如果当前线程还想获得,只能选择等待或者阻塞,直到别的线程释放这个锁。如果别的线程 永远不释放锁,那么线程只能永远地等下去。
相比之下,Lock类,可以拥有中断的能力。
synchronized原理
用javap反汇编出来,可以看到它里面有一个JVM指令monitorenter和moniterexit来加锁和释放,当出现异常的时候就会走到另一个moniterexit里。
monitorenter:线程每次进入时,计数器+1。如果重入,继续加。计数器大于0表示有线程持有锁,其他线程不可进入。
monitorexit:线程退出时,计数器-1.变为0时候,其他线程可以获取锁。