创建线程的两种方式
- 继承Thread类
定义一个MyThread类继承Thread,并赋予该类一个run方法,线程逻辑是在该方法里执行的。
启动顺序:调用start方法 ---> 等待CPU资源 ---> 转入run方法中执行线程逻辑 ---> 执行完线程就消亡了
class MyThread extends Thread{
......
@Override
public void run(){
...... //线程逻辑的执行区域
}
}
MyThread myThread = new MyThread(); //创建线程
myThread.start(); //启动线程
- 实现Runnable接口
定义一个MyThread2类再implements Runnable,赋予该类一个run方法,线程逻辑是在该方法里执行的。
启动顺序:调用start方法 ---> 接收从Thread传来的参数 ---> 等待CPU资源 ---> 转入run方法中执行线程逻辑 ---> 执行完线程就消亡了
class MyThread2 implements Runnable{
......
@Override
public void run(){
......
}
}
MyThread2 myThread2 = new MyThread2();
Thread td = new Thread(myThread2); //创建线程
td.start(); //启动线程
- 两种方法的共同点
创建线程都需要new出来一个MyThread的对象
启动线程都需要调用这个对象中的start方法
- 两种方法的比较
- 1、Runnable方式可以避免Thread方式由于java单继承特性带来的缺陷
补充:java中一个子类可以实现多个接口但是只能继承一个父类(如下图)
- 2、Runnable的代码可以被多个线程(Thread实例)共享,适合于多个线程处理统一资源的情况
卖票实例测试
- Thread模拟测试:
class MyThread extends Thread{
private int ticketsCont = 5; //一共5张火车票
private String name; //窗口,即线程名字
public MyThread(String name) {
this.name = name;
}
@Override
public void run() {
while(ticketsCont > 0) {
ticketsCont --; //如果还有票,就卖掉一张
System.out.println(name+"卖了1张票,剩余票数为:"+ticketsCont);
}
}
}
public class TicketsThread {
public static void main(String[] args) {
//创建三个线程,模拟三个窗口买票
MyThread mt1 = new MyThread("窗口1");
MyThread mt2 = new MyThread("窗口2");
MyThread mt3 = new MyThread("窗口3");
//启动三个线程,即是窗口,开始买票
mt1.start();
mt2.start();
mt3.start();
}
}
- 运行结果:
- Thread模拟测试:
class MyThread implements Runnable{
private int ticketsCont = 5; //一共5张火车票
@Override
public void run() {
while(ticketsCont > 0) {
ticketsCont --; //如果还有票,就卖掉一张
System.out.println(Thread.currentThread().getName()/*获得当前线程的名字*/+
"卖了1张票,剩余票数为:"+ticketsCont);
}
}
}
public class TicketsRuunable {
public static void main(String[] args) {
MyThread mt = new MyThread();
//创建三个线程,模拟三个窗口买票
Thread th1 = new Thread(mt,"窗口1");
Thread th2 = new Thread(mt,"窗口2");
Thread th3 = new Thread(mt,"窗口3");
//启动三个线程,即是窗口,开始买票
th1.start();
th2.start();
th3.start();
}
}
- 测试结果:
补充:控制台输出的语句顺序,主要看是哪个线程先拿到CPU的资源(如上图)
课外小知识
- 调用start()方法是启动线程的正确方式
- 停止线程有三种方式:
一、使线程正常运行完run()方法后,自动终止;
二、使用stop()方法强行终止,但这个方法是不安全的,已经是废弃掉的方法;
三、使用interrupt方法中断线程
线程的生命周期
线程生命周期状态转换图
创建:新建一个线程对象,如:Thread thd = new Thread()
就绪状态:创建了线程对象后,调用了线程的start()方法(注意:此时线程只是进入了线程队列,等待获取CPU服务,具备了运行的条件,但并不一定已经开始运行了)
运行状态:处于就绪状态的线程,一旦获取了CPU资源,便进入到运行状态,开始执行run()方法里的逻辑
终止:线程的run()方法执行完毕,或者线程调用了stop()方法,线程便进入终止状态(注意,stop()方法已经很少使用了)
阻塞状态:一个正在执行的线程在某些情况下,由于某种原因而暂时让出了CPU资源,暂停了自己的执行,便进入了阻塞状态,如:调用了sleep()方法
线程的守护神–守护线程
java线程有两类:
1、用户线程:运行在前台执行具体的任务(如:程序的主线程、连接网络的子线程等)
2、守护线程:运行在后台,为其他前台线程服务(特点:一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作;应用:数据库连接池中的检测线程、JVM虚拟机启动后的检测线程)
设置守护线程
调用Thread类的setDaemon(true)方法将当前线程设置为守护线程
注意事项
- setDaemon(true)必须在start()方法之前调用,否则会抛出IllegalThreadStateException异常
- 在守护线程中产生的新线程也是守护线程
- 不是所有的任务都可以分配给守护线程来执行,如:读写操作、计算逻辑
thread、notify、join、yield的方法说明
notify():如果一个线程执行了notify方法,那么就会唤醒以锁对象为标识符的线程池中等待线程中其中一个
join():join方法是实现线程同步,可以将原本并行执行的多线程方法变成串行执行
yield():使当前线程从运行状态变为就绪状态。cpu会从众多的就绪状态里选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到了。
线程异常处理
测试代码:
class MyThread extends Thread{
public void run(){
System.out.println("Throwing in " +"MyThread");
throw new RuntimeException();
}
}
class Main {
public static void main(String[] args){
MyThread t = new MyThread();
t.start();
try{
Thread.sleep(1000);
}
catch (Exception x){
System.out.println("Caught it" + x);
}
System.out.println("Exiting main");
}
}
测试结果:
死锁的解决方案
导致死锁的原因
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,而该资源又被其他线程锁定,从而导致每一个线程都得等其它线程释放其锁定的资源,造成了所有线程都无法正常结束。
死锁产生的四个必要条件
- 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
- 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放
- 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有
- 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路
当上述四个条件都成立的时候,便形成死锁
解决方案
- 死锁预防:破坏导致死锁必要条件中的任意一个就可以预防死锁。例如,要求用户申请资源时一次性申请所需要的全部资源,这就破坏了保持和等待条件;将资源分层,得到上一层资源后,才能够申请下一层资源,它破坏了环路等待条件。预防通常会降低系统的效率
- 死锁避免:避免是指进程在每次申请资源时判断这些操作是否安全,例如,使用银行家算法。死锁避免算法的执行会增加系统的开销
- 死锁检测:死锁预防和避免都是事前措施,而死锁的检测则是判断系统是否处于死锁状态,如果是,则执行死锁解除策略
- 死锁解除:这是与死锁检测结合使用的,它使用的方式就是剥夺。即将某进程所拥有的资源强行收回,分配给其他的进程