JAVA学习 并发编程

文章详细阐述了CPU的硬件结构,包括指令寄存器、数据寄存器和程序计数器等,以及Java代码如何通过JVM转换成机器指令执行。讨论了线程的生命周期、进程与线程的区别,强调了线程安全和同步的重要性,介绍了volatile关键字的作用以及同步机制如synchronized和锁的升级过程。此外,还提到了并发编程的特性,如可见性、有序性和原子性,并探讨了并发控制的各种策略,如CAS和线程池的工作原理。
摘要由CSDN通过智能技术生成

CPU的硬件结构:
指令寄存器:也叫小内存、储存指令。
数据寄存器:储存程序运行过程中产生的临时数据。
PC程序计数器:确定下一条指令执行的地址。offset 偏移量
算术逻辑运算ALU:例如:加法器,做加减乘除、与或非位移运算。

程序怎么运行的:
Java代码通过系统线从磁盘加载到内存以后,JavaC把代码编译成字节码文件,JVM将字节码文件再转换成机器可以读懂的指令,栈去调用方法,再把指令发送给CPU的指令寄存器,CPU一行行执行指令,程序跑起。

进程和线程
最开始是一个程序是一个进程,一个程序中的任务,例如下载或播放任务、也是一个独立的进程,分配不同的内存空间去独立执行,进程中存在相互通信,例如在浏览器下载软件、浏览器是主进程,而下载任务是其他进程,下载完成以后通知浏览器主进程,这时、病毒也是一个进程、和病毒相互通信会被病毒操作运行的进程、修改内存中的数据。
改良安全性以后,提出线程的概念,多个(有限个)线程在内存中、分配的共享的进程空间中运行,共享CPU的计算资源,这样线程既可以完成指令,也不可以访问其他进程的内存空间,隔离病毒的影响范围,提高安全性。
进程强调内存资源的分配,线程强调计算资源的分配。

CPU逻辑内核中,由系统内核选择切换到不同的用户态内核来模拟多个软件同时在运行,运行时不同的线程切换、保存和恢复数据的过程叫上下文切换。系统内核与用户态内核的切换与上下文数据的保存与恢复比较消耗资源。

创建无返回值的线程:
继承thread类、重写run()、调用start()。
实现Runnable接口:
new Thread(Runnable target).start():调用thread类的start(),start核心方法是native类型的start0方法,start0()会调用thread里的run()(源码无法看见),run()判断传入的target不为空,实现Runnable接口的子类作为target传入就实现了重写的子类的run()。
target可以用匿名内部类。

创建有返回值的线程:
实现Callable接口、重写有返回值的call(),在main函数中创建任务futureTask,futureTask作为Runnnable接口的实现类创建线程,调用futureTask的get()获取返回值来阻塞主线程,主线程获取结果以后继续运行。
因为futureTask implements RunnableFuture,RunnaleFuture extends Runnable,所以futureTask是Runnable接口的的实现类、作为thread的target,创建一个线程。

守护线程:守护创建它的线程。Threadt2. set Deamon(true);
不可以将线程1设为主线程的守护线程,线程2设为线程1的守护线程,因为用户线程结束,守护线程写也结束。主线程的任务只有创建线程等待系统调用,创建线程结束,那么主线程结束,线程1结束,线程2也结束,一开始什么也没打印就会结束运行。

线程的生命周期:
NEW:被创建未start();
RUNNABLE:线程在JVM中已被执行等待系统调用;
BLOCKED:线程阻塞,被挂起被动等待;
WAITTING:主动等待;
TIMED-WAITTING:有时限的主动等待;
TERMINATE:线程结束。
无论是被阻塞或主动等待都需要返回到RUNNABLE状态,等待系统调用。

join():阻塞创建它的线程。

CPU中的缓存架构
CPU的速度和内存的速度比的是读取的速度,因为寄存器在CPU内,而从主存中加载数据还需要走总线,所以CPU中的寄存器的读取速度高于从内存中读取的速度。CPU中有一级缓存和二级缓存在核内,三极缓存由CPU中的核共享,主存由CPU共享。

CPU每次读取的不只是数据的本身,而是根据局部性原理,在主存中把目标数据附近的64个字节的数据、全部读取到CPU的高速缓存中,局部性原理的优点是更好的命中缓存,但是每次读取的速度会变慢以及读取时数据刷到一级缓存或二级缓存中,对其他核其他线程不可见,当修改数据来不及刷回主存时,其他核其他线程就把又把修改前的数据加载到缓存中了。这也是局部性原理导致的数据读取不一致的问题、多线程争夺数据资源的安全性问题。

