欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java线程

程序员文章站 2022-03-08 18:53:16
...

要说线程,那么首先应该聊聊什么是进程,简单来说一个运行的程序就叫进程(至少有一个进程),如果该应用程序没有运行,那么就只是一个程序而已,谈不上进程。更进一步来说,进程就是一块包含了某些资源的内存区域,负责内存的划分。
那么,又何谓线程?
线程在一个进程中负责代码的执行,是执行的最小单位。一个进程至少包含一个线程。如果把进程比作是加工厂,那么里面在流水线上作业的工人就是一个个的线程。
多线程,顾名思义,就是一个进程中有多个线程执行不同的任务,就像加工厂中不同岗位的工人加工不同的产品,即所谓不同的任务。

  • 使用多线程的好处:

1.解决了一个进程同时能执行多个任务的问题。比如QQ,我们在视频聊天的同时也是文字聊天,好比工厂中的多个工人同时工作加工多种产品
2.提高了资源的利用率。比如迅雷的下载,多线程就是多个任务可以同时执行,假设有2M带宽,下载速度一般可以是200kb/s,有一视频因为下载人数过多使得下载速度是30kb/s,如果只能单线程的话,那么剩余的带宽是被浪费掉的;多线程即多任务下载恰好可以对剩余的带宽进行充分的利用,从而保证资源利用率最大化

  • 利弊不分家,肯定也有弊端

1.增加了CPU的负担。多个线程之间的来回切换确实是需要一定的开销
2.降低了一个进程中一个线程被执行的概率。如果某一个进程是单线程的,那么当CPU运行该程序时,线程的执行率是100%;相反,如果是多线程,那么运行的概率就被均分,是减小的
3.引发线程安全问题
4.出现死锁现象



Q:上面说过线程负责代码的执行,但是我们之前并未学过线程,为什么代码可以执行呢?
A:这是因为强大的JVM,任何一个Java程序都是有main方法的,每个程序在运行的时候,JVM就会为每个程序创建一个main线程执行main函数中的所有代码

Q:一个Java应用程序中至少有几个线程?
A:两个,一个是main线程负责main方法的执行,另一个就是垃圾回收器线程,负责回收垃圾


那么,如何去创建一个线程呢?

  • 方法一:

首先我们考虑Java是不是已经为我们提供了一个线程类,答案是肯定的,Thread类就是这样一种存在。那么如何去利用Thread类创建自定义线程呢?
1.自定义类继承Thread类即可
2.重写Thread类的run方法,把自定义线程的任务写在方法中
Q:为什么要重写run方法?
A:每个线程都拥有自己的任务代码,比如JVM创建的主线程就是执行main函数内的代码;而自定义线程的目的无非是想让线程去执行一些任务,那么就需要把任务代码写在run方法中。自定义线程负责了run方法的执行
3.创建Thread子类对象,然后通过对象调用start方法开启线程
注意:一个线程一旦开启,就会自动执行run方法中的代码,run方法不能直接调用,否则就是一个普通的方法而已

对于一个应用程序来说,有启动有运行也有结束,线程作为最小执行单位,其必定也会有生命周期,下面来谈谈一个线程的”生老病死“…….
线程从创建到销毁,其任务就是抢占CPU执行自己的使命(也就是执行相应的代码),其中牵扯到CPU的就是CPU的等待资格和CPU的执行权
大致过程如下:
Java线程

看图分析一下:
对于创建一个新的线程,首先是通过创建一个线程对象,然后调用start方法启动线程,此时此刻的线程具有CPU的等待资格,即该线程可以在线程队列中排队等待使用CPU,当其获得了CPU的执行权,那么此时此刻的线程就处于运行状态,当该线程执行结束即完成了任务,也就是走向了”生命的终点“…..在这个过程中,当一个处于运行状态的线程调用sleep方法或者wait方法就进入了临时阻塞状态,也就是别挂起,如果线程是通过sleep函数进入的该状态,那么经过设定的时间后,线程会自动重新进入可运行状态等待获取CPU的执行权;如果线程是通过wait方法进入的临时阻塞状态,那么该线程必须通过其他线程的唤醒才能重新进入可运行状态。



