知识总结——Java

Java基础

封装、继承、多态

  • 封装:对象属性私有化,暴露外界可以访问的方法。

  • 继承:使用已有的类创建新类,新类可以增加属性和方法,也可以复用父类的。

  • 多态:程序中的引用变量指向的类或调用的方法在运行时才确定具体类型。

接口和抽象类

  • 抽象类:是对类的抽象,是一种模板设计,抽象类可以有非抽象方法,抽象方法不能用private修饰,因为要继承。

  • 接口:是对行为的抽象,是一种行为规范。接口中的方法默认都是public,9之后可以定义私有方法。接口中的变量必须由static或者final修饰,接口中的方法在java9之前不能有实现,9之后可以有默认实现。

final、static

  • final:可以修饰类、方法、变量。修饰类时表示类不能被继承,类中的方法都隐式地指定为final方法;修饰方法时,表示锁定方法,子类不能修改该方法的含义;早期的版本中,final修饰的方法会内嵌调用,效率快,现在已经不转换了。修饰基本类型变量时,变量必须被初始化,且值不能再被修改,修饰引用类型变量时,初始化之后不能再指向其他对象。

  • static

  • 修饰成员变量和成员方法:被static修饰的变量叫做成员变量,可以通过类名直

    接调用,成员变量存放在方法区。

  • 静态代码块:静态代码块只执行一次,执行顺序:静态代码块>非静态代码块>构造方

    法。

  • 静态内部类:静态内部类和非静态内部类的区别,非静态内部类在编译完成之后会隐

    式的保留一个它外围类的引用,但是静态内部类没有。没有这个引用意味着:静态内部类不依赖于外围类的创建;静态内部类不能使用外围类的成员变量和方法。

  • 静态导包。

jvm、jre、jdk

  • jvm:java虚拟机,运行.class文件

  • jdk:java的sdk,包含jre、javac、javadoc,能创建和编译程序

  • jre:java运行时环境,运行已编译的java程序,包括jvm、java类库和基础构件

重载、重写

  • 重写:是子类对父类非私有方法的实现过程重新编写。发生在子类中,方法名、参数列表都必须相同,返回值和抛出的异常范围小于等于父类,访问修饰符大于等于父类

  • 重载:发生在同一个类中,方法名相同,参数个数、参数类型、参数顺序、访问修饰符、返回值都可以不同

error和exception

java中的异常(Throwable)分为error和exception

  • error:是程序无法处理的错误。大多数错误是jvm的问题,如内存溢出、内存泄漏,虚拟机运行错误

  • exception:程序本身可以处理的错误,如空指针异常、算数运算错误、数组下标越界

异常处理

try catch块处理异常。try用于处理异常、catch用来捕获异常,finally不管有没有捕获到异常,都会执行。如果try或者catch中有return语句,finally会在return前执行。

有四种finally块不执行的情况:1、finally块发生了异常;2、 finally前有System.exit(int)退出了程序;3、程序所在的线程死亡;4、关闭cpu

string、stringBuffer、stringBuilder

  • 可变性:String类中使用final关键字来修饰字符数组,String类是不可变的,StringBuffer和StringBuilder是可变的

  • 性能:每次对String对象修改的时候,都会创建一个新的String对象,然后引用指向新对象,而StringBuffer和StringBuilder是对对象本身修改,相同情况下StringBuilder性能比StringBuffer性能提升10%——15%

  • 线程安全性:String中的对象是不可变的,所以可以理解为常量,线程安全。StringBuffer中的方法有同步锁,也是线程安全的,StringBuilder线程不安全。

==与equals

  • ==比较的是两个对象的地址是否相等,基本数据类型比较的是值,引用类型比较的是内存地址

  • equals有两种情况,不重写的时候跟==一样,重写后一般比较的是内容是否相同,String类中的equals方法是被重写过的

equals与hashcode

hashCode方法给对象返回一个int型的值,是对象在内存中的散列值。确定对象在哈希表中的位置,散列表存储的是键值对,他的特点是根据键快速查找到值。java中的集合包含两大类,List和Set,前者有序可重复,后者无序不可重复。如何保证不重复,就用到了hashCode。因为如果只用equals,值越多需要比较的次数越多,效率低。但是使用hashCode会先计算出一个物理位置,如果这个位置没有值,则直接存储,有值,再调用equals比较是否相等,相等则不存储,不相等再继续散列。这样就可以大大提升效率。

