十念 发表于 2023-4-4 14:34:58

Java多线程(一篇从0讲透)

多线程

思维导图看天下:
https://img2023.cnblogs.com/blog/2729274/202303/2729274-20230331214409378-1203111156.png
1. 概述


并行与并发

并行 :指两个或多个事件在同一时刻发生(同时发生)
并发 :指两个或多个事件在同一个时间段内发生。(交替执行)

线程与进程

进程:是指一个内存中运行的程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程
记忆:进程的英文为Process,Process也为过程,所以进程可以大概理解为程序执行的过程。
(进程也是程序的一次执行过程,是系统运行程序的基本单位; 系统运行一个程序即是一个进程从创建、运行到消亡的过程)
线程:进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。【java默认有两个线程:main、GC】

进程与线程的区别:

[*]进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
[*]线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多

2. 线程创建的五种方式

推荐使用Runnable接口的方式,因为Java是单继承的,所以使用Thread有OPP单继承局限性
https://img2023.cnblogs.com/blog/2729274/202303/2729274-20230331214424067-1909478726.png
2.1 背景介绍

线程类
Java使用 java.lang.Thread 类代表线程,所有的线程对象都必须是Thread类或其子类的实例
每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码
Java使用线程执行体来代表这段程序流。

2.2 ① 继承Thread类

2.2.1 线程实现

1)实现步骤


[*]继承Thread类的子类,并重写该类的run()方法(该run()方法的方法体就代表了线程需要完成的任务,因此run()方法称为线程执行体)
[*]创建Thread子类的实例,即创建了线程对象
[*]调用线程对象的start()方法来启动该线程

2)实现案例

自定义线程类:
https://img2023.cnblogs.com/blog/2729274/202303/2729274-20230331214441093-1217407369.png
主函数:
https://img2023.cnblogs.com/blog/2729274/202303/2729274-20230331214446254-2036611915.png
public static void main(String[] args) {
    MyThread myThread = new MyThread("MyThread");
    myThread.start();
    for (int i = 0;i<1000;i++){
      System.out.println("main"+i);
    }
}拓展:子类线程也获得不了父类线程设置的值,但可以通过用InheritableThreadLocal方法来解决这个问题。(在InheritableThreadLocal存放的内容,会自动向子线程传递)
//1.可以使用Thread类中的方法getName
String name = getName();
System.out.println(name);//创建时, 指定了名称,获取的就是指定的名称
//如果没有指定名称,获取的就是Thread-0
//2.可以先获取当前正在执行的线程
Thread currentThread = Thread.currentThread();
System.out.println(currentThread);//Thread
String name2 = currentThread.getName();
System.out.println(name2);//Thread-0
3.2.9 等待与唤醒


[*]等待wait和唤醒notify、notifyall都需要在同步代码内(锁方法 or 锁代码块)
[*]等待和唤醒只能由锁对象调用。(锁代码块的锁对象容易看出,锁方法的锁对象一般是this或方法所在的类)
public void wait() : 让当前线程进入到等待状态 此方法必须锁对象调用.
public void notify() : 唤醒当前锁对象上等待状态的线程 此方法必须锁对象调用.会继续执行wait()方法之后的代码
方法名作用wait()表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁wait(long timeout)指定等待的毫秒数notify()唤醒一个处于等待状态的线程notifyAll()唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度注意:均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常IllegalMonitorStateException

示例:
顾客与老板线程:
创建一个顾客线程(消息者):告诉老板要吃什么 调用wait方法,放弃cpu的执行,进入wating状态(无限等待)
创建一个老板线程(生产者):花5秒做好 做好后 调用notify方法 唤醒顾客 开吃
注意

[*]顾客与老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行同步使用的锁必须要保证唯一,
[*]只有锁对象才能调用wait和notify方法

顾客线程
https://img2023.cnblogs.com/blog/2729274/202303/2729274-20230331214718530-204254480.png
老板线程
https://img2023.cnblogs.com/blog/2729274/202303/2729274-20230331214723248-1730794907.png
MyThread myThread = new MyThread();
myThread.setName("myThreadName");
myThread.start();
3.2.10 小结


[*]进入计时等待状态的两种方式

[*]使用sleep(long m)方法,在毫秒值结束后,线程睡醒,进入Runnable/Blocked状态(抱着锁睡觉,不放锁)
[*]使用wait(long m)方法wait方法如果在毫秒值结束之后,还没有被唤醒,就会自动醒来,进入Runnable/Blocked状态(等待的时候会释放锁)

