八股文Java基础
Volatile关键字
首先需要知道并发三概念:
原子性,可见性,有序性。
原子性,用在关键操作操作上保证操作正确。常见的原子操作有
(1)基本类型的读取和赋值操作,且赋值必须是值赋给变量,变量之间的相互赋值不是原子性操作。
(2)所有引用reference的赋值操作。
(3)java.concurrent.Atomic.* 包中所有类的一切操作。
(4)实际调用的虚拟机指令,例如i++操作是由几个虚拟机指令实现的
可见性,因为线程的操作为了效率很多结果优先保存在本地内存中,如果不同步到主内存中,其他线程不知道修改后的结果。
有序性,因为优化可能会导致指令的重排,而多线程操作的时候,只有指令是原子性的,有些操作因为指令重排导致出现误会。(双重检查锁)
java内存模型
Java的内存模型JMM以及共享变量的可见性
JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
JMM是个抽象的内存模型,所以所谓的本地内存,主内存都是抽象概念,实际上都是在主存上。
对于普通的共享变量来讲,线程A将其修改为某个值发生在线程A的本地内存中,此时还未同步到主内存中去;而线程B已经缓存了该变量的旧值,所以就导致了共享变量值的不一致。解决这种共享变量在多线程模型中的不可见性问题,较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,比较合理的方式其实就是volatile。
主内存和工作内存交互规范
为了更好的控制主内存和本地内存的交互,Java 内存模型定义了八种操作来实现(以下内容只需要简单了解即可):
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的 load 动作使用
- load(载入):作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
- write(写入):作用于主内存的变量,它把 store 操作从工作内存中一个变量的值传送到主内存的变量中。
可以看到,对于主存的操作都是read,write,而对于本地内存的操作是load,store这些。
这写操作都是以主存变量为主人公的操作,读是对主存的直接操作,写也是对主存的直接操作。
happens-before 原则?
happens-before(先行发生)原则是 Java 内存模型中定义的用于保证多线程环境下操作执行顺序和可见性的一种重要手段。
举个例子来说,例如 A happens-before B,也就是 A 线程早于 B 线程执行,那么 A happens-before B 可以保障以下两项内容:
- 可见性:B 读取到 A 最新修改的值(通过内存屏障)。
- 顺序性:编译器优化、处理器重排序等因素不会影响先执行 A 再执行 B 的顺序。
实际上就是叫同步中后续线程能看到前面的线程的结果的最低保证(不需要同步的线程也就是没有共享变量的线程没有任何交集,自然不需要这个什么够吧东西保证)。为了实现这个保证,就会去同步到主存。例如在synchronized关键字修饰的方法上,线程执行完方法之后,为了保证这个happens-before也就是后续线程能看到他的执行结果,就必须要同步内存了,而在执行期间就不需要同步,这样可执行的很快,到了要退出的时候就需要同步了,不然无法保证happens-before原则。
提供happens-before保证的几种方法,
一种是锁的释放与获取其实synchronized也是一种锁,
另一中就是volatile变量的写操作,
还有一种就是线程的操作,例如线程的启动,join(),isAlive()都是通过查询主存来干的,所以都是有happens-before保证的。
内存屏障
先理解内存屏障,需要先知道内存乱序访问。
两条毫不相关的(编译器认为的不相关实际上是不直接相关)的指令,出于对内存访问效率的提升,编译器或者CPU在访问时都会尝试对其执行顺序进行优化,从而提高读取速率,加速指令的执行。
- 编译时,编译器优化进行指令重排而导致内存乱序访问;
- 运行时,多CPU间交互引入内存乱序访问。
在单线程情况下,根据分析指令得到的逻辑关系,优化当然不会破坏原来的逻辑。但是在多线程执行情况下,同样的优化执行顺序,可能会导致多线程协作逻辑的破坏。
内存屏障是对于操作来说的,因为这些操作是乱序的,为了保证一定的秩序来保证逻辑的实现,需要在某个节点将这些乱序操作都统一一下,到这个节点了,你们就等一下执行的慢一点的指令,只有在此点之前的所有读写操作都执行后才可以执行此点之后的操作。
volatile的作用
强制同步到主内存中,其他线程的本地内存会失效,导致性能有点影响。
禁止对volatile变量的指令重排,但是在不影响其他语句的重排下制定了禁止重排规则
(即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见)
1解决了可见性
2解决了重排问题
volatile的局限
- 没法解决复合操作的原子性。例如i++;需要读取,加操作,赋值,三步,三步需要合成一个原子操作才行。
使用synchronized或者lock能解决原子性问题。
锁
锁就是为了解决同步问题的,保证同一时间的唯一占有。
Java中分为两类锁,关键字锁synchronized,自定义对象锁。
关键字锁,是jvm层面实现的,我们只能直接通过关键字使用,使用时看不到。
自定义对象锁,是jdk5之后增加的lock接口及其后续实现类。这个是显式的锁,我们在代码层面使用这些锁,和jvm的实现无关,关键是锁内部的逻辑。
synchronized
需要先了解jvm中的实例对象的知识。
语法重理解
抽象类与接口
抽象类用于定义一些行为和一些属性。 所以抽象类相比于实体类更注重定义,具体实现也是为了服务于定义的逻辑,所以抽象类专精定义。
接口用于定义一些行为,也就是方法。所谓接口中的属性也是static final常量,实际上是为方法服务的,所以接口相比于抽象类更精,去掉了抽象类的属性定义。
更少的定义,也就意味着更小的限制,更多的兼容。所以接口是最兼容的,一个类可以实现无数的接口。
语法分析
抽象类:
- 不可以实例化
- 可以包含非抽象方法,也可以包含具体方法。
- 可以不用实现,接口中的方法,或者继承而来的抽象方法,也可以实现。
接口:
- 接口没有自己的构造方法,所以严格来说接口不是一个类。
- 访问修饰符也是public的
- 只能包含常量,不能有变量。