多线程与高并发学习笔记——第一章:线程的基本概念与使用

线程的基本概念与使用

本章包含知识点:并发简单概念,线程的简单创建与使用,线程的休眠,线程的生命周期,线程优先级,线程礼让,线程加入。

1. 进程和线程的概念

进程:一个程序运行所占用的资源的描述,一个程序被运行,系统就会为他开辟一个进程,是资源分分配的最小单位。进程之间不可相互通信。
线程:一个进程中有多个线程,在一个进程中可以建立新的现成线程是程序执行时的最小单位。进程包含线程,如果进程中的所有线程都没有了,进程就结束了,同一个线程内的进程何以共享信息!

2. 串行,并发,并行

我们在这里几个例子:
这里有一堆砖头,一堆沙子,一池子水,老板让你把这些都移走。

串行就是:无论先移动哪一个才,一次只能一个东西,等待这个东西全部搬完才可以移动下一个。先搬砖头,就不能搬沙子和水,等砖头搬完了才可以般沙子和水。即:一次只能执行一个任务

并发就是:你搬一块砖头,铲一铲沙子,倒一桶水,然后这个时候就像你在同时办这两件事情一样。再举个例子就是吃一口饭,喝一口水,看一会电视,就像同时在做三件事情一样。但实际上,你一次只能做一件事,只是在所有事情之间快速切换执行,好像所有事情同时执行一样

并行就是:老板让你干活,你直接又找了两个人,三个人一个人搬水,一个人搬砖,一个人搬沙子,是真正意义上的同时执行

电脑执行程序:
其实在电脑内,执行多线程任务任务时,是并行合并发同时存在的!
单核处理器处理多线程任务即为并发,多核处理器执行任务为并行,而电脑中,是多核处理器同时执行更多的任务,每一个核心执行一个任务只执行一点,然后处理器的执行能力被其他任务抢走。只不过处理器切换执行任务的时间非常快,我们感觉像是同时执行一样!

CPU时间片:
是分时操作系统分配给每个正在运行的进程微观上的一段CPU时间。程序执行,就是每个程序抢时间片,抢到时间片程序就会执行一点,每个时间片的长短并不相同,需要根据程序执行情况看。

3. 线程的开辟方式

线程开辟的方式有两种:

  • 继承Thread类,重写run方法
  • 实现Runnable接口,重写run方法

演示1:继承Thread类重写run方法

package com.wojiushiwo;

/**
 * @author 我就是我500
 * @date 2020-02-22 20:40
 * @describe
 **/
