1 泛型
1.1 解释
泛型通俗易懂就是接收的参数,类型并不固定。
1.2 疑问
Object不就是可以变成任何类型吗,为何还需要泛型?那我们来看下列实例
user实体类
如果参数是String我们强转成Integer,就会发生报错
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
参数String不能强转Integer,我们传参不能保证Object向下转型是否是安全的,也就是参数类型是否对应,而 泛型 就能完美的解决我们的问题,请看下列实例
实体类
同样的例子,换成泛型不仅不需要强转了而且还会直接警告,数据类型得到了保证。
1.3 使用语法
泛型类定义:
尖括号里面随便输入字母都可以但一般会使用 T
因为翻译为Type 而且可以输入多个如<T , E>使用逗号隔开,
此图T就是泛型可以当做属性的类型 。
具体使用:
创建对象时在尖括号里输入你需要的数据类型,T就变成相应的类型,图中为String,
如果有多个泛型,那就需要对应的数量
1.4 限制
泛型的限制就是只能输入引用类型,输入基本类型就会报错,不过我们可以用包装类
1.5 通配符
在开发中引用对象是非常常见的,在泛型类的操作中类型是必须要对应才能传递,而通配符就是可以传递任意,不用局限于比如user_4<String>
使用语法
方法名(形参对象<?> 名称)
在方法形参中,对象泛型设为通配符 ?如上图。
1.6 受限泛型
1.6.1 上限
使用语法
方法名(形参对象<? extends 最高上限引用类型> 名称) , 示例如下图
泛型上限,也就是设置泛型最高上限类型,上图的意思是,限制泛型只能设置为Number以及他的子类,如Integer,可是他设置Object(父类)就会报错
1.6.2 下限
使用语法
方法名(形参对象<? super 最低上限引用类型> 名称) , 示例如下图
泛型下限,也就是设置泛型最低下限类型,上图的意思是,限制泛型只能设置为Number以及他的父类类,如Object ,可是他设置Integer(子类)就会报错
1.7 泛型接口
语法
访问修饰符 接口关键字 接口名<泛型标识...>
public interface val<T>{
T show(T t);
}
在实现泛型接口时有两种方式:
第一种,可以在实现时为接口的泛型,写入引用类型
class valimpl implements val<String,Integer>{
@Override
public String show(String s, Integer i) {
return s;
}
}
第二种,泛型类实现,泛型接口直接用泛型写入不过,泛型一定要对应!
class valimpl<T,E> implements val<T,E>{
@Override
public T show(T s, E i) {
return s;
}
}
1.8 泛型方法
语法
访问修饰符 <泛型标识...> 方法名( 可以是前面定义的泛型 参数名 )
示例
2 注解
2.1 解释
在编程中我们了解注释,有三种单行注释、多行注释、以及文本注释,注释是给我们程序员看的,而注解就是给我们程序看的,注解可以加在方法、类、属性等等上,就跟注释一样取决我们想怎么用,他的本质其实就是跟注释一样他本身的翻译就是Annotation(注释)。
注解分为三种,分别是预定义注解、自定义注解、元注解。
2.2 预定义注解
预定义注解就是Java为我们已经定义好的,如重写的override主要用于在子类重写父类方法上可以见到。
预定义注解是被java内部自己扫描,就跟程序员一样,看到什么样的注释,让代码干什么事当然需要遵守定义的规范。
2.3 自定义注解
自定义注解顾名思义就是我们自己定义的注解,框架里的注解就属于这种,不过是框架自己扫描了,注解本身并没有什么用处就跟注释一样,如果被反射扫描的话主要提供一个坐标一样的作用,就如同Spring框架的@Controller注解,扫描之前需要开启注解,还需要提供扫描包的位置(ssm),
语法
访问修饰符 @interface 注解名{
可以新建属性 还可以定义默认值
}
public @interface test_my {
String value() default "";
// 也可以设置为数组等
// String[] values() default "";
}
在新建类中选择最下面可以创建注解类
使用案例
@test_my //注解1
public class test {
@test_my
public String n ;
@test_my("值") //注解2
public String name ;
@test_my(value = "值") //注解3
public static void main(String[] args) {
}
}
如果定义了属性我们可以在注解里输入值 如 注解3,如果设置了默认值可以不写 如 注解1,如果你属性中的名称是value那么可以省略,直接赋值 如 注解2 如果你想赋值数组需要,
value={"值","值2"} 如果是单个数值那就可以跟 注解3 一样使用 value="值"
自定义注解想要有作用需要用 反射,反射的具体使用可以在本文反射查看,
根据反射,获取到注解的位置与值就能进行一系列的操作了.
2.4 元注解
解释
所谓元注解,就是加在注解上的注解,在java7之前有四个他们分别是@Retention、@Target、@Documented和@Inherited.
- @Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
- @Documented - 标记这些注解是否包含在用户文档中。
- @Target - 标记这个注解应该是哪种 Java 成员(可多选,不写为全部)。
- @Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)
@Retention
源码时显示: SOURCE, 字节码时显示: CLASS (默认) , 运行时显示反射 RUNTIME
@Target
使用在类上:TYPE , 使用在属性上: FIELD , 使用在方法上: METHOD ,
使用在参数: PARAMETER , 使用在构造方法上:CONSTRUCTOR
@Documented
在生成api文档是是否含有该注解。
@Inherited
让@Inherited 所在的注解,他加在A类,A类直接让A的子类也有相同的@Inherited 所在的注解
@Target @Retention示例
@Inherited 示例 @My自定义注解所在的Mytest类被Myez继承,
是Mytest类的子类我们清晰的能看到,他是有My这个注解的,而他的父级与没继承他的类我们能看到都是为null。
3 反射
3.1 解释
反射(reflection)我们可以简单的理解为,反过来的映射 从哪里反?
我们平时创建对象都是直接new的,而反射我们直接反着来 ,直接获取被javac编译后的字节码文件并创建字节码对象(Class),在程序运行时JVM会用类加载(ClassLoader)把字节码文件加载到JVM方法区并转换成java.lang.Class对象, 我们可以通过字节码对象“反射”直接从JVM方法区获取各种对象。
反射是很多框架的底层原理比如Spring、Mybatis框架便是如此,反射是我们通往更高的级别以及了解底层原理是非常重要的,使用反射可以让jvm动态起来而不是只有静态,更好的提高了java的灵活性。
3.2 获取Class的四种方式
package write.w1;
public class user { //实体类
private String name;
public Integer age;
}
3.2.1 第一种: Class.forName("文件路径")
public class demo {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("write.w1.user");
}
}
一般用于加载资源文件等等。
3.2.2 第二种:类名.class
public class demo2 {
public static void main(String[] args) {
Class<?> userClass = user.class;
}
}
第二种一般用于赋值等等操作。
3.2.3 第三种:实例对象.getClass
public class demo3 {
public static void main(String[] args) {
user user = new user();
Class<?> aClass = user.getClass();
}
}
第三种一般用于获取对象信息。
3.2.4 第四种:使用类加载器(classLoader)获取 classLoader.loadClass("文件路径")
public class demo4 {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader = demo4.class.getClassLoader();
Class<?> aClass = classLoader.loadClass("write.w1.user");
}
}
第四种了解就好,与其他三个相比不常用。
3.3 获取对象
使用字节码对象.newInstance(); 直接创建,不过在java8以后就过时了,原因可能就是这样获取的只能是无参构造方法,过时肯定有替代,我们可以在构造方法里找平替。
public class demo {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
Class<?> aClass = Class.forName("write.w1.user");
user o = (user) aClass.newInstance();
System.out.println(o);
}
}
3.4 获取构造方法
3.4.1 获取公有的(Constructor)
使用字节码对象.getConstructor( ); 括号里可以加 参数,要跟实体类对象方法一样 没有就是无参构造,创建对象就是在后面加.newInstance(); 即可
public class demo2 {
public static void main(String[] args) throws Exception {
Class<?> userClass = user.class;
Constructor<?> constructor = userClass.getConstructor();
//创建对象
user o = (user) constructor.newInstance();
}
}
还有一个方法是获取数组的构造函数,把所有的访问修饰符为公有的都获取到。
public class demo2 {
public static void main(String[] args) throws Exception{
Class<?> userClass = user.class;
Constructor<?>[] constructors = userClass.getConstructors();
for (Constructor<?> constructor : constructors) {
user o = (user) constructor.newInstance();
}
}
}
3.4.2 获取私有的(DeclaredConstructor)
使用字节码对象.getDeclaredConstructor( ); 括号里可以加 参数,要跟实体类对象方法一样 没有就是无参构造,创建对象就是在后面加.newInstance(); 即可,这个是所有的构造方法都可以获取到如果是私有的,他需要开启访问权限即为:构造方法对象.setAccessible(true);
public class demo3 {
public static void main(String[] args) throws Exception {
user user = new user();
Class<?> aClass = user.getClass();
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor();
//开启访问权限
declaredConstructor.setAccessible(true);
user o = (user) declaredConstructor.newInstance();
System.out.println(o);
}
}
跟公有的一样也有一个数组的方法
public class demo3 {
public static void main(String[] args) throws Exception {
user user = new user();
Class<?> aClass = user.getClass();
Constructor<?>[] declaredConstructors = aClass.getDeclaredConstructors();
//开启访问权限
for (Constructor<?> declaredConstructor : declaredConstructors) {
declaredConstructor.setAccessible(true);
user o = (user) declaredConstructor.newInstance();
System.out.println(o);
}
}
}
3.5 获取属性
3.5.1 获取公有的(Field)
使用字节码对象.getField("属性名")获取属性,加.set("对象",值)就是为对象里的属性赋值
加.get(对象)就是输出哪个对象里面的哪个属性值,注:能获取父级(是长辈都行)的公有属性。
public class demo1 {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = demo1.class.getClassLoader();
Class<?> aClass = classLoader.loadClass("write.w1.user");
user o = (user) aClass.getConstructor().newInstance();
aClass.getField("age").set(o,6);
Object age = aClass.getField("age").get(o);
System.out.println(o+"user对象"+age);
//值为: user{name='null', age=6}user对象6
}
}
这个获取属性公有的,方法可以获取父级(是长辈都行,都能获取到)公有的属性,就以数组演示
数组也是 使用字节码对象.getFields(),获取所有的公有的属性
public class demo2 {
class S{
public String name_S;
private String value_S;
}
class A extends S{
public String name_A;
private String value_A;
}
class B extends A{
public String name_B;
private String value_B;
}
public static void main(String[] args) throws Exception {
Class<B> aClass = B.class;
Field[] fields = aClass.getFields();
for (Field field : fields) {
System.out.println(field);
}
}
}
//public java.lang.String write.w2.demo2$B.name_B
//public java.lang.String write.w2.demo2$A.name_A
//public java.lang.String write.w2.demo2$S.name_S
我们看下面的注释就是结果能看到只打印了三个name对象属性他们都是 public公有的
3.5.2 获取私有的(DeclaredField)
使用字节码对象.getDeclaredField("属性名")获取属性,加.set("对象",值)就是为对象里的属性赋值, 加.get(对象)就是输出哪个对象里面的哪个属性值。
私有的需要开启权限: 属性对象.setAccessible(true); 注:DeclaredField只能获取当前类所有属性
public class demo3 {
public static void main(String[] args) throws Exception {
Class<?> aClass = Class.forName("write.w1.user");
user o = (user) aClass.newInstance();
Field name = aClass.getDeclaredField("name");
name.setAccessible(true); //开启权限
name.set(o,"这个类型是");
System.out.println(o+"方法类型"+name.getType()+"这个name的值是:"+name.get(o));
//结果:user{name='这个类型是', age=null}方法类型class java.lang.String
// 这个name的值是:这个类型是
}
}
上图实体类,name属性是私有的,获取私有的也是有数组的
public class demo4 {
class C{
public String name_C;
private String value_C;
}
class D extends C {
public String name_D;
private String value_D;
}
public static void main(String[] args) throws Exception {
Class<D> aClass = D.class;
Field[] declaredFields = aClass.getDeclaredFields();
for (Field field : declaredFields) {
System.out.println(field);
}
//public java.lang.String write.w2.demo4$D.name_D
//private java.lang.String write.w2.demo4$D.value_D
//final write.w2.demo4 write.w2.demo4$D.this$0 这个不用管 是 地址 对象 this
}
如果只是打印不进行赋值等操作是可以不用开启权限的 , 如图所示他只打印自己类的属性。
获取方法里使用的实体类
public class Cat extends zoon {
public Cat() {
}
public void show(String s) {
System.out.println("Cat公有的show方法"+s);
}
private void show_s() {
System.out.println("Cat私有的show_s方法");
}
}
class zoon{
public zoon() {
}
public void publi(String s) {
System.out.println("zoon公有的publi方法"+s);
}
private void privat() {
System.out.println("zoon私有的privat方法");
}
}
3.6 获取方法
3.6.1 获取公有的(Method)
使用字节码对象.getMethod(“方法名称”,"需要参数可以在这边添加类型 如果是无参可以不填"...) 方法对象.invoke("实例对象","需要参数可以这边写"...) 可以让实例对象调用方法
public class demo1 {
public static void main(String[] args) throws Exception {
Class<Cat> catClass = Cat.class;
Cat cat = catClass.getConstructor().newInstance();
Method show = catClass.getMethod("show", String.class);
show.invoke(cat,"6");
}
}
//结果: Cat公有的show方法6
方法也是有数组获取方式的使用字节码对象.getMethods(); 获取的也包裹父级(辈)的公用方法
public class demo2 {
public static void main(String[] args) throws Exception {
Class<Cat> catClass = Cat.class;
Method[] methods = catClass.getMethods();
for (Method method : methods) {
System.out.println(method);
}
}
}
//public void write.w3.Cat.publi(java.lang.String)
//public void write.w3.Cat.show(java.lang.String)
//下面都是Object的public方法
//public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
//public final void java.lang.Object.wait() throws java.lang.InterruptedException
//public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
//public boolean java.lang.Object.equals(java.lang.Object)
//public java.lang.String java.lang.Object.toString()
//public native int java.lang.Object.hashCode()
//public final native java.lang.Class java.lang.Object.getClass()
//public final native void java.lang.Object.notify()
//public final native void java.lang.Object.notifyAll()
众所周知Object是所有类的父级,所以也就把他所有的public方法也打印了
3.6.2 获取私有的(DeclaredMethod)
使用字节码对象.getDeclaredMethod(“方法名称”,"需要参数可以在这边添加类型 如果是无参可以不填"...) 方法对象.invoke("实例对象","需要参数可以这边写"...) 可以让实例对象调用方法
public class demo3 {
public static void main(String[] args) throws Exception {
Class<Cat> catClass = Cat.class;
Cat cat = catClass.getConstructor().newInstance();
Method show = catClass.getDeclaredMethod("show_s");
show.setAccessible(true); //可不能忘记开启权限
show.invoke(cat);
}
}
//结果:Cat私有的show_s方法
数组获取方式的使用字节码对象.getDeclaredMethod(); 获取的只能是当前类的所有方法
public class demo4 {
public static void main(String[] args) throws Exception {
Class<Cat> catClass = Cat.class;
Method[] methods = catClass.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method);
}
}
}
//public void write.w3.Cat.publi(java.lang.String)
//public void write.w3.Cat.show(java.lang.String)
//private void write.w3.Cat.show_s()
没有赋值或调用所以不用开启权限
为什么Cat类会有publi方法因为子类默认会重写父类公有的方法
如:方法名(参数){ super.方法(参数); }
因此所以继承就能直接调用父类的方法。
3.7 获取注解
使用字节码对象.get你注解写到什么上面就写什么(方法、属性等等)
.getAnnotation("注解的字节码对象")
@lby("什么鬼")
public class test {
public static void main(String[] args) {
Class<test> testClass = test.class;
lby annotation = testClass.getAnnotation(lby.class);
System.out.println(annotation.value());
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface lby{
String value() default "";
}
//结果:什么鬼
反射的.get属性、构造、方法、类他们都有.getAnnotation,同样也有注解的数组。
3.8 泛型、反射、注解结合小练习:
构建MybatisPlus框架最底层,如果想要结合练习可以去试试
http://t.csdn.cn/s8aFhhttp://t.csdn.cn/s8aFh
4 多线程
4.1 进程与线程
进程就是所谓 “进行中的程序” 而线程也被称为“轻量级进程”是“CPU调度”的最小单元,线程可以理解为进程中执行的任务,一个进程至少有一个或多个线程组成。
CPU调度就是一种分配CPU资源的过程,就是CPU内存有限只能执行一个或俩决定谁先执行的然后分配的过程。
4.2 并发与并行
并发我们可以理解为,我们CPU是一核的每次只能执行一个东西,但是他交换着执行不会一下执行完,A执行0.1秒后B再执行0.1秒不过顺序 是不固定的有可能A执行了一大半才开始执行B 但是我们人眼观察的是 “宏观” 的也就是感觉不到他在交替,感觉像是同时执行。
并行就是我们所理解的同时执行。
在CPU为单核就是并发,因为没有“物理条件”,而CPU是多核就是并发且并行着同时执行一部分程序,同时交替着一部分程序。
在Java中垃圾回收机制就是多线程(GC与main主线程),我们并没有自己回收。是GC(垃圾回收器)在我们程序运行时同时帮我们把执行过的变量与对象等等回收。
4.3 创建线程类的三种方式
4.3.1 第一种方式
继承Thread(线程类)并重写run方法,方法里面就是写我们要执行此线程的任务,写循环是为了能直观的看到 并发 此线程与主线程的穿插执行。
public class MyThread extends Thread{
@Override
public void run() {
for (int i=0;i<11;i++){
System.out.println("My线程:"+i);
}
}
}
Test测试类 .start就是开启线程的方法。
public class Test {
public static void main(String[] args) {
//创建实例对象
MyThread myThread = new MyThread();
//开启线程
myThread.start();
for (int i=0;i<11;i++){
System.out.println("main主线程:"+i);
}
}
}
我在主线程(main)中设置了循环,在下图看结果。
能明显看到线程的运行,是交替的而且是没有规律的随机的有兴趣可以自己试试。
4.3.2 第二种方式
实现Runnable接口,这个与第一种的运行方式不同需要再创建一个Thread线程类让这个线程类对象执行。
比起第一种的优点:java中类只能单继承但实现可以多个接口,第一种直接占用了我们继承的位置,而实现Runnable接口的方式这样更利于我们业务的操作。
public class MyThreadTwo implements Runnable {
@Override
public void run() {
for (int i=0;i<11;i++){
System.out.println("MyTwo线程:"+i);
}
}
}
TestTwo测试类,这种方式需要再创建一个线程类接受,让线程类实例对象开启线程。
public class TestTwo {
public static void main(String[] args) {
//创建实例对象
MyThreadTwo myThreadTwo = new MyThreadTwo();
//创建线程类实例对象
Thread thread = new Thread(myThreadTwo);
thread.start();
for (int i=0;i<11;i++){
System.out.println("main主线程:"+i);
}
}
}
4.3.3 第三种方式
实现Callable接口经过FutureTask包装器来建立Thread线程类对象。
MyThread类实现Callable接口<泛型>这个是有返回值的,这种创建方式被称为是第二种Runnable的进一步封装,因为FutureTask类他实现的RunnableFuture接口的父亲是Runnable。
public class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 0;
}
}
Test测试类
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();
//把Callable实现类MyThread实例传入FutureTask类
FutureTask<Integer> futureTask = new FutureTask<Integer>(myThread);
//把FutureTask类实例传入线程类里
Thread thread = new Thread(futureTask);
}
}
4.4 线程方法
多线程中也是有许多的方法的比如命名与延时等等。
4.4.1 线程名Name
默认的线程名是Thread+n这个n就是线程数从零往上加,主线程叫做main线程名 方法有三种
第一种是必须在继承Thread线程类才能使用的this.getName与this.setName,
第二种是Thread.currentThread().getName()这个方法是因作用范围比较广泛所以用的比较多。
第三种是Runnable接口的实现类,在把实例给Thread对象的时候把名称传过去。
Thread点里面的currentThread(当前线程)方法里的get与setName方法。
第一种方法运用
public class MyThread extends Thread{
@Override
public void run() {
System.out.println(this.getName()+"线程");
this.setName("MyThread");
System.out.println(this.getName()+"线程");
}
}
第二种方法运用
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
System.out.println(Thread.currentThread().getName()+"线程");
Thread.currentThread().setName("我定义的主main");
System.out.println(Thread.currentThread().getName()+"线程");
}
}
运行效果:
第三种方法运用
跟创建线程类第二种方法相结合使用。
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread,"线程名");
4.4.2 线程延时方法sleep
Thread线程类中的sleep()静态方法,参数是毫秒,是指让方法后面的代码都延时,一般用在线程里而不是主线程里这里演示使用的主线程,延时同时把cpu的时间段交给别的线程,但是“锁”不会给这个“锁”后面会讲到。
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
//延时,方法里的单位是毫秒
myThread.sleep(1000);
myThread.start();
//因为是静态方法所以可以直接调用
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"中的");
}
}
这里有动图,首先是执行的是延时1秒,然后执行后面的myThread线程因为main线程的上面也加了个延时1秒所以最后出现main打印。
4.4.3 线程优先级方法Priority
线程类.setPriority方法参数是整型1-10默认是5最大为10最小为1但是执行不一定按照设置优先级执行,跟顺序与优先级级别有关系,顺序靠前执行第一次几率大一些,级别更高执行几率更高一些但也是不能确定。
MyThread类:
public class MyThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程中的run");
}
}
Test测试类:
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread);
Thread thread2 = new Thread(myThread);
Thread thread3 = new Thread(myThread);
thread3.setPriority(10);
thread2.setPriority(5);
thread1.setPriority(1);
thread1.start();
thread2.start();
thread3.start();
}
}
运行结果: 结果随机
4.4.4 线程放弃当前线程方法yield
线程类中的静态yield方法,有着让已经获取到CPU时间段的线程把这一次获取到的时间段让出去回到就绪状态,重新再次争夺时间段,这个方法主要应用在这个线程优先级低要让高线程执行的时候调用
//线程类
public class MyThread extends Thread{
@Override
public void run() {
//把刚抢到的时间段扔出去,再次去重新争夺时间段
Thread.yield();
System.out.println(this.getName()+"线程执行");
}
}
Test测试类
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();
//设置分支优先级
myThread.setPriority(1);
//运行线程,变为可以去争夺时间段的就绪状态
myThread.start();
//设置主线程优先级
Thread.currentThread().setPriority(10);
System.out.println(Thread.currentThread().getName()+"线程执行");
}
}
运行结果:能看到级别低也是能运行的不过在五次的运行里运行的较为少
4.4.5 线程等待方法join
这个方法与延时方法类似,不过join是最多等待而不是必须等待,而sleep是必须等待设置多少时间就要多少时间后执行,join是可以不设置值默认就是比如:A线程设置了join()里面不设置值就是A线程执行完下面的代码才能执行。 如果设置了值就是最多需要等待多久,但设置的线程执行完了也会执行下面的代码不比等待完整时间,除非直接设置在线程类里面。
join的时间 小于 sleep的时间,那么sleep继续运行,不过join时间已经运行了。
join的时间 大于 sleep的时间,那么sleep就会接着运行,运行时间是sleep的时间会把 join 的时间顶替;
线程类MyThread
public class MyThread extends Thread {
@Override
public void run() {
try {
System.out.println("join是否执行完了");
Thread.sleep(2000);System.out.println(this.getName()+"线程");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
测试类Test
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
MyThread myThread3 = new MyThread();
myThread1.start();
myThread1.join(1500);
System.out.println(Thread.currentThread().getName());
Thread thread = new Thread(){
@Override
public void run() {
System.out.println(this.getName()+"线程:只有我死了你才能执行");
}
};
thread.start();
thread.join();
System.out.println("啊对对对");
}
}
运行结果: join先执行,1.5秒时间到了就执行后面的主方法,与join不加参数就是必须thread结束后才能执行后面的啊对对对,然后MyThread里面的sleep时间到了执行后面的线程名。
4.4.6 线程守护方法Daemon
此方法是开启守护线程就是相当于暗中运行的代码,在其他线程运行完,不管守护线程有没有运行完都会结束,我们JVM的GC(垃圾回收器)就是守护线程。
MyThread类
public class MyThread extends Thread{
@Override
public void run() {
for(int i=0;i<11;i++){
System.out.println(this.getName()+"第"+i);
}
}
}
Test类
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();
MyThread myThread1 = new MyThread();
myThread1.setName("1线程");
myThread1.start();
//开启守护线程
myThread.setDaemon(true);
myThread.setName("守护线程");
myThread.start();
for(int i=0;i<3;i++){
System.out.println("www");
}
}
}
测试结果:可以看到我们的守护线程也打印了如果我们把1线程去掉他就不答应了因为main线程直接执行完了,看图二。
图二
4.5 线程安全问题
我们在不同的线程中,如果公用同一个变量那么会发生什么,让我们看看下列示例:
MyThread类
public class MyThread extends Thread{
private static int X=18;
@Override
public void run() {
while (X>0){
X--;
System.out.println(Thread.currentThread().getName()+"售出一张剩余"+X+"张票");
}
}
}
Test测试类
public class Test {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
MyThread thread3 = new MyThread();
MyThread thread4 = new MyThread();
MyThread thread5 = new MyThread();
MyThread thread6 = new MyThread();
MyThread thread7 = new MyThread();
MyThread thread8 = new MyThread();
MyThread thread9 = new MyThread();
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
thread6.start();
thread7.start();
thread8.start();
thread9.start();
}
}
为什么我会要这么多线程,因为太少根本卡不出来效果,电脑配置越好越不容易出来,所以这里测试的线程多让他们好抢。
测试结果
出现同一个票数这说明什么在,剩第九张票的时候有两个线程都在那个方法体里打印了剩余九张票,那如果在剩余1的时候同时有两个线程减去1会怎么样,变为负数!这肯定是不行的所以我们要把数据给“锁”着就是别的线程在用这个变量时,你这个线程没法用得等他用完才能用。 锁分为俩种分别是自动锁(synchronized)与手动锁(lock)。
4.5.1 自动锁(synchronized)
使用语法为 synchronized (参数必须是对象){方法体},或者是当做修饰符直接加到方法上等等,这边直接把上面的MyThread修改一下,synchronized 里的需要是唯一的可以明确这一把锁的钥匙只有你设置的唯一才行比如字节码对象众所周知字节码对象都只加载一次所以无论已创建多少个MyThread类他指向的依旧是同一个字节码对象,指的是MyThread这个本身对象,里面也可以写字节码对象。
public class MyThread extends Thread{
private static Integer X=200;
@Override
public void run() {
while (true){
synchronized (MyThread.class){
if (X>0){
X--;
System.out.println(Thread.currentThread().getName()+"售出一张剩余"+X+"张票");
}else {
break;
}
}
}
}
}
测试结果: 你会发现他们就没有任何错误了因为锁定的代码都是原子操作。
4.5.2 手动锁(lock)
手动锁是直接定义一个ReentrantLock对象直接使用需要手动开启(lock)与关闭锁(unlock)。
public class MyThread extends Thread{
private Lock lock = new ReentrantLock();
private static Integer Y=50;
@Override
public void run() {
while (true){
lock.lock();
if(Y>0){
Y--;
System.out.println(Thread.currentThread().getName()+"售出一张剩余"+Y+"张票");
}else {
break;
}
lock.unlock();
}
}
}
4.5.2 关于程序死机,死锁
我们的线程A在运行锁里面的代码时,线程B是调用不了的因此如果我们锁里面有个锁需要别线程运行结束因为那个锁他占着呢,可是它里面也有一个需要我们的锁,但是我们占着呢,你需要他,他需要你就卡着了,也就是“死锁”
例子:我在你家,你在我家但门都锁着呢我们都出不去,你需要我的钥匙,我需要你的钥匙。
MyThread类
public class MyThread {
public static Object n_k=new Object();//你家钥匙
public static Object w_k=new Object();//我家钥匙
Thread t1 = new Thread(){
@Override
public void run() {
synchronized (n_k){
System.out.println("你进入我家了");
synchronized (w_k){
System.out.println("需要我家钥匙你才能出来");
}
}
}
};
Thread t2 = new Thread(){
@Override
public void run() {
synchronized (w_k){
System.out.println("我进入你家了");
synchronized (n_k){
System.out.println("需要你家钥匙我才能出来");
}
}
}
};
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.t1.start();
myThread.t2.start();
}
}
运行结果:就这样一直不动卡死了,也就是我们所说的死锁。
解决办法:
(1) 减少锁的嵌套。(2)设置锁的超时时间到点自己就解开了。(3)可以使用java的工具类Java提供了许多并发工具类,可以帮助我们避免死锁的发生。
4.6 线程通信
线程的通信,是调用方法把A类陷入等待状态,然后B类经过条件把A类唤醒(通知)结合业务逻辑就是线程通信。
所用的有四个方法一个是notify与notifyAll分别是随机唤醒一个与唤醒全部还有进入等待的方法wait方法默认值为空可以陷入等待必须等待唤醒如果设置时间的或就是以毫秒为单位,时间过后就自动解开等待模式可以避免死锁等操作。
public class MyThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.test();
}
void test(){
Water water = new Water();
ThreadGetWater getWater = new ThreadGetWater(water);
ThreadSetWater setWater = new ThreadSetWater(water);
getWater.start();
setWater.start();
}
class Water{ //水井类
public int barrel; //水容量
public boolean isWater; //是否有水
public synchronized void getWater(int x) throws InterruptedException { //取水方法
if(!isWater){
wait();//线程进入等待状态
}
barrel-=x;
isWater=false;
System.out.println(Thread.currentThread().getName()+"往水桶里中取了"+x+"毫升;水桶里有:"+barrel+"毫升水");
notifyAll(); //开启所有在等待状态的线程
}
public synchronized void setWater(int y) throws InterruptedException { //填水方法
if(isWater){
wait(); //线程进入等待状态
}
barrel+=y;
isWater=true;
System.out.println(Thread.currentThread().getName()+"往水桶里中填入了"+y+"毫升;水桶里有:"+barrel+"毫升水");
notify(); //开启在等待状态的线程 随机的
}
}
class ThreadSetWater extends Thread{
private Water water;
public ThreadSetWater(Water w) {
water=w;
}
@Override
public void run() {
try {
for (int i=0;i<11;i++){
water.setWater(1000);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
class ThreadGetWater extends Thread{
private Water water;
public ThreadGetWater(Water w) {
water=w;
}
@Override
public void run() {
try {
for (int i=0;i<11;i++){
water.getWater(1000);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
运行结果:
为什么使用wait要再synchronized修饰的方法体下因为,为了避免在wait封锁期间有其他线程篡改其的共享数据。
4.6.1wait与sleep的区别
他们的区别是
(1)wait是属于Object类下的 而 sleep属于Thread线程类的。
(2)wait进入等待的时候会释放锁,而sleep不会。
(3)使用方式不同,sleep是用时间的而wait默认是必须等待开启的。
4.6.2 notify与notifyAll的区别
他们的区别可以从后缀就看出来,All即为开启所有,而notify只是开启随机一个。
4.7 线程状态
线程中有六种状态,他们分别是:
一、初始化或创建状态(new)
二、运行状态与就绪状态(RUNNABLE) 注:就绪READY与RUNNABLE 属于一种状态就绪(READY)就是没抢到CPU时间段,还在抢 运行(RUNNABLE)是一已经抢到了在执行中。
三、阻塞(BLOCKED)
四、等待或无期等待(WAITING)
五、定时等待或超时等待(TIMED-WAITING)
六、终止(TERMINATED)
4.7.1 初始化状态
在new出来即为初始化或者说是创建状态。
4.7.2 运行状态
即为开启start()就是就绪状态、抢夺到时间片即为 运行状态,但他们可以统称为
运行状态(RUNNABLE)
4.7.3 阻塞状态
什么时候为阻塞状态?在当前线程需要锁的时候,锁正在被别的线程使用中被卡着了即为阻塞状态(BLOCKED)
4.7.4 等待状态
等待状态就是开启join或wait的无参方法时,由于不止何时才能等待结束,也可能永远都不会结束所以也被成为无期等待(WAITING)
4.7.5 超时等待状态
看名字都能看出来与等待多了个时间,就是sleep方法或者join与wait的写入时间后产生的状态,他们有明确的时间何时等待结束所以被称为超时等待(TIMED-WAITING)或者有期等待,定时等待...
4.7.6 终止状态
终止也就是结束,线程任务结束即为终止状态还有就是异常时直接中断也算是终止状态(TERMINATED)
4.8 线程池
为何要使用线程池?跟我们数据库连接池一样的,在创建与销毁我们线程类时会消耗大量的资源,不如设置一个池子固定线程数我们用完这个线程他就回到这个池子里或者直接去执行在等待的任务,需要再调用。
单个线程的创建为大约1MB可是积少成多内存消耗极大,过多分配易内存溢出且频繁的创建销毁会增加虚拟机回收频率以及资源开销造成程序性能下降。
清楚了为什么要用线程池,我们需要了解Executor他是线程池的根接口就跟Object是类的根一样他们都有个工具类都是名称加个s我们可以使用里面定义的方法。
Executor他的工具类Executors创建连接池需要用Executor的子类ExecutoService接收,Executor的子类接口ExecutorService里面有很多抽象方法可以关闭以及判断连接池。
我们要用那些封装好的连接池只需要改动几个数值即可使用请根据实际应用选择。
4.8.1 固定线程池创建
ExecutorService executorService = Executors.newFixedThreadPool("要几个连接池");
MyThread类
public class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+"运行了吗");
int x = 3+3;
return x;
}
}
Test测试类
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
MyThread myThread2 = new MyThread();
FutureTask<Integer> futureTask = new FutureTask<>(myThread);
ExecutorService executorService = Executors.newFixedThreadPool(1);
//execute意思为执行但他们能执行 Runnable类型
executorService.execute(futureTask);
Integer o = futureTask.get();
System.out.println(o+"o");
//submit提交也是执行,不过他可以执行Callable与Runnable类型两种
Integer integer = executorService.submit(myThread2).get();
System.out.println(integer+"integer");
}
}
运行结果:他们线程的名称是连接池-序号-线程类名-序号,可是程序没有结束因为连接池没有关闭。
void shutdown():关闭线程池。需要等任务执行完毕。
shutdownNow(); 立即关闭线程池。 不在接受新的任务。
isShutdown(): 判断是否执行了关闭。
isTerminated(): 判断线程池是否终止。表示线程池中的任务都执行完毕,并且线程池关闭了
submit(Callable<T> task);提交任务,可以提交Callable
submit(Runnable task): 提交任务,可以提交Runnable任务
4.8.2 单一线程池创建
ExecutorService executorService = Executors.newSingleThreadExecutor();
MyThread类
public class MyThread implements Runnable{
@Override
public void run() {
System.out.println("我还活着!");
}
}
Test类
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();
ExecutorService executorService = Executors.newSingleThreadExecutor();
//接收并开启任务
executorService.execute(myThread);
//关闭连接池
executorService.shutdown();
}
}
测试结果:现在关闭了因为调用了关闭线程池
4.8.3 缓存线程池创建
ExecutorService executorService = Executors.newCachedThreadPool();
public class Test {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i=0;i<15;i++){
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":叫什么");
}
});
}
//关闭连接池
executorService.shutdown();
}
}
运行结果:从1到10他会来一个任务增加一个,后面如果还有任务,前面有执行完的就直接再去顶替上去了,但如果执行完的少于剩下的任务量也会适量增加线程数。
4.8.4 延迟线程池创建
ScheduledExecutorService executors = Executors.newScheduledThreadPool(5);
MyThread类
public class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+"执行了呢");
return 666;
}
}
Test类
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
ScheduledExecutorService executors = Executors.newScheduledThreadPool(5);
for (int i=0;i<101;i++){
//设置为十秒 参数为:执行的代码,数值,单位(这里我设置为秒)
executors.schedule(myThread,10, TimeUnit.SECONDS);
}
executors.shutdown();
}
}
运行效果:
4.8.5 原生线程池创建
上面通过Executors工具类创建线程池,但是阿里巴巴不建议使用。阿里建议使用原生的模式创建线程池,说是比较灵活。
请记住下面五个参数:
int corePoolSize,核心线程的个数
int maximumPoolSize,最多的线程个数
long keepAliveTime, 线程空闲时间。
TimeUnit unit, 空闲的单位BlockingQueue<Runnable> workQueue:等待队列 意思是线程都在执行,又来了任务就可以在等待队列里等着了,设置等待队列里最多可以有几个任务,多出来就抛异常。
ThreadPoolExecutor executor=new ThreadPoolExecutor(
int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue );
MyThread类
public class MyThread {
public static void main(String[] args) {
BlockingQueue<Runnable> workQueue=new ArrayBlockingQueue(5);//最多5等待的任务
//最少5个,最多10个,等待时间的数值(到了就减少线程最低减到五个),等待时间的类型(秒?或是毫秒或是分钟等等) , 等待队列
ThreadPoolExecutor executor=new ThreadPoolExecutor(5,10,10, TimeUnit.SECONDS,workQueue);
for (int i=0;i<10;i++){
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行了呢");
}
});
}
executor.shutdown();
}
}
执行效果:
但是如果把循环次数调到16,请看运行效果: 也就是等待室满了抛出了一个异常。
但如果是设置了一个延时,他的其他线程执行完毕回到线程池里了就不会报错了甚至只需要5个线程就能执行完毕。