JAVA多线程超详细讲解,看不懂来打我

1.多线程基础

1.1 线程和进程
进程:

是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用 程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基 本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

线程:

进程内部的一个独立执行单元;一个进程可以同时并发的运行多个线程,可以理 解为一个进程便相当于一个单 CPU 操作系统,而线程便是这个系统中运行的多个任 务。

2.多线程的创建方式

第一种继承Thread类 重写run方法 (无法设置返回值)

  • 创建一个继承自java.lang.Thread类的子类,并重写run()方法来定义线程执行的任务。然后,创建子类的实例并调用start()方法启动线程。

class MyThread extends Thread {
    public void run() {
        
    }
}

MyThread thread = new MyThread();
thread.start();

第二种实现Runnable接口,重写run方法 (无法设置返回值)

  • 创建一个实现java.lang.Runnable接口的类,实现其run()方法来定义线程执行的任务。然后,创建一个Thread对象,并将Runnable对象传递给它,最后调用start()方法启动线程。

class MyRunnable implements Runnable {
    public void run() {
        
    }
}

Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();

第三种实现 implements Callable接口(可以存在线程返回值 Object)

  • 创建一个实现java.util.concurrent.Callable接口的类,实现其call()方法来定义线程执行的任务,并可以返回一个结果。使用ExecutorService来提交Callable任务并获取执行结果。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

class MyCallable implements Callable<String> {
    public String call() {
        
        return "Task completed";
    }
}

ExecutorService executor = Executors.newFixedThreadPool(2);
Callable<String> callable = new MyCallable();
Future<String> future = executor.submit(callable);

实现Runnable接口比继承Thread类所具有的优势:
  1. 适合多个相同的程序代码的线程去共享同一个资源。

  2. 可以避免java中的单继承的局限性。

  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和数 据独立。

  4. 线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread 的类

使用线程池

  • 线程池是一种更高级的多线程管理方式,它可以重复使用线程来执行多个任务。使用ExecutorService接口来创建和管理线程池,然后通过submit()方法提交任务。

  1. import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    ExecutorService executor = Executors.newFixedThreadPool(2); 
    Runnable runnable = () -> {
        
    };
    executor.submit(runnable);
    
    

这些方式都可用于创建多线程,具体的选择取决于你的需求和设计。线程池是一种高效的方式,可以减少线程创建和销毁的开销,并更好地管理线程的生命周期。同时,使用Callable接口可以获得任务的执行结果,而Runnable则用于执行无需返回结果的任务。

线程池的工作流程

线程池是一种用于管理和复用线程的机制,它可以提高多线程应用程序的性能和资源管理效率。以下是典型的线程池的工作流程:

  1. 初始化线程池

    • 创建一个线程池并初始化其参数,包括最小线程数、最大线程数、任务队列大小、线程空闲时间等。线程池的大小通常根据应用需求和系统资源来确定。

  2. 提交任务

    • 当需要执行任务时,将任务提交给线程池。任务可以是一个RunnableCallable对象,表示需要执行的工作单元。

  3. 任务队列

    • 线程池维护一个任务队列,所有提交的任务都会排队在这个队列中等待执行。如果线程池中有可用的线程,它们会从队列中取出任务并执行。如果没有可用线程,任务会等待,直到有线程可用。

  4. 线程执行任务

    • 线程池中的线程会循环地从任务队列中取出任务并执行它们。一旦任务完成,线程将返回线程池中,准备执行下一个任务。

  5. 线程复用

    • 线程池会复用线程,而不是在每个任务之后销毁线程。这减少了线程创建和销毁的开销,提高了执行效率。

  6. 线程池管理

    • 线程池负责管理线程的数量和状态。它可以根据需要动态调整线程数量,以适应不同的工作负载。例如,可以根据队列中的任务数量来增加或减少线程的数量。

  7. 任务完成

    • 当任务执行完成后,可以获取任务的执行结果(如果任务是Callable类型的)。然后可以对结果进行处理或返回给调用者。

  8. 关闭线程池

    • 当不再需要线程池时,应该显式地关闭它。关闭线程池会停止接受新任务,并等待已提交的任务执行完成。然后线程池中的线程会被终止。关闭线程池是为了释放资源并避免内存泄漏。