[*]两种唤醒的方法

[*]public void notify()
随机唤醒1个
[*]public void notifyall()
唤醒锁对象上所有等待的线程.


4. 线程安全

4.0 线程同步机制

多个线程操作同一个资源
并发:同一个对象被多个线程同时操作
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候我们就需要线程同步。线程同步其实就是一个等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程时候完毕,下一个线程再使用。
线程同步

[*]由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。存在以下问题

[*]一个线程持有锁会导致其他所有需要此锁的线程挂起
[*]在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
[*]如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题


4.1 什么是线程安全

多线程访问了共享的数据,就会产生线程的安全
举例:

[*]多个窗口,同时卖一种票.   如果不进行控制, 可以会出现卖重复的现象
[*]多个窗口,同时在银行同一账户取钱,银行不进行控制就会亏钱
[*]ArrayList线程不安全

4.1.1 买票问题

解决措施:可锁代码块可锁方法,后面的解决方案是以买票问题为例
代码演示:
public class MyThread extends Thread{
    //定义指定线程名称的构造方法
    public MyThread(String name) {
    super(name);
}结果:9997,少了三个,是因为前面插入数据的时候有三个下标被重复赋值,导致有三次赋值被覆盖了。

4.2 解决线程安全

锁类模板 和 锁用该类模板创建出来的对象 两者之间互不影响!
4.2.1 synchronized锁代码块

同步代码块synchronized的格式:
/*程序在执行第二秒时, 会暂停2秒,2秒后,继续执行后面程序*/
for (int i = 1; i <=60; i++) {
    System.out.println(i);
    /*让程序睡眠1秒钟   1秒=1000毫秒*/
    try {
      Thread.sleep(2000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
}注意
1.锁对象可以是任意对象new Personnew Student ...(一般是锁变化的对象,需要增删改的对象)
2.必须保证多个线程使用的是同一个锁对象
3.锁对象的作用:把{}中代码锁住,只让一个线程进去执行
1)锁实例对象

适用于使用同一个Runnable对象创建多个线程的情况,不适用于多个Runnable对象分别创建多个线程的情况
作用范围是对象实例,不可跨对象,所以多个线程不同对象实例访问此方法,互不影响,无法产生互斥。由于本题抢票中是多个线程使用同一个Runnable对象,所以得到的锁是同一个对象产生的obj,可以实现线程隔离。但银行例子中是多个线程分别使用不同的Runnable对象,所以使用锁实例对象是没用的。
示例
https://img2023.cnblogs.com/blog/2729274/202303/2729274-20230331214807448-51768351.png
//实现Runnable接口
public class RunnableImpl implements Runnable{
    //2.重写Runnable接口中的run方法,设置线程任务
    @Override
    public void run() {
      //新线程执行的代码
      for (int i = 0; i <20; i++) {
            System.out.println(Thread.currentThread().getName()+"===>"+i);
      }
    }
}
public static void main(String[] args) {
      //3.创建Runnable接口的实现类对象
      RunnableImpl r = new RunnableImpl();
      //4.创建Thread类对象,构造方法中传递Runnable接口的实现类对象
      Thread t = new Thread(r);//打印20次i
      //5.调用Thread类中的start方法,开启新的线程,执行run方法
      t.start();//【一般16-18行简写为:new Thread(r,"线程名").start();】
      //主线程开启新线程之后继续执行的代码
      for (int i = 0; i <20; i++) {
            System.out.println(Thread.currentThread().getName()+"===>"+i);
      }
    }总结:
同步监视器的执行过程(同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class)

[*]第一个线程访问,锁定同步监视器,执行其中的代码
[*]第二个线程访问,发现同步监视器被锁定,无法访问,处于阻塞状态,一直等待
[*]第一个线程访问完毕,解锁同步监视器
[*]第二个线程访问,发现同步监视器没有锁,然后锁定并访问

2)锁类

适用于使用同一个Runnable对象创建多个线程的情况,也适用于多个Runnable对象分别创建多个线程的情况
虽然是通过对象访问的此方法,但是加锁的代码块是类级别的跨对象的,所以锁的范围是针对类,多个线程访问互斥。
public class MyCallableImpl implements Callable<Boolean> {
    @Override
    public Boolean call() throws Exception {
      PictureCatch t = new PictureCatch();
      t.test(url,name);
      System.out.println("下载了文件名:"+name);
      return true;
    }

    String url; //网址
    String name;    //保存的文件名

    MyCallableImpl(String url,String name){
      this.url=url;
      this.name=name;
    }