下面再来说一下其中比较常用的一些方法:
1.public static void sleep(long millis) throws InterruptedException
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
+该方法是抛出异常的,那么在自定义线程类中的方法中使用该方法时,应该用try catch语句去捕获该异常,而不是简单地throws抛出,因为超类Thread线程的run方法中没有抛出任何异常,所以子类也不能抛出任何异常(可以通俗的理解为父亲清清白白,那么儿子也不能做坏事)。
+该方法是静态方法,哪个线程中执行该函数,那么该线程就会休眠设定的时间,与调用的对象无关
看如下代码:

public class myThread extends Thread {
    public void run(){
        System.out.println("myThread...");

    }
    public static void main(String[] args)throws InterruptedException{
        myThread thread = new myThread();
        System.out.println("Start sleep...");
        thread.sleep(1000);
        thread.start();
    }
}

其中虽然是对thread调用的sleep方法,但实际上睡眠的是main线程,因为上面说过,哪个线程中执行该函数,该例子中就是在main线程中执行该函数,所以是main线程,不管前面调用的对象是什么。通常习惯用类去调用该方法,即Thread.sleep(1000);
2.public final int getPriority()
返回线程的优先级,其中默认的线程优先级是5
3.public final void setPriority(int newPriority)
更改线程的优先级
+线程优先级的取值范围是1—10,虽然可以设置优先级,但是线程真正被执行时的优先级取决于底层的操作系统,优先级大也并不代表一定是最先执行的,只能说被先执行的概率大
4.public static Thread currentThread()
返回对当前正在执行的线程对象的引用
+静态方法,哪个线程执行了Thread.currentThread()这个代码,那么获取的就是哪个线程,跟sleep类似



下面利用线程来模拟一个买票的场景,假设有三个买票窗口,总共有50张票,这种情况下就应该用三个线程去模拟三个窗口的卖票过程,代码如下:

public class TicketThreadTest {
    public static void main(String[] args){
        TicketThread thread1 = new TicketThread("窗口1");
        TicketThread thread2 = new TicketThread("窗口2");
        TicketThread thread3 = new TicketThread("窗口3");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}
class TicketThread extends Thread{
    int num = 50;//票数总数为50
    public TicketThread(String name){
        super(name);
    }
    public void run(){
        while(true){
            if(num > 0){
                System.out.println(Thread.currentThread().getName()+"售出了第"+num+"张票");
                num--;
            }else{
                System.out.println("售罄...");
                break;
            }
        }
    }
}

运行程序,结果如下:
窗口1售出了第50张票
窗口1售出了第49张票
窗口1售出了第48张票
窗口1售出了第47张票
窗口1售出了第46张票
窗口1售出了第45张票
窗口1售出了第44张票
窗口1售出了第43张票
窗口1售出了第42张票
窗口1售出了第41张票
窗口1售出了第40张票
窗口3售出了第50张票
窗口3售出了第49张票
窗口3售出了第48张票
窗口3售出了第47张票
窗口2售出了第50张票
…..
由于结果太长,截取了一部分,但是这一部分仍然能反映出存在的问题
问题1:同一张票都被售卖了3次,即50张票售出了150次,这肯定是不合理的!那么,出现这个问题的原因是什么呢?
+出现的原因:因为num是非静态的,每个实例对象都会维护一份非静态成员数据的,所以,每个对象都会有自己的num成员变量,三个对象就会有三份数据!
+解决方案:把num票数共享出来给三个对象,使用static修饰就可。

static int num=50;

运行代码,执行结果如下(同样截取一部分):
窗口1售出了第50张票
窗口1售出了第49张票
窗口1售出了第48张票
窗口3售出了第50张票
窗口3售出了第46张票
窗口3售出了第45张票
……
此时的程序没有同一张票被售卖三次的情况了,因为打印出的全部结果是50+条记录,所以上述的问题解决了,但是怎么还存在50号票被卖了两次的情况呢?
问题2:为什么存在同一张票被卖了两次的情况?
+出现的原因:线程非安全问题
对于为什么出现上述结果,现在我们来分析一下,现有线程1,线程2,线程3,当执行下述代码时:

public void run(){
        while(true){   
            if(num > 0){   //1
                System.out.println(Thread.currentThread().getName()+"售出了第"+num+"张票");  //2
                num--;   //3
            }else{
                System.out.println("售罄...");
                break;
            }
        }
}

假设线程1首先获得了CPU的使用权,在执行上述代码//2结束后,num仍是50,此时线程3抢夺了CPU的使用权,对于//2的语句,首先是先去执行这句Thread.currentThread().getName()+"售出了第"+num+"张票" ,之后再是输出,那么此时线程3恰好还未输出,只是先获得了num的值,即50。此时CPU的使用权又被线程1夺走,执行//3,此时num是49,之后一直是线程1执行,直至售出第48张票后,此时num是47,线程3重新获得CPU使用权,此时继续执行输出打印语句,即打印的是出售第50张票的信息。
+解决方案:
sun提供了同步机制来解决线程非安全问题
Java的同步机制有两种:

方式一:同步代码块

使用格式:
synchronized(锁对象){需要被同步的代码}
所以上述代码可以修改为如下:

class TicketThread extends Thread{
    static int num = 50;//票数总数为50