线程池的主要优点在于可以有效地管理和复用线程,降低了线程创建和销毁的开销,提高了应用程序的性能和响应速度。它还可以控制并发线程的数量,避免资源耗尽问题。因此,在多线程应用程序中,使用线程池通常是一种良好的实践。

import java.util.concurrent.*;

public class ThreadPoolExample {

    public static void main(String[] args) {
        
        ThreadFactory threadFactory = Executors.defaultThreadFactory();

        
        RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy();

        
        int corePoolSize = 5;
        int maxPoolSize = 10;
        long keepAliveTime = 60; 
        TimeUnit unit = TimeUnit.SECONDS; 
        int queueCapacity = 100; 

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize,
                maxPoolSize,
                keepAliveTime,
                unit,
                new LinkedBlockingQueue<>(queueCapacity),
                threadFactory,
                rejectedExecutionHandler
        );

        
        for (int i = 0; i < 10; i++) {
            final int taskId = i; 
            executor.execute(new Runnable() {
                public void run() {
                    System.out.println("Task " + taskId + " is executing by " +
                            Thread.currentThread().getName());
                    
                    
                }
            });
        }

        
        executor.shutdown();
    }
}

在这个示例中,我们首先通过Executors.defaultThreadFactory()创建了一个默认的线程工厂,用于创建线程池中的线程。

然后,我们创建了一个拒绝策略ThreadPoolExecutor.AbortPolicy(),它表示当线程池饱和时(线程池和任务队列都满了),拒绝接受新的任务并抛出RejectedExecutionException异常。

最后,我们在创建ThreadPoolExecutor时,将线程工厂和拒绝策略作为额外的参数传递进去。

通过自定义线程工厂和拒绝策略,我们可以更灵活地控制线程池中线程的创建过程和任务的拒绝处理。

3.守护线程

Java中有两种线程,一种是用户线程,另一种是守护线程。 用户线程是指用户自定义创建的线程,主线程停止,用户线程不会停止。 守护线程当进程不存在或主线程停止,守护线程也会被停止

守护线程(daemon thread)是在计算机程序中运行的一种特殊线程。它的主要特点是当所有非守护线程结束时,守护线程会自动退出,而不会等待任务的完成。

守护线程通常被用于执行一些后台任务,如垃圾回收、日志记录等。它们在程序运行过程中默默地执行任务,不会阻塞主线程或其他非守护线程的执行。

与普通线程不同,守护线程的生命周期并不影响整个程序的生命周期。当所有非守护线程结束时,守护线程会被强制退出,无论它的任务是否完成。

需要注意的是,守护线程不能用于执行一些重要的任务,因为它们可能随时被强制退出。此外,守护线程也无法捕获或处理异常。

总结来说,守护线程是一种在后台执行任务的线程,当所有非守护线程结束时会自动退出。它们通常用于执行一些不重要或周期性的任

thread1.setDaemon(true); 

4.线程安全相关问题

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静 态变量只有读操作,而无写 操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一 般都需要考虑线程同步, 否则的话就可能影响线程安全。

5.如何解决

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容 易出现线程安全问题。 要解决上述多线程并发访问一个资源的安全问题,Java中提供了同步机制 (synchronized)来解决。

一.同步代码块 (自动锁) (重量锁)

二.同步方法

三.lock锁同步 (手动锁)

ReentrantLock lock = new ReentrantLock()
 lock.lock()
 sell(name)
 lock.unlock()

面试题: JVM指令集