指令重排:为了增加内部运算单元的充分利用,转换以后的代码会乱序执行,为了保证并行的结果与串行的相同,有依赖的项不可以被重排,但是无依赖的项会被乱序执行,会被重排,有时候很难保证运行结果的准确性。

JVM屏蔽不同操作系统的硬件差异。
JMM是Java的内存模型,每个线程都有自己的工作内存。

volatile内存屏障避免线程读写操作时指令重排,让线程从内存中获取变量。

指令重排导致的对象半初始化:new一个对象要先从堆内存中分配空间,然后从方法区调用invokespecial初始化值,再ldc将符号引用压到栈顶并指向堆内存中的对象。

对象半初始化:必须在内存中划分空间以后,才可以初始化和把符号引用压到栈顶并指向堆内存中的对象,初始化调用构造器中的代码执行可能会延迟,所以在代码乱序执行的情况下,实际的执行情况是会先在堆内存中分配好空间以后,就把符号引用压栈顶并指向堆中内存地址对应的空间,再初始化值。如果在未初始化之前调用对象的属性、输出的是默认值的状态,叫对象半初始化。

案例:让flag=false,让线程利用flag死循环,接着打印一句话,启动线程以后,在主存中再修改flag的值,理论上这能让线程停止,接着打印一句话,但在实际的运行中没有出现打印效果说明了线程一直在空转,没有机会到主存中获取已经刷新的数据,这就说明了线程间存在不可见的问题,主线程与主线程创建的线程是两条线程且缓存互不可见。

volatile可以防止单线程的情况下指令重排:每条有读写操作的指令会加上内存屏障lock。变量使用volatile修饰以后,对变量有读写操作会立即刷回主存,而且其他缓存区内的该变量会设置成不可用。解决了线程间不可见的问题。

as-if-serial:规范,所有有数据依赖的项不会被重排,保证单线程的情况下乱序执行的结果与顺序执行的结果相同。volatile
happen-before:多线程同步操作没有先后顺序,但又互相依赖的数据的乱序执行结果,应该与顺序执行的结果相同。

synchronized:重量级锁
在方法上加锁synchronized、保证线程对主存数据串行操作。

线程安全的实现方法
数据不可变:final修饰的基础数据类型、不可变的字符串,final修饰的包装类型不可以改变引用地址、但对象的属性的值可以修改。

用户态和内核态的切换、上下文的切换、数据的保存和恢复、互斥同步、阻塞同步、悲观并发策略:synchronized关键字实现的,多线程争抢资源时一个个挂起排队,等待系统唤醒调用。

非阻塞同步、乐观并发策略:通过for循环自旋的方式,不停地去尝试征用共享的资源,直到没有竞争的线程或征用资源成功为止。

线程竞争激烈,用阻塞同步挂起排队,避免过多线程自旋带来性能开销。少数线程争抢资源,可以使用自旋重试别的线程已释放的共享资源,来提高运行效率。
空转也占用资源。所以,更高并发的情况下更适合悲观策略。

无同步方案:变量每个线程单独使用,改变了不需要刷回主存。由threadlocal实现、创建threadLocal,在线程中调用threadLocal的set()、get()。

synchronize的使用:
synchronized static:所有线程共享的资源的锁。
实例方法的synchronized/synchronized(this){}:堆中每个实例对象都有的、指向的方法区方法的锁,不停地创建实例对象调用该方法、无法串行。但,只要保证this指代唯一一个不变的对象final this就可以实现串行,用static和final修饰实例对象即可。
synchronized(.class){}:调用代码块的唯一实例对象的锁,当前类的实例传入、可以反复完整的执行代码块。
synchronized(new Object):可以传入任意实例对象。static final类型的Object也可以实现串行。

锁:
锁信息和对象的年龄都记录在对象头。
synchronized 字节码层面的锁:monitorenter、monitorexit
无锁
偏向锁:属于一个经常访问的线程的锁。
轻量级锁、自旋锁:线程以循环自旋的方式不断尝试获取锁。
重量级锁:高量线程之间竞争激烈,线程挂起排队等待系统唤醒,访问资源。

