匿名内部类创建线程对象;Thread类中的常用方法;多线程中的线程安全问题及处理方法,Java Day22

一,使用匿名内部类创建线程对象

  • 什么是匿名内部类:没有名字子类对象
  • ​ 本质:是一个对象
  • ​ 使用前提:
  1. 必须有继承或实现关系
  2. 一定有重写方法
  • ​ 格式:new 父类或接口名 (){ 重写的方法 };
  • 多线程的两种实现方式正好满足匿名内部类的使用前提。意味着可以使用匿名内部类实现多线程

代码示例

public class Demo01 {
    public static void main(String[] args) {
        // 创建 一条新线程出来 创建 Thread类的对象
       // 继承 [匿名内部类],匿名内部类不用写子类直接继承 Thread() 父类
    // 线程的子类对象


//线程一 new
Thread() {
//重写方法 @Override public void run() { // run 方法里面写线程的任务 for (int i = 0; i < 6; i++) { System.out.println("java 就是好"); } } }.start(); // 开启线程
        //线程二
        new Thread() {
//重写方法 @Override public void run() { // run 方法里面写线程的任务 for (int i = 0; i < 6; i++) { System.out.println("越学越简单"); } } }.start(); // 开启线程 //实现接口 //这是一个线程任务对象Runnable target = new Runnable() {
//重写方法 @Override public void run() { System.out.println("风光无限好"); } }.start; //线程任务对象 Runnable target1 = new Runnable() {
//重写方法 @Override public void run() { System.out.println("只是近黄昏"); } };
//创建 Thread 的对象并绑定线程任务 Thread t1 = new Thread(target1);t1.start();


//创建线程对象 Thread t2 = new Thread(new Runnable() { //重写方法 @Override public void run() { System.out.println("花花世界无限好"); } }); t2.start(); } }

二,Thread类中的常用方法

  • 获取线程名称

  • getName():获取线程名称 【普通方法,被线程对象调用】线程是可以人为命名的
  • 设置线程名称

  • setName(String name) :给线程改名

代码示例

public class Demo02 {
public static void main(String[] args) {
    //创建线程对象
    Thread t1 = new Thread() {
        @Override
        public void run() {
            System.out.println(123);
        }
    };
    Thread t2 = new Thread() {
        @Override
        public void run() {
            System.out.println(123);
        }
    };
    Thread t3 = new Thread("夏天") {
        @Override
        public void run() {
            System.out.println(123);
        }
    };
    //获取线程的名称 [默认的名字]
    System.out.println(t1.getName());  //Thread-0
    System.out.println(t2.getName());  //Thread-1
    System.out.println(t3.getName());  //夏天
    
    //设置线程的名字 [修改线程的名字]
    t1.setName("花花");
    t2.setName("花花");
    System.out.println(t1.getName());  //花花
    System.out.println(t2.getName());  //花花, 可以重名
}
}
  • 结论:
  1. ​ 线程没有指定名称的时候,默认名称是以thread开头后面跟数字从0 开始依次递增1
  2. ​ 线程可以指定名称,可以构造指定也可以是set方法指定,可以重名
  • 获取当前线程对象

  1. 作用:可以在任意位置,获取当前正在执行这段代码的线程对象
  2. 静态方法:Thread Thread.currentThread();
  3. 返回当前正在执行这段代码的线程对象的引用
  4. 哪条线程在执行这段代码,返回的就是哪条线程的对象

代码示例

public class Demo03 {
    public static void main(String[] args) {
        // 获取 main 的线程对象 Thread tname = Thread.currentThread();
        System.out.println(tname);// Thread[main,5,main]----main方法,线程优先级5,线程的名字是main

        Thread thread =new Thread() {
            public void run() {
                Thread tname2 = Thread.currentThread();
                System.out.println(tname2); //Thread[Thread-0,5,main]----线程的名称,优先级5,main方法
                System.out.println(tname2.getName()); //Thread-0, 获取一条正在执行代码的线程的名称
            }
        }/*.start()*/;// 启动线程,如果不启动的化,run方法执行不了
        //调用 start() 得到的线程对象 说明启动了新的线程
        thread.run();  //调用run方法得到的main线程对象,意味着没有启动新的线程
    }
}
  • 练习

  • 获取主方法所在的线程的名称
  • 获取垃圾回收线程的线程名称
  • 分析:
  1. 获取线程名称【 getName()被谁线程对象调用】
  2. ​ 相办法获取当前线程对象【 Thread.currentThread() 】,
  3. ​ 调用垃圾回收器:System.gc ( )
  4. ​ 如何确定垃圾回收线程工作?重写 Object 中的 finalize()

代码示例

public class Test01 {
    public static void main(String[] args) throws Throwable {
        // 获取主线程的线程对象Thread tname = Thread.currentThread();
        // 获取线程名称System.out.println(tname.getName());
        // 获取垃圾回收线程
        // 创建一个匿名对象
        // Test01 test01 = new Test01();// 把对象赋值给变量 gc方法调用的是回收不到对象gc线程就没有执行
         // 匿名对象  只能使用一次
         new Test01();// 匿名对象没名字,gc方法回收这个对象, finalize()就会执行,意味着gc的线程就开启执行了
         System.gc(); // 调用finalize()是垃圾线程的
         test01.finalize(); // 是main方法行为不是垃圾线程的行为
    }
  
    @Override
    public  void finalize() throws Throwable {
        // 监听垃圾回收线程的对象和名称
        Thread thread = Thread.currentThread();
        System.out.println(thread);
        System.out.println(thread.getName());
    }
}
  • 线程休眠

  1. Thread.sleep(long time):使开启的线程休眠time常时间。以毫秒为单位,他是一个静态方法。

代码示例

public class Demo04 {
public static void main(String[] args) throws InterruptedException {
    //  要主线程休息1 s,过了一秒之后才执行
    //Thread.sleep(1000);
    //System.out.println(123);
    new Thread() {
        @Override
        public void run() {
            try {
                Thread.sleep(2000);  //sleep方法的异常在 run方法中只能捕获处理
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("春夏秋冬");  //春夏秋冬
        }
    }.start();

    Thread.sleep(1000);
    System.out.println(123);//123
    //先执行春夏秋冬一秒后执行123
}
}
  • 注意事项:
  1. sleep 方法存在一个 InterruptedException 异常的。该异常在 main 方法中可以声明处理也可以捕获处理。
  2. 如果在 run 方法中,InterruptedException 异常只能捕获异常处理。
  • 守护线程

  • 概述:给普通线程执行创造执行环境的线程,守护普通线程可以安全的执行。一般守护线程看不到的,默默无闻做奉献的一个线程。比如:垃圾回收线程
  • 如果所有的普通线程都消失,守护线程就没有存在的必要。【普通线程消失,守护线程也会及时的关闭】
  • 方法:
  1. setDaemon(boolean flag):给线程设置成为守护线程
  2. isDaemon():判断该线程是否为守护线程

代码示例

public class Demo05 {
public static void main(String[] args) {
    //验证垃圾回收线程是否为守护线程, 结果为true, 所以这个线程是守护线程
    new Demo05();System.gc();
    //创建一条新的线程
    Thread t1 = new Thread() {
        @Override
        public void run() {
            System.out.println(123);
    }
    };
    System.out.println(t1.isDaemon());  //false,  t1不是守护线程
    
    //将t1转换为守护线程 t1.setDaemon(true);
    System.out.println(t1.isDaemon()); //true, t1是守护线程
}
@Override
    protected void finalize() throws Throwable {
        boolean b = Thread.currentThread().isDaemon();
        System.out.println(b); //true
    }
}
  • 线程优先级

  • jvm 对线程的执行采用的是抢占式调度,线程的执行顺序没法控制的,但是有的时候对于一些特殊的线程要求提前执行情况,想办法人为的控制一下执行顺序,给线程增加了优先级的概念。优先级数值越大优先级越靠前。但是优先级有范围的:1到10
  • setPriority(int newPriority) :更改线程的优先级。线程的默认优先级是5
  • thread类中有三个优先级的常量值:
  1. MAX_PRIORITY :线程可以具有的最高优先级。 10
  2. MIN_PRIORITY :线程可以具有的最低优先级。 1
  3. NORM_PRIORITY :分配给线程的默认优先级。 5

代码示例

public class Demo06 {
    public static void main(String[] args) {
        Thread t1 = Thread.currentThread();
        System.out.println(t1);// 显示优先级,Thread[main,5,main]默认是5
        t1.setPriority(10);
        System.out.println(t1);// Thread[main,10,main] 优先级由5变为了10

        for (int i = 0; i < 5; i++) {
            System.out.println("春暖花开");
        }

        // 创建一个新的线程
        new Thread() {
            @Override
            public void run() {
                Thread.currentThread().setPriority(Thread.MIN_PRIORITY);  //优先级为1
                // 没有指定优先级,默认的是5
                for (int i = 0; i < 50; i++) {
                    System.out.println("鸟语花香");
                }
            }
        }.start();

        new Thread() {
            public void run() {
                Thread.currentThread().setPriority(Thread.MAX_PRIORITY);  //优先级为10
                for (int i = 0; i < 200; i++) {
                    System.out.println("花花世界");
                }
            }
        }.start();
    }
}

三,多线程中的线程安全问题

  • 问题描述

  • 两个线程操作同一个对象中的资源,执行的时候导致,showA方法没有执行完毕,就去执行showB方法,导致两个方法的结果交叉感染了。出现问题了。
  • 原因:同一个对象的资源被两个不同的线程执行使用,线程执行机制是抢占式调度,执行时jvm两个线程之间来回的切换,出现一个线程方法还没有执行完毕,就去执行另一个线程,导致两个线程的一部分内容出现交叉问题。
  • 多线程问题的产生就是多个线程使用同一个资源 产生的

代码示例

//定义一个测试类
public class Person_Test {
    public static void main(String[] args) {
        // 创建了一个person对象  Person person = new Person();
        // 创建线程对象
        new Thread() {
            public void run() {
                person.showA();// 效果输出  我爱中国
            }
        }.start();
new Thread() {
            public void run() {
                person.showB();//  好大的家
            }
        }.start();
    }
}
//定义一个Person类
public class Person {
    public void showA() {
        while(true) {
            System.out.print("我");
            System.out.print("爱");
            System.out.print("中");
            System.out.print("国");
            System.out.println();
        }
    }
public void showB() {
        while(true) {
            System.out.print("好");
            System.out.print("大");
            System.out.print("的");
            System.out.print("家");
            System.out.println();
        }
    }
} 
  • 解决方案:
  • ​ 分析:问题产生的直接原因,线程没有执行完jvm就跑路
  • ​ 思路:想办法要jvm把正在做的事做完再跑路 【排个队,把线程事干完再去干别的线程不然不让jvm走】
  • ​ 方案:给jvm干的事加约束【锁】锁机制解决安全问题
  • ​ 线程要干的事使用锁锁起来把jvm执行权留在自己线程内部【jvm要执行线程的时候得到这个锁】,执行完毕之后把锁还给jvm,jvm拿着锁去执行所有的线程【的锁要求是同一个锁】
  • 加锁的方式:有同步代码块、同步方法
  • 同步代码块

  • 作用:就是解决多线程安全问题的。
  • 使用格式:

    synchronized (锁对象) {会发生线程安全的代码}

  • 使用同步代码块之后的效果:

代码示例//定义Person类

 Person {
    public void showA() {
        while(true) {
            // 同步代码块
            synchronized(this) {
                System.out.print("我");
                System.out.print("爱");
                System.out.print("中");
                System.out.print("国");
                System.out.println();
            }
        }
    }
    public void showB() {
        while(true) {
            synchronized(this) {
                System.out.print("好");
                System.out.print("大");
                System.out.print("的");
                System.out.print("家");
                System.out.println();
            }
        }
    }
}


//定义测试类public class Person_Test { public static void main(String[] args) { // 创建了一个person对象 Person person = new Person(); //Person person1 = new Person(); // 创建线程对象 new Thread() { public void run() { person.showA();// 效果输出 我爱中国 } }.start(); new Thread() { public void run() { person.showB();// 好大的家 } }.start(); } }
  • 同步代码块上锁:上在资源有可能产生问题的代码上
  • 同步方法

  • 把线程要执行的代码使用方法封装起来,然后我给方法上把锁,将来jvm想要执行这个方法,必须有这个方法对应锁。
  • 同步方法的格式:
    权限修饰符 synchronized 返回值类型 方法名称(参数列表) { 需要同步的方法体【多条线程需要共同执行的代码段】}

代码示例

public class Person {
    public void showA() {
        while(true) {
            // 使用同步方法解决            show();
        }
    }
    // 同步方法1
    public synchronized void show() {
        System.out.print("我");
        System.out.print("爱");
        System.out.print("中");
        System.out.print("国");
        System.out.println();
    }
public void showB() {
        while(true) {
            // 同步方法            print();
        }
    }
    public synchronized void print() {
        System.out.print("好");
        System.out.print("大");
        System.out.print("的");
        System.out.print("家");
        System.out.println();
    }
}
  • 锁对象的说明

  • 同步代码块的锁对象是谁?
  1. ​ 同步代码块的锁对象没有确定前可以是任意引用数据类型对象,一旦确定下来所有的同步代码块的锁对象保证唯一【同一个对象】
  2. ​ 锁要唯一。不然解决不了安全问题。
  • 同步方法的锁对象是谁?
  1. ​ 普通同步方法:默认的锁对象是this【当前调用对象】使用的时候必须保证所有的同步方法的调用对象是同一个对象
  2. ​ 静态同步方法:默认的锁对象是 字节码文件对象【类名.class】
  • 使用注意事项:【保证锁对象唯一】
  • ​ 如果单一方式使用:
  1. ​ 单一同步代码块:需要同步代码块的所有的锁对象上下一致,保证唯一
  2. ​ 单一使用同步方法:需要保证所有的同步方法的调用对象始终是同一个对象
  • ​ 混合使用:
  1. 普通同步方法和静态同步方法不能够混用。【锁对象不唯一】
  2. 同步代码块和同步方法混用:保证同步代码块的锁对象和同步方法的锁对象保持一致
  • 总结一句话:锁对象的使用【唯一性】
  • 死锁

  1. A线程需要甲资源,同时拥有乙资源才能继续执行【甲乙资源合起来是锁资源】;B线程需要乙资源,同时拥有甲资源才能继续,两条线程都不肯释放自己拥有的资源,同时也需要对方的其他的资源时,就都无法进行运行。形成“死锁”现象。
  2. 代码表现:有了同步代码块的嵌套,就可能发生死锁。某条线程获取了外层的锁对象A,需要内层的锁对象B,等待;另外一条线程获取了外层的锁对象B,需要内层的锁对象A,等待。两条线程就会形成死锁。

代码示例

public class Demo07 {
    public static void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                while(true) {
                    synchronized ("a") {
                        System.out.println(Thread.currentThread().getName()+"得到了a等待b");
                        synchronized ("b") {
                            System.out.println(Thread.currentThread().getName()+"得到了b可以继续执行了");
                        }
                    }
                }
            }
        }.start();
new Thread() {
            @Override
            public void run() {
                while(true) {
                    synchronized ("b") {
                        System.out.println(Thread.currentThread().getName()+"得到了b等待a");
                        synchronized ("a") {
                            System.out.println(Thread.currentThread().getName()+"得到了a可以继续执行了");
                        }
                    }
                }
            }
        }.start();
    }
}
  • 线程安全火车票案例

  • 分析:多窗口卖票,票唯一的资源。每个窗口的动作相同。一个窗口看成一个线程,线程任务是一样。只需要重写一次run方法指定任务。选择接口实现。任务是干卖票。
  • ​ 卖票过程:
  1. 有票【固定数 变量来模拟票】
  2. 出票 【一次只能出一张 ,出一张票的总数少一张】
  3. 票数为0不卖了
  • 步骤:
  1. 定义一个变量 充当票以及票数
  2. 写循环循环里面开始卖票 票数减1【循环条件,卖票的条件】

代码示例

//定义一个类继承接口
public class SellTicket implements Runnable{ int ticket = 100; int num = 0; @Override public void run() { while(true) { synchronized ("a") { // 票数为0 时不卖了 if (ticket == 0) { System.out.println("票已经卖完了"); break; }else if (ticket >0 && ticket <= 100) { try { // 出票 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() +"正在卖第"+(++num)+"张票剩余票数"+(--ticket)+"张"); } } } } }
//定义测试类public class Ticket_Test { public static void main(String[] args) { SellTicket target = new SellTicket(); // 创建窗口 new Thread(target, "窗口一").start(); new Thread(target, "窗口二").start(); new Thread(target, "窗口三").start(); } }
  • 多线程练习
  • 按要求编写多线程应用程序,模拟多个人通过一个山洞:
  • 这个山洞每次只能通过一个人,每个人通过山洞的时间为5秒;随机生成10个人,同时准备过此山洞,并且定义一个变量用于记录通过隧道的人数。
  • 显示每次通过山洞的人的姓名 [格式:pn n为人数的个数],和通过顺序
  • 分析:

    1.定义一个隧道类,实现Runnable接口:

    1.1 定义一个变量,用来记录通过隧道的人数;

    1.2 重写Runnable的run方法;

    1.3 定义一个同步方法,模拟每个人通过隧道需要5秒钟:

    1.3.1 子线程睡眠5秒钟,模拟每个人通过隧道需要5秒钟;

    1.3.2 改变通过的人次;

    1.3.3 打印线程名称及其通过隧道的顺序,模拟人通过隧道及其顺序;

    1.4 调用通过隧道的方法;

    2.定义一个测试类:

    2.1 在main方法中创建一个隧道类对象;

    2.2 在main方法中,循环创建10个子线程对象,通过构造方法把隧道对象

    和线程名(作为人的姓名)传递进去,并开启子线程;

代码示例

public class Tunnel implements Runnable {
    int count = 0;
    @Override
    public void run() {
        //调用通过隧道的方法
        try {
            crossPerson();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    //定义一个同步方法,模拟每个人通过隧道需要5秒钟
    public synchronized void crossPerson() throws InterruptedException {
        //子线程睡眠5秒钟,模拟每个人通过隧道需要5秒钟
        Thread.sleep(5000);
        //改变通过的人次
        count++;
        //打印线程名称及其通过隧道的顺序,模拟人通过隧道及其顺序
        System.out.println(Thread.currentThread().getName()+"正在通过山洞是第"+count+"个通过的");
    }
}

//定义测试类
package zuoye;
public class Tunnel_Test {
    public static void main(String[] args) {
        Tunnel tul = new Tunnel();
        //利用for 循环完成
        for (int i = 1; i < 10; i++) {
            Thread t = new Thread(tul,"p"+i);
            t.start();
        }
    }
}