    static Object o = new Object();
    public TicketThread(String name){
        super(name);
    }
    public void run(){
        while(true){
            synchronized (o){
                if(num > 0){
                    System.out.println(Thread.currentThread().getName()+"售出了第"+num+"张票");
                    num--;
                }else{
                    System.out.println("售罄...");
                    break;
                }
            }
        }
    }
}

此时输出结果就是正常的,不再存在同一张票被售卖多次的情况了。
原理分析:
任何一个对象都可以作为锁对象,可以理解为凡是对象,内部都维护了一个状态,Java同步机制就是使用了对象内部的状态作为了锁的标识,假设为state状态,0表示锁开,1表示锁关,每次执行同步代码块的时候,线程首先去判断锁的开关状态,如果是锁开的状态,那么线程就开始执行代码同时将锁关,此时即使再有其他线程想来运行这段代码,但由于是锁关的状态,所以其他线程只能等待,只有当前线程执行完毕后,此时锁开,其他线程才能去执行这段代码。
注意:
+同步代码块中即使调用了sleep方法,线程也不会释放锁对象,其他线程也是无法去执行当前代码的
+利用同步代码块虽然可以很好的解决线程非安全问题,但是效率不高,因为需要重复判断锁的开关状态!只有存在线程非安全问题的时候,才使用同步代码块
+多线程操作的锁对象也必须是共享的,因为需要判断锁的状态,如果是不同的锁,那么这个锁也就没有任何意义可言了

出现线程非安全的根本原因:
1.存在两个或者两个以上线程,并且线程之间共享资源
2.有多个语句操作了共享资源
就拿上述例子来说,如果是如下代码去掉注释掉的语句,那么就不会出现上述问题了,也就是说对一个资源的操作是完整的,而不是操作到一半被抢走

while(true){   
            //if(num > 0){   
                System.out.println(Thread.currentThread().getName()+"售出了第"+num+"张票"); 
              // num--;   
            }else{
                System.out.println("售罄...");
                break;
            }
        }

利用代码块实现模拟一个情景,Husband和Wife分别拿着银行卡和存折去银行取钱,总金额是5000块,每次取1000块,其中的同步机制是通过同步代码块实现的。

