栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

javaSE从入门到精通的二十万字总结(三)

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

javaSE从入门到精通的二十万字总结(三)

目录
  • 前言
  • 8. 线程
    • 8.1 线程与进程的概念
    • 8.2 线程创建方式
    • 8.3 线程生命周期
    • 8.4 线程方法
    • 8.5 数据安全
      • 8.5.1 synchronized关键字
      • 8.5.2 解决方法
      • 8.5.3 死锁
    • 8.6 守护线程
    • 8.7 定时器
    • 8.8 实现Callable接口
    • 8.9 wait和notify
  • 9. 反射机制
    • 9.1 获取class方式
    • 9.2 实例化对象
    • 9.3 获取配置文件
      • 9.3.1 相对路径
      • 9.3.2 绝对路径
      • 9.3.3 流的方式
      • 9.3.4 资源绑定器
    • 9.4 field

前言

在看这篇文章之前先预习java基础

这部分知识一共有4个文档
第四个是当前这个文档

  1. java零基础从入门到精通(全)
  2. javaSE从入门到精通的二十万字总结(一)
  3. javaSE从入门到精通的二十万字总结(二)
  4. javaSE从入门到精通的二十万字总结(三)
8. 线程

关于这部分知识可看我之前的文章

  1. 【操作系统】线程与进程的深入剖析(全)
  2. 【操作系统】守护线程和守护进程的区别
  3. JUC高并发编程从入门到精通(全)
  4. java之TimeUnit.SECONDS.sleep()详细分析(全)
  5. java并发之synchronized详细分析(全)
  6. java之Thread类详细分析(全)
  7. java之Thread类实战模板(全)
8.1 线程与进程的概念

关于这个概念上面给出了链接
【操作系统】线程与进程的深入剖析(全)

进程是一个应用程序(1个进程是一个软件)
线程是一个进程中的执行场景/执行单元

一个进程可以启动多个线程,进程之间是独立的,不共享资源
线程之间是堆内存和方法区内存共享,但是栈内存独立,一个线程一个栈

为此提出问题
1. 使用了多线程机制之后,main方法结束,是不是有可能程序也不会结束??

main方法结束只是主线程结束了,主栈空了,其它的栈(线程)可能还在压栈弹栈

2. 启动一个java程序代码的代码过程??
会先启动JVM,而JVM就是一个进程。
JVM再启动一个主线程调用main方法,同时再启动一个垃圾回收线程负责看护,回收垃圾。最起码,现在的java程序中至少有两个线程并发,一个是垃圾回收线程,一个是执行main方法的主线程。

3.对于单核的CPU,可以做到真正的多线程并发吗??
不能够做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉。
对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行做

4.分析程序中有多少个线程??

public class ThreadTest01 {
    public static void main(String[] args) {
        System.out.println("main begin");
        m1();
        System.out.println("main over");
    }

    private static void m1() {
        System.out.println("m1 begin");
        m2();
        System.out.println("m1 over");
    }

    private static void m2() {
        System.out.println("m2 begin");
        m3();
        System.out.println("m2 over");
    }

    private static void m3() {
        System.out.println("m3 execute!");
    }
}

结果是只有一个
因为程序中只有一个主栈,没有创建一个分栈,都是在主栈中调用其线程
最后执行的结果是

main begin
m1 begin
m2 begin
m3 execute!
m2 over
m1 over
main over

8.2 线程创建方式

线程的创建方式在上面已经给出了连接
是这两个文档

  1. java之Thread类详细分析(全)
  2. java之Thread类实战模板(全)

第一种方式
直接继承java.lang.Thread,重写run方法

start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)

public class ThreadTest02 {
    public static void main(String[] args) {
        // 这里是main方法,这里的代码属于主线程,在主栈中运行。
        // 新建一个分支线程对象
        MyThread t = new MyThread();
      
        // 启动线程     
        t.start();
        // 这里的代码还是运行在主线程中。
        for(int i = 0; i < 1000; i++){
            System.out.println("主线程--->" + i);
        }
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        // 编写程序,这段程序运行在分支线程中(分支栈)。
        for(int i = 0; i < 1000; i++){
            System.out.println("分支线程--->" + i);
        }
    }
}

第二种方式
编写一个类实现java.lang.Runnable接口

public class ThreadTest03 {
    public static void main(String[] args) {
        // 创建一个可运行的对象
        //MyRunnable r = new MyRunnable();
        // 将可运行的对象封装成一个线程对象
        //Thread t = new Thread(r);
        Thread t = new Thread(new MyRunnable()); // 合并代码
        // 启动线程
        t.start();

        for(int i = 0; i < 100; i++){
            System.out.println("主线程--->" + i);
        }
    }
}

// 这并不是一个线程类,是一个可运行的类。它还不是一个线程。
class MyRunnable implements Runnable {

    @Override
    public void run() {
        for(int i = 0; i < 100; i++){
            System.out.println("分支线程--->" + i);
        }
    }
}

第三种方式
将其上面两种方式结合在一起
使用匿名内部类结合在一起

