java线程(一)基础知识点

1. 内存模型

  • 内存划分
  1. JMM规定了内存主要划分为主内存和工作内存两种。主内存和工作内存只是jvm规范划分的两个抽象概念,为了屏蔽不同处理器的内存处理差异制定的规范。跟JVM内存模型(堆、栈、方法区)是在不同的层次上的描述,如果要对应起来,主内存相当于对应的堆空间、元空间,工作内存对应部分栈空间,从硬件角度,主内存对应的是硬件的物理内存,工作内存对应的寄存器和cpu高速缓存
  2. jvm规范约定:工作内存为线程独有内存,主内存为线程共享内存,多线程操作共享变量,每个线程会存在一份遍历变量副本在工作内存中,线程不会直接操作主内存。因此多线程操作共享变量,各线程工作内存中都存在变量副本,如果没有保证可见性,各线程的修改导致各自工作内存的变量副本数据不一致,造成数据错误。
  3. 可见性即被某个线程修改后,是否立即可被其他线程获取。(而不是只存储在自己的工作内存中)
  4. jvm规范定义的内存操作指令: lock锁住主内存变量,unlock解锁主内存变量,read读取主内存变量传到工作内存变量,load将读取到的变量赋值给工作内存的变量副本,use将工作内存变量值传递给执行引擎使用,assign执行引擎赋值给工作内存中的变量副本,store将工作内存的变量传给主内存,write写到主内存。
  • cpu高速缓存
  1. cpu缓存。由于cpu的运算速度远高于访问内存的速度,为了提高访问数据的速度,cpu中增加了cpu缓存缓存最小单位是缓存行,一个缓存行64字节。在多核处理器的cpu中,每个核中都有独立的cpu一级缓存和二级缓存,还有多核处理器共享的cpu三级缓存。在线程访问数据的时候,通过逻辑地址在cpu缓存中查找对应的数据(从内存中复制的变量副本),当缓存没有的时候,需要通过MMU(内存管理单元)通过页表将逻辑地址转换成物理地址,根据物理地址去内存中获取数据。
  2. TLB快表。单独的cpu寄存器。由于MMU位于内存区域,为了提高逻辑地址转换物理地址的速度,又提供了TLB快表(缓存页表,提高逻辑地址转换成物理地址的速度)。
  3. MESI(cpu缓存一致性,是一种抽象的协议,在不同处理器的实现不一致)。为了保证cpu各内核缓存一致性,硬件这通过MESI保证。MESI代表缓存行的四种状态:Modified修改状态、Exclusive独享状态、Share共享状态、Invalid失效状态。(1)Modified修改状态:本地已经修改了该缓存行,但是还没更新到内存,且该数据为当前cpu独有。此状态会监听其他cpu核读取该数据的动作,缓存一致性会确保被其他核读取之前把缓存刷新到内存中。(2)Exclusive独享状态:缓存和内存的数据一致,且为当前cpu核有该数据的缓存。此状态同样会监听其他cpu核读取该数据的动作,如果触发会将状态改为share状态(3)share共享状态。缓存核内存中一致,且存在多个cpu缓存存放该数据。此状态会监听修改该数据的动作,缓存行修改成无效状态。(4)Invalid无效状态:存在该缓存行且该缓存已经无效。MESI保证了缓存的一致性,但是也造成了一定的性能损耗,比如修改共享状态的缓存行需要通知其他缓存将该缓存失效,需要阻塞等待确认
  4. Store buffers 。为了优化等待其他内核设置缓存行为无效,导致cpu阻塞等待,引进了Store buffers缓存。在将数据写到内存之前,先写到内核的Store buffers中,此时cpu可以先去处理其他事情,等待其他处理器将缓存设为失效并确认后,再写到内存中。基于Store Forwarding,如果Store buffers中存在该数据会直接从buffer中读取。但是这种机制也会指令重排序和修改变量的可见性。假设线程A循环判断flag是否等于success,线程B先设置age=10然后设置flag=success,线程B设置age=10会先写到store buffer,然后继续执行flag=success,假设flag=success先设置完成更新了缓存和内存,这时线程A判断到flag=success,但是age却不一定等于10,因为age=10可能还在store buffer中等待其他处理器确认。这时可以通过内存屏障解决,在age=10后面加上写屏障,确保age=10更新到缓存和内存后在继续执行。
  5.  Invalidate Queue。由于Store buffers的内存空间有限,如果很多缓存行修改进入了Store buffers,都在等待其他处理器将缓存行设置为失效状态,如果容量超了cpu还是需要阻塞等待Store buffers清空后再处理。cpu设计者为了降低这种确认时延,引入了invalidate queue。即处理器在接收到缓存行失效的请求时,先将请求信息存放到invalidate queue中,然后回复确认。但是这也导致可见性问题。如果cpu内核A修改了变量age的值,cpu内核B把失效请求放到invalidate queue中,此时内核B去读取age变量还是老的值。这时也可以通过内存屏障解决,在读取age变量之前加上读屏障,确保队列中的失效请求都处理后再读取age变量。
  6. MESI协议,可以保证缓存的一致性,但是无法保证实时性。即最终一致性,可以通过内存屏障达到实时一致性。
  7. 嗅探技术。处理器会通过嗅探总线上检查自己的缓存是否已经过期(即监听失效请求),当处理器发现缓存的数据行已经被修改,则会将缓存行设为失效状态,当有查询或修改该数据的操作,会重新从内存中把数据读取到cpu缓存(保证cpu缓存和内存中的数据一致)。
  8. 伪共享。伪共享是指一个缓存行存储多个变量,各核多线程修改不同变量值,而这些变量共享一个缓存行,导致的竞争关系,影响性能。之所以会有伪共享,是因为cpu缓存数据的基本单位是一个缓存行,缓存行的大小在不同处理器的大小不一样,大部分是64个字节。cpu内核读取数据的时候会往一个缓存行写数据,直到被写满才往其他缓存行写数据。因此不同变量可能会存在同个缓存行。避免伪共享,可以通过填充对象大小的写法,既在一个java类中声明几个long填充变量,确保这个对象大小超过64字节,避免和其他变量共享缓存行。竞争关系: 当cpu内核修改某个share状态的共享变量时,会锁住该变量在cpu三级缓存所在缓存行,并发出失效请求让其他cpu内核中的一级缓存将该缓存行置为失效状态,等待确认后再更新缓存释放锁,如果其他线程此时要改的变量也在这个缓存行,就会被阻塞等待另一个变量的修改处理完才能开始处理
  • Memory barrier内存屏障(cpu指令)。
  1. 内存屏障有两个作用:1. 阻止内存屏障前后的指令发生重排序。2.强制将缓存失效读内存中的数据或将写入缓存的数据更新到内存。内存屏障分为两种Load Barrier(读屏障)和Store Barrier(写屏障)。在指令前加上读屏障可以让cpu缓存中的数据失效,强制从内存中加载数据。在指令后加入写屏障可以让写入缓存的数据更新到内存,让其他线程可见。
  2. java中的内存屏障分为4种:LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。StoreLoad屏障(开销比较大,但是最通用,所有处理器都支持):对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
  3. volatile的内存屏障。在每个volatile变量写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;在每个volatile变量读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;采取保守策略,确保所有处理器都能得到正确的实现。
  • 指令重排序
  1. 指令重排序分为编译器重排序和处理器重排序,指令重排序只能确保单线程的执行结果不受影响。
  2. 多线程环境下,指令重排序可能会导致一些情况,导致执行的结果与预想的不一致。假设线程A循环等待线程B初始化完设置flag=success,如果线程B设置这个状态被重排序提前设置,就会导致循环等待的线程被误导。
  • as-if-serial
  1. as-if-serial语义规定,无论怎么重排序都要保证单线程执行的结果不会改变,因此存在数据依赖的语句不会被重排序,如A=2; B=A。
  • happens-before
  1. 偏序关系。用于制定一些通用的可见性规则,比如A happens-before B,表示A操作的数据对于B都是可见的。且具备传递性,A hb B,B hb C,那么A hb C。以下列举几个JMM中happens-before场景。
  2. An unlock on a monitor happens-before every subsequent lock on that monitor。对于两个线程抢占一个锁,假设线程A获得锁,线程A修改的数据在解锁后对线程B都是可见的。
  3. A write to a volatile field happens-before every subsequent read of that volatile.对于一个volatile修饰的变量的写操作,对于之后的任一线程的读都是可见的。
  4. All actions in a thread happen-before any other thread successfully returns from a join() on that thread。一个线程B调用线程A的join方法,A的所有修改对于B都是可见的。
  5. Each action in a thread happens-before every subsequent action in that thread。同一个线程内,所有的写操作对于后面的读写操作都是可见的。
  • volatile
  1. volatile保证修饰的变量的可见性和防止重排序,就是通过内存屏障实现的,在写入变量前后加入写屏障(确保修改的数据是最新的,写入数据将数据从CPU缓存更新到内存,强制等待Store buffers队列中的请求处理完,让其他处理器的缓存失效),在读取变量前加入读屏障(强制Invalidate Queue队列的请求确认完,清除了cpu核的本地缓存,从内存中读取最新的数据)。
  2. volatile经典使用例子:饱汉式单例模式,防止指令重排序,双重检查对象是否创建,如果变量不声明volatile,可能会发生指令重排序,导致返回未初始化的对象从而空指针,new对象非原子操作,可以分为分配 A. 对象内存空间、B. 初始化对象、C. 将引用指向分配的内存地址。由于B和C并没有相互依赖,可能会被重排序,C比B先处理,导致其他线程判断不是空就拿该还没有初始化对象的引用去操作,所以要使用volatile防止指令重排序(syschronied不能防止内部重排序)。volatile保证可见性,concurrentHashMap由于读不加锁,写加锁,通过使用volatile可以保证并行操作时写优先于读,且读可以读到最新被其他线程修改的值。结合cas实现无锁修改共享数据。
  3. 部分使用场景总结: 一个线程自旋等待另一个线程设置共享变量的值,需要设置volatile,保证可见性。懒汉式单例。Unsafe cas + volatile + 自旋实现无锁修改变量值。
  4. volatile还可以修饰long类型、double类型确保读写是原子性的,如果没有volatile修饰,这两个类型的读写不是原子性的。long和double的大小都是8个字节,也就是64位。早期的32位处理器不能64位数的运算,jvm内存模型针对64位数读写会拆分成两个32位数运算,比如赋值,就可能导致前半段和后半段被两个线程并发赋值,导致数据出错。修饰volatile,jvm就不会去拆分64位数,直接按照64位数读写。
  • final
  1. final的作用 : 1. 修饰类不可继承;2. 修饰方法不可被重写;3. 修饰变量,如果是基础类型,则变量的值不可修改。修饰引用则引用指向的对象不可修改。修饰成员变量,必须直接赋值,或在构造器中赋值,否则编译报错。4. 禁止被修饰的成员变量初始化被重排序到引用赋值后面。下面细说。
  2. final修饰成员变量,对该成员变量的读写,编译器和处理器要遵循两个重排序规则。(1)在类A的构造函数内对final成员变量C进行赋值,与创建A对象并赋值给一个引用B,这两个的操作不能重排序。如果成员变量没有被final修饰,有可能被重排序到构造函数外。如果出现重排序,可能会导致引用B已经指向A对象,但是成员变量C还是空的情况。(如果一个线程创建对象,另一个线程循环判断引用是否为空,不为空则把成员变量C的值输出,可能会输出还未被赋值的变量C)具体实现是jmm会禁止编译器对final变量的写重排序到构造函数外,另外在final的写之后会插入StoreStore内存屏障,静止处理器将该写操作重排序到构造函数外。通过禁止重排序可以确保对象引用可见前,对象的final变量已经被初始化过了。如果成员变量是一个对象引用,那么构造函数中对该引用指向的对象的参数进行赋值,也不可被重排序到构造函数外面(2)初次读取引用B与随后初次读取成员变量C两个操作不能被重排序。即将引用B赋值给引用D,获取引用D指向的对象的成员变量C,这两个操作不可重排序。由于存在间接依赖关系,所以编译器重排序不会重排,但是少数处理器允许存在间接关系的操作重排序。所以通过这个约束避免。编译器会在读取final变量的操作前面插入一个loadload屏障,避免发生重排序到读取引用前。
  3. 简单总结 。创建对象前面讲过分三步,分配内存地址,初始化对象,将引用指向分配的内存地址。初始化即包括构造函数中的初始化操作,final变量在构造函数的初始化由于禁止指令重排,因此不会出现引用指向对象而final变量还没初始化的情况。
  4. 使用final要确保构造函数内不会发生this逃逸。如果在构造函数中,将this赋值给对象引用,可能会导致引用提前获取到对象,从而导致构造函数内对final变量还没有初始化,就可以通过对象引用操作对象。
  5. 可参考基本数据类型封装类中的值,都是通过final修饰的,确保构造函数的赋值是不可被重排序的。
  • transient   用于修饰字段,阻止字段被序列化。

