线程范围内的数据共享
该运用在web中比较常见,例如银行转账系统,可能会有多个用户同时进行转账,这个时候使用的数据只能是在当前线程中被共享,相对于线程外面必须是独立的,否则就会出现不可预料的错误
如何实现在线程范围内的数据共享?
思路:通过map集合的方式实现,让map集合保存当前线程(Thread.currentThread())和线程使用的数据,把线程保存为key,数据保存为map的value,要取出当前线程的数据就可以通过map的get方法来获取当前key的值,这样就能实现该值只属于某个线程
使用ThreadLocal类实现线程范围内的数据共享
该类提供了线程局部 (thread-local)
变量,相当于是一个map集合,与map集合不同的是它不需要设置key(当前线程),只是需要值,自动和当前线程绑定,通过get方法获取当前线程上的值
如果当线程消失后,该线程局部变量会被垃圾回收(除非存在对这些值的其他引用)
停止线程
Stop方法已经过时.
如何停止线程?
只有一种,run方法结束,开启多线程运行,运行代码通常是循环结构.只要控制住循环,就可以让run方法结束,也就是线程的结束.
特殊情况:当线程处于冻结状态,就不会读取到标记,那么线程就不会结束
当没有指定的方式让冻结的线程恢复到运行状态时,这时需要对冻结进行清除,强制让线程恢复到运行状态,这样就可以操作标记让线程结束
Thread类中提供了该方法interrupt();à将处于冻结状态的线程强制的恢复到运行状态,是在清除线程的冻结状态不是结束状态
数组
概念-> 同一种类型数据的集合,其实数组就是一个容器。
好处-> 可以自动给数组中的元素从0开始编号,方便操作这些元素
数组引用只能接收null和数组对象地址
一般定义方式:int [] arr = new int[9];
赋值:arr[4]=90;
int arr [] = new int[9];
int arr [] = new int []{1,5,5,5,};注意这里给了详细的数字就不能在给长度避免发生值和个数不匹配
int [] arr = {1,11,11,1};
注意:下标越界在编译的时候是检查不错错误的,因为在编译阶段检查的是语法错误,只有在运行的时候才会在堆中分配内存,这个时候才会检测出错误!
数组元素在没有赋值的情况下预设是0(数值数组).
Int[] arr; arr={1,2};//这种赋值方法是错误的,在定义数组的时候才支持arr={1}这种方式赋值(int[]
arr={1,5};//OK)
Int[] arr = new int[5];
arr={1,2};//error,可以使用数组下标进行赋值(arr[0]=1;//OK)
数组的操作
1.获取数组的元素
int [] arr = new int[3];
通常会用到遍历。(用for即可)
除了可以手动控制for中循环条件中数组长度还可以通过用---数组名.length(获取数组长度)属性来获取数组的长度
For(int i = 0;i
2.获取最值:
(1).获取最值需要对数组元素进行比较,每一次比较都会有一个较大的值,因为该值不确定需要通过一个变量进行临时存储。
(2).让数组中的每一个元素都和这个变量中的值进行比较。
(3).当所有的元素都比较完成,那么该变量中存储的就是数组中的最值(最大或最小)
步骤:
1.定义变量,初始化数组中的任意一个元素即可。
2.通过循环语句对数组进行遍历
3.在变量过程中定义判断条件,如果变量得到的元素比变量中的元素大或小,就赋值给该变数。
关于临储的值是否能为0,可以,这种方式实际上就是在初始化数组中的任意角标(参见截图中的:数组_最值1和最值2 )
3.排序:
(1).选择排序(截图名:选择排序)
选择排序原理:
从下标0开始,依次和所有元素比较,得出最值,通过临时变量来交换两数据!(下标0和1比,0和2比…….)
注意:外循环控制遍历次数,内循环用来比较和交换,最后一个下标可以不用在做比较,为了代码的效率一般在外循环的循环条件减1!
(2).冒泡排序(截图名:冒泡排序原理图)
原理:最相邻的两个元素进行比较如果符合条件就交换数据、
第一圈:最值出现了最后位
相比之下冒泡排序比较快
无论什么排序都需要对元素进行位置的置换
(3).普通查找(截图名:普通查找):
要知道该数是否在数组中存在,首先需要给这个函数(假设)发送我们需要查找的数组和需要查找的数,当在元素中找到该数时候就返回该数在数组中的下标,没有找到应该返回一个-1因为下标最低的是0,返回负数表示不存在,如果该数在数组中重复出现只能返回第一次找到的下标!
4.折半查找:
优点:可以提高查找效率
前提条件:数组必须是有序的
5.对数组的其他操作参见截图和程序(位置:G:\javacode\进制操作)
二维数组(参见截图:G:\数据\c语言和Java\Java笔记\截图)
定义方式:int [][] y = new int[5][];//可以不写第二个[]中的值,但是第一个必须要写
int y [][] = new int[][];
int
[] y[] = new int [][];
注意:int [] x,y[];这里不是表示定义了int [] x;int
y[];而是int []
x; int []
y[];一个一维数组和一个二维数组
注解:中括号跟着类型走表示后面的都适用,跟名字走就只适用它本身
概念:
二维数组就是数组中的数组,int [][] y = new int
[2][3];表示一个二维数组中有2个一维数组,每个一维数组里面有3个元素,更直白的理解可以用对二维数组中一种赋值来表示:int
[][] y
={{},{},{}};里面的小{}就是一个一维数组;注意的几个事项:y[0];表示的是第一个一维数组的地址(如果输出),默认为null;
函数(Function)
定义:
就是在类中独立的具有一些功能的模块
如何定义一个函数?
1.既然函数是一个独立的功能,那么该功能的运算结果是什么先明确,也就是返回值
2.在明确在定义该功能的过程中是否需要未知的内容参与运算(是自己独立就能够完成还是需要调用者给我们提供呢?),也就是参数(形参)
其实这两个就是明确了函数的返回值类型和形参列表(就是参数的类型和个数)
注意:
在函数定义的时候,只要完成相对应的功能就OK,不要完成其他的功能!(做好自己分内之事,功能要单一)
函数的重载(overload)
概念:同一个类中,多个方法功能相仿但是需要处理的数据不同(形参不同),这个时候命名成了问题,java为我们提供了重载机制,即在同一个类中,允许存在一个以上的同名函数,只要它们的参数类型和个数不同即可。
注意:重载和返回值类型没有关系
什么时候用重载:
当定义的功能相仿,但参与运算的数据(形参)或者参数位置内容不同,那么这个时候就可以用同一个函数名称,方便阅读,但是参数列表不同!
函数的重写(override)
重写的三种情况:
1. 当子类和父类出现相同名称的变量时候:
如果子父类中出现非私有的同名成员变量时,子类要访问本类中的变数,用this(不加默认也是当前物件);子类要访问父类中的同名变数时用super
Super的使用和this的使用几乎一致
This代表的是本类对象的引用
Super代表的是父类对象的引用
2. 子父类中的函数
当子类出现和父类一模一样的函数时,子类对象调用该函数,会运行子类函数的内容,父类的函数被覆盖掉(也在非静态方法区只是没有运行)
这种情况就是函数的另一个特性:重写或覆盖
当子类继承了父类,沿袭了父类的功能到子类中,但是子类虽然具备该功能,但是功能的内容和父类不一致,这时,没有必要定义新功能,而是使用函数的覆盖特性,来重写父类的功能,保留父类的定义
注意:
1. 子类覆盖父类,必须保证子类权限大于等于父类权限,才可以覆盖,否则错误
2. 静态只能覆盖静态(一般不用)
3. 记住:重载只看同名函数的参数列表
重写子父类必须要一模一样
3.子父类中的构造函数
在对子类对象进行初始化时,父类的构造函数也会运行。那是因为子类的构造函数默认第一行有一条隐式的super();在调用父类的构造函数;而且子类的所有构造函数都默认的在第一行添加super();
为什么子类一定要访问父类中的构造函数?
因为父类中的成员子类可以直接获取,所以子类对象在建立时,所以子类需要在查看父类构造函数是如何对数据初始化的,所以就先访问一下父类的构造函数;如果要访问父类指定的构造函数,可以通过修改super();来实现
注意:super语句必须定义在子类构造函数的第一行
子类的实例化过程
子类的所有的构造函数,默认都会访问父类中的空参数的构造函数,因为子类每一个构造函数内的第一行都有一句隐式super();
当父类中没有空参数的构造函数时,子类必须手动通过super语句来指定访问父类的构造函数
当然子类的构造函数第一行也可以手动执行this语句来访问本类中的其他构造函数,子类中至少有一个构造函数的第一条语句是访问父类的构造函数
Final关键词
定义:使用该关键字修饰的类、方法等 都是最终的,不能在进行修改
1. 可以修饰类,函数,变量
2. 被final修饰的类不可以被继承
3. 被final修饰的方法不能被复写
4. 被final修饰的变量是一个常量只能被赋值一次,即可以修饰成员变量,也可以修饰局部变量;当在描述事物时,一些数据的出现值是固定的,那么这时为了增强阅读性,都给这些值起个名字,方便阅读,而这些值不需要改变,加final修饰。
作为常量:常量的书写规范所有字母都大写,如果由多个单词组成。单词间用_连结。
5. 内部类定义在类中的局部位置时,只能访问该局部被final修饰的局部变量
抽象类(方法)
当多个类中出现相同功能,但是主体不同,这是可以进行向上抽取,这时,只抽取功能定义,而不抽取功能主体。
抽象:即笼统的,不实际的,简单说就是看不懂的
抽象类的特点:
1. 抽象方法一定在抽象类中,抽象类中不一定有抽象方法
2. 抽象方法和抽象类都必须被Abstract关键词修饰
3. 抽象类不可以new创建对象,因为调用抽象方法没有意义
4. 抽象类中的抽象方法要被使用,必须由子类复写其所有的抽象方法后,建立子类对象调用,如果子类只是覆盖了部分抽象方法,那么该子类还是一个抽象类
5. 抽象方法没有方法体,这样可以强制子类写符合本类的方法主体
抽象类和一般类没有太大不同,该如何描述事物就如何事物,只不过该事物中出现了看不懂的东西,这些不确定的部分,也是该事物的功能,需要明确出现,但是无法定义主体,通过抽象方法来表示。
抽象类比一般类多了抽象方法
抽象类不可以实例化
特殊:抽象类中可以不定义抽象方法,这样做仅仅是为了不让该类建立对象(也可以访问抽象类的静态普通方法)
接口(interface)
初期理解,可以认为是一个特殊的抽象类;当抽象类中的方法都是抽象的,那么该类可以通过接口的形式来表示。
Class 用于定义类
Interface 用于定义接口
Implements 实现接口关键词
接口定义时,格式特点:
1. 接口常见的定义:常量,抽象方法
常量:public static final
方法:public abstract
注意: 接口中的成员都是public
接口是不可以创建对象的,因为有抽象方法
需要被子类实现,子类对接口中的抽象方法全都覆盖后,子类才可以实例化,否则子类是一个抽象类
接口可以被类多实现,也是对多继承不支持的转换形式,java支持多实现(因为接口中的方法是没有方法体的,子类愿意怎么实现就怎么实现,假设两个接口中有两个方法一样,那么子类在实现的时候只需要实现他们中的一个就OK,两个interface中的方法都被复写了)
普通类可以同时继承普通类和多个接口,但是extends必须在implements之前
基本体系在类中实现,扩展功能可以在接口中实现
内部类
定义:将一个类定义在另一个类的里面,对里面的那个类就称为内部类(内置类,嵌套类)
访问特点:
内部类可以访问外部类中的成员,包括私有成员
而外部类想访问内部类中的成员必须要建立内部类的对象
之所以可以访问外部类中的成员,是因为内部类中持有一个外部类的引用,格式:外部类名.this
访问格式:
1.当内部类定义在外部类的成员位置上,而且非私有,可以在外部其他类中,直接建立内部类对象
格式:
外部类名.内部类名 变量名 = new 外部类().new内部类()
Outer.Inner in test = new Outer().new Inner();
2.当内部类在成员位置上,就可以被成员修饰符所修饰,如private将内部类在外部类中进行封装
Static:内部类具备static的特性
当内部类被static修饰后,只能访问外部类中的static成员,出现了访问局限
在外部其他类中,如何直接访问静态内部类呢
New.外部类名().内部类名().内部类方法名();//就是建立一个内部类的对象
在外部其他类中,如何直接访问static内部类的静态成员呢
外部类名.内部类名.成员名();
注意:当内部类中定义了静态成员,该内部类必须是静态的
当外部类中的静态方法访问内部类时,内部类也必须是静态的
内部类何时使用:
当描述事物时,事物的内部还有事物,该事物用内部类来描述。因为内部事物在使用外部事物的内容。
局部内部类:
内部类定义在局部时候:
1.不可以被成员修饰符修饰
2.还可以直接访问外部类中的成员,因为还持有外部类中的引用。但是只能直接访问它所在的局部中被final修饰的局部变量
匿名内部类
好处:简化书写
弊端:在多态的情况下不能访问子类对象特有成员,也不能强转;在父类方法过多的情况下,不使用匿名内部类,那样会使得代码阅读性非常差
1. 匿名内部类其实就是内部类的简写格式
2. 定义匿名内部类的前提条件:
内部类必须是继承一个类或者实现接口
3. 匿名内部类的格式:
new 父类或者接口(){复写父类或接口或自定义方法};
4. 其实匿名内部类就是一个匿名子类对象,而且这个对象有点胖,也可以理解为带内容的对象
5. 匿名内部类不可以访问外部非final修饰的变量
适用范围:
如果一个类的语句比较少,逻辑简单,而且不经常变动,这个时候可以使用匿名内部类
以下情况不建议使用:
如果一个类包含了很重要的逻辑,将来要经常修改,则该类就不应该当作匿名内部类来使用,否则会导致代码混乱
包(package)
Package必须放在程序的代码的第一行,分号结束
包存在的时候,类名就是包名.类名
包也是一种封装形式(类,函数等都是)
一个包中的类要被访问,必须要有足够大的权限,所访问的类要被public修饰
类公有后,被访问的成员也要公有
权限: public protected default private
同一个类中 ok ok ok ok
同一个包中 ok ok ok
子类 ok ok
不同包中 ok
总结:
不同包之间进行访问,被访问的包和包中的类和成员要被public修饰
不同包中的子类还可以访问父类中被protected修饰的权限修饰的成员
包与包之间可以使用的权限只有两种,public protected
为了简化类名的书写,使用一个关键词:import(导入包中类)
Import导入的是包中的类,通配符*表示当前目录下的所有类
但是建议不要写*,这样比较占用内存空间,要用那个类就导入那个类
导入不同包中相同类名的时候,必须加包名加以区分,否则报错
建议定义包名不要重复
Jar包
定义:就是java文档的压缩包
好处:
方便项目的携带
方便使用,只要在classpath设置jar包目录即可
数据库驱动,SSH框架等都是jar包体现的
创建jar包
• jar -cvf mypack.jar packa packb
查看jar包
• jar -tvf mypack.jar [>定向文件]
解压缩
• jar -xvf mypack.jar
自定义jar包的清单文件(做一个双击可执行的jar包)
• jar
–cvfm mypack.jar mf.txt packa packb
步骤:
1. 将java源文件编译成class字节码文件(通过指定包可以在编译的时候把class文件放到一个文件夹中)
2. 在cmd中执行命令 jar – cvfm jarname.jar mymanifest
pack
需要自己手动指定
Main-Class:(空格隔开) 包名.主类 (必须有回车换行)
把这个信息保存在一个文本文件中 在mymanifest中输入该文件的名字即可
异常
什么是异常:程序在运行时出现不正常的情况,导致程序不能正常工作
异常的由来:问题也是现实生活中的具体事物,也可以通过java的类的形式进行描述,并封装成对象
其实就是java对不正常情况进行描述后的对象体现
对于问题的划分分为两种:
一种是严重的问题,一种非严重的问题
对于严重的,java通过Error类进行描述
对于error一般不编写针对性的代码对其处理
对于非严重的,java通过Exception类进行描述
对于exception可以使用针对性的处理方式进行处理
无论error或者exception都具有一些共性内容
比如:不正常情况的信息,引发原因等
异常继承关系:
Throwable
------Error 严重错误
------Exception 非严重错误
-----------RuntimeException 运行时错误,不会在编译时期发现
异常处理步骤:
Try
{
需要检测的代码
}
Catch(异常类 变数)
{
处理方法
}
Finally
{
一定会执行语句
}
对捕获到的异常对象进行常见方法操作:
getMessage();//异常信息
toString();//异常名称:异常信息
printStackTrace();//异常名称,异常信息,异常出现的位置
//其实jvm默认的异常处理机制,就是在调用printStackTrace方法,打印异常的堆栈的跟踪信息
异常在子父类覆盖中的体现
1. 子类在覆盖父类时,如果父类的方法抛出异常,那么子类的覆盖方法只能抛出父类的异常或者该异常的子类
2. 如果父类方法抛出多个异常,那么子类在覆盖该方法时只能抛出父类异常的子集
3. 如果父类或者接口的方法中没有异常抛出,那么子类在覆盖方法时也不可以抛出异常,如果子类方法发生了异常,就必须要进行try处理,绝对不能抛出
对多异常的处理
1. 声明异常时,建议声明更为具体的异常,这样处理可以更具体
2. 对方声明几个异常,就应该有几个catch
3. 如果多个catch块中异常存在继承关系,父类异常catch放在子类后面
4. 建立在进行catch处理时,catch中一定要定义具体处理方式,不要简单定义一句
e.printStackTrace();也不要简单的就书写一句输出语句等
自定义异常
出现背景:因为项目中会出现特有的问题,而这些问题并未被java所描述并封装对象,所以对于这些特有的问题可以安装java对问题的封装思想,将特有的问题,进行自定义的异常封装
当函数内部出现了throw抛出异常对象,那么就必须要给对应的处理动作,要么在函数内try
catch处理,要么就throws抛出给调用函数处理--------一般情况下,函数内部出现异常,函数上需要声明
自定义异常信息
因为父类中已经把异常信息的操作都完成了
所以子类只是在构造时,用super调用父类的构造方法把子类的信息传到父类就OK
自定义异常注意
必须是自定义类继承Exception
继承Exception原因:
异常体系有一个特点:因为异常类和异常对象都被抛出
他们都具有可抛性,这个可抛性是Throwable这个体系中独有特点
只有这个体系中的类和对象才可以被throws和throw操作
如果该异常的发生,无法在继续进行运算,就让自定义异常继承RuntimeException
异常其他细节
Throws和throw区别
1.Throws使用在函数上
2.Throw使用在函数类
3.Throws后面跟异常类,可以跟多个,用逗号隔开
4.Throw后面跟对象
RuntimeException
如果该函数内容抛出该异常,函数上可以不用声明,编译一样通过(可处理可不处理的异常)
如果在函数上声明了该异常,调用者可以不用进行处理,编译也可以通过
-------之所以不用在函数声明,是因为不需要调用者处理,当该异常发生,希望程序停止。因为在运行时,出现了无法继续运算的情况,希望停止程序后,对代码修正
对于异常分为两种:
1. 编译时被检测的异常
2. 编译时不被检测的异常(运行时异常,runtimeexception以及它的子类)
Finally
定义:无论该异常是否执行都会执行finally代码块里面的内容主要用来释放资源
注意:
Catch是用于处理异常,如果没有catch就代表异常没有被处理,如果该类是检测时异常就必须声明
在异常处理的过程中:编译时异常(checked Exception)在向上抛的时候应该将其转换为运行时异常(unchecked
Exception),编译时异常抛给调用者没有太多的意义,会给上层程序造成麻烦,但是这里发生的异常应该上层调用者知道又不给上层程序带来麻烦,所以将其转换为运行时异常,上层程序可处理可不处理(注意:在向上抛运行时异常时,应该将本异常信息(catch到的异常)封装进行里面,异常链不能断)
示例:
Try{…}catch(Exception e){throw new RuntimeException(e);}
多线程
进程:是一个正在执行中的程序,每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元
线程:就是进程中的一个独立的控制单元,线程在控制进程的执行
一个进程中至少有一个线程
进程是一个静态的概念,进程执行指的是进程中的主线程开始执行,而非进程本身执行
Java VM
启动的时候就会有一个进程java.exe,该进程中至少有一个线程负责java程序的执行,而且这个线程运行的代码存在于main方法中,该线程称之为主线程
扩展:其实更细节说明虚拟机,JVM启动不止一个线程,还有负责垃圾回收机制的线程
封装(Encapsulation)
定义:是指隐藏对象的属性和实现细节,仅对外提供公有访问方式
好处:将变化隔离、便于使用、提高重用性、提高安全性
原则:将不需要对外提供的内容都隐藏起来
把属性都隐藏,提供公用方法对其访问
权限:
Public(公有)
Private(私有)
默认(什么都不写)
Protected(保护,在包中使用较多)
一个.java档里面,不能出现两个或者以上的public类或者接口
不能修饰方法中的变量,只能修饰成员(成员变量,成员方法)
继承(Inheritance)
(1).提高了代码的复用性
(2).让类与类之间产生了关系,有了这个关系,才有了多态的特效
注意:千万不要为了获取其他类的功能简化代码而继承,必须是类与类有所属关系才可继承。所属关系 is
a
(3).java语言中:java只支持单继承,不支持多继承(接口支持)
多继承容易带来安全隐患:当多个父类中定义了相同功能,但功能内容不同时,子类对象不确定运行那个
Java保留了这种机制,并用另一种体现形式来表现,叫多实现也就是接口
Java支持多层继承,也就是一个继承体系
如何使用一个继承体系中的功能呢?
要想使用体系,先查阅体系父类的描述,因为父类中定义的是该体系中共性功能。通过了解共性功能,就可以知道该体系的基本功能,那么这个体系基本可以使用了;那么在具体调用时,要创建最子类对象,为什么?一是因为有可能父类不能创建对象(如抽象类),二是创建子类对象可以使用更多功能,包括基本的也包括特有的。
简单一句话:查阅父类功能,创建子类对象使用功能。
聚集: has a
聚合:球队中的球员
组合:人和人的手
多态
定义:可以理解为事物存在的多种体现形态
1.
多态的体现
父类的引用指向了自己子类的对象
父类的引用也可以接收自己的子类对象
2.
多态的前提
必须是与类之间有关系,要么继承,要么实现
通常还有一个前提,存在覆盖
3.
多态的好处
多态的出现大大的提高程序的扩展性
4.
多态的应用
通过一个函数调用这个继承体系中的某一个方法
5.
多态在代码中的特点(多态使用的注意事项)
成员函数的特点:
在编译时期:参阅引用型变量所属的类中是否有调用的方法,如果有,编译通过,否则失败
在运行时期:参阅对象所属的类中是否有调用方法。
简单结论:成员函数在多态调用时,编译看左边,运行看右边
在多态中成员变量的特点:
无论编译和运行,都参阅左边(引用类型变数所属的类)
在多态中,静态成员函数的特点:
无论编译和运行都参考左边
6.
多态的弊端
提高了扩展性,但是只能使用父类的引用访问父类的成员(不能直接通过父类引用访问子类特有成员)
理论概念
在实际开发中其实就是
找对象----建立对象----使用对象----维护对象之间关系
先有对象还是先有类:答案不确定,对于计算机而言是先有类再有对象,而实在生活中我们是先看到对象才会去描述这个对象(描述就是提取对象中的共性内容,对具体的抽象)
什么是类:类是一个静态属性和动态可执行操作的结合(对现实生活中的事物的描述)
对象:就是这类事物,实实在在存在的个体(类的实例化)
映像到java中,描述就是class定义的类,具体对象就是对应java在堆内存中用new建立的实体。
设计模式
解决某一类问题最行之有效的方式
Java中有23种设计模式
单例设计模式:
让一个类在内存只有一个对象
保证对象的唯一性:
1. 为了避免其他程序过多的建立该类对象,所以必须禁止其他程序建立该类对象(构造方法私有化)
2. 但是其他程序有可以访问到该类对象,只好在本类中,自定义一个物件(private static A aa = new A();)
3. 为了方便其他程序对自定义对象的访问,可以对外提供一些访问方式(提供一个公有方法返回我们aa对象的地址)
两种方式:
1. 饿汉式:就是先初始化对象,一般开发中常用
2. 懒汉式(面试常考):对象是方法被调用时,才初始化,也叫对象的延时加载(特点),只有调用提供给用户的方法时候才在内存中new对象
---------------在实际开发中区别不大,因为要创建这个对象肯定要用,不用就不用创建,一般使用饿汉式,因为比较直观安全(这里要涉及到CPU的知识,CPU在某一个时刻只能处理一个程序,多线程同步的问题)!懒汉式在多线程中存在安全隐患,可以使用同步函数来解决,但是会影响程序效率,解决这个问题可以通过双重判断来实现。
自定义线程
通过对API的查找,java已经提供了对线程这类事物的描述,Thread类。
第一种方式
一个类继承Thread并重写run方法(目的就是将自定义代码存储在run方法中,让线程运行),调用start方法,start有两个作用:启用线程 调用run方法
发现每一次运行结果都不同:因为多个线程都获取CPU的执行权,CPU执行到谁,谁就运行,明确一点,在某一个时刻,只能有一个程序在运行,多核除外;CPU在做着快速的切换,以达到看到去是同时运行的效果,我们可以形象把多线程的运行行为看作在互相抢夺CPU执行权,这就是多线程的一个特效:随机性,谁抢到谁执行,至于执行多长CPU说了算!
为什么要覆盖run方法呢?
Thread类用于描述线程,该类就定义了一个功能,用于存储线程要运行的代码,该存储的功能就是run方法;也就是说Thread类中的run方法用于存储线程要运行的代码。
局部变量在每一个线程区域当中都有独立的一份
线程类中常见的几个方法:
Static Thread currentThread();获取当前线程物件
getName();获取线程名称
设置线程名称:SetName或者构造函数
创建线程的第二种方式,实现runnable接口
步骤:
1. 定义类实现runnable接口
2. 覆盖runnable接口中的run方法
将线程要运行的代码存放在该run方法中
3. 通过Thread类创建线程对象
4. 将runnable接口的子类对象作为实际参数传递给Thread类的构造函数
Why?因为,自定义的run方法所属对象是runnable接口的子类对象,所以要让线程去指定对象的run方法,就必须明确该run方法所属对象
5. 调用Thread类的start方法开启线程并调用runnable接口的子类run方法
实现方式和继承方式有什么区别呢?
实现方式好处:避免了单继承的局限性,在定义线程时,建议使用实现方式
两种方式的区别:
继承Thread:线程代码存放在Thread子类run方法中
实现runnable:线程代码存放在接口的子类的run方法中
线程的同步:(同步有两种表现形式:同步代码块和同步函数)
java对于多线程的安全问题提供了专业的解决方式就是->同步代码块
synchronized(对象)//对象就是锁
{
需要被同步的代码
}
对象如同锁,持有锁的线程可以在同步中执行,没有持有锁的线程即使获取了CPU的执行权也进不去,因为没有锁
形象的说明就像:火车上的卫生间
Synchronized(类对象名aa)的含义:判断aa是否已经被其他线程霸占,如果发现已经被其他线程霸占,则当前线程陷入等待中,如果发现aa没有被其他线程霸占,则当前线程霸占住aa对象,并执行同步代码块,在当前线程执行同步代码块时,其他线程将无法在执行同步代码块中的代码,当前对象执行完同步代码块后,会自动释放aa对象的霸占,此时其他线程会互相竞争对aa的霸占,最终CPU会选择其中的某一个线程执行
最终导致的结果是:一个线程正在操作某一个资源的时候,将不允许其他线程操作操作该资源,即一次只允许一个线程该资源
同步的是实现(synchronized(监视器)):
Synchronized语句会有一个监视器,也叫锁,锁有两种状态(锁旗标),分别是零和一,默认为1,当有线程进入锁的时候,锁的标志位自动变为0,这个时候其他线程则不能进入;当该线程执行完同步中的代码时,监视器又会自动将锁旗标改为1,等待下一个线程的进入!
同步的前提:当使用了synchronized后没有达到效果,就应该看看条件是否满足
1.必须要有两个或者两个以上的线程
2.必须是多个线程使用同一个锁
必须保证同步中只有一个线程在运行,这个也是使用同步的目的
非静态的同步函数使用的是this锁,静态的同步函数使用的是*.class锁
好处:解决了多线程的安全问题
弊端:多个线程都需要判断锁,较为消耗资源
如何找问题
1.明确那些代码是多线程运行代码
2.明确共享资料
3.明确多线程运行代码中那些语句是操作共享数据的
同步函数
同步函数用的是那一个锁呢?
函数需要被对象调用,函数都一个所属对象的引用this,所以同步函数使用的锁是this
如果同步函数被静态修饰后,使用的锁是什么?
通过验证,发现不再是this,静态中没有this
静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象
类名.class 该对象的类型是Class
静态的同步方法,使用的锁是该方法的所在类中的字节码文件对象。
死锁:
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
导致死锁的根源在于不适当地运用“synchronized”关键词来管理线程对特定对象的访问。“synchronized”关键词的作用是,确保在某个时刻只有一个线程被允许执行特定的代码块,因此,被允许执行的线程首先必须拥有对变量或对象的排他性的访问权。当线程访问对象时,线程会给物件加锁,而这个锁导致其它也想访问同一物件的线程被阻塞,直至第一个线程释放它加在对象上的锁。
线程之间的通信
定义:其实就是多个线程在操作同一个资源,但是操作不同(InputOutputDemo.java)
线程等待机制
Wait; 等待
Notify(); 唤醒线程池中的第一个
notifyAll(); 唤醒线程池全部线程
都使用在同步中,因为要对持有监视器(锁)的线程操作,所以要使用同步中,因为只有同步才具有锁。
为什么这些操作线程的方法要定义在object类中呢?
因为这些方法在操作同步中线程时,都必须要表示它们所操作线程持有的锁,只有同一个锁上的被等待线程,可以被同一个锁上notify唤醒,不可以对不同锁中的线程进行唤醒。
也就是说,等待和唤醒必须是同一个锁
而锁可以是任意对象,所以可以被任意对象调用的方法定义在object类中
对于多个生产者和消费者为什么要定义while判断标记?
原因:让被唤醒的线程再一次判断标记
为什么要使用notifyAll?
原因:因为需要唤醒对方线程,notify容易出现只唤醒本方线程的情况,导致程序中的线程都等待。
If notify 只能用于一个线程生产一个线程消费
While notifyAll 用于多个线程生存和多个线程消费
JDK1.5 中提供了多线程升级解决方案
将同步Synchronized替换成了显示Lock操作
将object中的wait,notify,notifyall,替换成了condition物件
该对象可以Lock锁,进行获取