lock 锁和 syn 哪个锁的性能更好呢? 1.8之前lock 锁更强 1.8(包含) syn 和 lock 没啥区别

  1. 同步代码块 与 同步方法有什么区别? 锁对象不同 同步方法锁对象为this 同步代码块的锁对象为任意对象(必须保证唯一)

  2. synchronized 实现原理? monitorenter和monitorexit字节码指令

  3. lock 锁 与 synchronized 区别?

  4. lock 是乐观锁还是悲观锁? 得看实现类 ReentrantLock 悲观锁 读写锁 乐观锁

  5. ReentrantLock 是公平锁还是非公平锁 ? 无参非公平,代参公平锁

使用锁 会引起 ---- 死锁 : 线程间的互相等待。

多线程死锁:同步中嵌套同步,导致锁无法释放

如何避免 : 尽量方式锁中嵌套锁

6.线程状态

状态描述: NEW(新建) :线程刚被创建,但是并未启动。

RUNNABLE(可运行) :线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。

BLOCKED(锁阻塞) :当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。

WAITING(无限等待) :一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。

TIMED_WAITING(计时等待) :同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。

TERMINATED(被终止) :因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

wait() 让线程处于等待状态,并且释放当前锁资源 需要手动唤醒
sleep() 不会释放锁 让线程处于等待状态 自然醒来
  • 对于sleep()方法,首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。

  • sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

    wait()是把控制权交出去,然后进入等待此对象的等待锁定池处于等待状态,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

  • 在调用sleep()方法的过程中,线程不会释放对象锁。而当调用wait()方法的时候,线程会放弃对象锁。

7.线程结束:

结束线程有以下三种方法: (1)设置退出标志,使线程正常退出。 (2)使用interrupt()方法中断线程。 (3)使用stop方法强行终止线程(不推荐使用Thread.stop, 这种终止线程运行的方法已经被废弃,使用它们是极端不安全的!)

Thread.sleep(1000l);

t.interrupt();
t.stop(); 

8.线程优先级

现今操作系统基本采用分时的形式调度运行的线程,线程分配得到时间片的多少决定了线程使用处理器资源的多少,也对应了线程优先级这个概念。

在JAVA线程中,通过一个int priority来控制优先级,范围为1-10,其中10最高,默认值为5。

线程优先级 并不能觉得线程的执行顺序,只是让当前线程能够获得更多的cpu资源而已

优先级可以增加线程获取cpu资源的多少,但是不能决定线程的执行顺序

t.setPriority(1);  

join()方法 (让线程顺序执行)

join作用是让其他线程变为等待。thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

yield方法

Thread.yield()方法的作用:暂停当前正在执行的线程,并执行其他线程。(可能没有效果) yield()让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。

9.多线程并发的3个特性 (重点)

原子性 :即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要 么就都不执行

可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即 看得到修改的值 (volitale)

有序性:程序执行的顺序按照代码的先后顺序执行

解决可见性问题方案:

1.同步方式解决可见性问题

while (flag) {
            synchronized (this) {
            }
        }

线程解锁前(退出同步代码块时):必须把自己工作内存中共享变量的最新值刷新到主内存中

线程加锁时(进入同步代码块时):将清空本地内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(加锁与解锁是同一把锁)

自旋锁

所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是 否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。

适应自旋锁

即自适应自旋锁。所谓自适应就意味着自旋的 次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来 决定。

锁消除 (JDK对象Syn 优化的实现)

为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但 是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进 行锁消除。锁消除的依据是逃逸分析的数据支持 。

JVM可以明显检测到变量vector没有逃逸出方法vectorTest() 之外,所以JVM可以大胆地将vector内部的加锁操作消除。

关于 Java 逃逸分析的定义:

逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术。

锁粗话

但是如果一系列的连续加锁解锁操作, 可能会导致不必要的性能损耗,所以引入锁粗化的概念。

就是将多个连续的加锁、解锁操作连接在一起,扩展成 一个范围更大的锁。

重量锁 (SYN)

操作系统实现线程之间的切换需要从
用户态到内核态的切换,切换成本非常高。

10.Volatile介绍 (面试点)

面试问题:volatile 能够保证线程安全问题吗?为什么?

不能,volatile 只能保证可见性和顺序性,不能保证原子性。

作用:解决内存可见性的问题
public volatile boolean flag = true;

