并发编程-多线程基础

1.引言

推荐书籍

深入理解Java并发编程
Java并发编程

核心知识点

多线程基础知识
同步和异步的概念
线程安全(线程同步)相关
线程通讯
java1.8并发包
线程池原理分析
锁的概念

专题类学习

并发编程
性能优化
源码分析
高并发与高可用
分布式缓存
分布式协调工具
分布式常用解决方案
互联网安全体系架构
互联网工具
java内存模型  堆和栈?  线程可见性

1.计算机发展历史

1.一开始我们的计算机其实就是相当于一个计算器,你输入一个指令,然后经过计算机进行运算,最后得出结果,这样的缺点显而易见,就是计算机有很长时间都是空着的,没有充分发挥它的价值!
在这里插入图片描述

输入一个指令,输出一个结果

2.解决这种问题,人们就在想,我可不可以把指令预先写好,然后放入计算机上执行呢?这就引发了磁带的出现,所谓磁带就是批处理指令,这样就可以让计算机通过读写磁带去工作了,但是这样的缺点也很明显就是 很多情况下,在进行IO流读写操作的时候,容易发生阻塞,进而导致计算机停在那!

攒成一大波输入(批处理指令),然后顺序计算

3.解决这个问题,人们就会想,我可不可以让这些指令或者任务切换执行呢?就是说这一指令执行失败,不影响我下一个指令的运行!这个时候就出来 进程这个概念!进程其实 可以理解为一个任务或者一个批处理指令或者指代我们现在的一个程序,但是本质还是批处理指令。为了达到多个进程并行的目的,采取分时的方式,即把CPU的时间分成很多片段,某个片段只能执行某个进程中的指令,虽然说从操作系统和CPU的角度来说还是串行处理的,但是由于CPU的处理速度很快,从用户的角度感觉是并行的!

在这里插入图片描述
4.这样做法虽然说进程之间是并行的,但是进程内部还是串行的,也就是说,如果进程内部一条指令发生阻塞,那么同样会导致这个进程进行不下去,那么为了解决这个问题,就引入了线程这个概念,就是把进程进一步的细分为一个个线程,线程之间来回切换执行,这样我们系统调度的最小单位就变为了线程
在这里插入图片描述
5.多进程多线程之间虽然让多任务并行处理的能力大大提升,但是本质上还是分时系统,并不是时间上真正的并行,解决这个问题的方式显而易见,就是让多个CPU能够同时计算任务,从而实现真正意义上的多任务并行!----> 从而引发了多核CPU的产生,那我们i7的cpu,4核心8线程的,最多能有几个进程处于执行状态? 即物理核心是4个,但是从计算机的角度来看,被虚拟为了8个核心,所以同时可以开启8线程同时执行,8个线程又属于8个进程!
在这里插入图片描述
此处补充一下CPU核心数,线程数的概念:

CPU的核心数是指物理上,也就是硬件上存在着几个核心。比如,双核就是包括2个相对独立的CPU核心单元组,四核就包含4个相对独立的CPU核心单元组。

线程数是一种逻辑的概念。通常来说,一个计算机核心在一瞬间只能执行一个线程,但是使用超线程技术(Intel才有,酷睿全系和志强部分处理器支持),一个核心可以同时运行两个线程,因此,线程数总是大于或等于核心数的

这里我再强调一下:

线程:资源调度的最小单位
进程:批处理指令
存在的目的:就是解决阻塞问题

那我们就会产生这样一个问题:既然分时执行可以做到用户层面上的并行,但如何才能提高效率呢?

原来:进行读写操作的时候,假设A进程负责读任务,B进程负责写任务,那么分时执行,其实出现的可能情况就是,我A读完了,B才刚开始写,这样的话,效率相对较低!那我们怎么提高效率呢?那就是要让进程,线程之间可以通信,这就好比A读了一部分,B检测到A已经读取一部分,那么B立马就会写,这样效率自然而然提高了!那我们怎么做通信呢?一般无非 管道,信号量,消息队列,共享存储!进程之间一般使用消息队列,而线程一般使用共享存储。

2.线程与进程

2.1.线程的由来

第三代计算机中,每一个进程处理一个程序,(单核+多道)实际上实现了多进程的并发处理。

进程的定义:

进程就是一个程序在一个数据集上的一次动态执行过程。 进程一般由程序、数据集、进程控制块三部分组成。(程序,是一组指令的有序集合,可以理解成第二代计算机中的批处理指令)

这样做法虽然说进程之间是并行的,但是进程内部还是串行的,也就是说,如果进程内部一条指令发生阻塞(阻塞),那么同样会导致这个进程进行不下去,那么为了解决这个问题,就引入了线程这个概念,就是把进程进一步的细分为一个个线程,线程之间来回切换执行,这样我们系统调度的最小单位就变为了线程。

线程的出现:

① 是为了降低上下文切换的消耗,
② 提高系统的并发性,并突破一个进程只能干一样事的缺陷, 使到进程内并发成为可能。

2.2.线程和进程的关系

