Java基础关键_029_线程(二)
目 录
一、定时任务
二、合并线程
1.说明
2.实例
三、线程生命周期中的 JVM 调度
1.优先级
(1)说明
(2)实例1
(3)实例2
2.让位
(1)说明
(2)实例
四、线程安全
1.说明
2.实例
3.同步代码块
4.问题1
5.问题2
6.问题3
7.问题4
一、定时任务
通过定时器(Timer)和定时任务(TimerTask),可以实现每间隔一定时间执行一次某部分程序。
public class TimerTaskTest extends TimerTask {@Overridepublic void run() {SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss SSS");Date date = new Date();String format = dateFormat.format(date);System.out.println("当前时间:" + format);}
}
public class ThreadTest {public static void main(String[] args) {SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss SSS");Date start = null;try {start = simpleDateFormat.parse("2025-03-26 12:50:03 789");} catch (ParseException e) {throw new RuntimeException(e);}Timer timer = new Timer(true); // 创建一个定时器,无参构造方法表示创建一个非守护线程,有参构造方法参数若为true,表示创建一个守护线程timer.schedule(new TimerTaskTest(), start, 1000);for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + ":" + i);try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
二、合并线程
1.说明
- 调用 join 方法完成线程合并,该方法是一个实例方法;
- 假设在主线程中调用分支线程对象的 join 方法,会将分支线程合并到主线程,主线程进入阻塞状态。直到分支线程执行结束,主线程阻塞状态才被解除;
- join 方法 与 sleep 方法的比较:
- sleep() 是静态方法,join() 是实例方法;
- sleep() 可以指定休眠时长,而 join() 不能保证阻塞时长;
- 两者都是让当前线程进入阻塞状态;
- sleep() 解除阻塞条件:时间到;
- join() 解除阻塞条件:调用该方法的线程结束。
2.实例
public class MyClassThread extends Thread {@Overridepublic void run() {for (int i = 0; i < 10; i++) {Thread.currentThread().setName("分支线程");System.out.println(Thread.currentThread().getName() + ":" + i);}}
}
public class ThreadTest {public static void main(String[] args) {MyClassThread thread = new MyClassThread();thread.start();System.out.println("主线程开始执行");try {thread.join();} catch (InterruptedException e) {throw new RuntimeException(e);}for (int i = 0; i < 10; i++) {Thread.currentThread().setName("主线程");System.out.println(Thread.currentThread().getName() + ":" + i);}System.out.println("主线程执行结束");}
}
三、线程生命周期中的 JVM 调度
1.优先级
(1)说明
- 线程是可以设置优先级的,优先级较高的,抢占 CPU 时间片的概率较高,仅仅是概率,并不是一定;
- JVM 采取的是抢占式调度模型;
- 在 Java 中,默认情况,一个线程的优先级是 5 。最低是 1 ,最高是 10 。
(2)实例1
public class ThreadTest {public static void main(String[] args) {int maxPriority = Thread.MAX_PRIORITY;int minPriority = Thread.MIN_PRIORITY;int normalPriority = Thread.NORM_PRIORITY;System.out.println("最高优先级:" + maxPriority);System.out.println("最低优先级:" + minPriority);System.out.println("默认优先级:" + normalPriority);int mainPriority = Thread.currentThread().getPriority();System.out.println("主线程优先级:" + mainPriority);System.out.println("============================");Thread.currentThread().setPriority(7);mainPriority = Thread.currentThread().getPriority();System.out.println("主线程优先级:" + mainPriority);}
}
(3)实例2
public class MyClassThread extends Thread {int count = 0; // 为了展现结果时方便比较,特别加此属性,无其他特殊意义@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.print(Thread.currentThread().getName() + ":" + i + "\t");count++;if (count == 6) {System.out.println();}}}
}
public class ThreadTest {public static void main(String[] args) {MyClassThread t1 = new MyClassThread();MyClassThread t2 = new MyClassThread();t1.setName("t1");t2.setName("t2");t1.setPriority(Thread.MAX_PRIORITY);t2.setPriority(Thread.MIN_PRIORITY);t1.start();t2.start();}
}
2.让位
(1)说明
- 通过 yield 方法实现让位,该方法是一个静态方法;
- 该方法会让当前线程让位;
- 让位不会让线程进入阻塞状态,只是放弃目前占有的 CPU 时间片,进入到就绪状态,继续抢占 CPU 时间片;
- 只能保证在概率上,大概率到了某个节点让位一次。
(2)实例
public class MyClassThread extends Thread {int count = 0; // 为了展现结果时方便比较,特别加此属性,无其他特殊意义@Overridepublic void run() {for (int i = 0; i <= 10; i++) {System.out.print(Thread.currentThread().getName() + ":" + i + "\t");if ("t1".equals(Thread.currentThread().getName()) && (i % 2 == 0)) {Thread.yield();System.out.println("[" + Thread.currentThread().getName() + " yield,下标是:" + i + "]");count++;}if (count == 3) {System.out.println();}}}
}
public class ThreadTest {public static void main(String[] args) {MyClassThread t1 = new MyClassThread();MyClassThread t2 = new MyClassThread();t1.setName("t1");t2.setName("t2");t1.start();t2.start();}
}
四、线程安全
1.说明
- 需要考虑线程安全的情况:
- 多线程并发;
- 有共享数据;
- 共享数据涉及修改操作。
- 一般情况下:
- 局部变量不存在该问题,存储在栈中,栈是私有的,不共享。特别是基本数据类型;
- 实例变量可能存在该问题,因为实例变量存储在堆中,堆内存是多线程共享的;
- 静态变量可能存在该问题,因为静态变量也是存储在堆内存中。
- 线程排队执行即线程同步机制,线程并发即线程异步机制。异步效率高,但是可能存在安全隐患
2.实例
前情提要:假设小明和小红夫妻双方拥有同一张银行卡,并且两人分别对这张卡绑定了微信支付和支付宝支付。有一天在同一时间,恰巧两人用这个账户同时支付 500 元。如果没有线程安全保障,小明扣款成功的信息没有传回银行服务器,而小红也申请支付,那么会导致两人总共扣款500元。示例如下:
public class BankAccount {private long accountNumber;private double balance;public BankAccount() {}public BankAccount(long accountNumber, long balance) {this.accountNumber = accountNumber;this.balance = balance;}public long getAccountNumber() {return accountNumber;}public void setAccountNumber(long accountNumber) {this.accountNumber = accountNumber;}public double getBalance() {return balance;}public void setBalance(double balance) {this.balance = balance;}/** 取款* */public void withdraw(double money) {double oldBalance = this.getBalance();System.out.println(Thread.currentThread().getName() + "准备取款" + money + "元,当前账户余额为:" + oldBalance);try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}this.setBalance(oldBalance - money);System.out.println(Thread.currentThread().getName() + "完成取款" + money + "元,当前账户余额为:" + this.getBalance());}
}
public class Withdraw implements Runnable{private BankAccount account;public Withdraw(BankAccount account) {this.account = account;}@Overridepublic void run() {account.withdraw(500);}
}
public class ThreadTest {public static void main(String[] args) {BankAccount bankAccount = new BankAccount(123456789, 10000);Thread t1 = new Thread(new Withdraw(bankAccount));Thread t2 = new Thread(new Withdraw(bankAccount));t1.setName("小明");t2.setName("小红");t1.start();t2.start();}
}
3.同步代码块
语法:
synchronized (需要排队的几个线程共享的对象){
// 同步代码块
}
为什么会造成这样严重的损失问题呢?
因为小明和小红两个线程不是同步的,只要修改两线程为同步操作,就避免了这种问题的发生。那么如何实现这种线程同步机制呢?
一个方法是可以使用 synchronized 关键字。原理:假设某个对象是 t1、t2 两个线程共享的,两个线程一定有一个先抢占到 CPU 时间片。假设 t1 先抢占到,则会去寻找共享对象的对象锁,找到后立即占有,占有对象锁就有权执行同步代码块内的代码。而 t2 则需要等待 t1 执行完同步代码块,释放该对象锁。
需要修改 BankAccount.java 文件,示例如下:
public class BankAccount {private long accountNumber;private double balance;public BankAccount() {}public BankAccount(long accountNumber, long balance) {this.accountNumber = accountNumber;this.balance = balance;}public long getAccountNumber() {return accountNumber;}public void setAccountNumber(long accountNumber) {this.accountNumber = accountNumber;}public double getBalance() {return balance;}public void setBalance(double balance) {this.balance = balance;}/** 取款* */public void withdraw(double money) {synchronized (this) {double oldBalance = this.getBalance();System.out.println(Thread.currentThread().getName() + "准备取款" + money + "元,当前账户余额为:" + oldBalance);try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}this.setBalance(oldBalance - money);System.out.println(Thread.currentThread().getName() + "完成取款" + money + "元,当前账户余额为:" + this.getBalance());}}
}
注意:控制同步代码块的范围,其范围越小,效率越高。 另外,也可以在实例方法上添加 synchronized 关键字,如此这个实例方法整体便是同步代码块,其共享对象的对象锁一定是 this 。
4.问题1
题目:下面给出的代码中,m2() 是否会等待 m1() 执行完成后再执行?
public class Method {public synchronized void m1(){System.out.println("m1 开始执行");try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("m1 执行结束");}public void m2(){System.out.println("m2 开始执行");System.out.println("m2 执行结束");}
}
public class MyClassThread implements Runnable {private Method method;public MyClassThread(Method method) {this.method = method;}@Overridepublic void run() {if ("t1".equals(Thread.currentThread().getName())) {method.m1();}if ("t2".equals(Thread.currentThread().getName())) {method.m2();}}
}
public class ThreadTest {public static void main(String[] args) {Method method = new Method();Thread t1 = new Thread(new MyClassThread(method));Thread t2 = new Thread(new MyClassThread(method));t1.setName("t1");t2.setName("t2");t1.start();try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}t2.start();}
}
答案:答案是否定的,因为 m2() 并没有被 synchronized 关键字修饰,所以无需寻找对象锁。
5.问题2
题目:若将问题1中的 m2() 也加上 synchronized 关键字,那么 m2() 是否会等待 m1() 执行完成后再执行?
public class Method {public synchronized void m1(){System.out.println("m1 开始执行");try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("m1 执行结束");}public synchronized void m2(){System.out.println("m2 开始执行");System.out.println("m2 执行结束");}
}
答案:答案是需要,因为只创建了一个 Method 对象,两个线程共享同一个对象,所以只有一个对象锁,所以需要等待。
6.问题3
题目:已知问题2是因为只创建了一个 Method 对象,导致只有一把对象锁,所以需要等待。那么创建两个 Method 对象,结果会怎样呢?
public class ThreadTest {public static void main(String[] args) {Method m1 = new Method();Method m2 = new Method();Thread t1 = new Thread(new MyClassThread(m1));Thread t2 = new Thread(new MyClassThread(m2));t1.setName("t1");t2.setName("t2");t1.start();try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}t2.start();}
}
答案: 答案当然是不需要等待了,因为是两个 Method 对象,就会有两个对象锁,各自执行自己的同步代码块。
7.问题4
题目:若在问题3的基础上,为 m1() 和 m2() 加上 static 关键字修饰,结果又如何呢?
public class Method {public static synchronized void m1() {System.out.println("m1 开始执行");try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("m1 执行结束");}public static synchronized void m2() {System.out.println("m2 开始执行");System.out.println("m2 执行结束");}
}
答案:答案是 m2() 需要等待 m1() 执行完成再执行。因为当 synchronized 出现在 静态方法上时,线程就会去找类锁,无论创建多少个对象,一个类只有一把类锁。
由此可得,静态方法上添加 synchronized 关键字,实际是为了保证静态变量的安全 。
在实例方法上添加是为了保证实例变量的安全。