今日学习线程有关的知识点

1.什么是线程,什么是进程,两者的区别

  • 让我们通过一个小故事来了解一下线程和进程
    班级10人被派遣帮工,由A整理工作任务,由B负责指挥监管,B要求所有工作指派一人,每过5分钟重新指派任务,人员是否休息,工作是否暂停,均由B管理。
    此时A整理出5个任务,5个任务中,由两件任务需要同一件物品的帮助,在进行中,两个任务如果同时需要这件物品时,只能由一个人进行,另一个人需要在一旁等待工具用完。每过5分钟,重新分配任务,每个人所做的事不一定是同一件,每件事也不一定是同一个人做的。
    接下来再次整理出10个任务,此时有15个任务,任务执行顺序按照重要程度分配人手,重要事情优先分配人手。15个任务,却只有10个人,同时最多做10个,另5个任务没有分配人去做,当有某些其他任务完成时,便腾出人手完成这些。如果有些任务需要在特定时间特定情况执行或者需要其他任务做出的结果时,B会暂时搁置这项任务,不会参与考虑,等时机成熟再重新考虑分配人手。
    A看大家工作很忙,设立了一个工作岗位,帮助其他人整理工具,运送物资,这个岗位不会因为某项任务完成而终止,需要等到所有任务结束才结束。但是这个工作谁来做,依旧由B说了算。
  • 我们来分析一下:
    1. 由A整理工作任务:系统整理进程任务
      由B负责指挥监管:cpu调度中心
    2. B要求所有工作指派一人:每个进程一时间由一个线程处理(人去处理任务可以看作线程去完成进程)
    3. 每过5分钟重新指派任务:OS的时间片,在时间片结束时不管任务是否完成,线程都撤出,即cpu都脱离当前任务。
    4. 此时A整理出5个任务:当前有5个进程需要运行
    5. 两件任务需要同一件物品:资源争抢。
    6. 两个任务如果同时需要这件物品时,只能由一个人进行,另一个人需要在一旁等待工具用完:一个进程等待另一个进程释放资源
    7. 每个人所做的事不一定是同一件,每件事也不一定是同一个人做的:每次时间片轮换,进程和线程组合不一定相同。
    8. 接下来再次整理出10个任务:可以理解为又开了10个软件(十个进程)
    9. 任务执行顺序按照重要程度分配人手:cpu调度中心根据进程的优先级排列顺序进程排在队列中,排列顺序程序员无法直接操作,不会出现程序员说就一直执行某个进程。是否执行,什么时候执行都是cpu调度中心说了算。
    10. 15个任务,却只有10个人,同时最多做10个,另5个任务没有分配人去做:开了15个软件,但是机器只有10个核心,同一时间只能处理其中10个任务,会有5个任务处于暂停状态。
    11. 如果有些任务需要在特定时间特定情况执行或者需要其他任务做出的结果时,B会暂时搁置这项任务,不会参与考虑,等时机成熟再重新考虑分配人手:cpu调度中心对无法执行的进程挂起,称为阻塞。不会对这个进程考虑排队。
    12. A看大家工作很忙,设立了一个工作岗位,这个岗位不会因为某项任务完成而终止,需要等到所有任务结束才结束:称为守护线程。比如说jvm中垃圾处理机制,就是一个伴随java虚拟机运行,一直在执行的程序,不会自己停止。守护线程的运行自身不控制结束,主线程停止时守护线程自动停止,一般用来做监控,数据归档等操作
  • 进程:正运行中的应用程序叫进程,每个进程运行时,都有自已的地址空间(内存空间)
  • 线程:线程是轻量级的进程,是进程中一个负责程序执行的控制单元,线程没有独立的地址空间(内存空间),线程是由进程创建的(寄生在进程中),一个进程可以拥有多个线程,至少一个线程