锁升级的过程:偏向锁时可以通过自旋抢锁,抢锁成功改变偏向,重新记录线程的指针,抢锁失败,锁升级成轻量级锁,JVM决定自旋抢锁的次数,默认10次,再次抢锁失败,升级为重量级锁,线程挂起阻塞、排队等待操作系统的唤醒和状态、数据的恢复。

死锁deadLock:资源互斥使用且不可抢占,线程互相请求对方保存的资源。

锁重入:线程可多次进入同一把锁。

线程常用的方法:
创建类的对象的方法:
wait()/notify():实例对象的方法。线程调用wait()会释放锁,其他线程拿到相同的锁才可以notify()在wait()中的线程。
wait():永久等待。
wait(time):在时间期限内等待、无线程唤醒自己醒。
notify():随机唤醒一个在wait()中的线程。
notifyAll():把所有线程唤醒。

wait()和sleep()的区别:等待和睡眠期间都释放CPU的计算资源,但是wait()会释放锁,sleep()不会释放锁。

线程的静态方法:
yield():要用这个方法会优先让别的线程执行。用的少。

创建线程才可以调用的方法:
join():阻塞创建它的线程。
sleep():和wait()相似,释放CPU的计算资源但不释放锁。
interrupt():只能打断正在睡眠中的线程,无法打断死循环。
wait()和sleep()是阻塞过程,for循环是运行过程无法被打断。

线程退出:一般线程执行完就灭亡,直接退出。
lockSupport:t1. park()、lockSupport. unpark(t1)、lockSupport. park(block)、lockSupport. get Block()

synchronize:内置锁、隐式锁,解锁不用管,用C语言实现的。
Java的concurrent包实现的锁(lock Interface function)
void lock():通过自旋的方式获取锁。
boolean tryLock():尝试获取锁,无法获取锁就放弃。
void unlock():释放锁,不管tryCatch中有没有将代码块执行完、都要用finally强制释放锁。强制释放锁的原因是怕代码执行过程中报错,一直占有锁导致其他线程无法调用。

ReadWriteLock读写锁:只允许一个线程写,写的时候不可以读;读的时候可以所有线程一起读,且读时不能写。保证安全性的同时提高性能。

并发编程的三大特性:
可见性:JMM、线程和线程之间对共享数据进行读写操作是否可见。
有序性:有内存屏障,按流程有序执行。
原子性:必须完整,有序地执行代码,运行时不可以中断,代码可以包含多条指令,一个线程必须全部完整地执行完、下一个线程才可以接着执行。

一个线程修改了变量就算立马刷回主存、也是需要时间的,如果其他大量线程在还没有把修改好的变量刷回主存时,就把修改前的变量压到栈中,结果就有误。
volatile不具备原子性的案例:count++包含从方法区中获取静态常量,压到每个线程私有的方法栈中,将数据弹出,自增以后,再压回栈顶,再刷回主存。
volatile只能保证每一个线程都按照这样的流程操作和每一个修改数据的线程都从主存中取数据,不能保证每个线程执行完了下一个线程才能接着操作。
在for循环创建线程时故意睡眠来模拟实际中、高并发多线程创建、修改同一个数据的场景,模拟当前一个线程还执行到一半时,后几个线程被创建起来开始执行,由于线程之间是互不干扰的,所以因为时间差各个后面被创建的线程从主存中获取的数据就可能有误,线程被创建的越多、偏差就越大。

cas:保证一个变量修改时的原子性操作。
多个线程的方法栈从主存的方法区中获取静态变量,称为期望值,压到栈顶、然后弹栈自增,在刷回主存前先判断主存中的该变量是否与获取时相等、与期望值相等,相等就把自增以后更新的值刷回主存,不相等就自旋重试。
CPU中保证比较并交换的原子性。
volatile保证有序性,每个线程从内存获取变量。

代码模拟cas:用volatile定义静态常量。compareAndSet()传入expect和update值,期待值是线程从内存中获取过来的值,判断内存的值与获取到线程的值相等则可以修改,并返回布尔类型,多线程场景中调用方法返回false修改失败则自旋重试。主线程打印常量。加synchronized保证一段代码的原子性操作。

unsafe类:CPU元语级别的原子性方法。
compareAndSwapObject(Object var1,Object var2,Object var3,Object var4)
4个参数分别是:对象头、属性的值在对象头的偏移量、内存中的期望值、属性要修改后的值。

