Java——多线程

一、概述

  进程:

    正在运行的程序,是系统进行资源分配和调用的独立单位

    每一个进程都有它自己的内存空间和系统资源

  线程:

    是进程中的单个顺序控制流,是一条执行路径

    一个进程如果只有一条执行路径,则为单线程程序

    一个进程如果有多条执行路径,则称为多线程程序

java程序运行原理

    java命令会启动java虚拟机,JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个“主线程”,然后主线程再去调用某个类的main方法。

    所以main方法运行在主线程中。在此之前的所有线程都是单线程的。

 

二、多线程的实现

    java提供了一个类描述线程   Thread,一个JVM中可以创建多个线程同时执行,每个线程都有优先权。

    java中创建线程对象的几种方式

      1.自己写一个A类继承Thread类,重写run方法,这个A类就叫线程类

      2.自己写一个A类实现Runnable接口,实现run方法,这个A类就叫线程类

      3.自己写一个A类实现Callable接口,实现call方法,这个A类就叫线程类,需要结合线程池才能使用  

    

    线程实现方式1:自己写一个A类继承Thread类,重写run方法,这个A类就叫线程类

    注意

      1.run方法就是一个线程对象要做的事情

      2.线程对象不能靠调用run方法来启动线程,只能算普通的对象调用了一个普通的方法,和线程没关系了

      3.要想启动一个线程,应该调用start()来进行启动,JVM进程会为这个线程开辟一个对象,这个线程内部调用对应的run方法

      4.调用完start()方法之后,线程只是具备了执行的资格,但是还没有真正的执行,只有抢到了CPU执行权的时候,才会执行

        (执行run方法里的逻辑,run方法执行完了,线程也就结束了)

Thread构造方法

  Thread()分配一个新的Thread对象

  Thread(String name)分配一个新的Thread对象

Thread类中的成员方法 

  public final String getName()   获取当前线程的名字
  public final void setName(String name)   将此线程的名称更改为等于参数name 。
  public static Thread currentThread()   获取当前方法所在的线程 主线程的名字叫做main

 

MyThread1 t1 = new MyThread1();   // 在使用无参构造方法的时候,给创建的对象起了名字

MyThread1 t1 = new MyThread1("xxx");  //使用带参数的构造方法,在创建线程对象的时候给线程起名字

三、线程调度

  每个线程都存在优先级

  线程有两种调度模型

    分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用的CPU的时间片

    抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一点  

  Java使用的是抢占式调度模型  

    public final int getPriority()   获取线程的优先级

    默认情况下,线程的优先级都是5    

    public final void setPriority(int newPriority)   修改线程的优先级 范围是[1,10]

    优先级高的线程只是说先执行的概率会大一些,并不是一定会执行

四、线程控制

  public static void sleep(long millis)  休眠线程

  

  public final void join()   其他线程会等待该线程执行结束   加入线程

  

  礼让线程:public static void yield() 为了让线程之间更加和谐

  

 

  后台线程(守护线程):

      线程分为两种:

          用户线程:是没有设置Daemon的线程

          守护线程:当没有用户线程的时候守护线程自动死亡

  

  中断线程:

      public final void stop() 直接将线程停止,已被弃用

      public void  interrupt()
  

四、生命周期

  

 

五、Runnable接口(多线程的实现2)   

    自己写一个A类实现Runnable接口,实现run方法,这个A类就叫线程类,需要借助Thread类来创建线程对象

    因为是实现了Runnable接口,接口中只有一个抽象run方法,找不到getName()方法,所以需要间接的获取当前线程对象,然后再获取线程名字   

    System.out.println(Thread.currentThread().getName()+" -- "+i);

实现接口的好处

    可以避免由于java单继承带来的局限性

    适合多个相同程序的代码去处理同一个资源的情况,把线程同程序的代码,数据有效分离

如何创建线程对象并启动呢?

  MyRunnable myRunnable = new MyRunnable();

  需要借助Thread类来创建线程对象 public Thread(Runnable target)

  Thread t1 = new Thread(myRunnable);

  t1.setName("大哥");

 

  public Thread(Runnable target, String name)   创建线程对象的同时给线程起名字

  Thread t1 = new Thread(myRunnable, "大哥");

六、多线程程序练习、

  

为了模拟真实的收票场景,可以在售票过程中加入延迟

  加入延迟之后出现的问题:

    1、出现了售卖重复的票的现象。原因:CPU的一次操作必须是原子性的,CPU小小的时间片可以执行很多次

    2、出现了售卖第0张票和第-1张票的现象。原因:线程的执行具有随机性

  其实出现的现象就是线程不安全的现象

  如何判断是否存在线程安全的问题?三个缺一不可

    1.是否存在多线程环境?符合

    2.是否存在共享变量(共享数据)?符合

    3.是否存在多条语句操作着共享变量(共享数据)?符合

  如何解决线程安全的问题?

    1.synchronized 同步代码块

    2.加锁 

同步代码块

  java给我们提供了一个关键字给我们使用:synchronized

  定义格式:

    synchronized(对象){

      操作共享数据的代码

    }

  对象有个要求:必须是多个线程共享对象且唯一的

 

同步方法的锁对象是谁

  是当前整在执行的锁对象自身 this

同步的特点:

  前提

    多个线程

    多个线程使用的是同一个锁对象

  好处

    解决了多线程的安全问题

  弊端

    当线程较多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

同步静态方法的锁对象是谁?

  是当前类的class文件对象。 

七、Lock锁

  解决多线程安全问题的方案之一:使用Lock锁来实现

  Lock本身是一个接口,所以找一个实现类:ReentrantLock

    void lock()加锁

    void unlock()释放锁

死锁问题

  同步弊端  

    效率低

    如果出现了同步嵌套,就容易产生死锁问题

  死锁问题

    是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象

 

八、等待唤醒机制

  共享数据:学生对象
  生产者(线程):给学生对象成员变量赋值
  消费者(线程):取出学生对象的成员变量值并打印
  测试类:StudentDemo

  问题1:我们正常编写代码后发现,运行的结果是null---0 这是必然的结果
    因为消费者中拥有一个只属于自己的学生对象,而不是与生产者共享的
    解决方案:只需要在外面new出来,以参数的形式传入到生产者和消费者中

  问题2:我们将问题1进行了修改,将学生对象放在了外部创建,但是运行的时候发现,还是会有null---0的结果出现,这是偶然
    这是因为线程之间执行具有随机性导致的,解决方案:为了看的更加明显,我加入循环

  问题3:我们在问题2的基础之上加入了循环,让它赋不同的值,让结果看的更加丰富,于是我们又发现出现了学生的姓名和年龄对应不上的现象。
    这其实是线程安全的问题。

  判断是否存在线程安全的问题三要素:
    1、是否存在多线程环境?是
    2、是否存在共享数据?存在
    3、是否存在多条语句操作着共享数据?存在
    解决方案:同步代码块 到这里只是解决了姓名和年龄不匹配的问题,依旧没有解决问题2的null--0问题

  问题4:如何解决依旧存在null---0的问题
    这是一个等待唤醒机制的问题(建立线程安全的基础之上)

生产者

  作为生产者,应该先 看一看数据有没有被消费,如果被消费才会生产

  如果上一次的数据没有被消费,应该等待消费者消费,等待的同时要通知消费者将上一次生产的数据进行消费

消费者

  作为消费者,在消费数据之前应该先看一看有没有数据,如果有数据才消费

  如果没有数据,等待生产者生产数据,同时也要通知生产者生产数据

也要创建一个共享数据类:学生对象

还有测试类要创建生产者线程对象和消费者线程对象

SetStudentThread setStudentThread = new SetStudentThread(student);
GetStudentThread getStudentThread = new GetStudentThread(student);

九、线程组

  java中使用ThreadGroup来表示线程组,它可以对一批线程组进行分类管理,java允许程序直接对线程组进行控制

  默认情况下,所有的线程都属于主线程组

    public final ThreadGroup getThreadGroup()

  我们也可以给线程设置分组

    Thread(ThreadGroup group,Runnable target,String name)

构造方法

  ThreadGroup(String name)构造一个新的线程组

 

  创建一个线程组对象
  ThreadGroup tg1 = new ThreadGroup("帅哥组");
  ThreadGroup tg2 = new ThreadGroup("美女组");

 

  如何将一个线程放入到一个线程组中? 以Thread类构造方法的方式指定
  Thread(ThreadGroup group, String name)
  分配一个新的 Thread对象。
  创建一个线程对象
  MyThread t1 = new MyThread(tg1,"xxx");
  MyThread t2 = new MyThread(tg1,"xxx");

 

  获取线程组的名字
  1、先获取线程所在的线程组
  2、再获取线程组的名字
  System.out.println(t1.getThreadGroup().getName());
  System.out.println(t2.getThreadGroup().getName());

 

  tg1.setDaemon(true);   直接对组进行操作,也会影响到组中的线程,有了线程组,方便管理和分类

十、线程池

  程序启动一个新线程的成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建打来那个生存期很短的线程时,

  更应该考虑使用线程池。

  线程池例的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对像来使用。

方法

  static ExecutorService newCachedThreadPool()   创建一个根据需要创建新线程的线程池,但在可用时将重新使用以前构造的线程。

  static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)   创建一个线程池,可以调度命令在给定的延迟之后运行,或定期执行。

  static ExecutorService newSingleThreadExecutor()   创建一个使用从无界队列运行的单个工作线程的执行程序

  static ExecutorService newFixedThreadPool(int nThreads)   创建一个线程池,该线程池重用固定数量的从共享无界队列中运行的线程。

 

 

  

  创建一个固定大小的线程池
  ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2);   2指的是同一时刻最多只有两个线程执行

  线程一旦被放入了线程池中就会开始执行(相当于调用了start()方法,具备了执行资格)
  Future<?> submit(Runnable task)
  提交一个可运行的任务执行,并返回一个表示该任务的未来。
  MyRunnable myRunnable = new MyRunnable();
  Thread t1 = new Thread(myRunnable);

  线程池中的线程之间会互相抢CPU执行权
   fixedThreadPool.submit(new MyRunnable());   这里底层相当于有一个Thread类包装成线程对象了并且也开始执行
   fixedThreadPool.submit(new MyRunnable());   这里底层相当于有一个Thread类包装成线程对象了并且也开始执行
  fixedThreadPool.submit(new MyRunnable());   这里底层相当于有一个Thread类包装成线程对象了并且也开始执行

  线程池需要手动关闭,一般情况下需要不断地放入线程执行,一般不需要关。
  fixedThreadPool.shutdown();

  <T> Future<T> submit(Callable<T> task)   创建线程第三种方式
  提交值返回任务以执行,并返回代表任务待处理结果的Future。
  fixedThreadPool.submit(new MyCallable());
  fixedThreadPool.submit(new MyCallable());
  fixedThreadPool.submit(new MyCallable());

十一、Callable接口(多线程的实现3)

  好处

    可以有返回值

    可以抛出异常

  弊端

    代码比较复杂,所以一般不用

十二、匿名内部类方式使用多线程

  new Thread(){代码...}.start();

  NEW Thread(new Runnable(){代码...}.start();

  

  使用匿名内部类创建线程对象并启动

  new 类/接口/抽象类(){

   重写方法   

  }

十三、定时器

  定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。

  在java中可以通过Timer和TimerTask类来实现定时调度功能

构造方法

  Timer()  创建一个新的定时器

 

  创建一个定时器对象
    Timer timer = new Timer();

  调用方法定时,告诉定时器到了时间该干什么
  void schedule(TimerTask task, long delay) 在指定的延迟之后安排指定的任务执行。
  第一个参数指的是到时间要做的事情,第二个参数是延迟多久单位是毫秒
     timer.schedule(new MyTask(timer),5000);
    timer.cancel();

  void schedule(TimerTask task, long delay, long period)   在指定的延迟之后开始 ,重新执行固定延迟执行的指定任务。
    timer.schedule(new MyTask(),10000,2000);

 

 

posted @ 2023-11-15 22:02  在这么冷的天  阅读(468)  评论(0)    收藏  举报