总结:两个对象相等,则hashCode相同;相同hashCode的对象不一定相等。 

成员变量与局部变量

  • 语法上:成员变量属于类,局部变量属于类中某方法。成员变量可以被访问修饰符和static修饰,但局部变量不能。局部变量可以被final修饰

  • 存储方式上:成员变量被static修饰属于类、不被static修饰属于实例,局部变量存储在栈内存,对象存储在堆内存。

  • 生存时间上:成员变量是对象的一部分,生命周期跟随对象。局部变量的生命周期随便方法的调用而消失。

  • 初始化:不被final修饰的成员变量在创建时如果没有赋值,则会自动按类型默认赋值。局部变量不会自动赋值。

构造方法

对象有默认的构造方法,也可以自定义构造方法,构造方法的作用是初始化对象。构造方法的名称与类名相同,没有返回值。生成类的对象时自动执行,不需要调用。

深拷贝、浅拷贝

浅拷贝:增加一个指针,指向已存在的内存地址;深拷贝:增加一个指针,并申请一块新的内存,新指针指向新的内存地址

IO流

InputStream/Reader:输入流,字节/字符

OutputStream/Writer:输出流,字节/字符

BIO、NIO、AIO

  • BIO:同步阻塞IO模式,一个连接一个线程,客户端有连接请求服务端就创建一个线程去处理,适用于并发小的场景,可以通过线程池去改善。适用于连接数目比较小且固定的架构

  • NIO:同步非阻塞IO模式,一个请求一个线程,客户端发送的请求会注册到多路复用器上,多路复用器轮询到有IO请求时才启动一个线程进行处理,用户进程也需要时不时地询问IO操作是否就绪。适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器

  • AIO:异步非阻塞IO模式,基于事件和回调机制完成,用户进程只需要发起请求并返回,等操作完成后应用程序会得到通知。适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作

 

Java高级

List、Set、Map

  • List:继承至Collection接口,里面的元素有序可重复。常用的有ArrayList、LinkedList、Vector。

  • ArrayList:底层是数组,查询快,增删慢,线程不安全,效率高。特点:可变长,初始长度为10,内部使用Object数组来存储,容量不够可以扩容,扩容后的容量是之前的1.5倍,然后把原来的数据拷贝到新建的数组中。

  • LinkedList:底层是链表,查询慢,增删快,线程不安全,效率高,JDK1.6前用的是双向循环链表,7之后取消循环。

  • Vector:底层是数组,查询快,增删慢,线程安全,效率低,初始长度为10,扩容后是之前的2倍,但是它可以设置容量,ArrayList不能设置。

ArrayList比LinkedList查询快,因为LinkedList访问元素需要移动指针;LinkedList比ArrayList增删快,因为ArrayList需要移动数据;两者在末尾增加一个元素的开销是固定的。对于ArrayList来说,在末尾增加一个元素,是在内部数组增加一项;对于LinkedList来说,就是分配一个内部Entry对象。在中间新增或删除一个元素,对于ArrayList来说,要移动元素并且重新分配内存;对于LinkedList来说,只需要断开指针指向的元素并且重新指向新的元素即可,不需要移动元素。ArrayList的空间浪费主要体现在list结尾会预留一定的容量空间,而LinkedList的空间浪费主要体现在他的每一个entry对象都会浪费一定的容量空间。

怎么把线程不安全的集合变为安全的?答:使用Collections.synchronizedList(list)方法。

  • Set:继承至Collection接口,里面的元素无序不可重复。常用的有HashSet、TreeSet、LinkedHashSet。

  • HashSet:底层是哈希表,元素无序不重复。根据hashCode()和equals()保证元素唯一性。

  • TreeSet:底层是红黑树,红黑树后面统一描述。

  • LinkedHashSet:底层是链表和哈希表,元素有序不重复。链表保证有序,哈希表保证不重复。

  • Map:独立接口,常用的有HashMap、Hashtable、TreeMap、LinkedHashMap。

  • HashMap:底层数据结构1.8之前是数组+链表,1.8之后是数组+链表/红黑 树,当链表长度大于8时链表转换为红黑树,小于6时恢复为链表,以减少检索 时间。HashMap允许null键null值。线程不安全。

扩容:初始容量为16,加载因子为0.75。每次以2的n次方为倍数和hash算法(散列算法,根据关键码计算出存储位置。)扩容,为了尽量实现key的均匀分布,HashMap中采用index = hash(key) & (length - 1)计算出key的存储位置,为什么不采用取余操作计算?因为hash & (length - 1)在保证length为2的幂次倍时,效率比直接%高。