    public static void main(String[] args) {
      MyCallableImpl t1 = new MyCallableImpl("https://img0.baidu.com/it/u=1151663768,725447312&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500","t4");
      MyCallableImpl t2 = new MyCallableImpl("https://img0.baidu.com/it/u=1648512719,1593015989&fm=253&fmt=auto&app=120&f=JPEG?w=891&h=500","t5");
      MyCallableImpl t3 = new MyCallableImpl("https://img2.baidu.com/it/u=863703859,746061395&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500","t6");

      ExecutorService ser = Executors.newFixedThreadPool(3);
      Future<Boolean> result1 = ser.submit(t1);
      Future<Boolean> result2 = ser.submit(t2);
      Future<Boolean> result3 = ser.submit(t3);
      try {
            boolean r1 = result1.get();
            boolean r2 = result2.get();
            boolean r3 = result3.get();

            System.out.println(r1);
            System.out.println(r2);
            System.out.println(r3);
      } catch (InterruptedException e) {
            e.printStackTrace();
      } catch (ExecutionException e) {
            e.printStackTrace();
      }
      ser.shutdownNow();
    }


    class PictureCatch{
      void test(String url, String name){
            try {
                FileUtils.copyURLToFile(new URL(url), new File(name));
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("获取文件出错!");
            }
      }
    }
}
4.2.2 synchronized锁方法

锁的是this,也就是主方法里调用该方法的对象
同步方法解决线程安全的格式:
public class MyTimer {

    public static void main(String[] args) {
      timer();
    }

    /**
   * 指定时间 time 执行 schedule(TimerTask task, Date time)
   */
    public static void timer() {
      Timer timer = new Timer();
      // 设定指定的时间time,此处为2000毫秒
      timer.schedule(new TimerTask() {
            public void run() {
                System.out.println("执行定时任务");
            }
      }, 2000);
    }

}使用步骤
1.创建一个方法,方法的修饰符添加上synchronized
2.把访问了共享数据的代码放入到方法中
3.调用同步方法
1)锁普通方法(对象锁)

适用于使用同一个Runnable对象创建多个线程的情况,不适用于多个Runnable对象分别创建多个线程的情况
普通方法作用范围是对象实例,不可跨对象,所以多个线程不同对象实例访问此方法,互不影响,无法产生互斥。由于本题抢票中是多个线程使用同一个Runnable对象,所以得到的锁是同一个类对象this,可以实现线程隔离。但银行例子中是多个线程分别使用不同的Runnable对象最后锁的this也是不同类对象的this,所以使用锁普通方法是没用的。
示例
https://img2023.cnblogs.com/blog/2729274/202303/2729274-20230331214825222-1807683300.png
new 父类/接口(){
    重写父类/接口中的方法
};锁对象是谁???
锁对象为this
https://img2023.cnblogs.com/blog/2729274/202303/2729274-20230331214835008-460385882.png
public static void main(String[] args) {
    new Thread(){    //new 没有名称的类 继承Thread
      //重写run方法,设置线程任务
      @Override
      public void run() {
            for (int i = 0; i <20 ; i++) {
                System.out.println(Thread.currentThread().getName()+"==>"+i);
            }
      }
    }.start();
}
2)锁静态方法(类锁)

适用于使用同一个Runnable对象创建多个线程的情况,也适用于多个Runnable对象分别创建多个线程的情况
静态方法是通过类访问,是类级别的跨对象的,所以锁的范围是针对类,多个线程访问互斥。
示例:变化的量记得也要static
https://img2023.cnblogs.com/blog/2729274/202303/2729274-20230331214844296-581610816.png
new Thread(new Runnable() {//new没有名称的类实现了Runnable接口
    //重写run方法,设置线程任务
    @Override
    public void run() { //实现接口当中run方法
      for (int i = 0; i <20 ; i++) {
            System.out.println(Thread.currentThread().getName()+"-->"+i);
      }
    }
}).start();锁对象是谁???
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)
https://img2023.cnblogs.com/blog/2729274/202303/2729274-20230331214852360-1983534457.png

4.2.3 Lock锁

概述

[*]从jdk5.0开始,java提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当
[*]java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前先获得Lock对象
[*]。ReetrantLock(可重入锁)类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常见的是ReetrantLock,可以显示加锁、释放锁
Lock接口中的方法
public class ThreadStopDemo implements Runnable {
    private boolean flag = true;
    @Override
    public void run() {
      int i = 1;
      while (flag) {
            System.out.println("run..."+(i++));
      }
    }
       