① 一个程序至少有一个进程,一个进程至少有一个线程.(进程可以理解成线程的容器);
② 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,各个线程共享资源;
③ 进程是资源分配和调度的最小单位,线程是是CPU调度和分派的基本单位,是进程的一个实体;

在这里插入图片描述

总结:进程是所有线程的集合,每一个线程是进程中的一条执行路径。

2.3.多线程

进程之间,变量不共享,涉及到通信的问题,一般使用消息队列(此外还有管道,信号量等)
线程之间,多个线程修改同一个变量,涉及到同步的问题,使用锁来解决。

在这里插入图片描述

2.3.多核心CPU的进程和线程

在单核时代实际上只能实现并发处理,即:

采取分时的方式,即把CPU的时间分成很多片段,某个时间片只能执行某个线程,虽然说从操作系统和CPU的角度来说还是串行处理的,但是由于CPU的处理速度很快,从用户的角度感觉是并行的。

随着计算机的发展,出现了多核cpu,实现了并行处理,如下图,多个线程之间不存在对CPU的抢占关系,而是微观上同时执行。
在这里插入图片描述

3.多线程应用场景

举例: 迅雷多线程下载、数据库连接池、分批发送短信等。
在这里插入图片描述

主要能体现到多线程提高程序效率。

4.多线程创建方式

4.1.第一种继承Thread类 重写run方法

package com.bruceliu.demo;

/**
 * @Auther: bruceliu
 * @Date: 2020/2/21 11:44
 * @QQ交流群:750190373 (攻城狮大本营)
 * @Description:
 */
// 1.什么是线程 线程是一条执行路径,每个线程都互不影响。
// 2.什么是多线程,多线程在一个进程中,有多条不同的执行路径,并行执行。目的为了提高程序效率。
// 3.在一个进程中,一定会主线程。
// 4.如果连线程主线程都没有,怎么执行程序。
// 线程几种分类 用户线程、守护线程
// 主线程 子线程 GC线程
public class Test001 {
    // 交替执行
    public static void main(String[] args) {
        System.out.println("main... 主线程开始...");
        // 1.创建线程
        ThreadDemo01 threadDemo01 = new ThreadDemo01();
        // 2.启动线程
        threadDemo01.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("main..i:" + i);
        }
        System.out.println("main... 主线程结束...");
    }
}

//1. 继承thread类,重写run方法,run方法中,需要线程执行代码
class ThreadDemo01 extends Thread {
    // run方法中,需要线程需要执行代码
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.print("子id:" + this.getId() + ",");
            System.out.print("name:" + getName());
            System.out.println("i:" + i);
        }
    }
}

4.2.同步和异步

在这里插入图片描述

4.3.实现Runnable接口,重写run方法

package com.bruceliu.demo;

/**
 * @Auther: bruceliu
 * @Classname Test002
 * @Date: 2020/2/21 12:42
 * @QQ交流群:750190373 (攻城狮大本营)
 * @Description: 实现Runnable接口,重写run方法
 */
public class Test002 implements Runnable  {

    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(" 子 i:" + i);
        }
    }

    public static void main(String[] args) {
        System.out.println("main... 主线程开始...");

        // 创建线程
        Test002 test002 = new Test002();
        Thread t1= new Thread(test002);
        // 启动线程
        t1.start();

        for (int i = 0; i <10; i++) {
            System.out.println("main..i:"+i);
        }
        System.out.println("main... 主线程结束...");
    }
}

4.4.使用继承Thread类还是使用实现Runnable接口

实现Runnable接口避免了单继承的局限性,所以较为常用。实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦。

4.5.启动线程是使用调用start方法还是run方法

java的线程是通过java.lang.Thread类来实现的。VM启动时会有一个由主方法所定义的线程。可以通过创建Thread的实例来创建新的线程。每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,方法run()称为线程体。通过调用Thread类的start()方法来启动一个线程。

4.6.使用匿名内部类方式

package com.bruceliu.demo;

/**
 * @Auther: bruceliu
 * @Classname Test003
 * @Date: 2020/2/21 12:49
 * @QQ交流群:750190373 (攻城狮大本营)
 * @Description: 使用匿名内部类方式
 */
public class Test003 {

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(new Runnable() {

            public void run() {
                while (true) {
                    try {
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        // TODO: handle exception
                    }
                    System.out.println("我是子线程(用户线程)");
                }
            }
        });

        // 标识当前方法为守护线程
        t1.setDaemon(true);
        t1.start();

        for (int i = 0; i < 10; i++) {
            Thread.sleep(300);
            System.out.println("main:i:" + i);
        }
        System.out.println("主线程执行完毕...");
    }
}

5.线程常用API

在这里插入图片描述

6.守护线程

Java中有两种线程,一种是用户线程,另一种是守护线程。用户线程是指用户自定义创建的线程,主线程停止,用户线程不会停止守护线程当进程不存在或主线程停止,守护线程也会被停止。使用setDaemon(true)方法设置为守护线程

/**
 * @Auther: bruceliu
 * @Classname Test004
 * @Date: 2020/2/21 13:59
 * @QQ交流群:750190373 (攻城狮大本营)
 * @Description: 什么是守护线程? 守护线程 进程线程(主线程挂了) 守护线程也会被自动销毁.
 */
