多线程实现及基本概念

多线程


一、基本概念

1、任务

任务是一个逻辑概念, 指由一个软件完成的活动, 或者是一系列共同达到某一目的的操作。通常一个任务是一个程序的一次运行,一个任务包含一个或多个完成独立功能的子任务,这个独立的子任务就是进程或是线程。

2、多任务

多任务处理是指用户可以在同一时间内运行多个应用程序,每个应用程序被称作一个任务。多任务系统中有3个功能单位:任务、进程和线程。

3、程序

程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念

4、进程

程序的一次执行的过程,是一个动态的概念。是操作系统资源分配的基本单位

5、线程

线程是处理器任务调度和执行的基本单位

6、进程与线程的区别

进程和线程的根本区别是进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。另外区别还有资源开销、包含关系、内存分配、影响关系、执行过程等。

**资源开销:**每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小(每个线程都有自己的工作内存)。

**包含关系:**如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

**内存分配:**同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。

**影响关系:**一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

**执行过程:**每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。

7、总内存和工作内存

–第7点转载自CSDN

在这里插入图片描述

Java内存模型将内存分为了 主内存和工作内存 。类的状态,也就是类之间共享的变量,是存储在主内存中的,每个线程都有一个自己的工作内存(相当于CPU高级缓冲区,这么做的目的还是在于进一步缩小存储系统与CPU之间速度的差异,提高性能),每次Java线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在自己的工作内存中有一份拷贝,运行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的那一份。在线程代码执行完毕之后,然后在某个时间点上再将最新的值更新到主内存中去。
这样导致的问题是,如果线程1对某个变量进行了修改,线程2却有可能看不到线程1对共享变量所做的修改。

public class TestMain3{
    
    
    public static void main(String[] args){
    
    
        A a = new A();
        new Thread(a, "a").start();
        new Thread(a, "b").start();
    }
}

class A implements Runnable {
    
    
    static int number = 1;
    @Override
    public void run() {
    
    
        System.out.println(Thread.currentThread().getName() + "判断时" + number);
        while (number > 0) {
    
    
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "要减时" + number);
            number--;
            System.out.println(Thread.currentThread().getName() + "减完后" + number);
        }
    }
}
a判断时1
b判断时1
a要减时1
b要减时1
a减完后0
b减完后-1
    怎么解释??????

二、线程实现(重点)

1、继承Thread类
  1. 一个类继承Thread类
  2. 重写run()方法
  3. new一个新类后,运行start()方法即可开启线程,线程不一定立即执行。(如果运行run()方法,就和普通方法一样)
public class TestMain3 extends Thread{
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("=====子线程中的i=" + i);
        }
    }

    public static void main(String[] args){
    
    
        TestMain3 testMain3 = new TestMain3();
        testMain3.start();
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("主线程中的i=" + i);
        }
    }
}
2、实现Runable接口

比起继承方式,更推荐使用接口方式,因为继承有局限性,不能继承其它类了。

方便一个对象被多个线程使用,而继承方式只能启用一次start。

  1. 一个类实现Runable接口
  2. 重写(实现)run()方法
  3. new一个类,new一个线程对象,以该类为构造参数**(静态代理)**
  4. 启动线程对象的start()方法(启动run()方法和普通方法调用一样)
public class TestMain3 implements Runnable{
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("=====子线程中的i=" + i);
        }
    }

    public static void main(String[] args){
    
    
        TestMain3 testMain3 = new TestMain3();
        Thread thread = new Thread(testMain3);
        thread.start();

        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("主线程中的i=" + i);
        }
    }
}
3、实现Callable接口

call调用,callable可调用的

public class TestMain3{
    
    
    public static void main(String[] args) throws InterruptedException, ExecutionException {
    
    
        A a = new A();
        //创建执行服务
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        //把实现callable接口的对象丢入执行
        Future<Integer> submit = executorService.submit(a);
        //得到返回值
        Integer integer = submit.get();
        //关闭服务
        executorService.shutdown();
    }
}

class A implements Callable<Integer>{
    
    

    @Override
    public Integer call() throws Exception {
    
    //重写call方法,返回值和接口泛型保持一致
        System.out.println("执行了call方法");
        return 123;
    }
}

三、线程状态

1、线程五大状态
  1. 创建(新生)状态 创建线程对象就进入到新生状态
  2. 就绪状态 调用start()方法线程进入就绪状态,等待CPU调度
  3. 运行状态 cup调度,线程执行线程体的代码块
  4. 阻塞状态 当调用sleep,wait或同步锁定时,线程进入阻塞状态,代码不能往下执行,阻塞事件解除后,重新进入就绪状态
  5. 死亡状态 线程中断或者结束,一旦进入死亡状态,就不能再次启动