2. java线程中的六种状态

  • 初始状态 NEW:  创建了线程,还没调用start。
  • 运行状态 RUNNABLE :  运行状态包含就绪状态(ready)和运行中状态(running)。调用start方法后触发。就绪状态是随时可以被调度得到cpu的使用权,运行中状态及就绪状态的线程在获取cpu时间线后变成运行中。系统调度或者调用yield方法失去cpu片执行权会再变回就绪状态。
  • 阻塞状态 BLOCKED :表示线程阻塞在等锁。如synchronized,已经有一个线程拿到锁在执行,另一个线程在等锁时就被阻塞等待获取锁。
  • 等待状态 WAITING: 线程处理等待中,等待唤醒或等待其他线程处理完。比如调用了wait方法,join方法。如果是wait可以通过notify唤醒回到运行状态。
  • 超时等待状态 TIMED_WAITING:类似等待状态,但是带上超时时间,超过超时时间就返回运行状态。通过调用wait(long)方法,join(long)方法,sleep(long)方法可以进入超时等待状态。同样wait方法进入等待的可以通过notify唤醒。
  • 终止状态 TERMINATED。线程执行完成后进入终止状态。

3. Thread常用方法和参数

  • Thread.yield().  调用此方法,会让出当前线程的cpu使用权,从运行中状态切换到就绪状态,当前线程和其他线程共同再参与cpu使用权的竞争,不释放锁
  • thread.join()。线程调用此方法会进入等待状态,等待调用的线程处理后再继续执行,比如t1调用t2.join,t1会等待t2执行完再继续执行。此方法可以带上一个long参数,表示最多等待多久回到运行状态。不释放锁
  • Thread.sleep(long)。线程调用此方法进入超时等待状态,超时等待结束后,回到运行状态不释放锁
  • obj.wait()。 只能在获取到锁之后调用,线程获取到对象锁后,通过调用对象锁的wait方法,当前线程会释放锁并进入等待状态,进入等待队列。等待唤醒。此方法也可以接收个long参数,调用后进入超时等待状态。
  • obj.notify()。只能在获取到锁之后调用,通过调用此方法,可以随机唤醒一个该通过该锁进入等待的线程,从等待池(通过调用该锁的wait方法会进入等待池)中随机唤醒,唤醒到锁池(等待锁的队列),被唤醒的线程从等待状态切换到阻塞状态
  • obj.notifyAll()。与notify类似,但是会将通过该对象锁进入等待队列中的线程都唤醒到同步队列中,被唤醒的线程从等待状态切换到阻塞状态
  • interrupt()。中断线程,线程1的interrupt方法被调用,线程1的中断状态会被设置成ture。线程1某些情况下会被中断,可中断的情况一般都是使用的方法中会去不断判断中断状态,发现状态是已中断则终止,达到中断的目的。以下列举。
  1. 如果线程调用了sleep、wait、join等方法后,处于等待状态可以被中断,中断后会报错中断异常,如果try catch处理了异常,并且想要线程中断,要在catch之后调用Thread.currentThread().interrupt() 这个方法进行设置回中断状态。因为抛出中断异常之后中断状态会被设置成false。
  2. 线程线程处于阻塞状态,如果是通过synchronized关键字获取锁,在等锁的是不可以中断的。如果是通过reentrantLock的lock方法也是不可中断的。如果是通过reentrantLock的tryLock 和 lockInterruptibly方法处于阻塞状态,是可以中断的,被中断会抛出中断异常错误。
  3. 线程处于等待io处理等待的状态,如果是通过inputstream等传统api是不可被中断的,如果是通过使用socketChannel、fileChanneldeng等实现了InterruptibleChannel接口的类被io阻塞,则可以被中断,会抛出ClosedByInterruptException错误。如果是传统api可以通过强行调用io流的close关闭流从而达到中断的目的。
  4. 如果线程处理运行状态,这种情况是不能直接被中断的,需要在线程执行方法中根据Thread.currentThread().isInterrupted()这个方法判断是否处于中断状态(interrupted方法也可以判断,但是会同时修改中断状态),如果处于中断状态再退出线程。或者通过线程共享变量实现,根据判断变量值决定中断。
  • stop().已经被标记过时。可以强制中断线程执行。
  • priority 优先级:范围是1-10,没有指定默认优先级与父线程优先级一致。