put方法解析:HashMap中存储的是key-value键值对,每个键值对是一个Entry,Entry分散在一个数组中。当put元素时,eg:put("hjx",0);,会先利用hash函数计算key的index,数组长度有限,会遇到index冲突的情况,这时候采用头插法插入链表,如果当前位置存在元素,要判断元素内容是否一致,一致则覆盖,不一致则插入。

      并发会产生的问题:

1.put方法导致数据不一致,线程A和B,线程Aput元素时,计算index值准备插入,这时由于时间片分配的问题,线程B被调度,如果index一样,线程B插入后线程A再次被调度,会覆盖B插入的值。

2.resize导致死循环:HashMap自动扩容时,当两个线程同时检测到元素个数超过,调用resize方法,同时修改一个链表结构会产生一个循环链表,再调用get方法,获取某一元素时就会产生死循环。

  • Hashtable:线程安全,因为全局加了synchronized,效率差。初始容量为11,每次扩容后都是原来的2n+1。

  • TreeMap:底层红黑树,红黑树统一描述。

  • LinkedHashMap:继承自HashMap,底层是数组+链表/红黑树,其中链表是双向链表。

  • ConcurrentHashMap:底层数据结构在1.7之前采用分段数组+链表,1.8之后跟HashMap一样。1.7之前采用分段锁保证线程安全,效率比Hashtable高,因为它不是全局加同步,是把数组划分成一段段,给小段加可重入锁,多线程访问不同段里的数据,就不会出现锁竞争,提高并发访问效率。在1.8之后改成了HashMap一样的数据结构,采用synchronized和CAS来保证线程安全。

线程、进程

  • 进程:进程是系统运行程序的基本单位,是动态的,系统运行一个程序就是进程创建、运行到消亡的过程。eg:QQ、微信,jvm进程。一个进程拥有一套变量。

  • 线程:线程是比进程更小的执行单位,一个进程包含多个线程。是系统最小的调度单位。eg:QQ中的一个聊天窗口,jvm中的main(main也成为主进程),多个线程间共享进程的数据、堆和方法区资源,但是每个线程拥有自己的程序计数器、虚拟机栈和本地方法栈。所以系统产生一个线程要比进程负担小,线程也被称为轻量级进程。这里的线程是用户线程,当我们创建线程后不需要自己实现操作系统调度算法和对cpu资源的抢占。操作系统会从用户态转换成内核态,完成线程的切换,这一操作比较耗资源。

创建线程

  • 继承Thread类,实现run方法,调用start方法开启线程,不推荐使用。

  • 实现runable接口,实现run方法。

  • 创建线程池,从池中获取线程。

线程的生命周期

  • 1.new:初始状态,线程被构建,但还没启动。(未调用start方法)

  • 2.runbale:可运行状态,就绪和运行统称为可运行状态。

    调用start方法后,线程处于runnable状态,至于运行没有,取决于操作系统给线程提供的时间。现在大部分的桌面和服务器操作系统都采用抢占式调度系统,当系统给一个可运行的线程分配时间片后,线程开始运行,时间片用完后,操作系统剥夺该线程的运行权利,操作系统会考虑线程的优先级。

    具有多个处理器的机器上,每一个处理器处理一个线程,多个线程并行运行,但是线程的数目多于处理器数时,调度器仍然采用时间片机制。手机可能使用协作式调度,线程只有在调用yield方法或被阻塞或等待时,线程才失去控制权。

  • 3.blocked:阻塞状态,线程阻塞于锁。

线程处于这种状态时暂时不活动,不运行任何代码且消耗最少的资源。

  • 1.当线程试图获取一个内部的对象锁,而该锁被其他线程持有,该线程进入阻塞状态。

  • 2.当线程等待另一个线程通知调度器一个条件时,它进入等待状态。eg:Object.wait()或Thread.join()方法或Lock或Condition

  • 4.waiting:等待状态,表示线程等待其他线程的通知或中断。

  • 5.time_waiting:超时等待,与waiting不同的是这个状态有超时时间,超过后可自行返回。

  • 6.terminated:终止状态,表示线程已执行完毕。

1.run方法正常退出自然死亡

2.因为一个没有捕获的异常而终止

        sleep和wait:

1.sleep是线程中的方法,wait是Object的方法

2.sleep不会释放锁,但wait会释放锁

3.sleep不依赖于synchroniza器,但是wait依赖于synchronized关键字