public class BankThreadTest {
    public static void main(String[] args){
        BankThread thread1 = new BankThread("Husband");
        BankThread thread2 = new BankThread("Wife");
        thread1.start();
        thread2.start();
    }
}
class BankThread extends Thread{
    static int count = 5000;
    public BankThread(String name){
        super(name);
    }
    public void run(){
        while(true){
            synchronized("Lock"){
                if(count > 0){
                    System.out.println(Thread.currentThread().getName()+"取走了1000块,剩余"+(count-1000)+"块...");
                    count-=1000;
                }else{
                    System.out.println("取光了...");
                    break;
                }
            }
        }
    }
}

方式二:使用同步函数,也就是通过在函数名前添加关键字synchronized

无论是同步代码块还是同步函数,实现同步的原理机制都是通过“锁”来实现的,代码块我们可以自定义锁对象,那么对于同步函数来说,锁对象是谁呢?

  • 如果是非静态函数,那么锁对象就是this对象,即调用该函数的实例对象
  • 如果是静态函数,那么锁对象就是当前函数所属类的字节码对象(实际指的就是Class对象)

如果同步函数是非静态的,那么执行过程中的锁对象是不共享的,所以即使使用了同步函数也无法解决线程非安全问题,所以说必须使用静态同步函数,所属类的字节码文件是共享的

下面解释一下同步函数的锁对象:class
Java线程
执行图片中的右边代码时(其中的BankThread是自定义的线程类),在栈内存中存储thread1变量,之后的new对象,就用到该类的字节码文件,那么此时JVM从硬盘加载该程序的字节码文件到内存中,然后对字节码的所有成员进行解剖,就可以解析出很多信息来,如果信息散乱就会比较难管理,所以就定义了Class类去存储类文件的信息。整个代码里存在唯一一份。

对于取钱的情景,我们此时通过同步函数去实现同步操作,改动后代码如下:

class BankThread extends Thread{
    static int count = 5000;
    public BankThread(String name){
        super(name);
    }
    public void run(){
        getMoney();
    }
    public synchronized void getMoney(){
        while(true){
            synchronized("Lock"){
                if(count > 0){
                    System.out.println(Thread.currentThread().getName()+"取走了1000块,剩余"+(count-1000)+"块...");
                    count -= 1000;
                }else{
                    System.out.println("取光了...");
                    break;
                }
            }
        }
    }
}

输出结果我们发现一个问题,全部是被同一个人取走的,要么是Husband,要么是Wife,通过分析代码就可以得知,无论是哪个线程首先获得了CPU的使用权,一旦开始执行getMoney方法,则会一直将钱取完才结束,所以使用同步函数有时候并不能非常完美地实现我们想要实现的任务。
所以,推荐使用同步代码块实现同步机制,有两大原因:
1.同步代码块的锁对象可以由我们自行制定,而同步函数中的锁对象是固定的,不能由我们来指定
2.同步代码块可以非常灵活的地控制我们想要同步代码的范围,但是同步函数必须是整块代码,我们都知道同步机制的执行效率并不高,所以本来可以不需要同步执行的代码由于同步函数的原因而必须同步执行,一定程度上执行效率相对较低。
虽然Java同步机制解决了线程非安全问题,但是利弊共存,同时也引发了死锁现象。
对于什么是死锁现象,假设每个线程执行代码需要多个资源,每个线程都持有一个资源等待其他线程释放资源,从而出现了这种互相等待的情况,“你不让我,我不让你”,从而出现一种“僵持”的局面,称为死锁现象。
那么,出现死锁现象的原因是什么呢?
1.首先是多线程,存在两个或两个以上的线程
2.其次是存在共享资源,两个或者两个以上
解决方案:
死锁现象是无解的!只能是我们在写代码的时候尽量避免
例如对于如下情景,Jerry和Maria都想吃牛奶面包,但是一次只能有一个人去吃,即两个人不能同时吃,那么这种情况下可能会出现死锁,因为可能分别被分配吃完一个东西后,等着吃下一个东西,但是这个东西被另一个人占有,而另一个人也是同样的状态,代码如下:

public class DeadThreadTest {
    public static void main(String[] args){
        DeadThread thread1 = new DeadThread("Jerry");
        DeadThread thread2 = new DeadThread("Maria");
        thread1.start();
        thread2.start();
    }
}
class DeadThread extends Thread{
    public DeadThread(String name){
        super(name);
    }
    public void run(){
        if("Jerry".equals(Thread.currentThread().getName())){
            synchronized ("Milk"){
                System.out.println("Jerry get the Milk,ready for Bread...");
                synchronized ("Bread"){
                    System.out.println("Jerry get both...");
                }
            }
        }
        if("Maria".equals(Thread.currentThread().getName())){
            synchronized ("Bread"){
                System.out.println("Maria get the Bread,ready for Milk...");
                synchronized ("Milk"){
                    System.out.println("Maria get both...");
                }
            }
        }
    }
}

可能输出结果:
Jerry get the Milk,ready for Bread…
Maria get the Bread,ready for Milk…
这就是因为出现了死锁现象

  • 方法二:实现Runnable接口类

1.自定义类实现Runnable接口
2.实现接口中的run方法,即将执行任务的代码放在run方法中
3.创建Runnable实现类对象
4.创建一个Thread类对象,并将上述创建的Runnable实现类对象作为参数传递
5.调用线程的start方法启动线程
写个实例演示具体如何实现的:

public class RunnableThread implements Runnable {  //1
    public void run(){       //2
        System.out.println(this);  
        //此处this是指自定义的实现了Runnable接口的类对象
        System.out.println(Thread.currentThread());  //此处是指执行该代码的线程,是sub-thread
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
    public static void main(String[] args){
        RunnableThread runnable = new RunnableThread();   //3
        Thread thread = new Thread(runnable,"sub-thread");  //4
        thread.start();   //5
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

Q1:Runnable对象是Thread对象吗?
A:Runnable对象不是Thread对象,不过是实现了Runnable接口的一个对象而已,只有是Thread或者Thread子类对象才算是Thread对象
Q2:为什么要把Runnable对象作为实参传递给Thread类对象呢?
A:这就需要我们去查看源码,Thread类的

public Thread(Runnable target, String name) {
    init(null, target, name, 0);
}

上述是Runnable对象作为参数传递给Thread类的target形参

@Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

上述Thread类的run方法是调用了target对象的方法,所以可以得知,之所以将Runnable类对象作为实参传递给Thread类,是因为Runnable类对象的run方法作为Thread类的任务代码了
下面利用方法二模拟一下窗口售票的场景,见代码:

public class TicketRunnable implements Runnable {
    int count  = 50;
    public void run(){
        while(true){
            synchronized ("Lock"){
                if(count > 0){
                    System.out.println(Thread.currentThread().getName()+"售出了第"+count+"号票");
                    count -- ;
                }else{
                    System.out.println("售罄...");
                    break;
                }
            }
        }
    }
    public static void main(String[] args){
        //创建一个Runnable实现类的对象
        TicketRunnable ticket = new TicketRunnable();
        //创建三个窗口模拟三个线程
        Thread thread1 = new Thread(ticket,"窗口1");
        Thread thread2 = new Thread(ticket,"窗口2");
        Thread thread3 = new Thread(ticket,"窗口3");
        //开启线程售票
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

对比我们利用方式一实现的代码,会发现此处的count并不是静态的,但是输出的结果是正常的,这是为什么呢?因为我们仅仅创建了一个Runnable类对象,三个线程共享该对象,其中count也是共享的,所以不会出现同一张票被卖3次的情况,是正常的。
推荐使用第二种方式去实现多线程,因为Java是单继承,多实现的



接下来再利用线程模拟一个场景,生产者和消费者的问题,下图表示了三者之间的关系
Java线程
我们利用方式一实现:

class Product{
    String name;
    double price;
}
class Producer extends Thread{
    Product p;
    public Producer(Product p){
        this.p = p;
    }
    public void run(){
        int i = 0;
        while(true){
            if(i%2 == 0){
                p.name = "apple";
                try{
                    Thread.sleep(10);   
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                p.price = 6.5;
            }else{
                p.name = "banana";
                p.price = 2;
            }
            System.out.println("生产者生产出了"+p.name+"  价格是:"+p.price);
            i++;
        }
    }
}
class Customer extends Thread{
    Product p;
    public Customer(Product p){
        this.p = p;
    }
    public void run(){
        while(true){
            System.out.println("消费者消费了"+p.name+"  价格是:"+p.price);
        }
    }
}
public class ProductThread {
    public static void main(String[] args){
        Product p = new Product();
        Customer customer = new Customer(p);
        Producer producer = new Producer(p);
        producer.start();
        customer.start();
    }
}

上述代码的执行结果是有问题的,为了使问题暴露的更明显,我们在Producer类中写了线程休眠的代码,输出结果很明显有问题,因为输出了:消费者消费了apple 价格是:2.0(明明我们设定的apple价格是6.5,消费的时候却是2.0)
这是因为:当生产者的线程生产完一个香蕉后,此时p对象的name值是banana,price值是2.0,此时生产值继续去生产,当把产品name设置为apple时,还没来得及修改price属性,此时CPU的使用权就被消费者剥夺,从而输出的结果就是name值为apple,price值为2.0,价格错乱,出现了线程安全问题!

如何解决这个问题呢?加锁!在这个实例中,锁对象设置为p对象再合适不过了

class Producer extends Thread{
    Product p;
    public Producer(Product p){
        this.p = p;
    }
    public void run(){
        int i = 0;
        while(true){
            synchronized (p){
                if(i%2 == 0){
                    p.name = "apple";
                    p.price = 6.5;
                }else{
                    p.name = "banana";
                    p.price = 2;
                }
                System.out.println("生产者生产出了"+p.name+"  价格是:"+p.price);
                i++;
            }
        }
    }
}
class Customer extends Thread{
    Product p;
    public Customer(Product p){
        this.p = p;
    }
    public void run(){
        while(true){
            synchronized (p){
                System.out.println("消费者消费了"+p.name+"  价格是:"+p.price);
            }
        }
    }
}

输出结果正常了:
生产者生产出了apple 价格是:6.5
生产者生产出了banana 价格是:2.0
生产者生产出了apple 价格是:6.5
生产者生产出了banana 价格是:2.0
生产者生产出了apple 价格是:6.5
消费者消费了apple 价格是:6.5
消费者消费了apple 价格是:6.5
消费者消费了apple 价格是:6.5
消费者消费了apple 价格是:6.5
消费者消费了apple 价格是:6.5
消费者消费了apple 价格是:6.5
消费者消费了apple 价格是:6.5
…….
但是好像要么就是生产一片,消费一片,能不能生产一个消费一个而不是一味的生产,一味的消费?
此时就涉及到线程之间的通信问题了,就好比两个线程可以相互提醒,生产者线程生产结束后,就通知消费者:喂,你可以消费了。消费者线程消费完就可以通知生产者:赶紧生产,我都消费完了!
如何实现?
notify()方法:唤醒等待的线程
wait()方法:如果线程执行了该方法,线程进入睡眠状态,但该线程必须由其他线程调用notify方法唤醒
使用上述两个方法注意是事项:
1.这两个方法是属于Object对象的
2.这两个方法必须在同步代码块中执行
3.这两个方法必须由锁对象调用

class Product{
    String name;
    double price;
    boolean flag=false; //是否生产完成的标志,默认没有生产完成
}
class Producer extends Thread{
    Product p;
    public Producer(Product p){
        this.p = p;
    }
    public void run(){
        int i = 0;
        while(true){
            synchronized (p){
                if(p.flag == false){
                    if(i%2 == 0){
                        p.name = "apple";
                        p.price = 6.5;
                    }else{
                        p.name = "banana";
                        p.price = 2;
                    }
                    System.out.println("生产者生产出了"+p.name+"  价格是:"+p.price);
                    p.flag = true;
                    i++;
                    p.notify();  //生产结束,通知消费者消费
                }else{
                    //生产完成,等待消费者消费
                    try{
                        p.wait();
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
class Customer extends Thread{
    Product p;
    public Customer(Product p){
        this.p = p;
    }
    public void run(){
        while(true){
            synchronized (p){
                if(p.flag == true){
                    System.out.println("消费者消费了"+p.name+"  价格是:"+p.price);
                    p.flag = false;
                    p.notify(); //消费结束,通知生产值生产
                }else{
                    //消费完毕,等待生产者生产
                    try{
                        p.wait();
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
public class ProductThread {
    public static void main(String[] args){
        Product p = new Product();
        Customer customer = new Customer(p);
        Producer producer = new Producer(p);
        producer.start();
        customer.start();
    }
}

通过在产品类中增加标志位,生产者和消费者进程在进行生产或者消费的时候首先判断产品是否生产完毕,如果生产完毕,则应该由消费者来消费,消费完毕后因该修改状态,通知生产者来生产。

守护线程:(后台线程)如果在一个进程中只剩下了守护线程,那么守护线程也会死亡。

通俗理解就是,假设我们登录QQ,QQ后台自动为我们下载更新安装包,但是如果此时我们关闭了QQ应用,那么下载也会停止。
isDaemon():用来判断一个线程是否是守护线程
setDaemon(boolean on):设置线程为守护线程,默认为false,true表示设置为守护线程

public class ProtectThread extends Thread{
    public ProtectThread(String name){
        super(name);
    }
    public void run(){
        for(int i=1;i<=100;i++){
            System.out.println(Thread.currentThread().getName()+":进度是 "+i+"%");
        }
    }

    public static void main(String[] args){
        ProtectThread thread = new ProtectThread("QQ下载 ");
//        System.out.println(thread.isDaemon());
        thread.setDaemon(true);
        thread.start();
        for(int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+"输出 "+i);
        }
    }
}

对于上述示例,手动设定thread为守护线程,当主线程的for循环执行结束后,无论守护线程是否执行完毕,该代码都会运行结束。



还有这么一种情形,比如说一个线程需要另一个线程完成一个任务后该线程才能继续进行,那么此时仅仅通过该线程去启动另一个线程是无法实现预想的效果的,但可以通过函数join()实现,下面举个例子,比如妈妈在做饭,但没有盐了,需要儿子先去买盐回来才能继续做饭,利用代码模拟上述场景
按照一贯的思维,代码如下:

class Mom extends Thread{
    public void run(){
        System.out.println("妈妈洗菜");
        System.out.println("妈妈切菜");
        System.out.println("准备做饭但是没盐了");
        //派儿子去买
        Son son = new Son();
        son.start();
        System.out.println("妈妈继续做饭,做完饭了");
        System.out.println("全家一起吃饭");
    }
}
class Son extends Thread{
    public void run(){
        System.out.println("儿子下楼");
        try{
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("儿子买上盐回家");
    }
}
public class JoinThread{
    public static void main(String[] args){
        Mom mom = new Mom();
        mom.start();
    }
}

运行完代码后,结果如下,好像不合理啊?
妈妈洗菜
妈妈切菜
准备做饭但是没盐了
妈妈继续做饭,做完饭了
全家一起吃饭
儿子下楼
儿子买上盐回家
正确的执行顺序应该是妈妈先等待儿子的线程执行完毕后再执行,join方法就能实现这种效果,如何修改代码呢?如下:

Son son = new Son();
son.start();
try{
    son.join();    
}catch (InterruptedException e){
    e.printStackTrace();
}

son.join()是什么意思呢?
一个线程执行join方法 ,那么就有新的线程加入,此时执行该语句的线程就要让步给新加入的线程,等待新的线程执行结束后才会继续运行



线程目前总结了这么多,后续再补充……