Volatile实现内存可见性的过程

线程写Volatile变量的过程:
  1. 改变线程本地内存中Volatile变量副本的值;

  2. 将改变后的副本的值从本地内存刷新到主内存

线程读Volatile变量的过程:
  1. 从主内存中读取Volatile变量的最新值到线程的本地内存中

  2. 从本地内存中读取Volatile变量的副本

Volatile实现内存可见性原理:

写操作时,通过在写操作指令后加入一条store屏障指令,让本地内存中变量的值能够刷新到主内存中

读操作时,通过在读操作前加入一条load屏障指令,及时读取到变量在主内存的值

Volatile 无法保证原子性

解决方案:

  1. 使用synchronized (不推荐)

    public synchronized void addCount() {
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    }
    
    
  2. 使用ReentrantLock(可重入锁)

    private Lock lock = new ReentrantLock();
    ​
    public void addCount() {
        for (int i = 0; i < 10000; i++) {
            lock.lock();
            count++;
            lock.unlock();
        }
    }
    
    
  3. 使用AtomicInteger(原子操作)

public static AtomicInteger count = new AtomicInteger(0);
public void addCount() {
    for (int i = 0; i < 10000; i++) {
        
        count.incrementAndGet();
    }
}

CAS介绍

什么是CAS?

CAS:Compare and Swap,即比较再交换。

jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。

CAS算法理解

对CAS的理解,CAS是一种无锁算法 (乐观锁),CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

假如说有3个线程并发的要修改一个AtomicInteger的值,他们底层的机制如下:

1.首先,每个线程都会先获取当前的值。接着走一个原子的CAS操作,原子的意思就是这个CAS操作一定是自己完整执行完的,不会被别人打断。

2.然后CAS操作里,会比较一下,现在你的值是不是刚才我获取到的那个值。如果是,说明没人改过这个值,那你给我设置成累加1之后的一个值。

3.同理,如果有人在执行CAS的时候,发现自己之前获取的值跟当前的值不一样,会导致CAS失败,失败之后,进入一个无限循环,再次获取值,接着执行CAS操作。

CAS缺陷

CAS虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方 法:循环时间太长、只能保证一个共享变量原子操作、ABA问题

存在问题:

1.可能cas 会一直失败,然后自旋

2.如果一个值原来是A,变成了B,然后又变成了A,那么在CAS检查的 时候会发现没有改变,但是实质上它已经发生了改变,这就是所谓的ABA问题。 对于ABA问题其解决方案是加上版本号,即在每个变量都加上一个版本号,每次 改变时加1,即A —> B —> A,变成1A —> 2B —> 3A。

JAVA之AQS

什么是AQS? (锁获取和锁释放)

它只是一个抽象类 ,但是JUC中的很多组件都是 基于这个抽象类,也可以说这个AQS是多数JUC组件的基础。

用于JUC包下的,核心组件 AQS(AbstractQueuedSynchronizer),即队列同步器。

JAVA之锁

ReentrantLock 可重入锁 (悲观锁)

获取锁 sync.lock();

释放锁 sync.release(1);

ReentrantLock与synchronized的区别

1.功能比synchronized 要多,拓展性更强

2.对待线程等待,唤醒操作更加详细和灵活。

3.ReentrantLock提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而synchronized则一旦进入锁请求要么成功要么阻塞,所以相比synchronized而言,ReentrantLock会不容易产生死锁些。

4.ReentrantLock支持更加灵活的同步代码块,但是使用synchronized时,只能在同一个synchronized块结构中获取和释放。

5.RentrantLock支持中断处理,且性能较synchronized会好些。

读写锁ReentrantReadWriteLock (乐观锁的实现)

读写锁维护着一对锁,一个读锁和一个写锁。通过分离读锁和写锁,使得并发性比一般的互斥锁有了较大的提升:在同一时间可以允许多个读线程同时访问,但是在写线程访问时,所有读线程和写线程都会被阻塞。

猜你喜欢

转载自blog.csdn.net/weixin_54542328/article/details/133352056