public class ThreadTest04 {
    public static void main(String[] args) {
        // 创建线程对象,采用匿名内部类方式。
        // 这是通过一个没有名字的类,new出来的对象。
        Thread t = new Thread(new Runnable(){
            @Override
            public void run() {
                for(int i = 0; i < 100; i++){
                    System.out.println("t线程---> " + i);
                }
            }
        });

        // 启动线程
        t.start();

        for(int i = 0; i < 100; i++){
            System.out.println("main线程---> " + i);
        }
    }
}

总结一下上面的方式大致如下
第一种方式

// 定义线程类
public class MyThread extends Thread{
	public void run(){
			
	}
}

// 创建线程对象
MyThread t = new MyThread();
// 启动线程。
t.start();

第二种方式

// 定义一个可运行的类
public class MyRunnable implements Runnable {
	public void run(){
			
	}
}

// 创建线程对象
Thread t = new Thread(new MyRunnable());
// 启动线程
t.start();

涉及到为什么要用start()方法(系统自动调用run方法)而不是直接让对象直接调用run方法的原因
可看我之前的文章
多线程中run()和start()的异同详细分析(全)

8.3 线程生命周期

线程的生命周期里面分为五个阶段

  • 新建状态
  • 就绪状态
  • 运行状态
  • 阻塞状态
  • 死亡状态
8.4 线程方法
方法功能
Thread.currentThread()获取当前线程对象
线程对象.getName()获取线程对象名字
线程对象.setName(“线程名字”)修改线程对象名字
static void sleep(long millis)参数是毫秒,让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其它线程使用
线程对象.interrupt();终止线程睡眠不终止线程执行
线程对象.stop()终止线程执行,已过时(不建议使用。),主要是因为不会保存信息

以上方法的示列代码如下

public class ThreadTest05 {
    public static void main(String[] args) {

        //currentThread就是当前线程对象
        // 这个代码出现在main方法当中,所以当前线程就是主线程。
        Thread currentThread = Thread.currentThread();
        System.out.println(currentThread.getName()); //main

        // 创建线程对象
        MyThread2 t = new MyThread2();
        // 设置线程的名字
        t.setName("t1");
        // 获取线程的名字
        String tName = t.getName();
        System.out.println(tName); //Thread-0

        MyThread2 t2 = new MyThread2();
        t2.setName("t2");
        System.out.println(t2.getName()); //Thread-1
        t2.start();

        // 启动线程
        t.start();
    }
}

class MyThread2 extends Thread {
    public void run(){
        for(int i = 0; i < 100; i++){
            // currentThread就是当前线程对象。当前线程是谁呢?
            // 当t1线程执行run方法,那么这个当前线程就是t1
            // 当t2线程执行run方法,那么这个当前线程就是t2
            Thread currentThread = Thread.currentThread();
            System.out.println(currentThread.getName() + "-->" + i);
        }
    }
}

使用sleep方法的代码示列如下
此处补充另外一个相关的函数
java之TimeUnit.SECONDS.sleep()详细分析(全)

public class ThreadTest06 {
    public static void main(String[] args) {

        for(int i = 0; i < 10; i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);

            // 睡眠1秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

此处补充sleep方法的一个面试题
以下代码会让线程t进入休眠状态吗?分支线程会有延迟嘛?

public class ThreadTest07 {
    public static void main(String[] args) {
        // 创建线程对象
        Thread t = new MyThread3();
        t.setName("t");
        t.start();

        // 调用sleep方法
        try {       
            t.sleep(1000 * 5); 
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 5秒之后这里才会执行。
        System.out.println("hello World!");
    }
}

class MyThread3 extends Thread {
    public void run(){
        for(int i = 0; i < 10000; i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}

答案如下:

在执行的时候还是会转换成:Thread.sleep(1000 * 5);,这行代码的作用是:让当前线程进入休眠,也就是说main线程进入休眠。但是分支的线程不会被延迟,start方法也就是开启了分支栈之后还会继续执行下面的代码

特别注意在run方法中调用某些方法需要不可以抛出异常,子类不可比父类多异常,所以只可以使用try catch

==interrupt()方法终止睡眠的示列代码如下 ==
终止睡眠而不终止线程的执行

  • 这种终断睡眠的方式依靠了java的异常处理机制
public class ThreadTest08 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable2());
        t.start();

        // 希望5秒之后,t线程醒来(5秒之后主线程手里的活儿干完了。)
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // 终断t线程的睡眠
        t.interrupt(); // 干扰
    }
}

class MyRunnable2 implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "---> begin");
        try {
            // 睡眠1年
            Thread.sleep(1000 * 60 * 60 * 24 * 365);
        } catch (InterruptedException e) {
            // 打印异常信息
            e.printStackTrace();
        }
        //1年之后才会执行这里
        System.out.println(Thread.currentThread().getName() + "---> end");

    }

}

stop()方法终止线程执行的示列代码如下
这种方式存在很大的缺点:容易丢失数据。因为这种方式是直接将线程杀死了
线程没有保存的数据将会丢失。不建议使用

public class ThreadTest09 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable3());
        t.start();

        // 模拟5秒
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 5秒之后强行终止t线程
        t.stop(); // 已过时(不建议使用。)
    }
}

class MyRunnable3 implements Runnable {