2.线程的生命周期,有哪些状态,能解决什么问题

  • 线程的生命周期:线程从创建(new)到死亡(Dead)
  • 线程有几个状态:1、创建(new)2、就绪(Runnable)3、运行(Running)4、阻塞(Blocked)5、死亡(Dead)
  • 线程能解决什么问题:解决资源浪费和分段一起执行;开启多个线程是为了同时运行多部分代码,每个线程都 有自已的运行的内容,这个内容可以称线程要执行的任务(放在run()方法中)
  • 多线程的优点:
    1. 多线程最大的好处在于可以同时并发执行多个任务;
    2. 多线程可以最大限度地减低CPU的闲置时间,从而提高CPU的利用率。
  • 多线程的缺点:
    1. 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
    2. 多线程需要协调和管理,所以需要CPU时间跟踪线程;
    3. 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题;
    4. 线程太多会导致控制太复杂,最终可能造成很多Bug。

3.线程的创建方式

1. 继承Thread类创建线程

//先创建一个类,继承Thread类创建线程,重写Thread类中的run方法
//在run方法中写创建新线程要完成的任务

public class 线程练习1 {
    public static void main(String[] args) {
        /*
         * Demo d1 = new Demo();// 继承thread实例 
         * d1.start();// 开启线程,执行 其实是进入到进程队列
         */
        Demo d1 = new Demo();
        d1.setName("c11111");//使用setName方法给线程起名
        d1.start();
        Demo d2 = new Demo();
        d2.setName("c22222");
        d2.start();
        Demo d3 = new Demo();
        d3.setName("c33333");
        d3.start();

    }
}

class Demo extends Thread {
    /*
     * int a = 0;
     * 
     * public void run() { // 在run方法中写创建新线程要完成的任务
     *  for (int i = 0; a < 10000; i++) {
     * System.out.println(a++); } }
     */

    /*
     * static int a=0; 
     * int count=1; 
     * public void run() { // 在run方法中写创建新线程要完成的任务 for
     * (int i = 0; a < 10000; i++) { 
     * int a1=a+1;
     * a=a1;
     * System.out.println(this.getName()+"\t线程"+a+"\t争抢过"+count+++"次");
     * //发生资源争抢,不断写入陈旧数据,要避免这种情况,引入资源锁,一个资源在同一时间只能由一个线程调用 } }
     */

    // 有资源锁
    static int a = 0;
    int count = 1;
    Object o = new Object();

    public void run() {
        // 在run方法中写创建新线程要完成的任务
        for (int i = 0; a < 10000; i++) {

            synchronized (o) {
                int a1 = a + 1;
                a = a1;
                System.out.println(this.getName() + "\t线程" + a + "\t争抢过" + count++ + "次");
//使用getName得到线程名字
            }
        }
    }
}

2. 实现Runnable接口创建线程

//先创建一个类,实现Runnable类创建线程,重写Runnable类中的run方法
//在run方法中写创建新线程要完成的任务

public class 线程练习2 {
    public static void main(String[] args) {
        Demo2 d1 = new Demo2();
        Demo2 d2 = new Demo2();
        Thread t1 = new Thread(d1, "第一线程");
//可以给线程起名字,但是参数传不过去,显示不出来
        Thread t2 = new Thread(d2, "第二线程");
        t1.start();
        t2.start();
    }
}

class Demo2 implements Runnable {
    @Override
    public void run() {
        // TODO Auto-generated method stub
        for (int i = 0; i < 1000; i++) {
        }
    }
}

3. 使用Callable和Future创建线程

//第一个类
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//跟前两种方法比较,这个方法有返回值,要注意返回值类型
public class 线程练习3_0 {
    public static void main(String[] args) {
        线程练习3 myCallable = new 线程练习3();// 创建myCallable对象
        FutureTask ft = new FutureTask(myCallable);// 使用FutureTask来包装myCallable对象
        // 实现Callable接口重写call方法,创建Callable接口的实现类传参创建FutureTask类对象,继续传参创建thread对象
        // FutureTask类的get方法可以获得Callable执行后的返回值
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Thread thread = new Thread(ft);// FutureTask对象作为Thread对象的target创建新的线程
                thread.start();
            }
        }
        System.out.println("主线程for循环执行完毕");
        try {
            String str = (String) ft.get();
            System.out.println("sum=" + str);
        } catch (InterruptedException e) {
            // TODO: handle exception
            e.printStackTrace();
        } catch (ExecutionException e) {
            // TODO: handle exception
            e.printStackTrace();
        }
    }
}

