结尾
学习html5、css、javascript这些基础知识,学习的渠道很多,就不多说了,例如,一些其他的优秀博客。但是本人觉得看书也很必要,可以节省很多时间,常见的javascript的书,例如:javascript的高级程序设计,是每位前端工程师必不可少的一本书,边看边用,了解js的一些基本知识,基本上很全面了,如果有时间可以读一些,js性能相关的书籍,以及设计者模式,在实践中都会用的到。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
高级程序设计,是每位前端工程师必不可少的一本书,边看边用,了解js的一些基本知识,基本上很全面了,如果有时间可以读一些,js性能相关的书籍,以及设计者模式,在实践中都会用的到。
* @return
*/
public long get() {
return v1;
}
}
假设有多个线程分别调用上面程序的3个方法,这个程序在语义上和下面程序等价。
package com.lizba.p1;
/**
*
* synchronized等价示例
*
*
* @Author: Liziba
* @Date: 2021/6/9 21:46
*/
public class SynFeatureExample {
/\*\* 定义一个64位长度的普通变量 \*/
long v1 = 0L;
/\*\*
* 使用同步锁对v1变量进行写操作
* @param l
*/
public synchronized void set(long l) {
v1 = l;
}
/\*\*
* 通过同步读和同步写方法对v1进行+1操作
*/
public void getAndIncrement() {
long temp = get();
// v1加一
temp += 1L;
set(temp);
}
/\*\*
* 使用同步锁对v1进行读操作
* @return
*/
public synchronized long get() {
return v1;
}
}
如上两个程序所示,一个volatile变量的单个读\写操作,与一个普通变量的读\写操作都是使用同一个锁来同步,它们之间的执行效果相同。
**上述代码总结:**
* **锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性**,这意味着对一个volatile变量的读,**总能看到(任意线程)对这个volatile变量最后的写入。**
* **锁的语义决定了临界区代码的执行具有原子性**。这意味着,即使是64位的long型和double型变量,只要它是volatile变量,对该变量的读/写就具有原子性。如果是多个volatile操作或类似于volatile++这种复合操作,这些操作整体上不具备原子性。
**总结volatile特性:**
1. 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
2. 原子性。对任意volatile变量的读/写具有原子性,但类似volatile++这种复合操作不具有原子性。
#### 2、volatile写-读建立的happens-before关系
对于程序员来说,我们更加需要关注的是volatile对线程内存的可见性。
从JDK1.5(JSR-133)开始,volatile变量的写-读可以实现线程之间的通信。从内存语义的角度来说,volatile的写-读与锁的释放-获取有相同的内存效果。
* volatile的写和锁的释放有相同的内存语义
* volatile的读和锁的获取有相同的内存语义
**代码示例:**
package com.lizba.p1;
/**
*
*
*
*
* @Author: Liziba
* @Date: 2021/6/9 22:23
*/
public class VolatileExample {
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 1; // 1
flag = true; // 2
}
public void reader() {
if (flag) { // 3
int i = a; // 4
System.out.println(i);
}
}
}
假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens-before规则,这个过程建立的happens-before关系如下:
1. 根据程序次序规则,1 happens-before 2, 3 happens-before 4。
2. 根据volatile规则,2 happens-before 3。
3. 根据happens-before的传递性规则,1 happens-before 4。
图示上述happens-before关系:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210610002642797.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQxMTI1MjE5,size_16,color_FFFFFF,t_70#pic_center)
总结:这里A线程写一个volatile变量后,B线程读同一个volatile变量。A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,将立即对B线程可见。
#### 3、volatile写-读的内存语义
##### volatile写的内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
以上面的VolatileExample为例,假设A线程首先执行writer()方法,随后线程B执行reader()方法,初始时两个线程的本地内存中的flag和a都是初始状态。
**A执行volatile写后,共享变量状态示意图**。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210610002715183.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQxMTI1MjE5,size_16,color_FFFFFF,t_70#pic_center)
线程A在写flag变量后,本地内存A中被线程A更新过的两个共享变量的值被刷新到主内存中,此时A的本地内存和主内存中的值是一致的。
##### volatile读的内存语义
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将会从主内存中读取共享变量。
**B执行volatile读后,共享变量的状态示意图。**
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210610002759752.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQxMTI1MjE5,size_16,color_FFFFFF,t_70#pic_center)
在读flag变量后,本地内存B包含的值已经被置为无效。此时,线程B必须从主内存中重新读取共享变量。线程B的读取操作将导致本地内存B与主内存中的共享变量的值变为一致。
**总结volatile的写和volatile读的内存语义**
1. 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。
2. 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
3. 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。
#### 4、volatile内存语义实现
程序的重排序分为编译器重排序和处理器重排序(我的前面的博文内容有写哈)。为了实现volatile内存语义,JMM会分别禁止这两种类型的重排序。
**volatile重排序规则表**
| **是否能重排序** | **第二个操作** | **第二个操作** | **第二个操作** |
| --- | --- | --- | --- |
| **第一个操作** | **普通读/写** | **volatile读** | **volatile写** |
| **普通读/写** | | | NO |
| **volatile读** | NO | NO | NO |
| **volatile写** | | NO | NO |
上图举例:第一行最后一个单元格意思是,在程序中第一个操作为普通读/写时,如果第二个操作为volatile写,则编译器不能重排序。
**总结上图:**
* 第二个操作是volatile写时,都不能重排序。确保volatile写之前的操作不会被编译器重排序到volatile之后
* 第一个操作为volatile读时,都不能重排序。确保volatile读之后的操作不会被编译器重排序到volatile之前
* 第一个操作为volatile写,第二个操作为volatile读时,不能重排序。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
**JMM采取的是保守策略内存屏障插入策略,如下:**
* 在每个volatile写操作屏障前面插入一个StoreStore屏障。
* 在每个volatile写操作的后面插入一个StoreLoad屏障
* 在每个volatile读操作的后面插入一个LoadLoad屏障。
* 在每个volatile读操作的后面插入一个LoadStore屏障。
### 文末
js前端的重头戏,值得花大部分时间学习。
![JavaScript知识](https://img-blog.csdnimg.cn/img_convert/701f4db8e7fc0c3ff4d87017d6c846be.png)
推荐通过书籍学习,《 JavaScript 高级程序设计(第 4 版)》你值得拥有。整本书内容质量都很高,尤其是前十章语言基础部分,建议多读几遍。
![前端电子书](https://img-blog.csdnimg.cn/img_convert/6065b7d33c9a5859971490467a967767.png)
**[开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】](https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0)**
另外,大推一个网上教程 现代 JavaScript 教程 ,文章深入浅出,很容易理解,上面的内容几乎都是重点,而且充分发挥了网上教程的时效性和资料链接。
学习资料在精不在多,二者结合,定能构建你的 JavaScript 知识体系。
面试本质也是考试,面试题就起到很好的考纲作用。想要取得优秀的面试成绩,刷面试题是必须的,除非你样样精通。
**这是288页的前端面试题**
![288页面试题](https://img-blog.csdnimg.cn/img_convert/6a0ed19303290ef201081fc6148f21db.png)