public class Test004 {

    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(100);
                    } catch (Exception e) {
                    }
                    System.out.println("我是子线程...");
                }
            }
        });
        thread.setDaemon(true);
        thread.start();
        
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(100);
            } catch (Exception e) {

            }
            System.out.println("我是主线程");
        }
        System.out.println("主线程执行完毕!");
    }

}

7.多线程运行状态

线程从创建、运行到结束总是处于下面五个状态之一:新建状态、就绪状态、运行状态、阻塞状态及死亡状态。
在这里插入图片描述

  1. 新建( new ):新创建了一个线程对象。
  2. 可运行( runnable ):线程对象创建后,其他线程(比如 main 线程)调用了该对象 的 start ()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获 取 cpu 的使用权 。
  3. 运行( running ):可运行状态( runnable )的线程获得了 cpu 时间片( timeslice ) ,执行程序代码。
  4. 阻塞( block ):阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice ,暂时停止运行。直到线程进入可运行( runnable )状态,才有 机会再次获得 cpu timeslice 转到运行( running )状态。阻塞的情况分三种:
    (一). 等待阻塞:运行( running )的线程执行 o . wait ()方法, JVM 会把该线程放 入等待队列( waitting queue )中。
    (二). 同步阻塞:运行( running )的线程在获取对象的同步锁时,若该同步锁 被别的线程占用,则 JVM 会把该线程放入锁池( lock pool )中。
    (三). 其他阻塞: 运行( running )的线程执行 Thread . sleep ( long ms )或 t . join ()方法,或者发出了 I / O 请求时, JVM 会把该线程置为阻塞状态。 当 sleep ()状态超时、 join ()等待线程终止或者超时、或者 I / O 处理完毕时,线程重新转入可运行( runnable )状态。
  5. 死亡( dead ):线程 run ()、 main () 方法执行结束,或者因异常退出了 run ()方法,则该线程结束生命周期。死亡的线程不可再次复生。

8.join()方法作用

当在主线程当中执行到t1.join()方法时,就认为主线程应该把执行权让给t1

需求:
创建一个线程,子线程执行完毕后,主线程才能执行。

/**
 * @Auther: bruceliu
 * @Classname Test005
 * @Date: 2020/2/21 14:15
 * @QQ交流群:750190373 (攻城狮大本营)
 * @Description: TODO
 */
public class Test005 {

    public static void main(String[] args) throws Exception {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {

                    }
                    System.out.println(Thread.currentThread().getName() + "i:" + i);
                }
            }
        });
        t1.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("main" + "i:" + i);
            if(i==5){
                // 当在主线程当中执行到t1.join()方法时,就认为主线程应该把执行权让给t1
                t1.join();
            }
            try {
                Thread.sleep(10);
            } catch (Exception e) {
            }
        }
    }
}

9.线程优先级

现代操作系统基本采用时分的形式调度运行的线程,线程分配得到的时间片的多少决定了线程使用处理器资源的多少,也对应了线程优先级这个概念。在JAVA线程中,通过一个int priority来控制优先级,范围为1-10,其中10最高,默认值为5。下面是源码(基于1.8)中关于priority的一些量和方法。

/**
 * @Auther: bruceliu
 * @Classname Test006
 * @Date: 2020/2/21 14:19
 * @QQ交流群:750190373 (攻城狮大本营)
 * @Description: 线程优先级
 */
public class Test006 {

    public static void main(String[] args) {
        PrioritytThread prioritytThread = new PrioritytThread();
        
        Thread t1 = new Thread(prioritytThread);
        Thread t2 = new Thread(prioritytThread);

        t1.start();
        // 注意设置了优先级, 不代表每次都一定会被执行。 只是CPU调度会有限分配
        t1.setPriority(10);
        t2.start();

    }

}

class PrioritytThread implements Runnable {
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().toString() + "---i:" + i);
        }
    }
}

10.Yield方法

Thread.yield()方法的作用:暂停当前正在执行的线程,并执行其他线程。(可能没有效果)
yield()让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。
在这里插入图片描述

结论:大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

11.案例

现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行

/**
 * @Auther: bruceliu
 * @Classname Test007
 * @Date: 2020/2/21 14:30
 * @QQ交流群:750190373 (攻城狮大本营)
 * @Description: TODO
 */
public class Test007 {

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println("t1,i:" + i);
                }
            }
        });
        
        Thread t2 = new Thread(new Runnable() {
            public void run() {
                try {
                    t1.join();
                } catch (Exception e) {
                    // TODO: handle exception
                }
                for (int i = 0; i < 20; i++) {
                    System.out.println("t2,i:" + i);
                }
            }
        });
        
        Thread t3 = new Thread(new Runnable() {
            public void run() {
                try {
                    t2.join();
                } catch (Exception e) {
                    // TODO: handle exception
                }
                for (int i = 0; i < 20; i++) {
                    System.out.println("t3,i:" + i);
                }
            }
        });
        
        t1.start();
        t2.start();
        t3.start();
    }
}
发布了274 篇原创文章 · 获赞 80 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/BruceLiu_code/article/details/104423502