    @Override
    public void run() {
        for(int i = 0; i < 10; i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

为了克服这种缺点,使得保全终止前信息还保留着
应该设置一个标志临界值来保存信息

public class ThreadTest10 {
    public static void main(String[] args) {
    
        MyRunable4 r = new MyRunable4();
        Thread t = new Thread(r);

        t.start();

        // 模拟5秒
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // 终止线程
        // 你想要什么时候终止t的执行,那么你把标记修改为false,就结束了。
        r.run = false;
    }
}

class MyRunable4 implements Runnable {

    // 布尔标记
    boolean run = true;

    @Override
    public void run() {
        for (int i = 0; i < 10; i++){
            if(run){
                System.out.println(Thread.currentThread().getName() + "--->" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else{

                // 在这里可以保存呀。
                //save....
                //终止当前线程
                return;
            }
        }
    }
}

这里补充一下线程调度的知识点
线程调度的模型有两种主要

  • 抢占式调度模型:那个线程的优先级比较高,抢到的CPU时间片的概率就高一些/多一些,java采用的就是抢占式调度模型
  • 均分式调度模型:平均分配CPU时间片。每个线程占有的CPU时间片时间长度一样

java中又提供了如下的方法和线程调度有关
以下只列出一些常用的方法

方法功能
实例方法:void setPriority(int newPriority)设置线程的优先级
实例方法:int getPriority()获取线程优先级
实例方法:void join()合并方法
静态方法:static void yield()暂停当前正在执行的线程对象,并执行其他线程

讲解以上方法的时候先补充一些知识点

  • 最低优先级1,默认优先级是5,最高优先级10,优先级比较高的获取CPU时间片可能会多一些
  • yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。注意:在回到就绪之后,有可能还会再次抢到
  • join() 当前线程进入阻塞,t线程执行,直到t线程结束。当前线程才可以继续(感觉这个才是让位,腾出线程地方)

线程优先级的示列代码如下

public class ThreadTest11 {
    public static void main(String[] args) {


        System.out.println("最高优先级" + Thread.MAX_PRIORITY);
        System.out.println("最低优先级" + Thread.MIN_PRIORITY);
        System.out.println("默认优先级" + Thread.NORM_PRIORITY);

        // 设置主线程的优先级为1
        Thread.currentThread().setPriority(1);

        // 获取当前线程对象,获取当前线程的优先级
        Thread currentThread = Thread.currentThread();
        // main线程的默认优先级是:5
        //System.out.println(currentThread.getName() + "线程的默认优先级是:" + currentThread.getPriority());

        Thread t = new Thread(new MyRunnable5());
        t.setPriority(10);
        t.setName("t");
        t.start();

        // 优先级较高的,只是抢到的CPU时间片相对多一些。
        // 大概率方向更偏向于优先级比较高的。
        for(int i = 0; i < 10000; i++){
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }


    }
}

class MyRunnable5 implements Runnable {

    @Override
    public void run() {
        // 获取线程优先级
        //System.out.println(Thread.currentThread().getName() + "线程的默认优先级:" + Thread.currentThread().getPriority());
        for(int i = 0; i < 10000; i++){
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }
    }
}

yield()方法示列代码如下
分支线程每100次数让一次给主线程的抢占
但这种都是大概率问题而已

public class ThreadTest12 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable6());
        t.start();

        for(int i = 1; i <= 10000; i++) {
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}

class MyRunnable6 implements Runnable {

    @Override
    public void run() {
        for(int i = 1; i <= 10000; i++) {
            //每100个让位一次。
            if(i % 100 == 0){
                Thread.yield(); // 当前线程暂停一下,让给主线程。
            }
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}

join方法合并示列代码
所谓的合并代码,也就是直接腾出空间,让其先执行

public class ThreadTest13 {
    public static void main(String[] args) {
        System.out.println("main begin");

        Thread t = new Thread(new MyRunnable7());
        t.setName("t");
        t.start();

        //合并线程
        try {
            t.join(); // t合并到当前线程中,当前线程受阻塞,t线程执行直到结束。
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("main over");
    }
}

class MyRunnable7 implements Runnable {

    @Override
    public void run() {
        for(int i = 0; i < 10000; i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }
}
8.5 数据安全

什么时候数据在多线程并发的环境下会存在安全问题?

  • 多线程并发
  • 有共享数据
  • 共享数据有修改的行为

满足以上3个条件之后,就会存在线程安全问题
可以使用同步模型,但是牺牲了效率有了安全而已
应该在数据安全的前提下,保全数据效率

异步编程模型:多线程并发(效率较高),异步就是并发。线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等谁
同步编程模型:线程排队执行,同步就是排队。线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,两个线程之间发生了等待关系

  • 堆和方法区都是多线程共享的,所以可能存在线程安全问题
  • 局部变量+常量:不会有线程安全问题
  • 成员变量:可能会有线程安全问题

实例变量在堆中,堆只有1个
静态变量在方法区中,方法区只有1个

实例变量:在堆中
静态变量:在方法区
局部变量:在栈中
以上三大变量中:
局部变量永远都不会存在线程安全问题,因为局部变量不共享(一个线程一个栈),局部变量在栈中。所以局部变量永远都不会共享

如果使用局部变量建议使用:StringBuilder。
因为局部变量不存在线程安全问题。选择StringBuilder,StringBuffer效率比较低。

  • ArrayList是非线程安全的。
  • Vector是线程安全的。
  • HashMap HashSet是非线程安全的。
  • Hashtable是线程安全的。

模拟不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题
账户类

public class Account {
    // 账号
    private String actno;
    // 余额
    private double balance;

    public Account() {
    }

    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    //取款的方法
    public void withdraw(double money){
        // t1和t2并发这个方法。。。。(t1和t2是两个栈。两个栈操作堆中同一个对象。)
        // 取款之前的余额
        double before = this.getBalance(); // 10000
        // 取款之后的余额
        double after = before - money;

        // 在这里模拟一下网络延迟,100%会出现问题
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 更新余额
        // 思考:t1执行到这里了,但还没有来得及执行这行代码,t2线程进来withdraw方法了。此时一定出问题。
        this.setBalance(after);
    }
}

模拟线程机制内的run内容

public class AccountThread extends Thread {

    // 两个线程必须共享同一个账户对象。
    private Account act;

    // 通过构造方法传递过来账户对象
    public AccountThread(Account act) {
        this.act = act;
    }

    public void run(){
        // run方法的执行表示取款操作。
        // 假设取款5000
        double money = 5000;
        // 取款
        // 多线程并发执行这个方法。
        act.withdraw(money);

        System.out.println(Thread.currentThread().getName() + "对"+act.getActno()+"取款"+money+"成功,余额" + act.getBalance());
    }
}

创建一个对象两个线程,两个线程争夺都启动

public class Test {
    public static void main(String[] args) {
        // 创建账户对象(只创建1个)
        Account act = new Account("act-001", 10000);
        // 创建两个线程
        Thread t1 = new AccountThread(act);
        Thread t2 = new AccountThread(act);
        // 设置name
        t1.setName("t1");
        t2.setName("t2");
        // 启动线程取款
        t1.start();
        t2.start();
    }
}

会出现数据安全的问题
既然不能使用同步机制,所以要用异步机制
应该引入synchronized关键字

8.5.1 synchronized关键字

关于这个可看我之前的文章知识点
java并发之synchronized详细分析(全)

引入这个关键字后,可以避开一些数据安全的不规范
使用的具体规范是

synchronized(){
   // 线程同步代码块。
}

synchronized后面小括号中传的这个“数据”是相当关键的,这个数据必须是多线程共享的数据。才能达到多线程排队

那要看你想让哪些线程同步:假设t1、t2、t3、t4、t5,有5个线程,你只希望t1 t2 t3排队,t4 t5不需要排队。要在()中写一个t1 t2 t3共享的对象。而这个对象对于t4 t5来说不是共享的。

这里的共享对象是:账户对象。账户对象是共享的,不一定是this就是账户对象,只要是多线程共享的那个对象就行

在java语言中,任何一个对象都有“一把锁”,其实这把锁就是标记。(只是把它叫做锁。),100个对象,100把锁。1个对象1把锁。

执行原理

  • 假设t1和t2线程并发,开始执行以下代码的时候
  • 假设t1先执行了,遇到了synchronized,这个时候自动找“后面共享对象”的对象锁,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的。直到同步代码块代码结束,这把锁才会释放====假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁, t2占有这把锁之后,进入同步代码块执行程序。这样就达到了线程排队执行。
  • 这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队,执行的这些线程对象所共享的。

主要修改账户类的内容

public class Account {
    // 账号
    private String actno;
    // 余额
    private double balance; //实例变量。

    //对象
    Object obj = new Object(); // 实例变量。(Account对象是多线程共享的,Account对象中的实例变量obj也是共享的。)

    public Account() {
    }

    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    //取款的方法
    public void withdraw(double money){
                
         */
        //Object obj2 = new Object();//内部对象,都会进行创建
        synchronized (this){
        //synchronized (obj) {
        //synchronized ("abc") { // "abc"在字符串常量池当中。
        //synchronized (null) { // 报错:空指针。
        //synchronized (obj2) { // 这样编写就不安全了。因为obj2不是共享对象。
            double before = this.getBalance();
            double after = before - money;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.setBalance(after);
        }
    }
}

如果作用在类上
在实例方法上可以使用synchronized。synchronized出现在实例方法上,一定锁的是this。这种方式不灵活。如果共享的对象就是this,并且需要同步的代码块是整个方法体,建议使用这种方式。

缺点:synchronized出现在实例方法上,表示整个方法体都需要同步,可能会无故扩大同步的范围,导致程序的执行效率降低。所以这种方式不常用。

优点:代码写的少了。节俭了。

总结使用该关键字的三种方式

第一种:同步代码块,灵活

synchronized(线程共享对象){
 //同步代码块;
}

第二种:在实例方法上使用synchronized,表示共享对象一定是this,并且同步代码块是整个方法体
第三种:在静态方法上使用synchronized,表示找类锁。类锁永远只有1把,就算创建了100个对象,那类锁也只有一把

  • 对象锁:1个对象1把锁,100个对象100把锁
  • 类锁:100个对象,也可能只是1把类锁

关于synchronized的一些面试题

  • 同一个对象两个线程调用两个方法(一个有synchronized,一个没有),不需要等待
  • 同一个对象两个线程调用两个方法(两个都有synchronized),需要等待
  • 两个对象分配不同的线程,调用不一样的方法(都有synchronized),不需要等待,两个对象,两把锁
  • 两个对象分配不同的线程,调用不一样的方法(都有synchronized但是被static修饰了),需要等待,因为静态方法是类锁,不管创建了几个对象,类锁只有1把

举例第一个示列代码如下

public class Exam01 {
    public static void main(String[] args) throws InterruptedException {
        MyClass mc = new MyClass();

        Thread t1 = new MyThread(mc);
        Thread t2 = new MyThread(mc);

        t1.setName("t1");
        t2.setName("t2");

        t1.start();
        Thread.sleep(1000); //这个睡眠的作用是:为了保证t1线程先执行。
        t2.start();
    }
}

class MyThread extends Thread {
    private MyClass mc;
    public MyThread(MyClass mc){
        this.mc = mc;
    }
    public void run(){
        if(Thread.currentThread().getName().equals("t1")){
            mc.doSome();
        }
        if(Thread.currentThread().getName().equals("t2")){
            mc.doOther();
        }
    }
}

class MyClass {
    public synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000 * 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }
    public void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

8.5.2 解决方法

不要轻易选择线程同步synchronized,synchronized会让程序的执行效率降低,用户体验不好,系统的用户吞吐量降低。

  • 第一种方案:尽量使用局部变量代替“实例变量和静态变量”。

  • 第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样
    实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,
    对象不共享,就没有数据安全问题了。)

  • 第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候
    就只能选择synchronized了。线程同步机制。

8.5.3 死锁

对于synchronized,如果轻易嵌套不好,可能会引来死锁问题
具体死锁的问题是互相抢占资源互相等待

死锁的代码要掌握背会
面试的时候可能会让你写死锁的代码

记住代码
背会死锁

背会死锁

记住代码

public class DeadLock {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();

        // t1和t2两个线程共享o1,o2
        Thread t1 = new MyThread1(o1,o2);
        Thread t2 = new MyThread2(o1,o2);

        t1.start();
        t2.start();
    }
}

class MyThread1 extends Thread{
    Object o1;
    Object o2;
    public MyThread1(Object o1,Object o2){
        this.o1 = o1;
        this.o2 = o2;
    }
    public void run(){
        synchronized (o1){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o2){

            }
        }
    }
}

class MyThread2 extends Thread {
    Object o1;
    Object o2;
    public MyThread2(Object o1,Object o2){
        this.o1 = o1;
        this.o2 = o2;
    }
    public void run(){
        synchronized (o2){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o1){

            }
        }
    }
}

  • 背会了嘛
8.6 守护线程

关于这部分知识点具体可看我之前的文章
【操作系统】守护线程和守护进程的区别

关于java语言中线程分为两大类:

  • 用户线程
  • 守护线程(后台线程)

其中具有代表性的就是:垃圾回收线程(守护线程)。

守护线程的特点:一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。比如之后要讲到的定时器(每天00:00的时候系统数据自动备份,这个需要使用到定时器,并且我们可以将定时器设置为守护线程,所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份)

将其设置为守护线程的主要代码为

线程对象.setDaemon(true);

注意:主线程main方法是一个用户线程
守护线程的代码示列

public class ThreadTest14 {
    public static void main(String[] args) {
        Thread t = new BakDataThread();
        t.setName("备份数据的线程");

        // 启动线程之前,将线程设置为守护线程
        t.setDaemon(true);

        t.start();

        // 主线程:主线程是用户线程
        for(int i = 0; i < 10; i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class BakDataThread extends Thread {
    public void run(){
        int i = 0;
        // 即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止。
        while(true){
            System.out.println(Thread.currentThread().getName() + "--->" + (++i));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
8.7 定时器

定时器的作用:间隔特定的时间,执行特定的程序。

有几种方式可以实现定时器:

  • 可以使用sleep方法,睡眠,设置睡眠时间,到时间点执行任务。这种方式是最原始的定时器
  • 在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持定时任务的。
  • 在实际的开发中,使用较多的是Spring框架中提供的SpringTask框架,这个框架只要进行简单的配置,就可以完成定时器的任务

主要示列代码如下

public class TimerTest {
    public static void main(String[] args) throws Exception {

        // 创建定时器对象
        Timer timer = new Timer();
        //Timer timer = new Timer(true); //守护线程的方式

        // 指定定时任务
        //timer.schedule(定时任务, 第一次执行时间, 间隔多久执行一次);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date firstTime = sdf.parse("2020-03-14 09:34:30");
        //timer.schedule(new LogTimerTask() , firstTime, 1000 * 10);
      

        //匿名内部类方式
        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                // code....
            }
        } , firstTime, 1000 * 10);

    }
}

// 编写一个定时任务类
// 假设这是一个记录日志的定时任务
class LogTimerTask extends TimerTask {

    @Override
    public void run() {
        // 编写你需要执行的任务就行了。
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String strTime = sdf.format(new Date());
        System.out.println(strTime + ":成功完成了一次数据备份!");
    }
}
8.8 实现Callable接口

实现线程的第三种方式:实现Callable接口
有返回值,可以返回线程的结果
接口中的call方法相当于run方法

  • 优点:可以获取到线程的执行结果。
  • 缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低
public class ThreadTest15 {
    public static void main(String[] args) throws Exception {

        // 第一步:创建一个“未来任务类”对象。
        // 参数非常重要,需要给一个Callable接口实现类对象。
        FutureTask task = new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception { 

                // 线程执行一个任务,执行之后可能会有一个执行结果
                // 模拟执行
                System.out.println("call method begin");
                Thread.sleep(1000 * 10);
                System.out.println("call method end!");
                int a = 100;
                int b = 200;
                return a + b; //自动装箱(300结果变成Integer)
            }
        });

        // 创建线程对象
        Thread t = new Thread(task);

        // 启动线程
        t.start();

        // 这里是main方法,这是在主线程中。
        // 在主线程中,怎么获取t线程的返回结果?
        // get()方法的执行会导致“当前线程阻塞”
        Object obj = task.get();
        System.out.println("线程执行结果:" + obj);

        // main方法这里的程序要想执行必须等待get()方法的结束
        // 而get()方法可能需要很久。因为get()方法是为了拿另一个线程的执行结果
        // 另一个线程执行是需要时间的。
        System.out.println("hello world!");
    }
}
8.9 wait和notify

wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方式是Object类中自带的。wait方法和notify方法不是通过线程对象调用,
不是这样的:t.wait(),也不是这样的:t.notify().

  • wait()方法作用
    让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止。
    o.wait();方法的调用,会让“当前线程(正在o对象上活动的线程)”进入等待状态。
Object o = new Object();
o.wait();
  • notify()方法作用
    唤醒正在o对象上等待的线程
Object o = new Object();
o.notify();
  • o.wait方法会让正在o对象上活动的当前线程进入等待状态并且释放之前占有的o对象的锁
  • o.notify方法只会通知,不会释放之前所占有的o对象锁

还有一个notifyAll()方法:这个方法是唤醒o对象上处于等待的所有线程

使用wait方法和notify方法实现“生产者和消费者模式”

生产线程负责生产,消费线程负责消费
生产线程和消费线程要达到均衡
这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法

  • wait和notify方法不是线程对象的方法,是普通java对象都有的方法。
  • wait方法和notify方法建立在线程同步的基础之上。因为多线程要同时操作一个仓库。有线程安全问题。

模拟这样一个需求:

仓库采用List集合,List集合中假设只能存储1个元素,1个元素就表示仓库满了。,如果List集合中元素个数是0,就表示仓库空了,保证List集合中永远都是最多存储1个元素。
必须做到这种效果:生产1个消费1个。

具体的代码示列如下
当前线程进入等待状态,并且释放Producer之前占有的list集合的锁。

public class ThreadTest16 {
    public static void main(String[] args) {
        // 创建1个仓库对象,共享的。
        List list = new ArrayList();
        // 创建两个线程对象
        // 生产者线程
        Thread t1 = new Thread(new Producer(list));
        // 消费者线程
        Thread t2 = new Thread(new Consumer(list));

        t1.setName("生产者线程");
        t2.setName("消费者线程");

        t1.start();
        t2.start();
    }
}

// 生产线程
class Producer implements Runnable {
    // 仓库
    private List list;

    public Producer(List list) {
        this.list = list;
    }
    @Override
    public void run() {
        // 一直生产(使用死循环来模拟一直生产)
        while(true){
            // 给仓库对象list加锁。
            synchronized (list){
                if(list.size() > 0){ // 大于0,说明仓库中已经有1个元素了。
                    try {
                        // 当前线程进入等待状态,并且释放Producer之前占有的list集合的锁。
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 程序能够执行到这里说明仓库是空的,可以生产
                Object obj = new Object();
                list.add(obj);
                System.out.println(Thread.currentThread().getName() + "--->" + obj);
                // 唤醒消费者进行消费
                list.notifyAll();
            }
        }
    }
}

// 消费线程
class Consumer implements Runnable {
    // 仓库
    private List list;

    public Consumer(List list) {
        this.list = list;
    }

    @Override
    public void run() {
        // 一直消费
        while(true){
            synchronized (list) {
                if(list.size() == 0){
                    try {
                        // 仓库已经空了。
                        // 消费者线程等待,释放掉list集合的锁
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 程序能够执行到此处说明仓库中有数据,进行消费。
                Object obj = list.remove(0);
                System.out.println(Thread.currentThread().getName() + "--->" + obj);
                // 唤醒生产者生产。
                list.notifyAll();
            }
        }
    }
}
9. 反射机制

通过java语言中的反射机制可以操作字节码文件(可以读和修改字节码文件。)
通过反射机制可以操作代码片段(class文件)

  • java.lang.Class:代表整个字节码,代表一个类型,代表整个类
  • java.lang.reflect.Method:代表字节码中的方法字节码。代表类中的方法
  • java.lang.reflect.Constructor:代表字节码中的构造方法字节码。代表类中的构造方法
  • java.lang.reflect.Field:代表字节码中的属性字节码。代表类中的成员变量(静态变量+实例变量)
java.lang.Class:
public class User{
	// Field
	int no;
	// Constructor
	public User(){
	}
	public User(int no){
		this.no = no;
	}
	// Method
	public void setNo(int no){
		this.no = no;
	}
	public int getNo(){
		return no;
	}
}
9.1 获取class方式

要操作一个类的字节码,需要首先获取到这个类的字节码,获取java.lang.Class实例的三种方式

  • 第一种:Class c = Class.forName(“完整类名带包名”);
  • 第二种:Class c = 对象.getClass();
  • 第三种:Class c = 任何类型.class;

关于Class.forName这个函数

  1. 静态方法
  2. 方法的参数是一个字符串
  3. 字符串需要的是一个完整类名
  4. 完整类名必须带有包名。java.lang包也不能省略

以下是三种创建方式的示意代码

Class c1 = null;
Class c2 = null;
try {
    c1 = Class.forName("java.lang.String"); // c1代表String.class文件,或者说c1代表String类型。
    c2 = Class.forName("java.util.Date"); // c2代表Date类型
    Class c3 = Class.forName("java.lang.Integer"); // c3代表Integer类型
    Class c4 = Class.forName("java.lang.System"); // c4代表System类型
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

// java中任何一个对象都有一个方法:getClass()
String s = "abc";
Class x = s.getClass(); // x代表String.class字节码文件,x代表String类型。
System.out.println(c1 == x); // true(==判断的是对象的内存地址。)

Date time = new Date();
Class y = time.getClass();
System.out.println(c2 == y); // true (c2和y两个变量中保存的内存地址都是一样的,都指向方法区中的字节码文件。)

// 第三种方式,java语言中任何一种类型,包括基本数据类型,它都有.class属性。
Class z = String.class; // z代表String类型
Class k = Date.class; // k代表Date类型
Class f = int.class; // f代表int类型
Class e = double.class; // e代表double类型

System.out.println(x == z); // true

使用这种方式获取class方式,如果只想获取类中单独的静态代码块可以使用这种方式
Class.forName("完整类名");这个方法的执行会导致类加载,类加载时,静态代码块执行

public class ReflectTest04 {
    public static void main(String[] args) {
        try {
            // Class.forName()这个方法的执行会导致:类加载。
            Class.forName("comjava.reflect.MyClass");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

具体实体化的MyClass类为

public class MyClass {

    // 静态代码块在类加载时执行,并且只执行一次。
    static {
        System.out.println("MyClass类的静态代码块执行了!");
    }
}
9.2 实例化对象

通过Class的newInstance()方法来实例化对象

注意:newInstance()方法内部实际上调用了无参数构造方法,必须保证无参构造存在才可以

先创建一个实体类

public class User {
    public User(){
        System.out.println("无参数构造方法!");
    }

    // 定义了有参数的构造方法,无参数构造方法就没了。
    public User(String s){

    }
}
public class ReflectTest02 {
    public static void main(String[] args) {

        // 这是不使用反射机制,创建对象
        User user = new User();
        System.out.println(user);

        // 下面这段代码是以反射机制的方式创建对象。
        try {
            // 通过反射机制,获取Class,通过Class来实例化对象
            Class c = Class.forName("com.java.bean.User"); // c代表User类型。

            // newInstance() 这个方法会调用User这个类的无参数构造方法,完成对象的创建。
            // 重点是:newInstance()调用的是无参构造,必须保证无参构造是存在的!
            Object obj = c.newInstance();

            System.out.println(obj); // com.java.bean.User@10f87f48
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
}

这种实例化对象不改变java源代码的基础之上,可以做到不同对象的实例化(符合OCP开闭原则:对扩展开放,对修改关闭)

9.3 获取配置文件

通过一个外置的文件来获取关键信息,而不修改代码,实现灵活性

9.3.1 相对路径

创建一个properties存储关键信息,来获取文件并且实例化对象
此处通过前面的io+properties来获取
具体代码简写格式为

 FileReader reader = new FileReader(" ");
// 创建属性类对象Map
Properties pro = new Properties(); // key value都是String
// 加载
pro.load(reader);
// 关闭流
reader.close();

具体代码格式为

public class ReflectTest03 {
    public static void main(String[] args) throws Exception{

        // 这种方式代码就写死了。只能创建一个User类型的对象
        //User user = new User();

        // 以下代码是灵活的,代码不需要改动,可以修改配置文件,配置文件修改之后,可以创建出不同的实例对象。
        // 通过IO流读取classinfo.properties文件
        FileReader reader = new FileReader("chapter25/classinfo2.properties");
        // 创建属性类对象Map
        Properties pro = new Properties(); // key value都是String
        // 加载
        pro.load(reader);
        // 关闭流
        reader.close();

        // 通过key获取value
        String className = pro.getProperty("className");
        //System.out.println(className);

        // 通过反射机制实例化对象
        Class c = Class.forName(className);
        Object obj = c.newInstance();
        System.out.println(obj);
    }
}

以上都是获取文件的相对路径

9.3.2 绝对路径

如果获取文件的绝对路径,即使移植到其它系统或者其他位置还可以识别
可以使用以下方法(但前提是:文件需要在类路径下。才能用这种方式)
所谓的类路径是src目录之下
具体获取绝对路径的代码为

Thread.currentThread().getContextClassLoader().getResource("src下的路径名").getPath(); 
  • Thread.currentThread() 当前线程对象
  • getContextClassLoader() 是线程对象的方法,可以获取到当前线程的类加载器对象
  • getResource() 【获取资源】这是类加载器对象的方法,当前线程的类加载器默认从类的根路径下加载资源
public class aboutPath {
    public static void main(String[] args) throws Exception{
      
        String path = Thread.currentThread().getContextClassLoader()
                .getResource("classinfo2.properties").getPath(); // 这种方式获取文件绝对路径是通用的。

        // 采用以上的代码可以拿到一个文件的绝对路径。
   
        System.out.println(path);

        // 获取db.properties文件的绝对路径(从类的根路径下作为起点开始)
        String path2 = Thread.currentThread().getContextClassLoader()
                .getResource("com//java/bean/db.properties").getPath();
        System.out.println(path2);

    }
}
9.3.3 流的方式

将其替换为
因为流的方式可以直接返回而不用多写一行代码

 // 获取一个文件的绝对路径了!!!!!
String path = Thread.currentThread().getContextClassLoader()
                .getResource("classinfo2.properties").getPath();
FileReader reader = new FileReader(path);

//替换为

// 直接以流的形式返回。
InputStream reader = Thread.currentThread().getContextClassLoader()
                .getResourceAsStream("classinfo2.properties");

完整代码如下

public class IoPropertiesTest {
    public static void main(String[] args) throws Exception{

        // 获取一个文件的绝对路径了!!!!!
        

        // 直接以流的形式返回。
        InputStream reader = Thread.currentThread().getContextClassLoader()
                .getResourceAsStream("classinfo2.properties");

        Properties pro = new Properties();
        pro.load(reader);
        reader.close();
        // 通过key获取value
        String className = pro.getProperty("className");
        System.out.println(className);
    }
}
9.3.4 资源绑定器

关于这个类可看我之前的文章
java之ResourceBundle类详细分析(全)

使用这个类直接获取,便于获取属性配置文件中的内容。
使用以下这种方式的时候,属性配置文件xxx.properties必须放到类路径下

public class ResourceBundleTest {
    public static void main(String[] args) {

        // 资源绑定器,只能绑定xxx.properties文件。并且这个文件必须在类路径下。文件扩展名也必须是properties
        // 并且在写路径的时候,路径后面的扩展名不能写。
        //ResourceBundle bundle = ResourceBundle.getBundle("classinfo2");

        ResourceBundle bundle = ResourceBundle.getBundle("com/java/bean/db");

        String className = bundle.getString("className");
        System.out.println(className);

    }
}

科普一下类加载器
概念:专门负责加载类的命令/工具(ClassLoader)
JDK中自带了3个类加载器

  • 启动类加载器:rt.jar
  • 扩展类加载器:ext/*.jar
  • 应用类加载器:classpath

String s = "abc";运行代码的时候,会将所需要类全部加载到JVM当中。通过类加载器加载,看到以上代码类加载器会找String.class,文件,找到就加载
首先通过“启动类加载器”加载(jdk中的jrelibrt.jar,jdk中最核心的类库),如果通过“启动类加载器”加载不到的时候,会通过"扩展类加载器"加载(jrelibext*.jar),如果“扩展类加载器”没有加载到,会通过“应用类加载器”加载(环境变量中的classpath中的类)

java中为了保证类加载的安全,使用了双亲委派机制,也就是按照启动类(父)->扩展类(母)->应用类的顺序进行加载

9.4 field
方法功能
Class.forName()获取整个类
getName()获取完整类名
getSimpleName()获取简单名
getFields()获取类中所有的public修饰的Field
getDeclaredFields()获取所有的Field
getModifiers()获取修饰符代号
Modifier.toString()代号数字转换为字符串
getType()获取属性类型

以上方法展示的代码如下

public class ReflectTest05 {
    public static void main(String[] args) throws Exception{

        // 获取整个类
        Class studentClass = Class.forName("com.bjpowernode.java.bean.Student");

        //com.bjpowernode.java.bean.Student
        String className = studentClass.getName();
        System.out.println("完整类名:" + className);

        String simpleName = studentClass.getSimpleName();
        System.out.println("简类名:" + simpleName);

        // 获取类中所有的public修饰的Field
        Field[] fields = studentClass.getFields();
        System.out.println(fields.length); // 测试数组中只有1个元素
        // 取出这个Field
        Field f = fields[0];
        // 取出这个Field它的名字
        String fieldName = f.getName();
        System.out.println(fieldName);

        // 获取所有的Field
        Field[] fs = studentClass.getDeclaredFields();
        System.out.println(fs.length); // 4

        System.out.println("==================================");
        // 遍历
        for(Field field : fs){
            // 获取属性的修饰符列表
            int i = field.getModifiers(); // 返回的修饰符是一个数字,每个数字是修饰符的代号!!!
            System.out.println(i);
            // 可以将这个“代号”数字转换成“字符串”吗?
            String modifierString = Modifier.toString(i);
            System.out.println(modifierString);
            // 获取属性的类型
            Class fieldType = field.getType();
            //String fName = fieldType.getName();
            String fName = fieldType.getSimpleName();
            System.out.println(fName);
            // 获取属性的名字
            System.out.println(field.getName());
        }
    }
}

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/463142.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号