2、线程停止
  1. 不使用stop()、destroy()等废弃的方法
  2. 推荐让线程自己停止下来
  3. 设置一个标志位,线程中定义线程体要使用的标志位,在线程体中应用标志位,对外提供设置标志位的方法
public class TestMain3 implements Runnable{
    
    
    private boolean flag = true;
    @Override
    public void run() {
    
    
        while (flag) {
    
    
            System.out.println("子线程在运行");
        }
    }

    public void stop() {
    
    
        this.flag = false;
    }

    public static void main(String[] args){
    
    
        TestMain3 testMain3 = new TestMain3();
        new Thread(testMain3).start();
        for (int i = 0; i < 1000; i++) {
    
    
            if (i == 999) {
    
    
                System.out.println("i==" + i + "停止子线程");
                testMain3.stop();
            }
        }
    }
}
3、线程休眠

可以扩大并发问题发生性。

  1. Thread.sleep(毫秒数)指定当前线程阻塞的毫秒数
  2. sleep存在异常InterruptedException
  3. sleep时间达到后线程进入就绪状态
  4. sleep可以模拟网络延时,倒计时等
  5. 每一个对象都有一个锁,sleep不会释放锁,即抱着锁睡觉
  6. 可以在项目代码中加入sleep语句,使得某个代码运行缓慢,客户给钱就优化,并得到明显的速度提升(狗头保命)
4、线程礼让

yield /jiːld/

v. 出产(产品或作物);产出(效果、收益等);生息;屈服;放弃;停止争论;给(车辆)让路;(在外力、重压等下)屈曲

n. 产量;利润,红利率

方法:Thread.yield()

  1. 礼让线程,让当前正在执行的线程暂停,但不阻塞

    (线程暂停:立即退出当前线程,进入就绪状态。

    ​ 线程阻塞:“卡”在当前线程某一行代码,阻塞完毕后退出当前线程,进入就绪状态,如sleep方法)

  2. 将线程从运行状态转为就绪状态

  3. 让CPU重新调度,礼让不一定成功。礼让只是让当前线程重新进入竞争就绪的状态,如果cpu依然调度原线程,则称礼让不成功。

5、线程强制执行

方法:线程对象.join()

  1. join合并线程,等此线程执行完成后,再执行其它线程,其他线程阻塞
  2. 即插队
  3. **注意:**谁调用的就插谁的队,比如程序有100个线程,A线程中调用了B线程的join方法,那么要等到B线程执行完毕后,A线程才会继续执行,并且,在B线程执行的期间,其它线程仍然可以和B交替。
public class TestMain3 implements Runnable{
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println(Thread.currentThread().getName() + "线程在运行=" + i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        TestMain3 testMain3 = new TestMain3();
        new Thread(testMain3, "A").start();
        new Thread(testMain3, "B").start();
        new Thread(testMain3, "C").start();

        for (int i = 0; i < 1000; i++) {
    
    
            if (i == 888) {
    
    
                Thread thread = new Thread(testMain3, "VIP!!");
                thread.start();
                thread.join();
            }
            System.out.println(Thread.currentThread().getName() + "(主)线程在运行" + i);
        }
    }
}
6、监测线程状态

Thread.State(枚举类型),用的时候应该用具体的线程对象来调用。

线程对象.getState();

(下面来自JDK1.8文档)

  1. NEW 尚未启动的线程处于此状态
  2. RUNNABLE 在Java虚拟机中执行的线程处于此状态,即启动start()方法后
  3. BLOCKED 被阻塞等待监视器锁定的线程处于此状态
  4. WAITING 正在等待另一个线程执行特定动作的线程处于此状态
  5. TIMED_WAITING 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
  6. TERMINATED 已退出的线程处于此状态
7、线程优先级
  • Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行

  • 线程的优先级用数字表示,范围从1到10

  • 常量:

    Thread.MIN_PRIORITY = 1

    Thread.NORM_PRIORITY = 5

    Thread.MAX_PRIORITY = 10

  • 方法:线程对象.getPriority() 线程对象.setPriority(int)

  • 设置优先级要在启动前。

  • 优先级只是反映获得cup调度的概率,不是决定调度顺序。(性能倒置)

8、守护线程
  1. 线程分为用户线程和守护线程
  2. 虚拟机必须确保用户线程执行完毕
  3. 虚拟机不用等待守护线程执行完毕
  4. 守护线程例子:后台记录操作日志,监控内存,垃圾回收GC等
  5. 方法:线程对象.setDaemon(boolean on)
public class TestMain3{
    
    
    public static void main(String[] args){
    
    
        Thread threadA = new Thread(new A());
        Thread threadB = new Thread(new B());

        threadB.setDaemon(true);
        threadA.start();
        threadB.start();
    }
}
class A implements Runnable {
    
    
    @Override
    public void run() {
    
    
        for (int i = 1000; i > 0; i--) {
    
    
            System.out.println(i);
        }
    }
}
class B implements Runnable {
    
    
    @Override
    public void run() {
    
    
        while (true) {
    
    
            System.out.println("无敌的我是守护线程");
        }
    }
}

