品谈Java中的线程

程序 进程 线程

程序(program)是为完成特定的任务、用某种语言编写的一组指令的集合。即指一段静态的代码

进程(process)就是正在执行的程序,从Windows角度来讲,进程就是操作系统进行资源分配的最小单位。

线程(thread)进程可进行进一步的细化,它是进程内部最小的执行单元,是操作系统进行任务调度的最小单位,隶属于进程。

注意:很多时候说的多线程是模拟出来的,真正的多线程是指有多个CPU,即多核。如服务器。如果是模拟出来的多线程。即在一个CPU的情况下,在同一时间点,CPU只能执行一个代码。但因为切换的很快,所以就有同时在执行的错觉。

线程和进程之间的关系

一个进程是可以包含多个线程,但一个线程只能住一个进程,线程是不能脱离进程而独立运行的。
每一个进程至少包含一个线程(称为主线程);在主线程当中开始执行程序,java程序的入口main()方法就是在主线程当中执行的。
在主线程中是可以创建并启动其他其他线程的。
一个进程内的所有线程是共享该进程中的内存资源。

总结线程进程 划分成的更⼩的运⾏单位。线程和进程最⼤的不同在于基本上各进程是独⽴的,⽽各线程则不⼀定,因为同⼀进程中的线程极有可能会相互影响。
线程执⾏开销⼩,但不利于资源的管理和保护;⽽进程正相反。

细说线程

● 线程就是独立的执行路径

● 在程序运行时,即使没有自己创建的线程,后台也会有多个线程,如主线程,GC线程。

● main()称为主线程,为系统的入口,用于执行整个程序。

● 在一个线程中,如果开辟了多个线程,线程的执行是由调度器来安排调度的,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的。

● 同一份资源操作的时候,会出现资源抢夺问题,这个时候就要加入并发来控制。

● 线程会带来额外的开销,如CPU调度所花费的时间,并发控制的时间开销。

● 每个线程在自己的工作内存交互,内存控制不当会造成数据的不一致问题。

为什么使用多线程

总体上来说
从计算机底层来说:线程可以比作是轻量级的进程,是程序执行的最小单位,线程之间的切换和调度的成本是远远小于进程的。另外,多核cpu时代就意味着多个线程是可以同时运行的,这减少了线程上下文切换的开销。

从当代互联网的发展趋势来说:现在的系统动不动就要求百万级的甚至千万级的并发量,而多线程的并发编程正是开发高效并发系统的基础,利用好多线程机制是可以大大提高系统整体的并发能力以及性能。

深入到计算机底层来讲
单核时代: 在单核时代里多线程主要是为了提高CPU和IO设备之间的综合利用率。
举个例子:当只有一个线程的时候会导致CPU计算时,IO设备的空闲;进行IO操作的时候,CPU空闲。我们可以简单的来说这两者之间的利用率都是在50%左右。但是当有两个线程的时候就不一样了,当一个线程执行cpu计算的时候,另外一个线程就可以进行IO操作,这样两个的利用率就可以在理想情况下达到100%了。

多核时代:多核时代多线程只要是为了提高CPU的利用率。(一个核同时只能执行一个线程)
举个例子:加入我们要计算一个复杂的任务,我们只用一个线程的话,CPU只会一个CPU核心为利用到,而创建多个线程就可以让多个CPU核心被利用到,这样也就提高了CPU的利用率。

使用多线程带来的问题

在并发编程的时候,目的是为了能提高程序的执行效率提高程序的运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄露、上下文切换、死锁还有受限于硬件和软件的资源限制问题。

线程的创建

先看一个我们平时写的一个简单代码:

public class Test01 {
    
    
    public static void main(String[] args) {
    
    
        System.out.println("main方法开始");
        method1("星辰");
        System.out.println("main方法结束");
    }
    public static void method1(String str){
    
    
        System.out.println("method1方法开始");
        method2(str);
        System.out.println("method1方法结束");
    }

    public static void method2(String str){
    
    
        System.out.println("method2方法开始");
        System.out.println(str);
        System.out.println("method2方法结束");
    }
}

得到的结果为:
在这里插入图片描述

我们在这就可以将这个程序的执行流程同图先画出来:
在这里插入图片描述

通过图我们就可以明显的看出,这就是一个单线程的程序,按照步骤一步一步的执行。

下面我们来看创建多线程的方式:

继承Thread类的方式

一句话总结就是:继承Thread类,重写run()方法,调用start开启线程

在Thread类中的run方法本身并不执行任何操作,所以我们重写了run方法,当线程启动时,它将执行 run方法。

//创建线程,在线程中写可以独立执行的任务代码。
public class MyThread01 extends Thread {
    
    

    //run方法中写我们需要执行的任务代码
    @Override
    public void run() {
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("MyThread01:" + i);
        }
    }
}

创建我们的main方法:

public class Demo01 {
    
    
    public static void main(String[] args) {
    
    
        //在主线程当中创建一个线程,并启动它
        MyThread01 myThread01 = new MyThread01();
//        myThread01.run();
        //启动线程,是不能调用run自己写的方法,这样调用,就是普通的方法调用
        myThread01.start();//启动的时候,是要用的start来启动线程的。
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("main:" + i);
        }
    }
}