    //设置一个专门修改标志位的方法来停止线程
    public void stop(){
      flag = false;
    }

    public static void main(String[] args) {
      ThreadStopDemo demo = new ThreadStopDemo();
      new Thread(demo).start();
      for (int i = 1; i <= 500; i++) {
            System.out.println("main..."+i);
            if (i == 300) {
                demo.stop();
                System.out.println("线程该停止了");
            }
      }
    }
}使用步骤
1.在成员位置创建一个Lock接口的实现类对象ReentrantLock
2.在可能会出现安全问题的代码前,调用lock方法获取锁对象
3.在可能会出现安全问题的代码后,调用unlock方法释放锁对象
示例
https://img2023.cnblogs.com/blog/2729274/202303/2729274-20230331214903112-229738487.png
public static void main(String[] args) {
    //模拟倒计时
    System.out.println("开始倒计时");
    int num = 10;
    while (true) {
      System.out.println(num--);
      Thread.sleep(1000);
      if (num <= 0) {
            break;
      }
    }
    System.out.println("开始报时");
    //打印当前系统时间
    int count = 10;
    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
    while (true) {
      System.out.println(LocalDateTime.now().format(dateTimeFormatter));
      Thread.sleep(1000);
      count--;
      if (count <= 0) {
            break;
      }
    }
}
4.2.4 synchronized与Lock对比


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

4.2.5 判断锁的对象是谁

8锁现象:
1)标准情况下,一个对象 两个同步方法 第一个线程先拿到锁 谁先执行
2)一个对象 两个同步方法 第一个线程先拿到锁 第一个方法延迟4S 谁先执行
3)一个对象 一个同步方法一个普通方法 第一个线程先拿到锁 谁先执行
4)两个对象 两个同步方法 第一个线程先拿到锁 第一个方法延迟4S 谁先执行
5)一个对象 两个静态同步方法 第一个线程先拿到锁 第一个方法延迟4S 谁先执行
6)两个对象 两个静态同步方法 第一个线程先拿到锁 第一个方法延迟4S 谁先执行
7)一个对象 一个静态同步方法一个普通同步方法 第一个线程先拿到锁 第一个方法延迟4S 谁先执行
8)两个个对象 一个静态同步方法一个普通同步方法 第一个线程先拿到锁 第一个方法延迟4S 谁先执行
public class ThreadYieldDemo implements Runnable {
    @Override
    public void run() {
      System.out.println(Thread.currentThread().getName()+"开始!");
      Thread.yield();
      System.out.println(Thread.currentThread().getName()+"结束!");
    }

    public static void main(String[] args) {
      ThreadYieldDemo demo = new ThreadYieldDemo();
      new Thread(demo, "a").start();
      new Thread(demo, "b").start();
    }
}小结:

[*]对于普通同步方法,锁是当前new实例对象。
[*]对于static 静态同步方法,锁是当前类模板的Class对象。

5. 生产者与消费者

5.1 问题介绍与分析

1.线程通信

[*]应用场景:生产者和消费者问题

[*]假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中的产品取走消费
[*]如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止
[*]如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止

https://img2023.cnblogs.com/blog/2729274/202303/2729274-20230331214920367-1825033592.png
2.线程通讯-分析

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

[*]对于生产者,没有生产产品之前,要通知消费着等待,而生产了产品之后,有需要马上通知消费者消费
[*]对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费
[*]在生产者消费者问题中,仅有synchronized是不够的,就需要用到之前讲的等待与唤醒。

[*]synchronized可以阻止并发更新同一个共享资源,实现了同步
[*]synchronized不能用来实现不同线程之前的消息传递(通信)



5.2 解决方法

5.2.1 管程法

生产者——缓存区——消费者
并发协作模型”生产者/消费者模式“-->管程法

[*]生产者:负责生产数据的模块(可能是方法,对象,线程,进程)
[*]消费者:负责处理数据的模块(可能是方法,对象,线程,进程)
[*]缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”
生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据

代码:
//餐厅模式:生产者————厨师、消费者————顾客public class 管程法 {    public static void main(String[] args) {      SynContainer container = new SynContainer();      new Productor(container).start();      new Cousumer(container).start();    }}/** * 生产者 */class Productor extends Thread {    /**   * 缓冲区   */    private SynContainer container;    public Productor(SynContainer container) {      this.container = container;    }    @Override    public void run() {      for (int i = 1; i
页: [1]
查看完整版本: Java多线程(一篇从0讲透)