四、线程同步(重点)

1、基本概念
  1. 并发Concurrence:并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。并发带来的问题:同一个对象被多个线程同时操作,即多个线程共享一个资源
  2. 线程同步:是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等前面线程使用完毕,下一个线程再使用
  3. 锁机制:当一个线程获得对象的排它锁,独占资源,其他线程必须等待该线程释放锁
  4. 锁带来的问题:
    • 一个线程持有锁会导致其他所有需要此锁的线程挂起
    • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
    • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致性能倒置
2、同步方法与同步块

同步方法

  • synchronized public void method(){}

  • synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronizied方法都必须获得调用该方法的对象的锁才执行,否则线程会阻塞。方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。

  • 锁太多,锁的方法太大会影响效率

同步块

  • synchrozied(Obj){}(ArrayList就是线程不安全的,可以用同步块),锁中可以含有锁,即同步块中可以再写同步块
  • Obj被称为同步监视器
  • Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
  • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身(实际上锁的是这个对象中所有被synchronized修饰的方法,理解成锁对象有利于分析程序),或者是class
  • 同步监视器的执行过程
    1. 第一个线程访问,锁定同步监视器,执行其中代码
    2. 第二个线程访问,发现同步监视器被锁定,无法访问
    3. 第一个线程访问完毕,解锁同步监视器
    4. 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
3、死锁deadlock
  • 多个线程各自占有一些共享资源,并且互相等待其它线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。

  • 简记:双方都占着对方需要的资源,形成僵持

  • 产生死锁的四个必要条件:

    • 互斥条件:一个资源每次只能被一个进程使用
    • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
    • 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺
    • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

    只要破解其中任意一个或多个条件就可以避免死锁发生。

public class TestMain3{
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        C c = new C(new A(), new B());
        new Thread(c, "test1").start();
        new Thread(c, "test2").start();
    }
}
class A {
    
    }
class B {
    
    }
class C implements Runnable{
    
    
    A a;
    B b;

    public C(A a, B b) {
    
    
        this.a = a;
        this.b = b;
    }

    @Override
    public void run() {
    
    
        if (Thread.currentThread().getName().equals("test1")) {
    
    
            synchronized (a) {
    
    
                System.out.println(Thread.currentThread().getName() + "拿到了a对象的锁");
                synchronized (b) {
    
    
                    System.out.println(Thread.currentThread().getName() + "拿到了b对象的锁");
                }
            }
        }
        if (Thread.currentThread().getName().equals("test2")) {
    
    
            synchronized (b) {
    
    
                System.out.println(Thread.currentThread().getName() + "拿到了b对象的锁");
                synchronized (a) {
    
    
                    System.out.println(Thread.currentThread().getName() + "拿到了a对象的锁");
                }
            }
        }
    }//run()
}
4、Lock
  • 从JKD5.0开始,Java提供了更强大的线程同步机制–通过显式定义同步锁对象来实现同步。同步锁使用Lock对象 充当
  • java.util.concurrent.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
  • ReentrantLock类(re + entrant)实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁
  • 对于lock.lock()和lock.unlock()之间的代码块,其它线程可以访问到资源(如对象),但不能直接访问到该代码块???

与synchronized比较

  • Lock是显式锁(手动开启和关闭锁),synchronized是隐式锁,出了作用域自动释放
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间一调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
  • 优先使用顺序:Lock > 同步代码块(已经进入方法体,分配了相应资源) > 同步方法(在方法体之外)

代码仅作为示例,非常不规范。。。

public class TestMain3{
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        A a = new A();
        Number1 number1 = new Number1();
        a.setNumber1(number1);
        new Thread(a).start();
        new Thread(a).start();
        System.out.println(Number1.value);//对于被锁住的代码块之间的资源,其它线程仍然可以访问
    }
}
class Number1 {
    
    
    static int value = 1;
}

class A implements Runnable{
    
    
    Number1 number1;
    private final ReentrantLock lock = new ReentrantLock();

    void setNumber1(Number1 number1) {
    
    
        this.number1 = number1;
    }

    @Override
    public void run() {
    
    
        while (true) {
    
    
            lock.lock();
            try {
    
    
                if (number1.value > 0) {
    
    
                    Thread.sleep(10000);
                    number1.value--;
                    System.out.println(Thread.currentThread().getName() + "====" + number1.value);
                } else {
    
    
                    break;
                }
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                lock.unlock();
            }
        }
    }
}

五、线程通信(协作)问题

1、基本方法

都是Object类下的方法。下面来自JDK1.8官方文档。