下面我截取了执行结果的一部分,我们就可以明显的看出,程序在执行的时候,是两个线程同时走的,就出现了这种穿插的样子。
在这里插入图片描述

实现Runnable接口的方式

public class MyRunnable01 implements Runnable{
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("MyRunnable:" + i);
        }
    }
}

实现调用这个线程:

public class Demo02 {
    
    
    public static void main(String[] args) {
    
    
        //创建线程要执行的任务
        MyRunnable01 myRunnable01 = new MyRunnable01();
        //创建线程,并为线程指定要执行的任务
        Thread thread = new Thread(myRunnable01);
        //执行线程
        thread.start();
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("main:" + i);
        }
    }
}

执行结果也是两个线程之间的同时进行
在这里插入图片描述

在Runnale接口当中,也就只有这一个方法,是专门来写创建线程后序偶需要执行的代码
在这里插入图片描述

同样的,在Thread类里面也是实现Runnable接口的。
在这里插入图片描述

我们看上面的代码,明明继承Thread类是比写实现Runnable接口是要方便的,那为什么还要有实现Runnable接口这样的写法呢?也是Runnable的优点:

其实道理很简答,因为在Java当中继承是只能单继承的,当选择继承Thread类的时候,就意味着不能继承其他类了,这就会对我们所写的代码造成很大的影响,而实现接口的方式,就可以很好的解决这个问题。对程序的扩展很好。
还有在使用Runnable接口的时候,就可以使多个线程来共享同一个接口实现类的对象,是非常适合多个相同的线程来处理同一份资源的。

实现Callable接口的方式

实现Callable接口与使用Runnable接口相比,Callable的功能更强大一些。
相比run()方法,它拥有了返回值。
方法也可以抛出异常
支持泛型的返回值
需要借助FutureTask类,获取返回的结果

接受任务
FutureTask< Integer > futureTask = new FutureTask(任务);

创建线程
Thread t = new Thread(futureTask);
t.start();
Integre val = futureTask.get();//获得线程call方法的返回值。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Demo11 {
    
    
    public static void main(String[] args) {
    
    
        //创建线程要执行的任务
        MyThread myThread = new MyThread();
        //Callable接口实现的要靠FutureTask来转换一下
        FutureTask<Integer> futureTask = new FutureTask<>(myThread);
        //转换后的任务创建为线程
        Thread thread = new Thread(futureTask);
        //启动线程
        thread.start();
        try {
    
    
            //通过FutureTask可以获得线程里面的返回值。
            Integer integer = futureTask.get();
            System.out.println(integer);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        } catch (ExecutionException e) {
    
    
            e.printStackTrace();
        }
    }
}


//通过Callable接口创建线程任务
class MyThread implements Callable<Integer> {
    
    
    //对0到10相加和
    @Override
    public Integer call() throws Exception {
    
    
        int num = 0;
        for (int i = 0; i <= 10; i++) {
    
    
            num += i;
        }
        return num;
    }
}

在这里插入图片描述

线程Thread类中常用的方法

Thread类的构造方法:

构造方法 说明
Thread() 创建一个新的线程
Thread(String name) 创建一个指定名称的线程
Thread(Runnable target) 利用Runnable对象创建一个线程,启动时执行该对象的run方法
Thread(Runnable target) 利用Runnable对象创建一个线程,并指定该线程的名称

Thread里面的常用方法:

方法原型 说明
void start() 启动线程
final void setName(Sting name) 设置线程的名字
final getName() 返回线程的名字
final void setPriority(int newPriority) 设置线程的优先级
final int getPriority() 返回线程的优先级
final void join() throws InterruptedException 等待线程终止
static Thread currentThread() 返回对当前正在执行的线程对象的引用
static void sleep(long millis) throws InterruptedException 让当前正在执行的线程休眠(暂停执行), 休眠时间由milli s(毫秒)指定

代码演示一下:

public class Demo10 {
    
    
    public static void main(String[] args) {
    
    
        //创建线程要执行的任务
        MyThread10 myThread10 = new MyThread10();
        //创建线程,并为线程指定要执行的任务
        Thread t1 = new Thread(myThread10);
        System.out.println("获得默认的t1名字" + t1.getName());
        //给线程赋予线程名字
        t1.setName("t1");
        //给线程赋予线程执行的优先级
        t1.setPriority(10);
        //输出线程的优先级
        int priority = t1.getPriority();
        System.out.println("t1线程的优先级为:" + priority);
        //启动线程
        t1.start();

        Thread t2 = new Thread(myThread10, "t2");
        String name = t2.getName();
        //获得线程的名字
        System.out.println("t2线程的名字为:" + name);
        //获得默认的线程优先级别
        System.out.println("t2线程的优先级(即默认的优先级):" + t2.getPriority());
        t2.start();

        //获得当前main线程对象的引用
        System.out.println("获得当前main线程对象的引用" + Thread.currentThread());
        //获得默认的main函数的默认名字
        System.out.println("获得默认的main函数的默认名字:" + Thread.currentThread().getName());
        //获得main函数的默认优先级
        System.out.println("获得main函数的默认优先级:" + Thread.currentThread().getPriority());
    }
}

