Java多线程的解析

时间:2024-01-25 20:46:43

一、进程和线程

如果程序要运行,需要将程序加载到内存中,通过编译器将其编译成计算机能够理解的方式运行,如果启动一个Java程序,先要创建一个JVM进程。进程是操作系统进行资源分配的最小单位,在一个进程中可以创建多个线程,各个线程各自拥有独立的局部变量、线程堆栈和程序计数器,能够访问共享的资源。

特点:

1.进程是操作系统分配资源的最小单元,线程是CPU调度的最小单位。

2.一个进程中可以包含多个线程。

3.进程与进程之间是相对独立的,进程中的线程之间并不完全独立,可以共享进程中的堆内存、方法区内存、系统资源等。

4.进程上下文切换要比线程的上下文切换慢得多。

5.某个进程发生异常,不会对其他进程造成影响,但某个线程发生异常,可能会对此进程中的其他线程造成影响。

二、线程组和线程池

线程组可以管理多个线程,顾名思义线程组就是把功能相似的线程放到一个组里,方便管理。

package learn;

/**
 * @author qx
 * @date 2024/1/4
 * @des 线程组测试
 */
public class ThreadGroupTest {
    public static void main(String[] args) {
        // 创建线程组
        ThreadGroup threadGroup = new ThreadGroup("qx");

        Thread thread = new Thread(threadGroup, () -> {
            // 线程组名称
            String groupName = Thread.currentThread().getThreadGroup().getName();
            // 线程名称
            String threadName = Thread.currentThread().getName();
            System.out.println("groupName=" + groupName);
            System.out.println("threadName=" + threadName);
        });
        thread.start();
    }
}

程序输出:

groupName=qx
threadName=Thread-0

线程组和线程池的区别:

1.线程组中的线程可以跨线程修改数据,而线程组之间不可以跨线程修改数据。

2.线程池就是创建一定数量的线程,批量处理任务,当任务执行完毕后线程又去执行其他任务,通过重用已存在的线程,降低线程创建和销毁造成的消耗。

3.线程池可以有效的管理线程的数量,避免线程的无限制创建,线程是很耗费系统资源的,可以有效降低系统消耗。

三、用户线程和守护线程

用户线程是最常见的线程,比如我们手动new Thread().start()开启的就是一个用户线程,守护线程的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是GC(java的垃圾回收器),只要还有一个用户线程在执行,守护线程就会一直运行。只有所有用户线程都结束的时候,守护线程才会退出。

Thread thread1 = new Thread(threadGroup,()->{
            System.out.println("守护线程");
        });
        thread1.setDaemon(true);
        thread1.start();

编写代码时,也可以通过thread.setDaemon(true)指定线程为守护线程。

守护线程的注意事项:

1.thread.setDaemon(true)要放在start()之前设置,否则会抛出java.lang.IllegalThreadStateException异常,不能把正在运行的线程设置为守护线程。

2.在守护线程中产生的新线程也是守护线程。

3.读写操作或者计算逻辑不可以设置为守护线程。

四、并行和并发

并行是指多核CPU中的一个CPU执行一个线程时,其他CPU能够同时执行另一个线程,两个线程不会抢占CPU资源,可以同时运行。

并发是指一段时间内CPU处理多个线程,这些线程会抢占CPU资源,CPU资源根据事件片周期在多个线程之间来回切换,多个线程在一段时间内同时运行,而在同一时刻不是同时运行的。

Java多线程的解析_线程组

并行和并发的区别:

1.并行指多个线程在一段时间的每个时刻都同时运行,并发指多个线程在一段时间内同时运行。

2.并行的多个线程不会抢占系统资源,并发的多个线程会抢占系统资源。

3.并行是多CPU的产物,单核CPU中只有并发,没有并行。

五、悲观锁和乐观锁

1.悲观锁

悲观锁在一个线程进行加锁操作后使得该对象变为该线程独有对象,其他线程都会被悲观锁阻拦在外,无法操作。

缺点:

1.一个线程获得悲观锁后其他线程必须阻塞。

2.线程切换时要不停的释放锁和获取锁,需要很大的系统开销。

3.当一个低优先级的线程获得悲观锁后,高优先级的线程必须等待,导致线程优先级倒置,synchronized锁就是一种典型的悲观锁。

2.乐观锁

乐观锁认为对一个对象的操作不会引发冲突,所以每次操作都不进行加锁,只是在最后提交更改的时候验证是否发生冲突,如果冲突则再试一遍直到成功为止,这个尝试的过程被称为自旋。乐观锁其实并没有加锁,但乐观锁也引入了诸如ABA、自旋次数过多等问题。

乐观锁一般会采用版本号机制,先读取数据的版本号,在写数据的时候比较版本号是否一致,如果一致则更新数据,否则再次读取版本号,直到版本号一致。

六、CAS

CAS(V,A,B),内存值V,期待值A,修改值B,判断V是否等于A,等于就执行,不等于将B赋给V。

CAS带来的问题:

1.ABA问题:CAS操作的流程是先读取原值,通过原子操作比较和替换,虽然比较和替换的原子性的,但是读取原值和比较替换这两步不是原子性的,期间原值可能被其他线程修改。ABA问题的解决方法是对该变量增加一个版本号,每次修改都会更新其版本号。JUC包中提供了一个类AtomicStampedReference,这个类中维护了一个版本号,每次对值的修改都会改动版本号。

2.自旋次数过多:CAS操作在不成功时会重新读取内存值并自旋尝试,当系统的并发量非常高时即每次读取新值之后该值又被改动,导致CAS操作失败并不断的自旋重试,此时使用CAS并不能提高效率,反而会因为自旋次数过多还不如直接加锁进行操作的效率高。

3.只能保证一个变量的原子性:当对一个变量操作时,CAS可以保证原子性,但同时操作多个变量时CAS就无能为力了。可以封装成对象,再对对象进行CAS操作,或者直接加锁。

七、锁的介绍

1.公平锁和非公平锁

公平锁:按照线程在队列中的排队顺序,先到者拿到锁。

非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的。

2.独占锁和共享锁

独占锁:当多个线程在抢夺锁的过程中,无论是读操作还是写操作,只能有一个线程获取到锁,其他线程阻塞等待。

共享锁:允许多个线程同时获取共享资源,采取的是乐观锁的机制,共享锁限制写写操作,读写操作,但是不会限制读读操作。

3.可重入锁和不可重入锁

可重入锁:一个线程可以多次占用同一个锁,但是解锁时,需要执行相同次数的解锁操作。

不可重入锁:一个线程不能多次占用同一个锁。

八、死锁、活锁、饥饿

1.死锁

多个线程互相持有对方需要的资源,导致多个线程互相等待,无法继续执行后续的任务。

2.饥饿

饥饿指的是线程由于无法获取需要的资源而无法继续执行。

产生饥饿的主要原因:

  1. 高优先级的线程不断抢占资源,低优先级的线程抢不到;
  2. 某个线程一直不释放资源,导致其他线程无法获取资源;

如何避免饥饿:

  1. 使用公平锁分配资源;
  2. 为程序分配足够的系统资源;
  3. 避免持有锁的线程长时间占用锁;

3.活锁

活锁指的是多个线程同时抢占同一个资源时,都主动将资源让给其他线程使用,导致这个资源在多个线程之间来回切换,导致线程因无法获取相应资源而无法继续执行的现象。

如何避免活锁?可以让多个线程随机等待一段时间后再次抢占资源,这样会大大减少线程抢占资源的冲突次数,有效避免活锁的产生。