java进阶学习之多线程

本文深入探讨Java多线程,包括进程与线程的区别、单核CPU下的并发、多核CPU的真正并发、线程创建方式、线程生命周期、线程同步机制(对象锁、类锁)、守护线程及其应用,以及定时器的使用。通过实例解析了线程安全问题及解决方案,如`synchronized`关键字的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

多线程的概念

1. 什么是进程?什么是线程?

进程是一个应用程序(1个进程是一个软件)。
线程是一个进程中的执行场景/执行单元。
一个进程可以启动多个线程。

2. 什么是进程和线程的例子

对于java程序来说,当在DOS命令窗口中输入:
java HelloWorld 回车之后。
会先启动JVM,而JVM就是一个进程。
JVM再启动一个主线程调用main方法。
同时再启动一个垃圾回收线程负责看护,回收垃圾。
最起码,现在的java程序中至少有两个线程并发,
一个是垃圾回收线程,一个是执行main方法的主线程。

3. 对于单核的CPU来说,真的可以做到真正的多线程并发吗?

单核的CPU表示只有一个大脑:
不能够做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉。
对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于
CPU的处理速度极快,多个线程之间频繁切换执行,跟人来的感觉是:多个事情
同时在做!!!!!
线程A:播放音乐
线程B:运行魔兽游戏
线程A和线程B频繁切换执行,人类会感觉音乐一直在播放,游戏一直在运行,
给我们的感觉是同时并发的。

4. 真正的多线程是指有多个cpu,即多核

	对于多核的CPU电脑来说,真正的多线程并发是没问题的。
		4核CPU表示同一个时间点上,可以真正的有4个进程并发执行。	

创建线程的几种方式

在这里插入图片描述

第一种方式:编写一个类,直接继承java.lang.Thread,重写run方法。

package test;

public class TheadTest {

    public static void main(String[] args) {
        StartThread startThread = new StartThread();

        startThread.start();
        for (int i = 0; i < 20; i++) {
            System.out.println("我正在摸鱼!");
        }
    }
}
package test;

public class StartThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("我正在听课!");
        }
    }
}

常用的几种方法说明
start方法的作用:启动一个分支线程,在jvm中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。
这段代码的任务是为了开启一个新的栈空间.只要新的栈空间开出来,start()。方法就结束。线程就启动成功了。
启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。
run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的
注意

  1. 方法体是自上而下运行的
  2. 直接调用run()是没有开启多线程还是main主线程、要开多线程必须调用start()方法,start()方法当开辟栈空间后就结束,线程会自动调用run方法

直接调用run和start方法的区别
start方法内存使用情况
在这里插入图片描述
直接调用run方法内存使用情况在这里插入图片描述
第二种方式:编写一个类,实现java.lang.Runnable接口,实现run方法。
一般使用匿名内部类写线程

public class RunnableTest {
    public static void main(String[] args) {
       /*MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("主线程:我在摸鱼!");
        }*/

       Thread thread = new Thread(new Runnable() {
           @Override
           public void run() {
               for (int i = 0; i < 1000; i++) {
                   System.out.println("主线程:我在摸鱼!");
               }
           }
       });
        thread.start();
    }
}
class MyRunnable implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("分支线程:我正在听课!");
        }
    }
}

线程的生命周期

在这里插入图片描述

线程的常用方法

静态方法:

获取当前线程

  • public static Thread currentThread()
    作用:返回对当前正在执行的线程对象的引用。
    使用:将他放在某个方法就返回对当前正在执行的线程对象的引用,所以一般放在run方法内。
  • 当前对象线程对象,当前对象是谁?
    • 当t1线程对象,调用了run方法,则当前线程是t1的
    • 当t2线程对象,调用了run方法,则当前线程是t2的

让当前线程睡眠(停止x秒)

  • static void sleep(long millis, int nanos)
  • static void sleep(long millis)
  • 方法参数
    • millis毫秒,睡眠几毫秒
      作用
      在指定的毫秒数内让当前正在执行的线程休眠(暂停执行、进入阻塞状态),此操作受到系统计时器和调度程序精度和准确性的影响。
      问题:以下代码在ThreadTest类中使用StartThread类型的对象t,调用sleep方法,是让t进入睡眠嘛?
      使用场景 sleep可以模拟网络延时,倒计时等。
package test;

public class ThreadTest {