需要好好理解***对象监视器(同步监视器)***的概念。

  1. wait() 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法。

        • public final void wait()
                          throws InterruptedException
          

          导致当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法。换句话说,这个方法的行为就好像简单地执行呼叫wait(0)

          当前的线程必须拥有该对象的显示器。 该线程释放此监视器的所有权,并等待另一个线程通知等待该对象监视器的线程通过调用notify方法或notifyAll方法notifyAll 。 然后线程等待,直到它可以重新获得监视器的所有权并恢复执行。

          像在一个参数版本中,中断和虚假唤醒是可能的,并且该方法应该始终在循环中使用:

            synchronized (obj) {
                   while (<condition does not hold>)
                       obj.wait();
                   ... // Perform action appropriate to condition
               } 
          

          **该方法只能由作为该对象的监视器的所有者的线程调用。**有关线程可以成为监视器所有者的方式的说明,请参阅notify方法。

          • 异常

            IllegalMonitorStateException - 如果当前线程不是对象监视器的所有者。

            InterruptedException - 如果任何线程在当前线程等待通知之前或当前线程中断当前线程。 当抛出此异常时,当前线程的中断状态将被清除。

  2. notify() 唤醒正在等待对象监视器的单个线程。

    public final void notify()唤醒正在等待对象监视器的单个线程。 如果任何线程正在等待这个对象,其中一个被选择被唤醒。 选择是任意的,并且由实施的判断发生。 线程通过调用wait方法之一等待对象的监视器。
    唤醒的线程将无法继续,直到当前线程放弃此对象上的锁定为止。 唤醒的线程将以通常的方式与任何其他线程竞争,这些线程可能正在积极地竞争在该对象上进行同步; 例如,唤醒的线程在下一个锁定该对象的线程中没有可靠的权限或缺点。

    该方法只能由作为该对象的监视器的所有者的线程调用。 线程以三种方式之一成为对象监视器的所有者:

    通过执行该对象的同步实例方法。
    通过执行在对象上synchronized synchronized语句的正文。
    对于类型为Class,的对象,通过执行该类的同步静态方法。
    一次只能有一个线程可以拥有一个对象的显示器。

    异常
    IllegalMonitorStateException - 如果当前线程不是此对象的监视器的所有者。

  3. notifyAll() 唤醒正在等待对象监视器的所有线程。

    public final void notifyAll()唤醒正在等待对象监视器的所有线程。 线程通过调用wait方法之一等待对象的监视器。
    唤醒的线程将无法继续,直到当前线程释放该对象上的锁。 唤醒的线程将以通常的方式与任何其他线程竞争,这些线程可能正在积极地竞争在该对象上进行同步; 例如,唤醒的线程在下一个锁定该对象的线程中不会有可靠的特权或缺点。

    该方法只能由作为该对象的监视器的所有者的线程调用。 有关线程可以成为监视器所有者的方法的说明,请参阅notify方法。

    异常
    IllegalMonitorStateException - 如果当前线程不是此对象的监视器的所有者。

2、生产者与消费者模式(问题)

是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。

  • 对于生产者,没有生产产品之前,要通知消费者等待(消费者自己启用wait方法)。而生产了产品之后,又需要马上通知消费者消费
  • 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费
  1. 管程法

    生产者 ↔ 缓冲区 ↔ 消费者

  2. 信号灯法:标志位

六、高级主题

1、线程池
  • 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
  • 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具
  • 好处:
    • 提高响应速度(减少了创建新线程的时间)
    • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
    • 全球线程管理
  • 关键字:
    • corePoolSize:核心池的大小
    • maximunPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间会终止
  • 线程池接口:ExecutorService,Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
public class TestMain3{
    
    
    public static void main(String[] args) throws InterruptedException, ExecutionException {
    
    
        A a = new A();
        B b = new B();
        //创建执行服务,创建一个大小为2的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        //把实现callable接口的对象丢入执行
        Future<Integer> submit = executorService.submit(a);
        //也可以把实现Runnable接口的对象丢入执行
        executorService.execute(b);
        executorService.execute(b);
        //实现callable接口的对象,可以得到得到返回值
        Integer integer = submit.get();
        //关闭服务
        executorService.shutdown();
    }
}

class A implements Callable<Integer>{
    
    

    @Override
    public Integer call() throws Exception {
    
    //重写call方法,返回值和接口泛型保持一致
        System.out.println(Thread.currentThread().getName() + "执行了call方法");
        return 123;
    }
}
class B implements Runnable {
    
    

    @Override
    public void run() {
    
    
        System.out.println(Thread.currentThread().getName() + "执行了run方法");
    }
}

后记:上面的大都是看狂神的视频做的笔记,加上自己找的一些资料和实例,感谢他的免费课程~~

猜你喜欢

转载自blog.csdn.net/qq_47234534/article/details/111144837