Java进阶 - 线程方法、线程安全问题、等待与唤醒
1.线程方法
(1)getName()获取线程名称
主方法: public class DemoThread { public static void main(String[] args) { MyThread01 thread1 = new MyThread01(); thread1.start(); //获取主线程的名称 System.out.println(Thread.currentThread().getName()); } } 线程子类: public class MyThread01 extends Thread { @Override public void run() { //获取线程名称方法1 /*String name = getName(); System.out.println(name);*/ //获取线程名称方法2 Thread t = Thread.currentThread(); System.out.println(t.getName()); } }
(2)setName(name)设置线程名称
可用线程调用setName方法设置名称;或者创建一个线程子类,并设置一个带参数的构造方法,并且调用父类的方法super(name);即可
(3)sleep()使当前执行的线程暂停指定的毫秒数后再继续执行,需要捕捉异常
public class DemoThread { public static void main(String[] args) { //模拟秒表 for (int i = 1; i <= 60; i++) { System.out.println(i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
2.创建线程的第二种方式:实现runnable接口 并定义一个run的无参数方法 再创建一个Thread对象 将该实现类对象传递进去
实现类: public class MyRunner implements Runnable { @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println(Thread.currentThread().getName() + i); } } } main方法: public class DemoThread { public static void main(String[] args) { MyRunner run1 = new MyRunner(); Thread t = new Thread(run1); t.start(); for (int i = 0; i < 20; i++) { System.out.println(Thread.currentThread().getName() + i); } } }
两种开启线程方法的区别:
实现runnable接口的好处:
(1)避免了单继承的局限性
(2)增强了程序的扩展性,降低了程序的耦合性
把设置线程任务和开启新线程(经由Thread的对象传参执行)进行了分离(解耦)
3.使用匿名内部类创建线程
public class DemoThread { public static void main(String[] args) { //第一种方式 new Thread(){ @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println(Thread.currentThread().getName() + i); } } }.start(); //第二种方式 接口实现 Runnable r1 = new Runnable(){ @Override public void run() { System.out.println(Thread.currentThread().getName()); } }; new Thread(r1).start(); //简化方式 new Thread(new Runnable(){ @Override public void run() { System.out.println(Thread.currentThread().getName()); } }).start(); } }
4.线程安全问题,三个线程共抢一个共享资源,这种情况时不允许出现的
实现类: public class MyRunner implements Runnable { private int ticket = 100; @Override public void run() { //模拟卖票 while (true){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } if (ticket>0){ System.out.println(Thread.currentThread().getName() + "正在卖第" +ticket +"张票" ); ticket --; } } } } 主线程: public class DemoThread { public static void main(String[] args) { MyRunner run = new MyRunner(); //开启三个线程卖票 Thread t1 = new Thread(run); Thread t2 = new Thread(run); Thread t3 = new Thread(run); t1.start(); t2.start(); t3.start(); } }
解决线程安全问题的三个方法
(1)同步代码块
(2)同步方法
(3)锁机制
(1)同步代码块
格式:
synchronized(锁对象){
可能出现线程安全问题的代码块(访问了共享数据的代码)
}
注意:1.同步代码块中的锁对象,可以是任何对象
2.但是必须保证多个线程使用同一个锁对象
3.锁对象的作用:把同步代码块锁住,只让一个线程在同步代码块中执行
public class MyRunner implements Runnable { private int ticket = 100; //创建一个锁对象,必须在run方法外面,要保证该对象的同一性 Object obj = new Object(); @Override public void run() { //模拟卖票 while (true){ //创建同步代码块 synchronized (obj){ if (ticket>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在卖第" +ticket +"张票" ); ticket --; } } } } }
原理:
(2)同步方法:它也会把方法内部的代码块锁住,其锁对象就是实现类对象new出来的那个,也就是this
实现类对象: public class ThreadSecurity implements Runnable { private int ticket = 100; @Override public void run() { while (true) { payTicket(); } } public synchronized void payTicket() { //模拟卖票 if (ticket > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票"); ticket--; } } } 主方法: public class DemoThread { public static void main(String[] args) { ThreadSecurity ts = new ThreadSecurity(); //开启三个线程卖票 Thread t1 = new Thread(ts); Thread t2 = new Thread(ts); Thread t3 = new Thread(ts); t1.start(); t2.start(); t3.start(); } }
也可用静态同步方法,但要注意访问的变量也是静态的 静态方法的锁对象是本类的class属性 - - > class文件对象(反射)
(3)Lock锁
首先在成员位置创建一个ReentrantLock对象,然后调用Lock接口的lock和unlock方法
public class ThreadSecurity implements Runnable { private int ticket = 100; Lock lock = new ReentrantLock(); @Override public void run() { while (true) { lock.lock(); if (ticket > 0) { try { Thread.sleep(10); System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票"); ticket--; } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); //无论程序是否异常都会释放锁 } } } } }
5.线程状态图
生产者消费者等待唤醒案例:
package basicpart.day01.MultiThread; public class PSmodel { public static void main(String[] args) { //1.创建一个锁对象 Object obj = new Object(); //2.创建消费者 new Thread(){ @Override public void run() { while (true){ //创建同步代码块,保证一个线程等待,另一个线程执行 synchronized (obj){ System.out.println("我要一个肉包"); System.out.println("----------------------"); try { obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("那我开吃啦"); System.out.println("吃完啦"); } } } }.start(); new Thread() { @Override public void run() { while (true) { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } //创建生产者 synchronized (obj) { System.out.println("您的包子做好了"); System.out.println("--------------"); obj.notify(); } } } }.start(); } }
什么是等待唤醒机制
这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。
就是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以notifyAll()来唤醒所有的等待线程。
wait/notify 就是线程间的一种协作机制。
等待唤醒中的方法
等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:
1. wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
2. notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
3. notifyAll:则释放所通知对象的 wait set 上的全部线程。
调用wait和notify方法需要注意的细节
1. wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
2. wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。
包子铺案例分析:
代码实现:
//资源类 包子 public class BaoZi { //包子皮 String pi; //包子馅 String xian; //包子状态,true or false,初始值为false boolean flag = false; }
/* 注意: 1.包子铺线程和包子线程关系 - - > 通信(互斥) 2.必须同时同步技术保证两个线程只能有一个在执行 3.锁对象必须唯一,可以使用包子对象作为锁对象 4.包子铺类和吃货的类需要把包子对象作为参数传递进来 既需要在成员位置创建一个包子变量 使用有参构造方法,为这个变量赋值 */ public class BaoZiPu extends Thread { private BaoZi bz; public BaoZiPu(BaoZi bz) { this.bz = bz; } //设置线程任务;生产包子 @Override public void run() { //定义一个变量,来交替生产包子 int count = 0; //让包子铺一直生产包子 while (true) { synchronized (bz) { //对包子的状态进行判断 if (bz.flag == true) { //有包子,让线程等待,让吃货去吃 try { bz.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //被唤醒之后执行,吃货吃完之后,包子铺生产包子 //增加一些趣味性:交替生产两种包子 if (count % 2 == 0) { //生产 薄皮 韭菜馅包子 bz.pi = "薄皮"; bz.xian = "韭菜馅"; } else { //生产 冰皮 猪肉馅 bz.pi = "冰皮"; bz.xian = "猪肉馅"; } count++; //每次生产完一个包子加一,包子就可以交替生产不同的馅了 System.out.println("包子铺正在生产" + bz.pi + bz.xian + "的包子"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } //包子铺生产好包子后,改变包子的状态 bz.flag = true; //唤醒吃货线程 bz.notify(); System.out.println("新鲜的" + bz.pi + bz.xian + "包子出炉啦!!!"); } } } }
public class ChiHuo extends Thread { private BaoZi bz; public ChiHuo(BaoZi bz) { this.bz = bz; } @Override public void run() { while (true){ synchronized (bz){ if(bz.flag == false){ try { bz.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //线程被唤醒之后,就是吃包子 System.out.println("吃货正在吃" + bz.pi + bz.xian + "的包子"); //吃货吃完包子,修改包子的状态 bz.flag = false; //吃货唤醒包子铺生产包子 bz.notify(); System.out.println("吃货已经把" + bz.pi + bz.xian + "的包子吃完了"); System.out.println(" ============================= "); } } } }
public class Demo { public static void main(String[] args) { BaoZi bz = new BaoZi(); //创建包子铺线程 new BaoZiPu(bz).start(); //创建吃货线程 new ChiHuo(bz).start(); } }