4.sleep不需要被唤醒,但是wait在没有超时时间时需要被唤醒

线程池

概述:创建和销毁线程耗资源,采用池化技术,设计线程池,里面包含许多准备运行的空闲线程。可以降低资源消耗,提高响应速度,统一调度和优化线程。阿里巴巴推荐通过ThreadPoolExecutor创建线程池而不使用Executors创建。

线程池的三种类型

  • 固定大小线程池FixedThreadPool:这个方法会返回一个固定线程数量的线程池,空闲线程会一直被保留。当有任务提交时,池中有空闲线程则执行,没有空闲线程,任务则进入任务队列等待。这个队列是无界队列LinkedBlockingQueue,队列容量为Integer.MAX_VALUE,这会导致以下问题:

1.线程池里的线程数量不超过corePoolSize,导致maximumPoolSize和keepAliveTime无用。

2.由于使用无界队列,所以FixedThreadPool永远不会拒绝,即饱和策略失效

public static ExecutorService newFixedThreadPool( int nThreads){ return new ThreadPoolExecutor(nThreads,nThreads,0L,TimeUnit.ILLISECONDS,new LinkedBlockingQueue<Runnable>());}
  • 单一线程池SingleThreadExecutor:初始化的线程池中只有一个线程,如果该线程异常结束,会创建一个新的线程继续执行任务,唯一的线程可以保证提交的任务顺序执行,跟FixedThreadPool一样使用无界队列保存等待执行的任务,饱和策略失效。

  • CachedThreadPool:返回一个可根据实际情况调整线程数量的线程池,线程数量不确定,有空闲的线程可以复用,则优先使用空闲线程,没有空闲线程则创建新的线程执行任务,执行完后会到池中等待被复用。

public static ExecutorService newCachedThreadPool(){ return new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue<Runnable>());}

1.线程池的数量可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作阻塞队列。

2.在没有任务执行且线程的空闲时间超过60s,会自动释放线程资源。所以长时间空闲的CachedThreadPool没有线程资源。

ThreadPoolExecutor类参数解析

  • 1.workQueue:用来保存等待被执行的阻塞队列

  • a.ArrayBlockingQueue:基于数组的有界阻塞队列,按FIFO排序任务。

  • b.LinkedBlockingQueue:基于链表的阻塞队列,按FIFO排序,吞吐比a高。

  • c.SynchronousQueue:不存储任何元素,每个插入操作必须等另一个线程调用移除操作,否则一直阻塞,吞吐量高于b。

  • d.priorityBlockingQueue:具有优先级的无界阻塞队列。

a和b在插入删除节点性能方便更优,但是二者在put、take任务时均需加锁。c使用无锁算法,根据节点的状态来判断执行,不需要锁,核心是Transfer.transfer()方法。

  • 2.handler:饱和策略。当阻塞队列满了,且没有空闲的线程,如果继续提交任务,就需要饱和策略来处理。

  • a.AbortPolicy:直接抛出异常,默认策略。

  • b.CallerRunsPolicy:用调用者所在的线程来执行任务。

  • c.DiscardOldestPolicy:丢弃阻塞队列中最靠前的任务,并执行当前任务。

  • d.DiscardPolicy:直接丢弃任务。

线程池的实现原理:

  • 1.线程池判断当前运行的线程数和核心线程数大小,小于则创建新工作线程执行任务,大于则执行2。

  • 2.判断阻塞队列是否满,不满则加入,满进入3。

  • 3.如果创建新的线程后线程数大于最大线程池,则交给饱和策略来处理。

并发、串行、并行

  • 并发:多个线程同时执行,这里的同时其实是经过了快速的上下文切换,并不是真正意义上的同时。

  • 并行:多处理器同时调度不同的进程执行不同的任务。

  • 串行:所有任务一一按顺序执行。

同步机制

在多线程中,可能有多个线程试图访问一个有限的资源,必须预防这种情况的发生。所以引入了同步机制:在线程使用一个资源时为其加锁,这样其他的线程便不能访问那个资源了,直到解锁后才可以访问。

上下文切换

多线程场景下,一个线程的时间片用完后会保存自己当前的状态,让出时间片给另一个线程执行,以便下次运行时获取到上一次最后的状态。概括为:任务从保存到再加载的过程。

