07 |「多线程与锁」

前言

多线程与锁

一、多线程

1、应用场景

  • 操作系统中每个程序都是一个进程,进程中有些操作比较慢,如果一个进程只有一个线程容易导致卡死;

  • 场景:当打开一个网站的页面后,页面加载会有很多操作(包括加载、渲染等)

    • 对于加载过程,加载图片和渲染其它地方是并行执行的,不会因为图片加载完再渲染其它地方;
  • 一个进程可以有多个执行顺序(线程),当一个执行顺序(线程)在进行耗时操作时,CPU 会自动执行其它线程,使 CPU 不闲置,提高运行效率;

  • 每一个程序都是一个进程;同一时间有多个线程在执行,高效;

2、代码场景

  • 正常代码执行是一行一行顺序执行的(没有多线程的情况下);
  • 当存在多线程时,整个代码的执行,并不是只有一条线;在执行中,有多条执行线,当一条执行线卡住处于执行状态时,操作系统会执行另外一条线,不会浪费 cpu 的资源;
  • 如下图所示,当主线程执行到 start 函数时,执行就变成了两条线(一条线去执行子线程的 run 函数,另一条线为主线程继续往后执行;主线程继续往后执行的代码是不会等子线程 start 函数中代码执行完再执行的,是同时执行的);

2、多线程的实现

  • 每个线程都是一个对象;
  • 将实现多线程的逻辑写在 run 函数中即可;
  • 启动一个子线程
    • 当主线程执行到 start 函数的时,整个程序的控制流分岔;
  • 代码首先顺序执行主线程,当主线程遇到 start,会新开一个线程,子线程自动执行 run 函数,原来的主线程控制流继续执行;
  • 子线程在执行的时候顺序是随机的,每个线程自己独立看起来的话,是同时执行的;但是,在 CPU 执行中,虽然每个线程看起来是并行做的,但是 CPU 是一个线程一个线程按顺序做的,同一时间只能执行一个线程,只是我们看起来多个线程是同时执行的;
    • 子线程在执行的时候顺序是随机的:

      • 这意味着,当你有多个线程同时运行时,你不能预测哪个线程会首先执行完其任务。线程的调度是由操作系统或线程库来管理的,并且可能会受到许多因素的影响,如线程的优先级、系统的负载等。因此,从编程的角度来看,线程的执行顺序是随机的。
    • 每个线程自己独立看起来的话,是同时执行的:

      • 这句话描述了多线程编程的一个关键特性,即并发性。从程序员的角度来看,多个线程似乎是同时执行的。这允许程序同时处理多个任务,从而提高效率。然而,这种“同时执行”的感觉只是表象,实际上线程的执行是由操作系统管理的,并且可能会受到很多因素的影响。
    • 但是,在 CPU 执行中,虽然每个线程看起来是并行做的:

      • 这里再次强调了并发和并行的区别。并发是指多个任务在逻辑上同时执行,而并行是指多个任务在物理上同时执行。在 CPU 层面上,多个线程看似并行执行(即同时执行),但实际上在任一时刻,CPU 只能执行一个线程。
    • 但是 CPU 是一个线程一个线程按顺序做的:

      • 这句话揭示了 CPU 的实际工作原理。CPU 有一个或多个核心,每个核心在任何给定的时间点上只能执行一个线程。当 CPU 切换到另一个线程时,当前线程的执行会被暂停,而新线程的执行会开始。这种切换非常快,以至于从用户的角度来看,多个线程似乎是同时执行的。
    • 同一时间只能执行一个线程:

      • 这是对 CPU 工作原理的再次强调。无论有多少线程在系统中运行,CPU 在任何给定的时间点上都只能执行一个线程。
    • 只是我们看起来多个线程是同时执行的:

      • 这句话总结了多线程编程的表象和实际工作原理之间的区别。由于 CPU 的快速切换和操作系统的调度,多线程程序在宏观上看起来是同时执行的,但实际上每个线程的执行都是按顺序进行的。