4. 线程和进程

区别

  1. 进程是资源分配和调度的一个独立单元,线程是cpu调度的基本单位。
  2. 进程包含多个线程,线程共享进程的资源,进程终止后,进程中的线程都会终止。
  3. 进程通过pcb控制块记录进程信息,线程通过tcb控制块记录线程信息
  • 进程状态。初始态、执行状态、等待(阻塞)状态、就绪状态、终止状态
  • 引进线程是因为进程切换开销比较大

进程通信方式IPC

  • 单工  单向通讯,a进程发信号,b进程只能接收信号
  • 半双工   两个进程都能发信号,不能同时发,类似对讲机
  • 全双工   两个都能发信号,且可以同时发,类似打电话

进程通信机制

  • 匿名管道通信(PIPE)。进程间通过管道通信,只能用在父子进程间(通过fork创建),同时只能单向流动,属于半双工通信。管道通信实际是通过往内核缓冲区(内核操作文件的缓冲区)的虚拟文件(不是真实的文件,不存在文件系统,只存在内核缓存区)中读写数据。父进程通过pipe函数创建管道(在内核缓冲区申请一个4k大小的缓冲区)后,子进程通过fork后也会拥有该管道的fd,读写相互阻塞,基于字节流通信。文件表项记录进程打开文件后的状态信息,父子进程共享文件表项,因此可以通过管道pipe函数通信。
  • 命名管道通信(FIFO)。同样是半双工通信,命名管道非父子进程间也可以通信。命名管道是基于文件系统中一个真实的文件进行读写,具有先进先出特性。通过mknod 或 mkfifo函数创建文件,指定路径和文件名,进程间通过该路径的文件进行通信。
  • 信号量(semaphore)。用于解决多进程访问共享资源的安全问题,进程操作信号量是原子操作,通过设置信号量的值和判断信号量的值决定是否有操作权限。假设通过semget()函数申请了一个信号量集,进程1访问一个系统资源前通过semop()函数设置信号量为-1(设置-1成为p操作),表示资源被占用,进程2再调用semop进行p操作会失败,进程1执行完将信号量设置成1(v操作)后,进程2才可以访问资源。
  • 共享内存。进程通过shmget()函数创建共享内存(获取到一个内存标识),通过shmat函数将共享内存挂载到进程的地址空间,即进程的页表中保存一个逻辑地址指向该内存区域的物理地址。另一个进程通过前面得到的内存标识,也操作同一块内存区域,达到通信的效果。默认没有同步互斥,两个进程可同时读写,如果要控制同步可以通过信号量控制。不通过信号量控制属于全双工通信
  • 其他的还有消息队列、套接字、信号等机制。

