匿名内部类创建线程对象;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类
public class 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();
}
}
}