每个对象都有一个锁池和一个等待池,当线程A获得了该对象的内部锁,线程B想访问该对象的synchronized方法,由于锁被占用,线程B会先进入锁池;当线程A调用了wait方法,线程A释放锁,进入等待池,线程B调用notifyAll方法,则该对象等待池中的所有对象都会进入锁池,准备争夺锁。

  • 根据粒度

  • 重量锁:synchronized关键字就是通过对象内部的监视器锁来实现的,而监视器锁又依赖于操作系统中的Mutex Lock来实现的。因为前面提到了操作系统处理线程时需要从用户态转换成内核态,这一操作成本很高,导致synchronized效率低下。这种锁就是重量锁。

  • 轻量锁:重量级锁使用的是系统互斥量,轻量级锁并不是要替代重量级锁,而是在没有多线程竞争的情况下,减少系统互斥量产生的性能消耗。如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

  • 偏向锁:偏向锁的目的是在某个线程获取某锁之后,消除它重入的开销。在无多线程情况下尽量减少不必要的轻量锁执行路径,因为轻量锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadId的时候依赖一次CAS原子指令。偏向锁只有在一个线程执行同步块时提高性能。

  • 分段锁:ConcurrentHashMap中的加锁思想,通过segment分段加锁,提高并发度,降低锁粒度。

  • 根据锁获取公平性

  • 公平锁:等待队列中先到先得。

  • 非公平锁:直接获取锁,获取不到才加入等待队列。它的效率比公平锁高5~10倍,因为不需要额外维护一个队列。synchronized和ReentrantLock中的lock()方法都是非公平锁。

  • 根据策略

  • 乐观锁:是一种乐观思想,认为遇到的操作读多写少,并发的可能性低,每次去拿数据的时候认为不会修改,不上锁,但是更新的时候会判断在此期间有没有更新这个数据,在写时写读出当前版本号,(比较跟上一次的版本号是否一致,一致则更新,失败则重读读-比较-写的操作。)然后加锁。Java中通过CAS原子操作实现。

  • 悲观锁:悲观锁认为读少写多,每次读写数据都会加锁,如synchronized。AQS框架下先尝试CAS乐观锁,获取不到则转为悲观锁,eg:ReentrantLock。

  • 自旋锁:如果持有锁的线程能在很短的时间内释放锁,则其他等待竞争该锁的线程就不必从用户态转为内核态进而阻塞挂起,只需要自旋(让cpu做无用功),前面的线程释放锁后可立即获取锁,避免了用户态转内核态的消耗,但是会消耗cpu,为避免让cpu一直等待需要设置一个自旋最大等待时间,持有锁的线程超过最大等待时间还没释放锁,则争用线程就会进入阻塞状态。优劣:在锁竞争不激烈或占用锁时间短的场景下适用,避免了线程阻塞和唤醒两次上下文切换,但是竞争激烈和锁占用时间长的情况下性能也较差。

  • 适应性自旋锁:在jdk1.6之前,自旋等待时间是固定的,可能会影响性能,1.6之后加入了适应性自旋锁,等待时间不固定,由上一次同一个锁的自旋时间和拥有者的状态决定,基本认为一次上下文切换的时间是最佳的等待时间。JVM针对当前cpu的负载作出以下优化:

    1.平均负载小于CPUs,一直自旋

    2.超过CPUs/2个线程自旋,后来的线程阻塞

    3.正在自旋的线程发现Owner发生了变化则延迟自旋时间或阻塞

    4.CPU处于节电模式:停止自旋

    自旋时间最坏的情况:CPU存储延迟

  • 根据不同的分类

  • 共享锁:允许多个线程获取锁,并发访问共享资源,eg:ReadWriteLock。是一种乐观锁。

  • 独占锁:每次只能有一个线程持有锁,ReentrantLock就是以独占锁方式实现了互斥锁。它是一种悲观保守的加锁策略,避免了读读冲突。

  • 可重入锁:也叫递归锁,指的是同一线程外层获得该锁后,内层仍然可以获取该锁。

  • 互斥锁

锁升级

随着锁的竞争,锁可以从偏向锁升级到轻量级锁,从轻量级锁升级到重量锁,但是不可以从高降低。

锁优化

  • 1.加锁耗费性能,只在并发场景下加锁。

  • 2.降低锁粒度,如ConsurrentHashMap。

  • 3.锁分离,根据功能分离,如读锁和写锁。

  • 4.不对同一个锁频繁的请求释放。

  • 5.锁消除,这是编译器级别的事情,在即时编译器时,如果发现不可能被共享的对象,则会消除这些对象的锁操作。