1)继承 Thread 类
  • 每创建一个线程的话,都需要 new 一个新的线程对象,不同线程的对象是不同的;

  • 遇到 start() 函数会新开一个线程,自动执行 run()

  • 多个线程执行的顺序是随机的;

  • 与实现 Runnable 接口的方式区别为不能共用同一个对象

    class Worker extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10; i ++ ) {
                System.out.println("Hello! " + this.getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Worker worker1 = new Worker();
            Worker worker2 = new Worker();
            worker1.setName("thread-1");
            worker2.setName("thread-2");
            worker1.start();
            worker2.start();
        }
    }
    
2)实现 Runnable 接口
  • 实现 Runnable 接口只用写一个类即可;

  • 不同线程可以共用一个对象

    • 实现两个线程的话,可以只用一个实例,两个线程执行的是一个实例中的 run 函数;
  • 用一个对象和用多个对象有什么区别——》涉及到锁(同步的概念)

    • 同步的话是需要同步同一个资源(对象)要用实现 Runable 的写法;
    class Worker implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i ++ ) {
                System.out.println("Hello! " + "thread-1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
    
    public class Main {
        public static void main(String[] args) {
        	Worker worker = new Worker();
            new Thread(worker).start();
            new Thread(worker).start();
        }
    }
    
3)常用 API 函数
  • start()
    • 开启一个线程;
  • Thread.sleep()
    • 表示让当前线程睡眠,在主线程中执行的话是让主线程睡眠,在子线程中执行的话是让子线程睡眠;
  • join()
    • 多个线程执行顺序是随机的,join 表示一个线程等待另一个线程执行完成结束;

    • 下面代码,主线程等两个子线程执行完成后再执行

      • worker1.join():当主线程执行到这句话时,主线程就会卡住,只有当 worker1 线程执行完成后,才会继续执行 worker1.join() 后面的代码;
      class Worker implements Runnable {
          @Override
          public void run() {
              for (int i = 0; i < 10; i ++ ) {
                  System.out.println("Hello! " + "thread-1");
                  try {
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      throw new RuntimeException(e);
                  }
              }
          }
      }
      
      
      public class Main {
      	    public static void main(String[] args) {
      	        Worker worker1 = new Worker();
      	        Worker worker2 = new Worker();
      	        worker1.setName("thread-1");
      	        worker2.setName("thread-2");
      	        worker1.start();
      	        worker2.start();
      	        
      	        worker1.join();
      	        worker2.join();
      			
      			System.out.println("Main thread finisher");
      	    }
      	}
      
  • interrupt():给线程发送一个异常;
  • setDaemon():将线程设置为守护线程。当只剩下守护线程时,程序自动退出;
    • 一个程序可能同时有很多线程,当只剩下守护线程时,守护线程就会自动结束(当其它线程都结束的时候,守护线程会自动结束)

    • 作用:每个 Java 程序都有垃圾回收机制,守护线程在所有用户线程结束之后结束掉;

    • 应用场景:需求:当其它有用线程都结束后,我们希望这个线程自动结束,就可以将这个线程设置为守护线程 ;

    • 先设置为守护线程,然后在执行 start

四、锁

1、为什么会有锁的概念?

  • 针对多线程,同一个进程的多个线程会共享一段内存空间,可能会出现读写冲突的问题;

2、背景举例

1)出现冲突
  • 背景问题:统计某个网页页面的某门课的报名人数;

  • cnt:存储报名人数;

  • 网站界面显示是存在多个线程执行的;

  • 每次执行后得到 cnt 值的结果都是不一样的,产生这个问题的原因就是两个线程存在读写冲突的问题;

  • 每个线程对应 一个人报名成功,cnt = cnt + 1(多线程运行过程中,多个线程在执行这条语句时,并不是某个线程会完整地执行完这个语句再执行下一个线程),多个线程在执行时可能会同时执行这条语句;

  • cnt = cnt + 1:对应三个操作,先取 cnt 的值,再将 cnt + 1,再给 cnt 赋值;

  • 出现的问题

    • 第一个线程取 cnt 的值,此时只取出来 cnt,且 cnt 的值为 0,然后被操作系统挂载;
    • 第二个线程开始执行,执行完 3 个操作,第二个线程此时执行完毕,此时 cnt = 1
    • 第一个线程继续执行,执行 cnt + 1(0 + 1 = 1),此时 cnt = 1
    • 导致两个人购课成功后,cnt 的值仍然为 1,出错;
  • 出现读写冲突的情况

    • 多个线程在同时访问同一个资源时,并不一定有冲突;
    • 两个线程只读取变量的信息时,不管多少个线程并行,都不会有问题,因为不管什么时候读,值是不会变的;
    • 当两个线程同时写一个变量值时,会出现问题,第一个线程会写一个版本,第二个线程也写一个版本,最终值取哪个值是不确定(因为线程执行的顺序是随机的);
    • 一个线程读,另一个线程写也会出现冲突,先读再写,读的是旧值,先写再读,读的是新值,也是不确定的(因为线程执行顺序是随机的);
    • 总结:当两个线程至少有一个线程时的情况下,如果同时访问同一个资源就会有冲突;
