全面理解多线程、守护线程、线程安全、线程同步、互斥锁

简介: 多线程基础强化

前言

很久之前就听过互联网架构中有三高,高可用、高并发、高性能,多线程是处理高并发问题的基石,起步阶段一定要对线程有一个系统深刻的印象,为以后做准备

一、进程与线程

线程(Thread):一个程序内部的一条执行路径。

调用main方法的过程也是线程执行的一种体现。在程序中,如果同一时刻只有一条执行路径,这个程序就是一个单线程程序,同一时刻,可以执行多个线程这个程序就是一个多线程程序

进程(Process):是计算机中的程序关于某数据集合上的一次运行活动,进程由线程创建,线程的容器也是程序的实体
在这里插入图片描述

多进程是指操作系统能同时运行多个任务(程序)
多线程是指在同一程序中有多个顺序流在执行,当main线程中同时执行多个子线程就是多线程的体现

在我们平时使用IDEA的过程中,执行完一段程序,运行框最后都会弹出这样的一段话,表示这个进程结束标志着程序退出
在这里插入图片描述

二、线程的创建

在Java中通过java.lang.Thread来代表线程,按照面向对象的思想,Thread类必须给咱提供实现多线程的方式,所以对于程序员来说就派生了以下几种实现方式

1.继承Thread类

①先定义一个子类MyThread继承线程类Java.lang.Tread(也就是Thread类),并重写他的内部run()方法
②创建MyThread类对象,然后调用线程对象的start()方法启动线程

就像这样:

public static void main(String[] args) {
     MyThread m1 = new MyThread();
     m1.start();//启动线程
   }
class MyThread extends MyThread{...}

==优缺==:在Java面向对象中的继承是单继承模式,通过上述方式创建线程虽然编码简单,但是会导致无法继承其他类,不利于扩展

2.实现Runable接口

①定义一个线程任务类(MyThread)实现Runnable接口,重写run()方法
②创建任务类(MyThread)对象
③把任务对象交给Thread处理,调用线程对象的start()方法启动线程

就像这样

public static void main(String[] args) {
       MyThread m1 = new MyThread();
       new Thread(m1).start();//实现接口启动线程的方式
    }
class MyThread extends MyThread{...}

==优点==:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性更强
==缺点==:多了一层对象包装,如果线程有执行结果是不可以直接返回的

3.匿名内部类实现

本质还是实现接口 ,只不过是一种简写形式

new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("通过匿名内部类方式启动的线程");
            }
   }).start();

4.实现Callable、FutureTask接口

①得到任务对象
1.定义类实现Callable接口,重写call方法
2.用EutureTask把Callable对象封装成线程任务对象
②把线程任务对象交给Thread处理
③调用Thread的start()方法启动线程,执行任务
④线程执行完毕后、通过Eutureask的get方法去获取任务执行的结果
这个方法比较复杂繁琐,现在使用的很少,了解就行

三、线程创建的本质(🚩)

通过上述三种方式,不难发现都需要通过start()方法启动线程,然后执行重写的run()方法,大家千万不要以为实现线程效果的是run()方法啊,他就是一个普通的不能再普通的方法,我们只是根据规则重写了一下,接口的目的是用来规范,这里的重写run()方法就是一种规范性的体现,甚至你去利用ctrl+B查看它的源码结果都是本地找不到用法
在这里插入图片描述
其实,==实现线程效果的本质是 start0()方法==而不是run(),start0()方法才是本地方法,是JVM机在调用,底层是通过C++来实现的,感兴趣的可以去debug追一下源码private native void start0();

四、Thread常用API、构造器

1.API: 比较常用的有,获取线程名称——getName()、设置名称——setName()、获取当前线程对象currentThread()、启动线程——start()、终止线程——interrupt()

public class Test {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("Hello world!");
        System.out.println("当前线程名称:"+Thread.currentThread().getName());
    }
}
运行结果:
  Hello world !
  当前线程名称:Thread-0

2.线程休眠——sleep(),让线程休眠指定时间(单位是毫秒)

Thread.sleep(1000);
API 用法
getName 返回该线程的名称
setPriority 更改线程的优先级
getPriority 获取线程的优先级
interrupt 中断线程(与终止不同),没有真正的结束线程,一般用于中断正在休眠的线程
yield 线程的礼让。让出cpu,让其他的线程执行,但礼让时间不确定,所以不一定礼让成功
join 线程的插队。线程的插队一旦插入成功,则肯定先执行插入的线程所有的任务

3.构造器

构造器 说明
public Thread( string name) 可以为当前线程指定名称
public Thread(Runnable target) 把Runnable对象交给线程对象
public Thread(Runnable target ,String name ) 把Runnable对象交给线程对象,并指定线程名称

五、用户线程与守护线程

Java线程主要分为==用户线程==和==守护线程==
理解这两个概念通过顾名思义的方式再适合不过了,用户线程是一个用户,守护线程是一个守护者时时刻刻守护用户,当用户程序执行完毕守护者就默默退出
最常见的:main线程退出后,里面的子线程还在运行,当开启守护线程后,子线程会随着main线程的结束而退出
创建守护线程只需通过该对象来调用setDaemon(true)实现即可

 public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("用户线程在运行");
            }
        });
        thread.setDaemon(true);  //开启守护线程
        thread.start();
        Thread.sleep(1000);
        System.out.println("main线程结束~");
    }

如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了要守护的人,守护线程没有工作可做,也就没有继续运行程序的必要了
守护线程开启前后对比:
在这里插入图片描述
==垃圾回收机制就是典型的守护线程!==

六、线程安全