cas的ABA问题:
线程一和线程二同时从主存中获取变量,线程二修改了变量的值以后又修改回原来的值再刷回去,线程一在刷回主存前判断期望值与获取的值一致,就把自己修改后的值再刷回主存,其实内存的值已经改变过了,如果修改的值是引用对象属性的值,原来的对象已经改变了而线程一无法察觉,基础数据类型则不敏感。

如何解决:从主存中获取变量时,也一同获取变量的版本号,如果修改了从主存中获取的变量并刷回主存,主存中的该变量的版本号自增。在与期望值比较时,也比较版本号,如果版本号不对,就更新失败。

synchronize为重量级锁,每条指令加锁,线程使用的资源加锁,线程要挂起串行执行,保证了并发编程的原子性、可见性和有序性,但是,为了提升效率,在线程竞争不激烈时,一般使用volatile+cas,刷回主存失败,就自旋重试,如果线程竞争非常激烈就挂起,避免过多的死循环空转,这也是非常庞大的系统性能开销。

aqs:加锁和释放锁
AbstractQueueSynchronize 抽象队列同步器
锁升级的过程:线程获取锁、执行代码块、释放锁,线程未获取锁,锁升级成自旋锁偏向锁、获取成功修改记录的线程指针,还是未获取、升级成自旋轻量级锁,还是未获取,升级为重量级锁线程park()挂起。

aqs源码:
head、tail :队列头/尾。
pre/next:队列是双向的。
thread:队列里放的是线程。
waitStatus:线程的状态:Cancelled(1)、Signal(-1 unparking)、Condition(-2 waiting)、propagate(-3 shared)
status:资源的状态,0没有线程占用,1有一个线程占用,2锁重入。
predecessor:拿到前一个线程节点。
nextWaiter:拿到后一个线程节点。

默认创建非公平锁在这里插入图片描述
lock()、nonfairLock():cas抢锁,成功currentThread setExclusive,失败则调用 tryAcquire() 的nonfairTryAcquire():获取当前线程,获取资源被占用的状态status,status等于0则表示没有线程占用,当前线程可以抢占资源,如果是当前线程持有的锁,则锁重入,重新计算status值+1。如果等于-1说明内存溢出了,要报错。如果有其他线程占用资源,占用资源的线程也不是获取的线程,则有其他线程占用返回false。

addWaiter():当前的线程创建为新的节点,获取链表的尾结点,尾结点不为空,新节点前一个指向尾节点,尾节点下一个指向新节点,新节点通过CompareAndSetTail()成为新的尾节点。如果尾节点为空,则调用enq()初始化等待的线程队列。

enq():创建一个空节点作为头节点和尾节点,在下一次进入循环时,进入有尾节点的逻辑,将新节点添加到线程队列,返回false退出for循环。Node t = tail;传过来的是Node node,t是空节点。

线程加入队列以后仍然没有挂起park(),继续尝试抢锁,acquireQueued(),调用predecessor()获取当前线程的前一个节点,如果前一个节点是头节点,&&短路运算符左边判断是true右边才可以接着运行,tryAcquire()让线程去抢锁,抢锁成功,当前线程节点设置为头节点,原来的头节点的下一个节点指向空helpGC。原因是前一个节点为头节点,说明很快就可以释放锁,可以让线程不停去自旋尝试抢锁。

如果线程没有抢到锁,调用ShouldParkAfterFailQcquired(),获取前一个节点的waitStatus,如果为signal说明前一个节点已经被挂起需要被唤醒,如果前一个节点是cancel状态,wait status>0,则把前一个节点删除,node .prev = pred = pred .prev不断循环,直到删除到前一个节点为signal状态,然后把传入的线程挂起LockSupport. park(this)。

释放锁
release():方法在父类当中,释放锁以后尝试去唤醒下一个线程。tryRelease():把锁状态减成0,其他线程通过cas把0改成1抢锁,如果资源不是被要释放锁的线程占有就要报异常。

尝试去唤醒下一个线程:unparkSuccess(h),把当前要释放的节点状态设置成0。找下一个要释放的节点,如果下一个节点是空,就从队列尾巴到要释放的线程节点为止找状态是signal或waiting的节点,作为下一个要unpark的节点,不为空就把这个节点的线程唤醒unpark()。