public class ThreadTest {

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        for(int i=0;i<10;i++)
        {
            System.out.println("主线程执行的逻辑!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class MyThread extends Thread
{
    @Override
    public void run() {
        for(int i=0;i<10;i++)
        {
            System.out.println("自定义线程执行的逻辑!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

执行结果:
多线程与高并发学习笔记——第一章:线程的基本概念与使用_第1张图片

我们程序中的代码是按照顺序执行的,而我们的程序是交替运行的,说明线程已经成功创建并运行!

演示2:实现Runnable接口,重写其中run方法

package com.wojiushiwo;
/**
 * @author 我就是我500
 * @date 2020-02-22 20:40
 * @describe
 **/
public class ThreadTest {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread);
        thread.start();
        for(int i=0;i<10;i++)
        {
            System.out.println("主线程执行的逻辑!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class MyThread implements Runnable
{
    @Override
    public void run() {
        for(int i=0;i<10;i++)
        {
            System.out.println("自定义线程执行的逻辑!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

还可以将显示实现Runnable接口方式改为使用lamda表达式方式:

package com.wojiushiwo;
/**
 * @author 我就是我500
 * @date 2020-02-22 20:40
 * @describe
 **/
public class ThreadTest {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            for(int i=0;i<10;i++)
            {
                System.out.println("自定义线程执行的逻辑!");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
        for(int i=0;i<10;i++)
        {
            System.out.println("主线程执行的逻辑!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

多线程与高并发学习笔记——第一章:线程的基本概念与使用_第2张图片
可以看到:两次执行虽然后是成功的但是结果并不相同,这是因为线程的本质是并发,是多个线程争抢CPU时间片的结果,而CPU时间片的争抢是随机的,也就是说不一定什么时候正在执行那条线程,所以造成了上方情况,下一章将对此详细介绍。

那么两种方法如何选择:

  • 继承Thread类:优点是简单,直接实例化继承后的类调用strat方法即可,缺点是由于JAVA的单继承性,无法再继承其它父类!

  • 实现Runnable接口:优点是不需要继承Thread类,并且可是使用lamda表达式语法,但是需要实例化Thread类,将Runnable接口作为参数传入。

    以上两种方法都可以使用,但是实际开发中,更推荐使用第二种方式(实现Runnable接口)!

4. 线程的休眠

线程休眠方法:

Thread.Sleep(休眠时间);
此方法会阻塞线程的运行,使线程从运行态变为阻塞态,当休眠时间超过指定时间,此线程就会被重新唤醒。
如上方代码所示,使用线程sleep方法可以使线程暂停一部分时间,但是此方法会抛出异常,所以必须使用try/catch代码块捕捉异常,并且线程休眠后不会释放锁的标记,属于“占着茅坑不拉屎的类型”,使用时必须注意!
关于线程锁的问题,有下一章笔记行详细介绍。

5. 线程的生命周期

线程从创建到销毁一共有5种状态:

  • 新生态(New):线程被实例化完成,还没有其他任何操作。
    new Thread()之后,但是还没有调用其他任何方法时是就绪态。
  • 就绪态(Ready):线程已经开始参与争抢CPU时间片,但还没有抢到时间片
    调用线程中的start方法,调用后线程会开始争抢CPU资源(时间片),但是线程没有抢到CPU时间片,线程还没有正式开始执行的这段时间线程处于就绪态。
  • 运行态(Run):线程抢到了CPU时间片,并开始执行其中的逻辑。
  • 阻塞态(Interrupt):一个线程在运行的过程中,受到某些操作的影响,放弃了已经获取到的CPU时间片,并且不再参与CPU时间片的争抢,此时线程处于挂起状态。
    比如调用了Thread.sleep()方法时,此时线程停止执行。
  • 死亡态(Dead):一个线程对象需要被销毁
    线程正常执行结束或抛出了异常

多线程与高并发学习笔记——第一章:线程的基本概念与使用_第3张图片
等待队列和锁池中的线程对象不严格来说也属于阻塞态,接下来会继续介绍。

6. 线程的命名

我们开启多个线程时,有时就需要区分这几个线程,这是我们可以在线程运行之前给线程命名。

获取当前线程名:

Thread.currentThread().getName();

我们分别来获取一下主线程和我们手动新建的线程的默认线程名:

public class ThreadTest {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());
        
        new Thread(()->{
            System.out.println(Thread.currentThread().getName());
        }).start();
    }
}

多线程与高并发学习笔记——第一章:线程的基本概念与使用_第4张图片
这时候我们就可以看到:主线程默认名称为main,自定义线程中的线程名默认为Thread-数字的格式。

设置线程名的方式:
我们假设一个场景,目前有两条自定义线程正在工作并输出结果,我们这是通过获取线程名的方式来判断到底是那一条线程输出了什么结果。

package com.wojiushiwo;
/**
 * @author 我就是我500
 * @date 2020-02-22 20:40
 * @describe
 **/
public class ThreadTest {
    public static void main(String[] args) {
        MyThread myThread =new MyThread();
        Thread thread1 = new Thread(myThread,"线程1");
        Thread thread2 = new Thread(myThread);
        thread2.setName("线程2");
        thread1.start();
        thread2.start();
    }
}
class MyThread implements Runnable
{
    @Override
    public void run() {
        for (int i=0;i<10;i++)
        {
            System.out.println(Thread.currentThread().getName()+"输出了"+i);

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

多线程与高并发学习笔记——第一章:线程的基本概念与使用_第5张图片
我们可以看到,执行同样的代码,可以通过线程名来区分线程的工作结果。

结合上述代码,我们发现,有两种方式可以给线程命名:

  • 第一种就是直接在构造函数中设置名称
  • 第二种可以在线程创建后通过setName()方法设置

至于使用哪种命名方式,根据情况而定即可!

7. 线程的优先级

多线程与高并发学习笔记——第一章:线程的基本概念与使用_第6张图片

查看上方的执行结果,我们不难发现,我们都是每执行一次循环逻辑就让他延迟(休眠)1秒,正常来说应该线程1和线程2交替执行,但是我们测试却发现在有些情况下,如红圈所示,两条线程会很杂乱的执行,这是因为我们的线程执行的本质是CPU在各个线程之间快速切换执行,而每个线程都需要抢到CPU时间片才可以执行,而什么时候抢到时间片是不确定的,所以如果某一个线程抢不到时间片,就会出现延后执行的情况下,所以如图红圈所示,我们会看到两条线程杂乱的执行,而不是顺序执行。

线程需要抢到CPU时间片才能执行,那么抢到时间片的概率就是我们的线程优先级,线程优先级高,就有更大的几率率先先执行完毕!

设置线程的优先级:
使用线程对象的setPriority()方法来设置线程优先级

注意:

  • 线程优先级是一个1-10之间的整数,数越大,线程的优先级越高,默认都为5 !
  • 线程的优先级是线程抢到时间片的概率,所以并不是线程优先级越高,一定会率先执行!
  • 设置线程优先级必须在线程开始前设置!

现在我们分别设置两条线程的优先级进行实实验:

package com.wojiushiwo;
/**
 * @author 我就是我500
 * @date 2020-02-22 20:40
 * @describe
 **/
public class ThreadTest {
    public static void main(String[] args) {
        MyThread myThread =new MyThread();
        Thread thread1 = new Thread(myThread,"线程1");
        Thread thread2 = new Thread(myThread,"线程2");
        thread1.setPriority(10);
        thread2.setPriority(1);
        thread1.start();
        thread2.start();
    }
}
class MyThread implements Runnable
{
    @Override
    public void run() {
        for (int i=0;i<100;i++)
        {
            System.out.println(Thread.currentThread().getName()+"输出了"+i);
        }
    }
}

多线程与高并发学习笔记——第一章:线程的基本概念与使用_第7张图片
可以看到,线程1的优先级最高,线程2的优先级最低,在开始阶段两条线程交替执行,而且线程1的执行速度明显大于线程2,线程1执行完毕,线程2才执行一半!
多线程与高并发学习笔记——第一章:线程的基本概念与使用_第8张图片
第二次进行测试,同样是优先级更高的线程1率先执行完毕,但可以看到这次线程2才执行到6,所哟我们也可以得出结论,优先级只是线程抢到CPU时间片的概率,并不是一定最先执行,并不是每一次执行结果都相同。

8. 线程的礼让

使线程放弃已经抢到的时间片,从运行态回到就绪态的操作

也就是说:当前线程抢到CPU时间片之后,线程礼让会使当前线程放弃已经抢到的时间片,不执行当前逻辑,所有线程重新抢夺CPU时间片就是线程礼让!

注意:假如有两个线程,线程1执行了礼让操作后,并不一定线程2一定执行操作,因为线程礼让是让线程1放弃CPU时间片后所有线程重新抢夺,如果此时依旧是线程1抢到CPU时间片,依旧是线程1继续执行!

线程礼让代码:

Thread.yield();

线程礼让示例:我们使循环执行到20的时候进行礼让操作

package com.wojiushiwo;
/**
 * @author 我就是我500
 * @date 2020-02-22 20:40
 * @describe
 **/
public class ThreadTest {
    public static void main(String[] args) {
        MyThread myThread =new MyThread();
        Thread thread1 = new Thread(myThread,"线程1");
        Thread thread2 = new Thread(myThread,"线程2");
        thread1.start();
        thread2.start();
    }
}
class MyThread implements Runnable
{
    @Override
    public void run() {
        for (int i=0;i<100;i++)
        {
            if(i==20)
                Thread.yield();
            System.out.println(Thread.currentThread().getName()+"输出了"+i);
        }
    }
}

在这里插入图片描述
在这里插入图片描述
可以看到,线程2执行到20时,进行了礼让,所以线程1抢到了CPU时间片输出了结果,线程1在执行到20时,也进行了礼让操作,然而由于他运气好,再一次抢到了CPU时间片,所以依旧是线程1继续输出!所以,线程礼让并不一定会使其它线程进行执行!

9. 线程的加入

线程加入就是吧同时执行的线程变为顺序执行!
线程加入的方法:线程对象.join();
join两种形式:

  • thread.join():join方法会把指定线程加入到当前线程,直到指定线程执行完毕,才开始当前线程的执行!
    假如线程1中调用了线程2.join(),线程1就会暂停执行,等待线程2执行完成,线程1才会继续执行。
  • thread.join(1000): 把指定线程加入到当前线程,使当前线程等待1000毫秒后执行!
    假如线程1中调用了线程2.join(1000),线程1就会暂停执行,等待1000毫秒后,线程1才会继续执行。

举例说明:join方法常用于在主线程中等待其它线程执行完成

package com.wojiushiwo;

/**
 * @author 我就是我500
 * @date 2020-02-22 20:40
 * @describe
 **/
public class ThreadTest {

    public static void main(String[] args) {
        MyThread myThread =new MyThread();

        Thread thread1 = new Thread(myThread,"线程1");
        thread1.start();
        try {
            thread1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        for (int i=0;i<10;i++)
        {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("主线程输出了"+i);
        }

    }
}

class MyThread implements Runnable
{
    @Override
    public void run() {
        for (int i=0;i<10;i++)
        {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"输出了"+i);
        }
    }
}

多线程与高并发学习笔记——第一章:线程的基本概念与使用_第9张图片

可以看到main线程中加入了线程1,原本应该同时执行的两条线程出现了等待,等待线程1执行结束后main线程才继续执行!

如果将join方法中加入参数:

thread1.join(3000);

多线程与高并发学习笔记——第一章:线程的基本概念与使用_第10张图片

可以看到main线程中加入了线程1,主线程等待了三秒,之后才交替执行。

你可能感兴趣的:(多线程与高并发学习笔记)