2)解决冲突
  • 方法 1

    • 等第一个线程执行完,再执行第二个线程,如下代码所示;

    • 但是这样就不是多线程代码了, 同一时间只能有一个线程在执行,并不高效 ;

      public class Main {
          public static void main(String[] args) throws InterruptedException {
              Worker worker1 = new Worker();
              Worker worker2 = new Worker();
      
              worker1.start();
              worker1.join();
              
              worker2.start();
              worker2.join();
      
              System.out.println(Worker.cnt);
          }
      }
      
  • 方法 2

    • 既保证高效又保证没有读写冲突,引入下面的

2、锁

1)方式 1
  • 访问公共资源(cnt),在访问之前,先去拿这把锁,同一个锁同一时间只能被一个线程锁定;
  • 一个线程拿到这把锁(lock() ),其余线程就会被阻塞在这句话上(停止不动);当一个线程处理完后,会自动将锁释放掉(unlock());
    class Worker extends Thread {
        public static int cnt = 0;
        private static final ReentrantLock lock = new ReentrantLock();  // 定义一把锁,静态成员变量,每个对象共享一个
        // 这样定义实现多个线程只有一把锁
        
        @Override
        public void run() {
            for (int i = 0; i < 100000; i ++ ) {
                // 当在操作公共资源时,先锁住(多个线程获取锁时只有一个线程可以获取到锁,其余线程卡在此处,不往下执行)
                lock.lock();  
                try {
                    cnt ++ ;// 实现了同一时间只会有一个线程完整地执行这句话(获取 cnt -> cnt + 1 -> 将cnt的值写会cnt)
                } finally {
                    lock.unlock();  // 释放锁
                    // 当锁释放后,操作系统会自动通知被卡在锁的其它线程
                }
            }
        }
    }
    
    public class Main {
        public static void main(String[] args) throws InterruptedException {
            Worker worker1 = new Worker();
            Worker worker2 = new Worker();
    
            worker1.start();
            worker2.start();
            
            worker1.join();
            worker2.join();
    
            System.out.println(Worker.cnt);
        }
    }
    