线程执行的内容:

public class MyThread10 implements Runnable {
    
    
    @Override
    public void run() {
    
    
        int item = 0;
        for (int i = 0; i < 100; i++) {
    
    
            item += i;
        }
        System.out.println(Thread.currentThread().getName() + ":" + item);
    }
}

执行结果:
在这里插入图片描述

对优先级有疑问的可以再往下看,详细说明一下优先级。

在api当中的一些方法:
在这里插入图片描述

线程优先级

对我们计算机来说,(先将CPU看成是单核的)计算机只有一个CPU,各个线程轮流获得CPU的使用权,才能执行任务。

而优先级高的线程有更多机会获得CPU的机会,反之亦然。

优先级是用整数表示的,取值为正数在0~10之间,一般情况下,线程的默认优先级都是5,但是也是可以通过setPriority来设置当前线程的优先级,用getPriority方法来获得当前线程的优先级。

在使用setPriority()方法设置优先级的时候,不仅可以直接设置数字,也可以使用Thread提供的3个静态常量的优先级

MAX_PRIORITY 取值为10,表示最高优先级
MIN_PRIORITY 取值为1,表示最底优先级
NORM_PRIORITY 取值为5,表示默认的优先级

在这要重点说明一下:优先级第只是意味着调度的概率低,并不是优先级低就不会被调用,这主要看的是CPU的调度。

而线程优先级这里的优先指的是CPU上时间片的优先。

那时间片是什么?
每个线程被分配一个时间段,称作它的时间片。如果在时间片结束时线程还在运行,则CPU将被剥夺并分配给另一个线程。如果线程在时间片结束前阻塞或结束,则CPU当即进行切换。而不会造成CPU资源浪费。
在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。
但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。

代码演示一下:
main方法

public class Demo03 {
    
    
    public static void main(String[] args) {
    
    
        MyThread03 thread = new MyThread03();
        Thread t1 = new Thread(thread, "t1");
        t1.setPriority(Thread.MIN_PRIORITY);
        t1.start();
        System.out.println("t1的优先级为:" + t1.getPriority());
        Thread t2 = new Thread(thread, "t2");
        t2.setPriority(6);
        System.out.println("t2的优先级为:" + t2.getPriority());
        t2.start();
    }
}
public class MyThread03 implements Runnable {
    
    
    @Override
    public void run() {
    
    
        int item = 0;
        for (int i = 0; i < 1000; i++) {
    
    
            item += i;
        }
        System.out.println(Thread.currentThread().getName() + ":" + item);
    }
}

在这里插入图片描述

线程状态

在这里插入图片描述

新建:当一个Thread类或其子类的对象被声明创建时,新生的线程对象处于新建状态

就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没有分配到CPU资源。

运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能

阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态

死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束。

线程的分类

线程是分为用户线程和守护线程

用过通俗的例子就是:任何一个守护线程都是整个JVM中所有非守护线程的保姆。

只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作。

只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。所以虚拟机不用等待守护线程的执行完毕。

守护线程的作用就是为了其他线程的运行提供遍历服务,如:监控内存、垃圾回收……

用户线程和守护线程两者是几乎没有区别的,唯一的不同之处就在于与虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了,因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。

当然,我们也可以将我们自己写的代码变为守护线程。

注意: 在设置守护线程的时候,必须在启动线程之前,否则我们的守护线程会一直运行下去并报出IllegalThreadStateException此异常,并且此程序不会终止。


下面我举个例子:
人生不过三万天
在这个例子当中说明一个人一生就三万天,而上帝一直存在的。在这我们就将人表示为用户线程,上帝表示为守护线程。

public class Demo05 {
    
    
    public static void main(String[] args) {
    
    
        GodThread godThread = new GodThread();
        PeopleThread peopleThread = new PeopleThread();
        Thread god = new Thread(godThread);
        //将god线程升为守护线程
        god.setDaemon(true);
        god.start();

        //启动用户线程
        Thread people = new Thread(peopleThread);
        people.start();
    }
}


//守护线程
class GodThread implements Runnable {
    
    

    @Override
    public void run() {
    
    
        while (true) {
    
    
            try {
    
    
                Thread.sleep(100);//使用sleep是为了便于结果查看
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("我为上帝,我在默默的守护着你");
        }
    }
}


//用户线程
class PeopleThread implements Runnable {
    
    

    @Override
    public void run() {
    
    
        for (int i = 0; i < 300; i++) {
    
    
            try {
    
    
                Thread.sleep(100);//使用sleep是为了便于结果查看
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("这是我度过的第" + i + "天");
        }
        System.out.println("Good-Bye");
    }
}

在这里插入图片描述

若我们将升为守护线程的语句写在启动线程之后就会报错,并且守护线程不会关闭,一直循环下去。
在这里插入图片描述


下一篇:===》Java中的多线程

猜你喜欢

转载自blog.csdn.net/weixin_45970271/article/details/125106946