公平锁和非公平锁的区别:
加锁lock(),非公平锁上来先抢锁,再锁重入,公平锁只锁重入。获取锁tryAcquired():nonfairSync直接cas抢锁。fairSync:!HasQueuedPredesseors(),当前线程的节点前面没有节点,才cas抢锁。非公平锁比公平锁多了两次抢锁的机会,非公平锁调用lock()时抢锁、尝试获取锁时抢锁,挂起前再抢一次锁,非公平锁效率更高,在挂起之前抢到锁就不必再挂起,挂起的就接着挂起。而公平锁可以保证线程的执行顺序。

原子类是线程安全的。atomicInteger. getIncrement()用的比较多,通过volatile的value和cas修改值保证原子性。

线程池:
newCachedThreadPool:一个空的线程池,来任务就创建线程,使用完以后线程可以回收复用,如果长时间没有复用线程就销毁。如果碰到死循环任务、不停创建线程,系统会出现问题OOM,内存溢出。
newFixedTheadPool:固定线程个数的线程池(可控制的线程最大并发数),如果任务过多线程不够用、任务就以数组或链表的形式挂起排队等待,线程执行完调度复用以后执行数组或链表中的任务。
newSingleThreadExecutor:单线程的线程池、不管提交几个任务,都只有一个线程在工作,保证所有任务有序执行。

线程池的源码:
参数:
corePoolSize:核心线程数量;
maximumPoolSize:最大线程数量;
(corePoolSize、maximumPoolSize依靠工作经验,电脑性能压测、调整)

keepAliveTime:空余或回收的线程的存活的时间;
unit:存活的时间单位;

workQueue:任务队列、用于存放提交但是尚未被执行的任务;
ArrayBlockingQueue:基于数组结构的有界阻塞队列。
LinkedBlockingQueue:基于链表结构的有界阻塞队列。

threadFactory:线程工厂用于创建线程。

handler:拒绝为任务调度线程;
线程提供的4种拒绝任务的策略:
AbortPolicy:直接抛异常的默认策略;
CallerRunsPolicy:调用者所在的线程来执行任务;
DiscardOldestPolicy:抛弃阻塞队列中最靠前的任务,并执行当前任务;
DiscardPolicy:静默丢弃任务。

线程池按以下行为执行任务:
线程池创建时也创建核心线程,当请求的任务超出核心线程的数量,就继续创建线程,直到线程池可容纳的最高线程数量,如果线程还是不够用,就将任务挂起排队等待唤醒,如果被挂起的任务数量超出了有界队列/数组的范围,就拒绝任务。

OOM:CacheThreadPool和无界的任务队列都会导致OOM。

自定义线程池:
可以去原生代码中复制和修改,根据项目需要确定核心线程数量、最大线程数量、空余线程的存活时长、定义超出线程数量的有界任务队列、调用父类(ThreadPoolExecutors)的拒绝任务的策略(default Policy)、实现父接口(ThreadFactory)的自定义线程工厂。
自定义线程池的原因是线程必须起名字,方便排查问题,而不是简单的编号。
如何查看正在运行的线程:启动项目以后,打开JDK的home打开JavaVirtualMachine,查看运行的线程。
代码怎么写:实现threadFactory接口,重写new thread(),使用AtomicInteger作为线程的编号,使用String类型的name prefix作为线程的区分,构造有参的prefix构造器name prefix=prefix+threatNumber. getAndIncrement(),在创建线程池时可以new一个自定义的线程工厂。
或添加两个jar包,用Google和Apache的线程工厂。
newThreadFactoryBuilder(). setNameFormat( retryClientpool). builder()
newBasicThreadFactory. Builder()。

CountDownLatch:自减到0执行主线程的任务。

cyclicBarrier:自增到规定的任务数自动执行另一个任务。

Semaphore:信号量,限流
定义无限容纳线程的线程池,调用semaphore限流创建的线程数。防止oom。

多线程下的单例模式:

//多线程的单例模式
public class Singleton4 {

    //实例
    private volatile static Singleton4 INSTANCE;

    //私有化空参构造器
    private Singleton4(){};

    //公开的方法
    //双重检查锁
    public static Singleton4 getInstance(){
        if (INSTANCE == null){ //如果有实例对象就不让线程排队了直接返回
            synchronized (Singleton4.class){ //只允许一个线程创建
                if (INSTANCE == null){ //让之前没有实例时排队的线程不要再创建了锁被释放以后直接获取资源就可以了
                    INSTANCE = new Singleton4();//创建实例对象仍然有可能有对象半初始化的风险
                    return INSTANCE;
                }
            }
        }
        return INSTANCE;
    }


}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值