2)方式 2(Synchronized)
  • 特点

    • 上面的过程中,先定义一把锁,然后 lock,再 unlock 比较麻烦,可以采用简写方式(语法糖)——》 只需要 Synchronized 就可以锁住一段代码;
    • Synchronized 达到锁住一段代码的作用时,Synchronized 的参数是对象类型,且对象要求是不可变的,多个线程操作的是同一个对象才可以;
  • 利用 Synchronized 锁住一段代码

    class Worker extends Thread {
        // 在第一种方法中,cnt 是一个 Integer 类型的对象,
        //而 Integer 对象是不可变的,所以不能直接使用 synchronized 关键字对它进行同步。
        //因为每次对 cnt 进行操作时,都会创建一个新的 Integer 对象。因此,这种写法是错误的。
        public static Integer cnt = 0;
        
        // 不能用下面的写法,因为 Integer 定义的对象是不可变的,cnt = cnt + 1 会生成新对象
        // synchronized 锁住的对象要求是不可变的
        @Override
        public void run() {
            synchronized(cnt) {
                for (int i = 0; i < 1000; i ++) {
                    cnt = cnt + 1;  
            }
        }
    
    
        // 采用下面这种写法
        // 静态对象,不同实例共享一个静态对象,同一时间只能有一个线程执行后面的代码
        private final static Object object = new Object();
        @Override
        public void run() {
            // 使用 synchronized(object) 来锁住代码块,确保同一时间只有一个线程可以执行其中的代码。这样就保证了对 cnt 的操作是线程安全的
            // 引入了一个静态的 Object 对象 object 作为同步锁。这个对象是不可变的,并且被所有 Worker 实例共享
            synchronized(object) {  // 保证了同一时间只有一个线程可以执行这段代码
                for (int i = 0; i < 1000; i ++) {
                    cnt = cnt + 1;  
            }
        }
            
    }
    
    public class Main {
        public static void main(String[] args) throws InterruptedException {
            Worker worker1 = new Worker();
            Worker worker2 = new Worker();
    
            worker1.start();
            worker2.start();
            
            worker1.join();
            worker2.join();
    
            System.out.println(Worker.cnt);
        }
    }
    
  • 利用 Synchronized 锁住方法

    class Worker implements Runable {
        public static Integer cnt = 0;
    
        private synchronized void work() {  // 因为作用的是同一个对象(this),所以可以加
            for (int i = 0; i < 1000; i ++) {
                cnt = cnt + 1;
            }
        }
        
        @Override
        public void run() {
            work();
        }
            
    }
    
    public class Main {
        public static void main(String[] args) throws InterruptedException {
            Worker worker = new Worker();
    
            // 两个线程传入的是同一个对象 worker
            // 实现了两个线程访问的是同一个对象
            Thread worker1 = new Thread(worker);
            Thread worker2 = new Thread(worker);
    
            worker1.start();  // 第一个线程
            worker2.start();  // 第二个线程
    
            worker1.join();
            worker2.join();
    
            System.out.println(Worker.cnt);  // 输出结果为 2000,结果正确
        }
    }
    
3)wait 和 notify(Synchronized 的代码中)
  • notify():随便唤醒一个线程,notifyAll():唤醒所有线程;
  • wait() :使线程阻塞(等待被唤醒);
  • notify():唤醒等待的线程;
  • 当一个线程执行到 wait 方法时,该线程被阻塞,会自动将锁释放掉,并且通知其它线程执行;
  • 当线程被唤醒后继续执行 wait 后面的代码;
    class Worker extends Thread {
        private final Object object;
        private final boolean needWait;
    
        public Worker(Object object, boolean needWait) {
            this.object = object;
            this.needWait = needWait;
        }
    
        @Override
        public void run() {
            synchronized (object) {
                try {
                    if (needWait) {
                        object.wait();
                        System.out.println(this.getName() + ": 被唤醒啦!");
                    } else {
                        object.notifyAll();
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
    
    public class Main {
        public static void main(String[] args) throws InterruptedException {
            Object object = new Object();
            for (int i = 0; i < 5; i ++ ) {
                Worker worker = new Worker(object, true);
                worker.setName("thread-" + i);
                worker.start();
            }
    
            Worker worker = new Worker(object, false);
            worker.setName("thread-" + 5);
            Thread.sleep(1000);
            worker.start();
        }
    }
    
  • 每个 worker 实例持有一个对象 object 和一个布尔值 needWait;
  • 在 Worker 的构造函数中,传入一个对象和一个布尔值,分别表示该 Worker 实例将要操作的对象以及是否需要等待;
  • 在 run() 方法中,使用 synchronized 关键字锁定了传入的对象 object。如果 needWait 为 true,则调用 object.wait() 使线程等待,直到其他线程调用 object.notifyAll() 或 object.notify() 唤醒它;如果 needWait 为 false,则调用 object.notifyAll() 唤醒所有等待该对象的线程;
  • 在 Main 类的 main 方法中,首先创建了5个 Worker 实例,它们都需要等待(needWait 为 true),然后再创建一个 Worker 实例,它负责唤醒所有等待的线程(needWait 为 false);
  • 主线程睡眠了一段时间后(1000毫秒),然后启动了这个唤醒线程;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

工科男小Y

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值