//第二个类
import java.util.Random;
import java.util.concurrent.Callable;

public class 线程练习3 implements Callable {
    static Random ran = new Random();

    @Override
    public String call() throws Exception {
        // TODO Auto-generated method stub
        System.out.println("Callable run");
        return ran.nextBoolean() ? "515" : "asd";
    }
}

4. 使用线程池例如用Executor框架

  • 一共四种方法创建线程:
    1.继承Thread类,重写run方法
    2.实现Runable类,重写run方法
    3.实现Callable接口,重写call方法,创建Callable接口的实现类传参创建FutureTask类对象,继续传参创建Thread对象;FutureTask类的get方法可以获得Callable执行后的返回值
    4.使用线程池
  • 锁:在使用多线程时,由于会争抢资源,写入重复陈旧资源,为了避免这样的情况,我们想让资源同步,这样就有了锁(synchronized)的概念
    synchronized(对象/方法){执行的代码块}
    锁的对象可以是任何对象(String、数组、this等等)或者方法,对什么上锁就是在同一时间内只能有一个线程执行run方法中synchronized里面的代码块,在多线程中,很多线程抢夺同一个资源,在时间片内未完成的任务会在下个时间片继续执行,这样就会重复写入数据,使用了锁会大大降低这个问题,甚至将锁的优先级提高会解决了重复写入的问题。
    1. 锁的前提:需要两个或两个以上的线程,并且多个线程使用的是同一个锁
    2. 锁的弊端:当线程相当多的时候,每个线程都去抢夺锁,这样会耗费资源,降低运行效率
    3. 还要注意:不要盲目对不需要上锁的资源添加锁,会降低运行效率
    4. 死锁的形成:两个线程互相有对方依赖的资源,而且都上了锁,会造成无限阻塞;根源在于不恰当的使用synchronized关键字来管理线程访问的资源
    5. 死锁的解决方法:让线程持有独立的资源,尽量不采用嵌套的synchronized语句,要设计出良好的算法来避免死锁的发生
  • 优先级:计算机只有一个CPU,各个线程轮流获得CPU的使用权,才能执行任务;优先级比较高的线程会获得更多的使用CPU的机会,反之亦然;优先级有10个等级:1~10,一般情况下线程默认优先级是5,也可以通过setPriority和getPriority方法来设置或返回优先级。
    注意:要想设置优先级,必须在线程启动前设置好它的优先级别
  • 启动与停止:使用start方法是让线程启动,使用stop方法强制让线程停止,会造成数据丢失或其他的逻辑错误,可以使用isInterrupted方法是否标记为关闭,如果标记为关闭,当前线程就调节为停止状态或开始做停止操作返回值Boolean型,这个方法不会停止线程,它是给线程一个停止信号,然后需要配合判断语句来进行线程的停止。
  • 睡眠与休眠和唤醒:sleep 睡眠wait 等待(休眠)notify唤醒
    对象.sleep(时间单位毫秒);睡眠sleep是Thread类的静态方法,后面必须指定时间,到点就醒,如果有资源锁的情况下,它会占用资源锁一起睡眠,到点醒时,完成锁内任务后回到争夺资源的队列。
    对象.wait(时间单位毫秒);休眠wait是object类的final方法,后面可以指定时间,也可以不指定时间,但是必须用notify唤醒,要不然这个线程会一直休眠,但是wait不会占用资源锁,别的线程会抢夺资源锁进行别的操作
    对象.notify();唤醒notify是object类的final方法,用来唤醒休眠的线程,让其继续工作
    ![图片说明](https://uploadfiles.nowcoder.com/images/20191128/263211851_1574949611091_8A5F006FFD4CE801C8E888EC3BC68361 "图片标题")
  • 守护线程:setDaemon(true)参数是true时是守护线程,主线程走完,守护线程就不走了
    类似JVM中的垃圾自动回收机制,只要main没停止,守护线程就可以执行下去,main要是执行结束,守护线程不管在什么状态都要结束,要写守护线程就要写在start方法前面。