锁粗化

遇到一连串对同一个锁的请求时,把所有锁请求合并成一个请求。

ReadWriteLock读写锁

java提供,分为读锁和写锁。

  • 读锁:没有写操作,多个读操作之间不会互有影响,加读锁。

  • 写锁:只修改数据,且只有一个人写,加写锁。

synchronized关键字

  • 概述:从Java1.0开始,Java中的每个对象都有一个内部锁,如果一个方法被synchronized关键字修饰,那么内部锁将保护整个方法在任意时刻只能有一个线程执行,所以线程要调用这个方法必须先获取锁。在jdk1.5之前它被设计为重量级锁,效率低下。

  • 使用方法:

      1.修饰实例方法:作用于当前对象的实例

      2.修饰静态方法:作用于当前类对象

      3.修饰代码块:synchronized(class),作用于类对象

尽量不要使用synchronized(String s),因为JVM字符串常量池有缓存功能

  • 具体代码体现:

  • 对象锁:如果用三个线程分别执行方法一,方法二,方法三,  当一个线程抢先获取得锁之后,其他线程在此刻不能继续执行,这种锁叫做对象锁.锁住的是整个对象.上面代码synchronized放在方法前以及代码块加(this),这两种方式.如果还有其他线程则会执行普通方法以及同步代码块前的普通代码。

public class MyClass{ public synchronized void method1(){ } public synchronized void method2(){ } public void method3(){ synchronized(this){ }     }        public void method4(){​ }  }
  • 类锁:

情况1:如果用两个线程去执行方法1和方法2,两个线程会获取各自的锁各自执行自己的代码,

情况2:两个线程都执行方法2,如果一个线程抢先获得锁,另一个线程不得执行

情况3:两个线程分别执行方法1和方法3,两个线程会获取各自的锁各自执行自己的代码,

情况4,两个线程都执行方法3,如果其中一个线程先获得锁另一个线程不得执行

情况5,两个线程分别执行方法2和方法3,如果其中一个线程先获得锁,另一个线程不得执行.

public class MyClass{ public synchronized void method1(){ } public static synchronized void method2(){ } public void method3(){ synchronized(MyClass.class){ } }}
  • 底层原理:JVM层面

    monitorenter指令和monitorexit指令分别表示同步代码块开始和结束位置,当计数器为0,线程试图获取锁时成功获取,计数器加1,执行monitorexit指令后,计数器设为0,表示释放锁。

死锁

多个线程被阻塞且一个或多个等待资源。eg:A账户有5块,B账户有10块。线程1:从A给B转10块。线程2:从B给A转15块。当线程1和2并发时,AB账户余额不足不能转账,两个线程都进行不下去。

死锁产生的四个必要条件:

1.互斥条件:某资源在同一时刻只有一个线程拥有

2.请求与保持条件:一个线程因请求资源而阻塞,不释放已获取的资源

3.不剥夺条件:线程已获得的条件其他线程不可剥夺,只能自己用完释放

4.循环等待条件:多个线程请求资源互相占用互相等待

避免死锁

1.死锁检测

2.

volatile

多线程场景下,可以用volatile关键字修饰变量,保持变量的可见性。当用volatile修饰变量时,JVM每次都会去主存中读取,避免了某些线程修改了变量的值而另外一些线程在使用该变量在寄存器中的拷贝造成的数据不一致。

注:volatile只能保证线程间变量的可见性,不能保证原子性。而且只能修饰变量,不能修饰方法和类。禁止指令重排序。

ThreadLocal

线程本地变量,存储每个线程的私有变量。创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,从而避免了线程安全的问题。ThreadLocal有一个叫做ThreadLocalMap的内部类,key为ThreadLocal类,value为对象。ThreadLocal可能发生内存泄漏,因为ThreadLocalMap中的key为弱引用,但是value为强引用,在垃圾回收时,key会被清理掉,部分Entry就会出现key为null,value就没办法被回收。

Atomic

原子类。多线程场景下,只有当前持有线程线程会对原子类产生影响。以AtomicInteger为例解释原理:

AtomicInteger 本身是个整型,所以最重要的属性就是value,它使用unsafe方法获取value在内存中的偏移量,使用volatile修饰value,保证了value的可见性;其中incrementAndGet()方法使用了compareAndSet(current, next)方法,用到了CAS原理(比较并替换),每次根据内存偏移量取出数据,将取出的值跟预期值相比,相同则更新。

AQS:

附脑图:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值