进程缓冲区和内核缓冲区

  • 系统分为内核态和用户态,用户态只能访问部分内存区域,内核态可以访问所有内存区域和外围设备。内核态运行在0级特权集上,用户态运行在3级特权集上。
  • 用户态想要访问系统资源(如文件)时,需要通过系统调用(系统函数)切换到内核态才能操作。
  • 内核态访问资源需要io操作,为了提高性能,通过在内存中建立内核缓冲区存储系统资源,读写资源都会通过缓冲区,避免每次都进行io操作。
  • 为避免用户态每次操作系统资源都要切换内核态,进程在内存中建立一个进程缓冲区存储系统资源,读写资源都通过进程缓冲区操作,缓冲区不存在才进行状态切换,避免每次都要切换状态。

  产生死锁的条件

  •  互斥条件,请求与保持,循环等待,不可剥夺
 
5. 创建线程
1 继承Thread类,实现run方法
2 实现Runnable接口,实现run方法,作为thread的构造参数
3 实现Callable接口.实现call方法,可以抛出异常(get方法会抛出错误)。配合线程池ThreadPoolExecutor的submit可以拿到future对象,通过get方法获取到返回值.(submit方法也可以接受实现Runnable,原理是通过RunnableAdapter适配器将runnable转成callable实现类)
4 通过创建FutureTask传入Task任务,作为thread的构造参数。FutureTask实际上也是实现了Runnable接口,因此可以传入Thread,不同的是FutureTask可以通过get方法获取到线程执行结果。
5 线程池。本质也是new thread,submit可以拿到future,execute则没有返回结果
 
本质上只有继承Thread类、实现Runnable接口。其他方式都是基于此基础的扩展。
FutureTask作为Thread的构造参数,本质是runnable。
比如实现Callable通过线程池submit,实际上也是会封装成FutureTask即实现runnable去调用execute,在run方法中加了设置返回结果的逻辑。
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值