匿名内部类创建线程对象;Thread类中的常用方法;多线程中的线程安全问题及处理方法,Java Day22
一,使用匿名内部类创建线程对象
- 什么是匿名内部类:没有名字子类对象
- 本质:是一个对象
- 使用前提:
- 必须有继承或实现关系
- 一定有重写方法
- 格式: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()); //花花, 可以重名 } }
- 结论:
- 线程没有指定名称的时候,默认名称是以thread开头后面跟数字从0 开始依次递增1
- 线程可以指定名称,可以构造指定也可以是set方法指定,可以重名
获取当前线程对象
- 作用:
可以在任意位置,获取当前正在执行这段代码的线程对象
- 静态方法:
Thread Thread.currentThread();
返回当前正在执行这段代码的线程对象的引用
哪条线程在执行这段代码,返回的就是哪条线程的对象
代码示例
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线程对象,意味着没有启动新的线程 } }
练习
- 获取主方法所在的线程的名称
- 获取垃圾回收线程的线程名称
- 分析:
- 获取线程名称【 getName()被谁线程对象调用】
- 相办法获取当前线程对象【 Thread.currentThread() 】,
- 调用垃圾回收器:System.gc ( )
- 如何确定垃圾回收线程工作?重写 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()); } }
线程休眠
- 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 } }
- 注意事项:
- sleep 方法存在一个 InterruptedException 异常的。该异常在 main 方法中可以声明处理也可以捕获处理。
- 如果在 run 方法中,InterruptedException 异常只能捕获异常处理。
守护线程
- 概述:给普通线程执行创造执行环境的线程,守护普通线程可以安全的执行。一般守护线程看不到的,默默无闻做奉献的一个线程。比如:垃圾回收线程
- 如果所有的普通线程都消失,守护线程就没有存在的必要。【普通线程消失,守护线程也会及时的关闭】
- 方法:
- setDaemon(boolean flag):给线程设置成为守护线程
- 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类中有三个优先级的常量值:
-
MAX_PRIORITY
:线程可以具有的最高优先级。 10 -
MIN_PRIORITY
:线程可以具有的最低优先级。 1 -
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(); } }
锁对象的说明
- 同步代码块的锁对象是谁?
- 同步代码块的锁对象没有确定前可以是任意引用数据类型对象,一旦确定下来所有的同步代码块的锁对象保证唯一【同一个对象】
- 锁要唯一。不然解决不了安全问题。
- 同步方法的锁对象是谁?
- 普通同步方法:默认的锁对象是this【当前调用对象】使用的时候必须保证所有的同步方法的调用对象是同一个对象
- 静态同步方法:默认的锁对象是 字节码文件对象【类名.class】
- 使用注意事项:【保证锁对象唯一】
- 如果单一方式使用:
- 单一同步代码块:需要同步代码块的所有的锁对象上下一致,保证唯一
- 单一使用同步方法:需要保证所有的同步方法的调用对象始终是同一个对象
- 混合使用:
- 普通同步方法和静态同步方法不能够混用。【锁对象不唯一】
- 同步代码块和同步方法混用:保证同步代码块的锁对象和同步方法的锁对象保持一致
- 总结一句话:锁对象的使用【唯一性】
死锁
- A线程需要甲资源,同时拥有乙资源才能继续执行【甲乙资源合起来是锁资源】;B线程需要乙资源,同时拥有甲资源才能继续,两条线程都不肯释放自己拥有的资源,同时也需要对方的其他的资源时,就都无法进行运行。形成“死锁”现象。
- 代码表现:
有了同步代码块的嵌套,就可能发生死锁。某条线程获取了外层的锁对象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方法指定任务。选择接口实现。任务是干卖票。
- 卖票过程:
- 有票【固定数 变量来模拟票】
- 出票 【一次只能出一张 ,出一张票的总数少一张】
- 票数为0不卖了
- 步骤:
- 定义一个变量 充当票以及票数
- 写循环循环里面开始卖票 票数减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(); } } }