Java学习笔记:2022年1月9日(其二)
Java学习笔记:2022年1月9日(其二)
摘要:这篇笔记主要记录了1月9日学习的第四章的类的基础知识,以及访问器以及访问器于多线程的意义。
1.多线程
多线程指的是在一个程序运行过程中,其内部的多个任务被同时执行,这时每个任务都会作为一个线程来执行。使用多线程的方式可以有效提升运行效率,当CPU的多线程能力很强的时候,计算机的多开能力也会得到提升,也就是说你能一次开多个QQ或者多个DNF,很多工作室都会强调计算机的多线程能力。CPU是被分为多个核心的,我之前给一个双核CPU开过核,发现里边确确实实有两个硅晶体芯片,现在的八核十六核乃至几十个核的CPU不知内部是什么样的,可能已经集成到一个芯片上去了,但是,核与核之间仍然是独立的,每个核在同一时刻都可以运行一个线程,而核多了就可以实现多线程并行。
但是我们需要注意的是,一个CPU尽管可以实现多线程并行,一个CPU中的一个核心可不能,CPU中的一个核心在同一时刻只能运行一个程序,因此当我们使用只有一个核的CPU时,其内部并不能真正的让线程并行,而是使用一种虚拟化的方式,让程序看上去在并发执行。当然,在系统中的线程数量过多时,即使多核CPU也会出现这种问题。
因此当总线程数大于CPU核心数时,CPU的每个核心会不得不进行时间片轮转,在一段时间内分别执行不同的线程,进而保证在一段时间以后这些线程全部完成,进而让这些线程看上去像是并发执行了。这种让任务在CPU核心上轮流执行的方式具体是为每个线程指定一个合理的时间运行方案,让他们间隔的在核心上执行,他们会被分配给一些时间片,消耗时间片规定的时间并按照一定的调度规则执行。
当一个线程的时间片濒临用完,系统会为它保存当前的运行状态,并进行线程切换,换下一个线程来执行,同时系统还会给它分配新的时间片,确保这个线程最终能够被运行完,这个保存状态并切换的行为叫做线程的切换。线程的切换需要一些开销,如空间开销,因为当前即将结束的线程以后还需要运行,因此系统需要保存它的被切换时的状态,以便下次又轮到它时能够顺利接着刚才的状态启动,同时还需要一定的时间开销。
线程的切换行为通常来说是在安全点上进行,所谓安全点指的是一个线程内的基本方法运行结束之后,下一个方法还没有运行的时候,线程切换会尽量避免在线程的某个方法执行的时候发生,因为在线程的一个方法执行时,往往有着更复杂的系统状态,而且对于作为一个整体的方法来说,贸然打断会破坏它的原子性,在某个方法中很可能某些变量正在发生改变,这是发生中断,很可能会破坏这些变量的改变,因此可能会发生回滚行为,这和数据库中的原子操作非常相似,因为在有些方法中会对堆区的内存进行操作,这个方法的中断会导致实参发生改变,但是系统又很难标记某个方法的中间运行状态,只能在下次运行时把这个方法重头开始,这时整体上就会发生错误,因此可能会导致不安全的现象,如变量改变两次等。因此系统会尽量避免这种情况的发生,一般情况下是在方法与方法之间的安全点中记性线程切换的,当然某些特殊情况下,确实也存在在方法运行时直接切换,只不过这种情况很少罢了。
在Java中存在多线程的功能,接下来我们结合代码来继续探讨多线程中的注意事项:
public class Test{
public static void main(String[] args) {
Person a = new Person();
Thread t1 = new Thread(){
@Override
public void run(){
System.out.println("Hello I'm a thread2");
int[] xx = a.a();
xx[0] = 123;
}
};
Thread t2 = new Thread(){
@Override
public void run(){
System.out.println("Hello I'm a thread2");
int[] xx = a.a();
System.out.println(we[0]);
}
};
t1.start();
t2.start();
//System.out.println("11");
}
}
如代码所示,为创建多个线程代码并进行多线程执行的方式,具体创建方式为使用线程构造方法并在其回调方法中书写每个线程的操作。而让线程执行的方式是对他们的调用start方法。
start是让他们执行的方法吗?实际上不是的,不仅如此,两个线程的执行先后顺序和谁先start谁后start无关,在这个程序中,t2是有可能比t1先执行的。这就是因为start方法并不是让每个线程执行的方法,而是让他们就绪的方法,在线程创建好后,不会瞬间被执行,而是处于一个默认的挂起状态。在系统中,想要被CPU执行的线程们,是位于一个就绪队列的,只有位于就绪队列中才会被CPU调度执行,刚被创建好的线程对象不位于这个队列中,而是存在于内存中,并不往CPU中输送,这时这是一个挂起状态,而使用start方法,就是让线程由挂起状态进入就绪状态,也就是送入就绪队列,这之后就可以被CPU执行了。在就绪队列中,谁先被执行谁后被执行和谁先进入队列没关系,一切要听CPU的,要看CPU想先执行谁,谁就先被执行。
当线程被执行时,Java虚拟机的运行时会为它们创建各自的新线程栈,它们会立即脱离主线程自立门户,拥有一个属于自己的线程栈,每个线程栈有一个属于自己的run方法,这个run方法类似于主线程栈的main方法,每个新线程的线程栈,是一个属于自己的run方法,我们所书写的一切线程中的代码都在这个run方法中运行,和main方法一样,当我们在线程中调用其他方法,也会发生压栈的情况。线程的创立就伴随着栈区新栈的创立,它们和主线程算是同级,这时候就不分谁先谁后运行了,一切由系统指挥。在新线程创立好之后,它们和主线程就完全没关系了,可以理解为直接分解,各奔东西了,他们之间不再存在相互制约的先后关系,谁先运行完要看系统怎么指挥。
然而通常来讲,都是主线程先运行完,因为在创立新线程的时候,主线程一定正在运行,它正在被执行,因此操作系统很大概率不会因为其他线程而换掉正在运行的主线程,而且一般情况下主线程也不会特别长,CPU很快就能运行完,因此就不会再临时切换线程,在主线程结束之后再运行其他线程。线程中可以出现同样名字的变量,因为他们属于不同的定义域,互不相干。
2.封装思想
封装是面向对象的语言的一大重要特性,Java中就很强调封装这一特性。封装就是里边的原理实现全部封装,只对外公开怎么用,也就是对外提供简单方便的接口,屏蔽内部复杂繁琐的原理。在Java中,要相对对象中的属性进行封装,则必须使用private前缀来标识属性,这样一来属性就不能通过对象直接访问,但是我们有时也需要使用对象中的属性,怎么办呢?这时候我们要在类中添加修改器和访问器,访问器就是得到这个属性值的方法,修改器就是修改这个属性值的方法,很多ide都可以实现修改器访问器直接生成。
public class Person{
private int[] arr = {23,45,55,66};
public String name;
public static int flag;
public static void m1(){}
public int[] get_a(){
int[] arr2 = new int[arr.length];
for(int i = 0;i < arr.length;i++){
arr2[i] = arr[i];
}
return arr2;
}
}
在以上代码中,对于私有属性arr,get_a方法,实际上就是一个访问器,当我们将一个属性设定为私有属性后,便无法通过对象直接使用这个属性,但是我们通过类中提供的公共方法可以间接的访问或者修改这个属性,我们可以给这个方法设定一些特殊操作,进而使得对于这个属性的操作更加安全,同时用户无需了解这个方法内部是什么,只需调用并填写自己的参数,就可以完成对这个属性的访问或者修改,这样能够很好的保证整个系统的安全性以及外界操作的方便性,这就是封装带来的好处。
在上面的代码中,我们为在arr属性的访问器中加入了一个深拷贝,也就是说我们通过它的访问器获得的arr数组,实际上是一个和arr数组一样的,但是实际上不是同一个,在内存上位于不同地址的数组,只是一个替身,这种深拷贝操作的好处实际上是有利于多线程中对同一资源访问时的安全性。当多个线程同时访问arr并进行一些修改操作时,就可能会导致线程之间的操作互相影响并导致最终运行错误,使用深拷贝操作的访问器,可以直接屏蔽掉线程与线程之间对同一资源进行操作时的影响,实际上,他们操作的根本不是同一个资源,同时这也会让线程们的操作无法影响原对象中的arr属性值,因为他们操作的只是一个替身,这对系统中的信息安全性有很大意义,这种深拷贝操作在多线程中也是会经常用到的一种操作,非常有效好用。
3.笔记原文
引用类型的=为:句柄等于值的地址,引用类型赋值就是把右侧的值的地址交给左侧的句柄。
句柄 = 值的地址(堆中地址),值和基本类型并不对应
C里边的指针也是这么用的
引用地址是栈中变量的地址,或者说是栈中变量的真实地址,变量是有自己地址的
这个过程实际上就是非常类似C语言中的指针,也就是地址之中存地址,进行这种连续的映射,栈的地址上映射上值的地址,值的地址映射到堆上面,其中栈也有自己的地址,这个地址并不对使用者开放,是封装的。
栈中有栈,函数栈内有变量栈。
传参时,形参会拷贝实参的值。用 = 为引用类型拷贝的就是地址,叫浅拷贝
java中引用类型进函数传参,形参被赋值,被赋予的是实参的指向,而不是被赋予的地址,在a方法里面无论写任何代码都没办法改变b方 法里边声明变量的指向。这个和C语言不太一样,因为传入的终究不是地址。a方法中的引用变量的值是地址,他们无论如何交换地址是无法影响 到外边的地址映射的,他们是值传递,值操作,但是这个地址是真的,我们真的获得了堆地址,对堆地址进行操作真的可以产生影响。
如果我们改变堆地址里的东西,就真的会产生影响。在java操作中用浅拷贝并不明智,当我们改值的时候,如果修改了公共部分,就会对函数外造成影响,如果没有改公共部分,那就没什么影响。用浅拷贝我们真正获得的是堆地址上的共享部分,我们可以用形参对堆地址部分进行真正的影响,这就导致了对外部变量产生的真实影响。指针也是这个原理
person a
person b
然后我们在方法中设定形参x = a,y = b。这样我们让形参获得的东西是对地址的指向,也就是说x指向了堆地址中的某个部分,y指向了堆地址中的某个部分。
堆地址是x和y的值。x和y和a,b完全没有关系,二者的值无论如何映射,如何改变,对外界都没关系,因为此时修改的是二者的值,但是通过二者的值我们可以真切的改变堆中信息的状态,因为外部的ab指向的也是堆中的地址,因此我们可以通过xy改变堆中地址,进而修改外部ab的信息。xy可以作为逻辑理解辅助符号
需要我们注意的东西就是,.就是地址访问符号。基本类型的值是直接拷贝,引用类型是改变指向。基本类型的值和句柄在一起,基本类型赋值,就直接拷贝来了。
操作系统中的栈都是用数组实现的,不是链式存储,都是顺序存储,因此他们确实是连续的,引用地址就是栈的地址,然后在栈的地址上边就是会直接存放句柄,然后基本类型就是真的放在这里,引用类型的句柄放在这里,句柄指向值的地址,对于栈的地址,就被称为引用地址。栈的地址上面直接存储的就是相关变量信息。
第四章很多面试要点,注意栈的地址被称为引用地址,这里是一个关键信息。
OOP是面向对象
封装 继承 多态 抽象,这是面向对象的四个特征
封装就是里边的原理实现全部封装,只对外公开怎么用,也就是对外提供简单方便的接口,屏蔽内部复杂繁琐的原理。
变量的地址看在哪声明,如果在类中声明,就是在堆中,在方法中就在栈中
静态类型的对象,句柄在方法区,值仍然在堆中,静态的变量类型,就是直接在方法区的静态资源区
字符数组之类的东西,他们在字符串常量池中,我们可以理解这个字符串常量池也在堆区。总之对于一些引用类型的东西,他们通常都保存在堆区,并且改他们的的值一般都是改引用指向,字符串是比较基本的引用类型了,组成很单一,多半是字符数组,因此很少对他们进行字符上的改动。
我们要改变引用类型是,单纯的改指向是没用的,这和C语言也是一样的,C语言中传进来的就是地址,但是Java传进来的其实是变量,我刚才这里有点隐约不清,但是想来实际上就是这样的。但尽管如此,Java中可以使用变量访问到真正的地址,这就是二者的差异之处,二者尽管函数方法声明调用很像,但实际上这里存在微小的差别,很容易混淆,Java中的类和C语言中的结构体很类似,C中有普通类型的指针,Java中没有。
不要把C中普通类型的指针和Java中的对象弄混,对象很像结构体类型指针,传进来的都是句柄,用.来进行访问,在C语言中还能用->访问
直接输出对象可以输出它的类型以及地址
对于更改访问私有属性的方法,就是更改器和访问器
更改器和访问器旨在保证安全,这个安全和黑客没有关系,这个是防止多线程下的数据相互干扰问题。能够保障多线程下,多进程下的数据相互干扰问题。简单的写法没有任何防御功能,必须加上一些处理才能有防御功能。
有了这种更改器和访问器,我们就可以把一个数据控制为只读或者只写,乃至直接封装起来,只用于内部辅助。
字符串安全问题?这里没听。
基本类型的等于号都是深拷贝,访问器里边必须加上深赋值处理才能保证多线程安全,否则不管用,我们可以更改访问的方式,如果没有这个处理,访问器没有作用。
这里一会再听一遍吧。
在类中,基本类型和字符串类型无论如何都是安全的,首先基本类型是深复制,而字符串类型是不可变类型,他们的访问都是安全的。
CPU的一个核心在同一时刻只能进行同一任务,也就是上边每个时刻只能执行一个线程,多个线程并发在上边轮流执行。多任务并行实际上是任务的快速切换。
线程进行切换要保持上个线程的状态,这样一来这个状态开始时可以继续这个线程的状态,这种切换是有开销的,有时间开销,也有空间开销,因为状态的记录与保存需要内存。
切换线程的时候是在安全点进行切换,一般来说安全点是在某个基础的方法结束之后,有些方法中间也会切换,但总体上来,一般是在空挡之间确认安全点。
因此线程立即执行,不是一个安全的做法,线程的具体执行方案是系统指定的,start方法是进入就绪态。操作系统中存在一个就绪队列的,队列中都是有待执行的任务,一个任务准备就绪之后操作系统才会调度他。所以我们可以解释,进入就绪状态的操作并不能决定执行的顺序,只能是听系统的。
进入就绪态的意思就是告诉操作系统说我可以被执行了。其实上就是正式进入就绪队列,就绪队列中进程都是等待调度的进程。
在线程进入就绪态之后,线程的执行是从run方法开始的,只要执行的话,他们会在栈中创建两个线程栈,每个线程栈里都是各自的run方法,各自的run方法中调用其他方法,其实就是相当于拷贝这些被调用的方法并压入自己的线程栈中。
也就是说,线程其实就是在栈里,线程的创立就伴随着栈的创立。我们自己在就java中建立的线程,他们会独自开辟自己的线程栈,他们属于独立的线程。
两个线程本身是对象,在调用的时候,会额外启动线程的线程栈。
程序能一直开着,是因为里边有个死循环。
//线程的执行互不等待
sleep是让线程沉睡,里边的参数是沉睡的毫秒数
有主方法的就叫主线程,在主线程中创建新线程后,会立刻构建一个新栈并成为一个新的线程,主子线程就没关系了,刚出生就分家。线程就是刚出生就分家。线程在创建好后,三个线程谁都不等谁,三者就没关系了。
在线程创建好后,他们就没有顺序关系了,线程们不会相互等待,和在代码里写的start顺序没有关系。
主线程属于当前正在执行线程,因此比其他线程的执行一般靠前,它有比较大的概率不会被CPU换掉,同时当主线程不长,很容易执行完,很可 能在当前时间片就能立刻执行完。
代码中出现主线程总是先执行完的原因,这是因为主线程正在执行,如果它比较短,可能来不及切换就被执行完了,新线程的创建是需要时间的。
这种情况就是,在激活了两个线程之后,再进行一个主线程的操作,这个主线程的操作虽然在后,但是往往先执行完,这是因为主线程已经存在了,正在运行,且主线程较短,因此主线程可能没有触发线程切换,就已经被执行完了。线程是一直在栈中的,只有执行结束后才会释放。
附录1:图片
线程中可以出现同名变量,因为作用域不同
java中的栈区的不同栈就是线程的实体,系统中的线程全部都是由数组构成的,他们都是连续的数组。到时候再好好整理一下吧。
附录1: