java学习之多线程篇学习总结

目录

 

一.  什么是多线程

1. 多线程概念

2. jvm内存区域介绍

二. 多线程的实现方法

  1. 继承Thread类

  2. 实现Ranable接口

  3. Callable和Future

4. 线程池的方式创建线程

 三. 线程的生命周期

四. 控制线程的方法

五. 线程同步

1. 概念

2. 同步互斥访问(synchronized)

3.synchronized关键字

1. synchronized使用范围

2.底层原理

3. synchronized锁升级

4. synchronized与lock区别

六. 多线程使用场景


 

一.  什么是多线程

1. 多线程概念

提到多线程,首先要学习一些概念性的词汇,线程和进程,并行和并发。

进程:一个正在系统上执行的程序为一个进程,一个进程包含多个线程。

线程:线程是操作系统能够进行运算调度的最小单位,多个线程会存在共享数据的情况。

并行:并行是实现多任务处理的方式。并行处理可以提高处理速度和效率,因为多个任务可以在同一时刻同时进行处理。在计算机领域,通常是通过多核处理器、GPU等技术来实现并行处理。并行处理的经典例子是在多核处理器上同时进行多个计算任务。

并发:多个程序或任务在同一时间段内同时执行的能力。在操作系统中,并发通常指的是多个程序在同一个处理机上运行,但任一时刻只有一个程序在处理机上运行。这些程序在宏观上看起来是同时运行的,但在微观上,它们是分时交替执行的。

 

2. jvm内存区域介绍

java内存区域

jvm内存模型的详细内容,可以通过上面文章了解。

简单来说,一个线程就是一个栈,他是线程不共享的,没有线程安全问题。而jvm最大的内存区域堆就不一样了,由于他线程共享的特点,因此在处理多线程问题的时候,共享的区域在不添加任何保护手段的情况下就会出现异常情况。

 

二. 多线程的实现方法

 

  1. 继承Thread类

  • 通过创建一个继承自Thread类的子类,并重写其run()方法来实现线程的逻辑。
  • 创建该子类的实例,并调用start()方法来启动线程。
class ExtendThread extends Thread {
    private String name;
 
    public ExtendThread(String name) {
        this.name = name;
    }
 
