文章目录
一、二进制
- 我们写的程序语言最后都要跑在 CPU 里面,而 CPU 只认识机器码(二进制)
- 计算机是用二进制保存所有数据
- 但是我们写代码的时候不可能全部用 0101 这样的二进制的方式去写,在二进制的基础上,衍生出人能看懂的数据类型,进而用看懂的语言来操作数据
二、数据类型
- 在实际项目开发中要挑选合适的数据类型
1、数据类型简介
- Java 中数据类型分为
- 值类型和引用类型这两大类
- 值类型:基本数据类型;
- 引用类型:字符串(String)、数组、对象;字符串和数组也是对象;
- 引用和值的区别
- 值类型是实实在在的一个数值;
- 引用类型是指向内存中的一个地址,修改一个引用可能会带来其他风险;
- 为了解决风险的问题,很多高级语言中就在函数调用的时候,按值传递,即将引用类型变量拷贝一份作为参数传递,而不是直接传递原来的引用;
// 值传递 int a = 10; function(a); void function(int num) { // 调用一个函数时,并且该函数接受值类型的参数(比如上面的 int) // 那么实际传递给函数的是实参(在这个例子中是变量 a)的值的拷贝。 num = 11; // 因此,函数内部对形参(这里是 num)的任何修改都不会影响到实参 a 的值。 } println(a); // a还是10,没有变化 // 引用传递 int[] nums = {1,2,3,4,5}; function(nums); void function(int[] nums) { nums[0] = 100; } // 因此,要尽量小心地使用引用类型,其实只要尽量少地修改参数即可避免这类问题。 println(nums[0]); // nums[0]不是1,是100了
2、字符串
- 字符串是引用类型,但是编译器对其做了特殊处理,使其使用起来就像值类型,目的是为了不可变
- 字符串是代码中使用最多的类型,如果所有的数据类型只能保留一个,那么这个类型一定是字符串(我们一定要像保护我方水晶一样保护我方字符串)
- 字符串很重要,我们要保护它的安全
- 如果字符串是引用类型,那么就很容易被修改,所以编译器对它进行了特殊处理,使其使用起来就像一个值类型一样
- Java 中的 String 就是 final,不但不让你修改,也不让你继承
- String 是为了不可变,那么字符串执行自加运算是如何执行的
String a = "abc";
a += "def"; // 结果a为abcdef
这个不是字符串对象变了,而是 a 指向了另一个地址
String a = "abc"; // a执行内存地址001,001放的是"abc"
a += "def"; // a指向的内存地址002,002放的是"abcdef",001放的还是"abc"
即,a 指向了 "abcdef"的内存地址而已,而原来存放 “abc” 的那个地址的值还是 “abc”,并没有变化
也就是说,字符串每次相加都会创建新的字符串对象
而我们知道,创建对象本来就是个高代价操作,所以要尽量避免频繁地执行字符串相加操作,例如下面操作
String name = "";
for (int i = 0; i < names.length; i ++) {
name += names[i];
}
所以,字符串也不能不加,如果想加,可以使用字符串提供的 API,例如,Java 中的 StringBuilder,StringBuffer;
3、数组
-
数组是比较特殊的引用类型,数组指向的是第一个元素在内存中的地址,而且数组是占用一块连续的内存空间
-
只需要知道数组的地址(数组的地址也就是数组第一个元素的地址),就能很快定位到第 n 个元素的地址,这也是数组可以根据下标访问元素的原因
-
数组必须要有连续的内存空间,如果现在的内存中没有一整块连续的内存空间;
-
数组对内存要求苛刻,一旦存满想扩容,就有可能扩容失败
-
缺点:内存要求高,扩展性差
4、对象
1)生活场景
- 碗筷和饭,人吃饭的场景
- 碗和饭都是对象
- 其中碗筷属于长生命周期对象,随着我家的存在而存在,随着我家的消失而消失
- 饭属于短生命周期对象,随出锅而出生,随着被吃下去而消失
- 所以,任何对象都有它的生命周期
2)函数调用
- 局部变量的生命周期,随着函数的调用出生,随着函数调用结束死亡
-
当一个函数执行到 return 语句时,函数会立即结束执行,并将 return 后面跟随的值(如果有的话)作为函数的返回值
fun userName() { // user 是一个局部变量 let user = new User(); // 对象出生 println(user.name); return user.name; // 对象死亡 } fun test() { let name = UserName(); } -
因为,调用一个函数的时候,会将函数的数据读入内存,函数调用完了,也就会把读入的数据清空
-
上面的例子,因为函数调用完了,引用不存在了,所以
User型对象没有被引用,会被自动回收掉 -
所以,每个变量都会定义一个作用域,局部变量的作用域仅限于它所在的函数,一旦函数调用完毕,作用域结束,生命周期走完了,变量就等于死亡了
-
- 还有一种情况,即使函数没有走完,对象也可能死掉
void test() { Object object = new User(); // 创建一个User对象,并用obj这个引用指向它 // 创建了一个String对象,并用obj这个引用指向它,此时User对象已经没有引用指向了 object = new String(); }- 当执行完 obj = new String() 后,User 对象已经没有引用指向了,此时进行垃圾回收就可以将 User 对象回收掉,也就是死亡了
- 上面的例子,虽然函数没有执行完,但是引用指向了另一个对象,所以也导致 User 没有被引用,也会被回收掉,也就是被干掉了
- 一个对象是否可以被回收,取决于执行垃圾回收时它有没有被引用
3, 将对象置为 null,null 也是一个对象,只不过是一个名字叫做 null 的里面啥都没有的对象
Object null = new Object(); // 创建一个名字为null的空对象
User user = new User(); // 创建一个User对象
user = null; // 将user指向null,原来的User对象没人指向了,就可以回收了
3)总结
三、代码的工作流程
1、代码执行过程
- 代码就是用控制语句操作数据类型,进而实现自己的逻辑,然后交给计算机去执行
- 大致流程
- 程序员用高级语言写的代码——>编译成机器代码 ——>从外存中读入内存 ——>CPU 执行;
- 这一系列操作需要需要调动 IO、控制器、运算器、寄存器、程序计数器等元件;
2、计算机元件组成
-
计算机是由
- 控制器、
- 运算器、
- 存储器、
- 输入设备:键盘、鼠标
- 输出设备:显示器
- 这五部分组成
-
控制器:是 CPU 中的一部分,用来控制程序的执行流程
-
程序计数器是控制器的一部分,程序计数器决定着程序的流程;我们的代码被编译成机器码之后,就是从上到下一行一行执行的,具体要执行哪一行,是由程序计数器来进行控制的
-
运算器:也是 CPU 中的一部分,和控制器共同组成 CPU
- 运算器的职责就是执行运算
- 运算器是从寄存器中读取数据,并不是从内存中读取数据
-
寄存器:也是在 CPU 内部的,是 CPU 中的一部分
- 通常说的,CPU 从内存中读取数据,就是把数据读入寄存器中;
- 但是数据并不是直接从内存中读入寄存器中,因为从内存中读取数据太慢,而是先读入高速缓存中,然后才读入寄存器中
- CPU 先把数据读入高速缓存中,以备使用,真正使用的时候,从高速缓存中读入寄存器
- 当寄存器使用完毕后,就将数据写回到高速缓存中,然后高速缓存再在合适的时机将数据写入到存储器中
- CPU 运算速度非常快,而从内存中读取数据非常慢,如果每次都从内存中读取数据,势必会拖累 CPU 的运算速度;为了解决这个问题,就在 CPU 和存储器之间放个高速缓存,CPU 和高速缓存中间的读写速度是非常快的;
- CPU 只管和高速缓存互换读取数据,而不管高速缓存和内存之间是怎么同步数据的,这就解决了内存读写慢的问题
3、程序的执行流
- 程序的执行流程分为
- 顺序执行
- 条件分支
- 循环执行
四、面向过程和面向对象
1、面向过程和面向对象的区别
-
代码执行
- 人写代码,写完后交给编译器编译为电脑认识的东西,然后电脑将它放到 CPU 中去高速执行,最后将结果展示给我们
- 分为两次过程
- 一是我们将写的代码转换为电脑认识的
- 二是电脑执行完后转换为我们认识的
- 即,我们写的代码是面向我们编写人员的,编译为电脑执行的代码是面向计算机的
- 分为两次过程
- 人写代码,写完后交给编译器编译为电脑认识的东西,然后电脑将它放到 CPU 中去高速执行,最后将结果展示给我们
-
面向过程
- 即,是以当前发生的事情为目标进行编程,以过程为核心,不考虑将来可能发生的事情(只注重当前,不注重未来的扩展性)
-
面向对象
- 将世间万物都视为对象,针对这些对象的具体行为进行编码
- 三大特点:封装、继承、多态
2、面向对象
1)封装
封装就是将现实事物封装成抽象的类,并可以将类的权限加以修改,把自己的数据和方法只让可信的类或者对象操作,对不可信的信息隐藏
即你只能看我给你看的,只能修改我让你改的
- 封装是类和对象概念的主要特性,封装让对象针对不同的角色有不同的表现,让程序更有层次感
- 如果没有封装,那么继承就没有意义,多态也就不存在
- 封装的作用
- 修改权限
- 将具体事物抽象成类的这个过程;例如,将现实中的一辆汽车,把它抽象成一个类:class Car { }
2)继承
让类拥有另一个类的数据和函数,表达一种我也是你的关系
- 继承的作用
- 继承可以节省大量代码,并且容易扩展
- 子类的公有属性和行为都可以放在父类中,而子类只需要放自己特有的属性和行为即可,这样可以统一处理子类的共有属性,方便管理,子类的特有属性又可以在内部自己处理
- 继承就是为了多态
- 假如,我有奥迪、宝马、奔驰、大众,我可以直接抽象出公共部分定义一个父类 Car(属性为 price,方法为 run),然后创建出各个车的类直接继承 Car 即可,就不用反复声明 price 和 run 了
- 但是,每个车的价格和速度又不一样,直接继承是有问题的,这就涉及到多态
- 即,继承就是为了多态
3)多态
多态指的是一个类的子类可以有不同的表现
- 多态的作用
- 为了让代码容易修改,容易扩展;
- 没有多态,就没有设计模式
4)接口
- 继承关系只能有一个,即只能有一个父类,那需要多继承的时候怎么办?通过接口
- 接口表示我具有某种功能,继承表示我就是某种东西;因为你只能是一种东西,所以只能单继承,但是可以具有多个功能,实现多个接口
interface CanFly {} interface CanSwim {} interface CanChange {} class NvWa {} class SunWuKong extends NvWa implements CanFly, CanSwim, CanChange {}
五、进程和线程
1、软件简介
- 什么是软件
- 软件就是程序的载体。例如,我们写的
hello world等代码,打包出来就是软件,可以安装在各种设备上,然后运行在操作系统上
- 软件就是程序的载体。例如,我们写的
- 软件分为
- 系统软件
- 应用软件
- 操作系统就是系统软件的一种,是最基础的软件,其余所有软件都要运行在操作系统上
2、操作系统简介
- 操作系统是软件,是专门用来管理软件的软件
- 作用
- 负责和硬件打交道,将用户的各种操作翻译给计算机硬件,然后将结果告诉用户
- 管理其他软件
3、进程和线程
-
操作系统是如何管理软件的
- 通过管理进程
- 软件是程序的载体,每个程序都有一个进程
-
每个程序都有一个进程,线程是进程的一部分,线程是操作系统能调度的最小单位
-
使用线程可以提高程序的运行速度,节省时间和成本
-
资源都是在进程中的,进程是持有资源的最小单位
-
线程是进程的一部分,是操作系统调度的最小单位,是调度的最小单位,不是持有资源的最小单位,线程可以使用资源,但是不能持有资源
-
每个进程都有一块内存,这个内存是所有线程所共享的
-
每个线程都有一个缓存,这个缓存是自己独占的
4、多线程
-
资源冲突问题
- 多个线程在同一时刻操作同一个资源;
- 多个访问资源不会产生冲突,操作资源会产生冲突(操作资源就是对资源进行修改)
-
解决资源冲突问题
-
不同时、不同目标
-
不同时:排队!将并行变为串行,可以在
i被读入线程A的内存中时,就加个标记,其它线程看到这个标记就会等待,等线程A执行完将i写回到内存中时,再把标记清除,并通知其它线程,其它线程再开始执行 -
并发即解决方案
-
加锁
- 在变量
a被一个线程读取后,就加个锁,其它线程过来之后发现有锁,就等着,就会导致同时只能有一个线程使用a
- 在变量
-
如果被写回来,就释放锁
-
通过线程获取锁的方式,实现了将并行转换为串行
class A { int a = 0; Thread t1 = new Thread() { synchronized void run() { for (int i = 0; i < 1000; i ++) a ++; } }; t1.start(); Thread t2 = new Thread() { synchronized void run() { for (int i = 0; i < 1000; i ++) a ++; } }; t2.start(); }
-
5、乐观锁和悲观锁
1)乐观处理和悲观处理
- 并发的处理免不了要等待操作
- 分为两种情况
- 我等着,你好了叫我
- 我等着,每隔一段时间再来看看你搞完了么
- 悲观处理:你好了叫我,可能需要等好久,所以就是假设要等很长时间,这是带有悲观色彩的,就称为悲观处理
- 乐观处理:我等一会再来看看,明显就是可能你很快就完了,也就是只等很短时间,这是带乐观色彩的,就称为乐观处理
2)乐观锁和悲观锁
-
悲观锁
- 悲观锁的核心就是等待,被唤醒
- 即我可能要等好久,那就算了,我不等了,你完事后跟我说
- 以此我们发明的悲观锁就叫
Synchronized
-
乐观锁
-
乐观锁的核心是自旋
-
CAS就是个乐观锁 -
乐观锁因为只要能很少的时间,所以,与其我回家等着,不如我就在这看着,反正也不会等很久,过一段时间就来问问
-
这种过一段时间就来看看的叫做轮询,其中过一段时间过来问问的叫做轮询,这段时间在干啥呢,是在原地转圈玩,反正不闲着,这就叫做自旋
-
因此,可以发明一种自旋锁,自旋锁就是乐观锁的一种
class A { int a = 0; boolean lock = false; // 开启一个线程 Thread t1 = new Thread() { // 加了synchronized关键字 void run() { // 运行时发现被锁了,就自己死循环 while (lock) { // 在 while(true)中让线程休眠一段时间,如果好几次都获取不到锁,那就增加休眠时间 int len = 0; while (true) { sleep((length ++) * 200); } } // 终于拿到锁了,执行自己的逻辑 lock = true; // 自己拿到锁了,标记一下 // 操作资源 for (int i = 0; i < 10000000; i ++) a ++; lock = false; // 自己执行完了,释放锁 } }; t1.start(); // 开启另一个线程 Thread t2 = new Thread() { // 加了synchronized关键字 void run() { // 运行时发现被锁了,就自己休眠 while (lock) { int len = 0; while (true) { sleep((length ++) * 200); } } // 终于拿到锁了,执行自己的逻辑 lock = true; // 自己拿到锁了,标记一下 // 操作资源 for (int i = 0; i < 10000000; i ++) a ++; lock = false; // 自己执行完了,释放锁 } }; t2.start(); }
-
-
很多语言中提供了现成的乐观锁的工具 API,有 CAS 这个关键字
-
总结
- 凡是要等很久的都是悲观处理,以此可以发明一个悲观锁
- 凡是可能等很少时间的都是乐观处理,以此可以发明一个乐观锁
- 根据具体的场合和场景去选用适合的锁
本文详细讲解了二进制基础、数据类型的选择与操作(包括值类型、引用类型、字符串、数组和对象),代码执行流程,以及面向过程和面向对象编程的区别,以及进程、线程和乐观/悲观锁的概念。
171万+

被折叠的 条评论
为什么被折叠?