当多个线程同时操作同一个共享资源的时候可能出现业务安全问题,称为线程安全
生活中取钱就是一个经典的线程安全问题
在这里插入图片描述
懒羊羊、美羊羊同时去取沸羊羊的钱,两个线程同时进行都符合判断条件,结果银行还亏了十万,这就是线程安全引起的生产事故
线程安全问题出现的原因是因为存在多线程并发、同时访问共享资源存在修改共享资源的动作
因此我们需要使线程同步!

七、加锁实现线程同步(🚩)

同步机制: 多线程编程情况中,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性。
互斥锁: Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。 每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
关键字synchronized来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问

在这里插入图片描述

1.同步代码块

==作用==:把出现线程安全问题的核心代码给上锁
==原理==:每次只能一个线程进入,执行完毕后自动解锁,其他线程进来执行

synchronized(同步锁对象){
     操作共享资源的代码(核心代码)
}

2.同步方法

==作用==:把出现线程安全的核心方法给上锁
==原理==:每次只能一个线程进入,执行完毕后自动解锁,其他线程再来执行

修饰符 synchronized 返回值类型 方法名称(形参列表){
         操作共享资源的代码(核心代码)
}

给对象上锁之后可以实现线程同步就不会出现多个线程同时访问同一个共享资源的情况(高速路上的收费站就可以抽象成一把锁)
==注意==:同步也有相应的局限性,会导致程序的执行效率降低,因为单位时间内启动的线程变少了

八、线程死锁

==多个线程都占用了对方的锁资源==,但不肯相让,导致了线程死锁
模拟一个场景:
在这里插入图片描述
代码模拟:

class DeadLockDemo extends Thread {
  static Object o1 = new Object();//资源1
  static Object o2 = new Object();//资源2
  boolean flag;
  public DeadLockDemo(boolean flag) {
  this.flag = flag;
 }
@Override
public void run() {
if (flag) {
   synchronized (o1) { //对象互斥锁,  下面就是同步代
   System.out.println(Thread.currentThread().getName() + " 进入 1"); 
   synchronized (o2) { //  这里获得 li 对象的监视权
   System.out.println(Thread.currentThread().getName() + " 进入 2");
     }
  }
} else {
   synchronized (o2) {
   System.out.println(Thread.currentThread().getName() + " 进入 3"); 
   synchronized (o1) { //  这里获得 li 对象的监视权
   System.out.println(Thread.currentThread().getName() + " 进入 4");
         }
      }
    }
 }
  1. 如果 flag 为 T, 线程 A 就会先得到/持有 o1 对象锁, 然后尝试去获取 o2 对象锁
  2. 如果线程 A 得不到 o2 对象锁,就会 Blocked
  3. 如果 flag 为 F, 线程 B 就会先得到/持有 o2 对象锁, 然后尝试去获取 o1 对象锁
  4. 如果线程 B 得不到 o1 对象锁,就会 Blocked

九、线程面试八股文整理(🚩)

面试官:请你说一下进程和线程有什么区别
答:
1.进程是线程的执行单元,一个进程里至少会有一个线程
2.进程有独立的内存空间,同一个进程内的线程是可以共享内存的
3.线程是内存处理器的调度单元而进程不是
4.不管是线程和进程都可以并发执行
5.每个独立的进程都会有一个对应的程序入口,但一个独立的线程是不能独立运行的,必须要依赖一个对应的应用程序
相关文章
|
11天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
35 1
|
9天前
|
Java 关系型数据库 MySQL
【JavaEE“多线程进阶”】——各种“锁”大总结
乐/悲观锁,轻/重量级锁,自旋锁,挂起等待锁,普通互斥锁,读写锁,公不公平锁,可不可重入锁,synchronized加锁三阶段过程,锁消除,锁粗化
|
1月前
|
监控 Java 数据库连接
Java线程管理:守护线程与用户线程的区分与应用
在Java多线程编程中,线程可以分为守护线程(Daemon Thread)和用户线程(User Thread)。这两种线程在行为和用途上有着明显的区别,了解它们的差异对于编写高效、稳定的并发程序至关重要。
36 2
|
1月前
|
监控 Java 开发者
Java线程管理:守护线程与本地线程的深入剖析
在Java编程语言中,线程是程序执行的最小单元,它们可以并行执行以提高程序的效率和响应性。Java提供了两种特殊的线程类型:守护线程和本地线程。本文将深入探讨这两种线程的区别,并探讨它们在实际开发中的应用。
34 1
|
2月前
|
供应链 安全 NoSQL
PHP 互斥锁:如何确保代码的线程安全?
在多线程和高并发环境中,确保代码段互斥执行至关重要。本文介绍了 PHP 互斥锁库 `wise-locksmith`,它提供多种锁机制(如文件锁、分布式锁等),有效解决线程安全问题,特别适用于电商平台库存管理等场景。通过 Composer 安装后,开发者可以利用该库确保在高并发下数据的一致性和安全性。
39 6
|
3月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
41 3
|
3月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
28 2
|
3月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
45 2
|
3月前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
50 1
|
2月前
|
数据采集 Java Python
爬取小说资源的Python实践:从单线程到多线程的效率飞跃
本文介绍了一种使用Python从笔趣阁网站爬取小说内容的方法,并通过引入多线程技术大幅提高了下载效率。文章首先概述了环境准备,包括所需安装的库,然后详细描述了爬虫程序的设计与实现过程,包括发送HTTP请求、解析HTML文档、提取章节链接及多线程下载等步骤。最后,强调了性能优化的重要性,并提醒读者遵守相关法律法规。
69 0