    @Override
    public void run() {   //重写run方法
        for (int i = 0; i < 5; i++) {
            System.out.println(name + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


//实现
public class Example {
    public static void main(String[] args) {
        ExtendThread t1 = new ExtendThread("线程1");
        ExtendThread t2 = new ExtendThread("线程2");
        t1.start();
        t2.start();
    }
}

 

  2. 实现Ranable接口

  • 创建一个实现Runnable接口的类,并实现其run()方法。
  • 将该类的实例作为参数传递给Thread类的构造函数,然后调用start()方法来启动线程。
class MyRunnable implements Runnable {  
    private String name;
 
    public MyRunnable(String name) {
        this.name = name;
    }
 
    public void run() {     //重写run方法
        for (int i = 0; i < 5; i++) {
            System.out.println(name + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
 
public class Example {
    public static void main(String[] args) {
        MyRunnable run1 = new MyRunnable("线程1");
        MyRunnable run2 = new MyRunnable("线程2");
 
        Thread t1 = new Thread(run1);
        Thread t2 = new Thread(run2);
        t1.start();
        t2.start();
    }
}

 

  3. Callable和Future

  • Runnable接口类似,但是Callable接口的call()方法可以有返回值,并且可以抛出异常。
  • 创建一个实现Callable接口的类,实现其中的call()方法来定义线程执行的逻辑。
  • 使用ExecutorService类的submit()方法来提交该Callable任务,从而创建并启动线程。
public class TestRandomNum implements Callable<Integer> {
    /*
    1.实现Callable接口,可以不带泛型,如果不带泛型,那么call方式的返回值就是Object类型
    2.如果带泛型,那么call的返回值就是泛型对应的类型
    3.从call方法看到:方法有返回值,可以抛出异常
    * */
    @Override
    public Integer call() throws Exception {
        int i = new Random().nextInt(10);
        return i;
    }
}
class Test{
    //这是一个main方法:是程序的入口
    public static void main(String[] args) {
        //定义一个线程对象
        TestRandomNum trn=new TestRandomNum();
        FutureTask ft=new FutureTask(trn);
        Thread t=new Thread(ft);
        t.start();
        //获取线程得到的返回值:
        Object o = null;
        try {
            o = ft.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(o);
    }
}

 

方法一和二类似,这两个创建线程的方法无法获取到线程执行的结果,而方法三不同,它可以通过Future可以异步获取Callable执行的结果的返回值。

 

4. 线程池的方式创建线程

  线程池创建线程的方式有很多很多,在这我只列举出一个我平常开发过程中用到最多的一个。

通过ThreadPoolExecutor创建线程池是一种在Java中管理并发任务的常用方式。以下是使用ThreadPoolExecutor创建线程池的基本步骤和注意事项:

  1. 构造方法参数
    • corePoolSize:线程池中保持的最小线程数。
    • maximumPoolSize:线程池中允许的最大线程数。
    • keepAliveTime:线程空闲时的存活时间。
    • unitkeepAliveTime的时间单位。
    • workQueue:用于保存等待执行的任务的阻塞队列。
    • threadFactory:用于创建新线程的工厂。
    • handler:拒绝策略,当线程池无法接受新任务时的处理方式。
  2. 创建线程池
    javaThreadPoolExecutor executor = new ThreadPoolExecutor(
        2, // 核心线程数
        4, // 最大线程数
        3, // 空闲线程存活时间
        TimeUnit.SECONDS, // 时间单位
        new ArrayBlockingQueue<>(4), // 有界队列
        new ThreadPoolExecutor.DiscardOldestPolicy() // 拒绝策略
    );
  3. 提交任务
    使用executor.submit()executor.execute()方法提交任务给线程池执行。
  4. 关闭线程池
    通过调用executor.shutdown()方法关闭线程池,这将等待所有已提交的任务完成后再关闭。

 

 三. 线程的生命周期

1、新建状态(new):新建一个线程对象

2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行的线程池中,变得可运行,等待获取CPU的使用权

3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码

4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

 

           64ab908f925e4237bcbaec14621119a8.png

 

四. 控制线程的方法

  1. sleep()方法:
    • 使当前线程暂停执行指定的时间,并交出CPU执行权。
    • 不会释放对象锁,其他线程无法访问该线程持有的同步资源。
    • 时间结束后,线程自动恢复到可运行状态,但不一定立即执行,需要等待CPU调度。
    • 可能会抛出InterruptedException,如果线程在睡眠过程中被中断。
  2. join()方法:
    • 当前线程等待调用join()方法的线程执行完毕后再继续执行。
    • 可以指定等待时间,如果超过指定时间,当前线程将继续执行。
    • 如果不指定时间,当前线程将一直等待,直到调用join()方法的线程结束。
    • 同样可能抛出InterruptedException
  3. yield()方法:
    • 暂停当前正在执行的线程,让同等优先级或更高优先级的线程获得执行机会。
    • 不会释放对象锁,与sleep()类似,但不会阻塞线程,而是让线程重回就绪状态。
    • 使用yield()后,线程需要与其他线程重新争夺CPU资源。
  4. interrupt()方法:
    • 中断线程,可以打断处于等待状态的线程,如在wait()sleep()中的线程。
    • 如果线程在等待过程中被中断,会抛出InterruptedException
    • 对非等待状态的线程调用interrupt()不会抛异常,需要手动检测线程状态并做相应处理。
  5. synchronized关键字:
    • 用于实现线程同步,确保同一时刻只有一个线程可以访问被synchronized修饰的方法或代码块。
    • 当一个线程进入同步块或方法时,它会获得对象的锁,其他线程必须等待锁被释放才能进入。
    • 释放锁的情况包括:线程执行完同步代码块、线程在同步代码块中调用wait()方法、线程出现未处理的异常或错误。
  6. wait()notify()notifyAll()方法:
    • 这些方法属于Object类,用于线程间的通信和协作。
    • wait()使当前线程等待,直到其他线程调用该对象的notify()notifyAll()方法。
    • notify()随机唤醒一个等待该对象锁的线程,而notifyAll()唤醒所有等待的线程。
    • 这些方法必须在synchronized代码块或方法中调用,因为它们涉及到对象锁的操作。
    • 如果线程在等待过程中被中断,会抛出InterruptedException。1234567

这些方法在多线程编程中起到关键作用,合理使用它们可以有效地控制线程的执行流程,实现线程间的协作和同步。

 

五. 线程同步

1. 概念

所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,同时其他线程也不能调用这个方法。

 为什么要线程同步

在项目使用场景中,常常会出现两个或者两个以上线程同时操作数据的情况,在这种高并发场景下,共享的数据往往是不安全的。

例如:一个热点商品超卖问题,当一个商品还剩最后一个的情况下,有一个线程查询数据库发现该商品的库存为1,此时执行扣减库存操作,在查询库存和扣减库存这段时间内,任何一个线程进入查询库存操作的查询结果都是1,此时再去扣减库存就出现了超卖问题。

         

2. 同步互斥访问(synchronized)

基本上所有解决线程安全问题的方式都是采用“序列化临界资源访问”的方式,即在同一时刻只有一个线程操作临界资源,操作完了才能让其他线程进行操作,也称同步互斥访问。

在Java中一般采用synchronized和Lock来实现同步互斥访问。

  

3.synchronized关键字

在单体架构中,被synchronized修饰对象的方法或者代码块,当某个线程访问这个对synchronized方法或者代码块时,就获取到了这个对象的锁,这个时候其他对象是不能访问的,只能等待获取到锁的这个线程执行完该方法或者代码块之后,才能执行对象的方法。

1. synchronized使用范围

修饰代码块(synchronized(对象),也叫对象锁),被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;此时,同一对象的代码块竞争锁。
修饰方法(在方法前加synchronized,也叫方法锁),被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象; 锁是当前实例对象。此时,同一对象同一方法需要竞争锁,等价于在方法内部调用synchronized(this)。
修饰静态方法(在静态方法前加synchronized,也叫类锁),其作用的范围是整个静态方法,作用的对象是这个类的所有对象; 锁是当前类的Class类对象。
修饰类(synchronized(静态对象),也叫类锁),其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象。

 

2.底层原理

synchronized实现锁的基础就是Java对象头,synchronized锁会将线程ID存入mark word(对象头由标记字)。关于mark word,先简要了解一下Java对象。

在Hotspot 虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。synchronized主要是跟对象头有关系,在对象头中包含了标记字(mark word)、类指针(klass word)和 数组长度(array length)。也就是通过mark word的字节位数来表示各种锁状态。

d79232a8187543ca8d4bddb371b489d8.png

 

3. synchronized锁升级
 

● synchronized锁在线程第一次访问的时候,实际上是没有加锁的,只是在mark word中记录了线程ID,默认也就是使用偏向锁。
● 当第二个线程来争用的时候,此时第二个线程会占用cpu,循环等待锁的释放,这时候偏向锁也就升级为自旋锁。
● 当自旋10次之后,就会升级为重量级锁,重量级锁是不占用cpu,他是使用OS的。
当线程数较少、运行时间较短的时候是比较适合使用自旋锁,反之则比较适合重量级锁。

 

 

4. synchronized与lock区别

 

  1. 存在层次
    • synchronized 是 Java 语言内置的关键字,属于 JVM 层面的锁。
    • Lock 是 Java 类库提供的一个接口,属于 Java 语言层面的锁。
  2. 锁的获取与释放
    • synchronized 的获取和释放是隐式的,即在进入同步代码块或方法时自动获取锁,并在退出时自动释放锁。
    • Lock 的获取和释放需要手动调用 lock() 方法获取锁,并在使用完后手动调用 unlock() 方法释放锁。
  3. 锁的释放(死锁产生)
    • synchronized 在发生异常时候会自动释放占有的锁,因此不会出现死锁。
    • Lock 在发生异常时候,不会主动释放占有的锁,必须手动 unlock 来释放锁,可能引起死锁的发生。
  4. 锁的状态
    • synchronized 无法判断锁的状态。
    • Lock 可以判断锁的状态。
  5. 锁的类型
    • synchronized 只有一种类型的锁,即互斥锁,它是非公平锁。
    • Lock 提供了多种类型的锁,包括公平锁和非公平锁。
  6. 性能
    • synchronized 是 JVM 内置的锁,效率相对较低,因为它会涉及到用户态和内核态的切换。
    • Lock 是 Java 类库提供的锁,性能较高,因为它使用了更底层的硬件级别的实现。
  7. 支持锁的场景
    • synchronized 只支持在代码块和方法上加锁。
    • Lock 支持更灵活的加锁和释放方式,例如可以在任意位置加锁和释放锁,支持多个条件变量的使用。
  8. 可重入性
    • synchronized 是可重入锁,即同一线程可以多次获取同一把锁而不会死锁。
    • Lock 也是可重入锁,但需要注意要手动调用相同次数的 unlock() 方法才能完全释放锁。
  9. 等待通知机制
    • synchronized 使用的是 wait() 和 notify()/notifyAll() 方法实现线程之间的等待和通知机制。
    • Lock 使用的是 Condition 对象来实现类似的功能。
  10. 可见性
    • synchronized 在进入同步代码块时会自动获取锁并刷新线程的工作内存,保证了线程间的可见性。
    • Lock 需要手动使用 volatile 关键字或者显式调用 lock() 和 unlock() 方法来保证可见性。
  11. 锁的粒度
    • synchronized 是对整个对象进行加锁的,即当一个线程获得了某个对象的锁后,其他线程无法获得该对象的任何锁。
    • Lock 可以实现更细粒度的锁定,例如可以对对象的某个属性或者某一段代码块进行加锁,从而提高并发性能。

 

六. 多线程使用场景

        多线程使用场景有很多,在日常开发中发布邮件,信息统计,记录日志,需要执行耗时操作且不阻塞用户界面的各种场景中,都有多线程的影子。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值