    public static void main(String[] args) {
        StartThread t = new StartThread();
        t.setName("t111");
        t.start();
        try {
            t.sleep(1000*5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("摸鱼!");
    }
}

package test;

public class StartThread extends Thread {
    @Override
    public void run() {

        System.out.println(Thread.currentThread().getName()+":正在摸鱼!");

    }
}

结果:进入睡眠的是main,不是t的线程,因为t.sleep(1000*5)是静态方法,与对象无关,t.sleep(1000*5)等价于Thread.sleep(1000*5)

与线程调度有关系的方法:

让位方法
static void yield()
作用:让当前线程让位,让给其它线程使用。
注意:

  • yield()方法不是阻塞方法。
  • yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。
  • void setPriority(int newPriority)设置线程的优先级
  • int getPriority() 获取线程优先级
    最低优先级1
    默认优先级是5
    最高优先级10(优先级比较高的获取CPU时间片可能会多一些。(但也不完全是,大概率是多的。))
    实例方法:
    等待当前线程终止
    void join()
    **作用:**如果一个main线程执行了 threadA.join() 语句,其含义是:当前main线程等待 threadA 线程终止之后才从 threadA .join() 返回继续往下执行自己的代码。

关于多线程并发环境下,数据的安全问题。(重点 )

  1. 为什么这个是重点?
    以后在开发中,我们的项目都是运行在服务器当中,
    而服务器已经将线程的定义,线程对象的创建,线程
    的启动等,都已经实现完了。这些代码我们都不需要
    编写。(其实也会自己用到线程池,这段话可以听个大概)
    最重要的是:你编写的程序需要放到一个多线程的环境下运行,你更需要关注的是这些数据在多线程并发的环境下是否是安全的。

  2. 什么时候数据在多线程并发的环境下会存在安全问题呢?
    三个条件:
    条件1:多线程并发。
    条件2:有共享数据。
    条件3:共享数据有修改的行为。
    满足以上3个条件之后,就会存在线程安全问题。

  3. 怎么解决线程安全问题呢?
    线程同步机制:线程排队执行。(不能并发),用排队执行解决线程安全问题。

  4. 线程同步机制的语法

synchronized(共享对象){
//线程同步代码块
}
  • 共享对象是什么意思
    共享对象是线程共同操作(作用)的对象,叫共享对象,这里设置的共享对象就像锁,t1,t2,t3共同操作作用同一一个account对象,使用synchronized(this)设置的共享对象就是只有线程t1、t2要去拿对象锁,t1如果这个对象锁没被使用,直接就能进入同步代码块,当执行完毕后释放对象锁,t2拿到对象锁才能执行同步代码块,以此类推,同不同步看共享对象。

以下代码的执行原理?

  1. 假如t1、t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
  2. 假设t1先执行了,遇到了synchronized ,这个时候自动找“后面共享对象”的对象锁,找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是占有这把锁的。直到同步代码块代码结束,这把领才会释放。
  3. 假设t1已经占有这把锁,此时t2也遇到synchronised关键字,也会去占有后面共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,
    直到tT把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁.然后t2占有这把领之后,进入同步代码块执行程序,这样就达到了线程排队执行。
    这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队,执行的这些线程对象所共享的。对象锁都在锁池

使用synchronized几种方式

  1. 实例方法上可以使用synchronized
    synchronized出现在实例方法上,一定锁的是this。没得挑。只能是this。不能是其他的对象了。所以这种方式不灵活。

    • 缺点
      表示整个方法体都需要同步,可能会无故扩大同步的范圉,导致程序的执行效率降低。所以这种方式不常用
    • 优点
      使用简洁
  2. 在静态方法上使用synchronized
    表示找类锁.
    类锁永远只有1把.
    就算创建了 100个对象,那类锁也只有一把

  3. 类锁与对象锁的区别

    对象锁:1个对象1把锁,100个对象100把锁。
    类锁:100个对象,也可能只是1把类锁。

对象锁(synchronized method{})和类锁(static sychronized method{})的区别。摘自:Kafka不卡

对象锁也叫实例锁,对应synchronized关键字,当多个线程访问多个实例时,它们互不干扰,每个对象都拥有自己的锁,如果是单例模式下,那么就是变成和类锁一样的功能。

对象锁防止在同一个时刻多个线程访问同一个对象的synchronized块。如果不是同一个对象就没有这样子的限制。

类锁对应的关键字是static sychronized,是一个全局锁,无论多少个对象是否共享同一个锁(也可以锁定在该类的class上或者是classloader对象上),同样是保障同一个时刻多个线程同时访问同一个synchronized块,当一个线程在访问时,其他的线程等待。

什么是守护线程?

java语言中线程分为两大类

一类是:用户线程
一类是:守护线程(后台线程)
其中具有代表性的就是:垃圾回收线程(守护线程)。

守护线程的特点

  • 一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。

守护线程的作用

每天00:00的时候系统数据自动备份。这个需要使用到定时器,并且我们可以将定时器设置为守护线程。一直在那里看着,没到00:00的时候就备份一次。所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份了。
注意:主线程main方法是一个用户线程。

如何使用守护线程呢

  • 在使用start()方法之前,使用线程对象.setDaemon(true),设置为守护线程,不管守护线程的run()的方法体是死循环都能停下来的。

定时器(重要)

定时器的作用

  • 间隔特定的时间,执行特定的程序。
  • 例子:每周要进行银行账户的总账操作。
    每天要进行数据的备份操作。

实际的开发中,每隔多久执行一段特定的程序有多种方式实现(知道有哪些方法就行了,了解即可)

  • 可以使用sleep方法,睡眠,设置睡眠时间,没到这个时间点醒来,执行
    任务。这种方式是最原始的定时器。
  • 在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。
    不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持定时任务的。
  • 在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架,这个框架只要进行简单的配置,就可以完成定时器的任务。

定时器的使用

在这里插入图片描述

实现线程的第三种方式::实现Callable接口。(JDK8新特性。)

为什么这么多实现线程的方法还在jdk8更新以下实现线程的方式呢?

  • 因为这种方式实现的线程可以获取线程的返回值,之前那两种方式是无法获取线程返回值的,因为run方法返回void。此实现方式是为了根据需求需要线程返回某返回值。

Callable接口实现线程的使用

在这里插入图片描述

在这里插入图片描述
注意:在使用FutureTask对象名.get()获取返回值对象时,会堵塞当前线程(main线程),等待返回对象才结束堵塞。

关于Object类中的wait和notify方法。(生产者和消费者模式!)

注意::wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方式是Object类中自带的。wait方法和notify方法不是通过线程对象调用,不是这样的:t.wait(),也不是这样的:t.notify()…不对。
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值