多线程知识点总结

345 阅读14分钟

进程与线程

进程:将程序读取到内存中,是操作系统进行资源分配的基本单位。

线程:是调度执行的基本单位(动态概念),多个线程共享同一个进程中所有的资源。

通俗的来说就是一个程序里的不同执行路径就叫做线程,如果程序里没有多条运行的路径,那就是单线程(主线程 main);当你启动一个程序,他会产生不同的分支,有多个分支在同时运行。

多线程的创建方式

  1. 继承Thread类,重写run方法
  2. 实现Runable接口,实现run方法
  3. 实现Callable接口,实现call方法
  4. 通过线程池来创建

线程池 7大参数

  1. 核心线程数 corePoolSize
  2. 最大线程数 maximumPoolSize 等于核心线程数加临时线程数
  3. 生存时间 keepAliveTime 临时线程的参数
  4. 时间单位 unit
  5. 任务队列 workQueue
  6. 线程工厂 threadFactory 设定线程名
  7. 拒绝策略 handler 持久化,或者直接丢掉,或报警,自定义

多线程同步

synchronized:

synchronized可修饰代码块和方法。
修饰普通代码块和方法时,作用对象是调用这个代码块或方法的对象。
修饰静态代码块和方法时,作用对象是所有该类的对象。

ReentrantLock:

在构造方法中传入true实现公平锁,默认非公平锁。
lock() 去获取锁,获取不到就一直等待。
tryLock() 去获取锁,获取不到就直接返回,可以设定时间参数,在规定时间内获取不到直接返回。
lockInterruptibly() 去获取锁,获取不到就一直等待,不过这个等待可以被Interrupt打断。

CountDownLatch:

门栓锁,等到入参的值为0的时候触发,也就是说入参值不为0时,await()会一直阻塞

CountDownLatch latch = new CountDownLatch(int count);
latch.countDown(); // 计数减1
latch.await();

CyclicBarrier:

栅栏锁,每达到一次入参的数量触发一次,同理await()会一直阻塞,达到入参的数量的条件是,有parties个线程在等待。

// barrierAction为触发的事件
CyclicBarrier cyclicBarrier = new CyclicBarrier(int parties, Runnable barrierAction);
CyclicBarrier cyclicBarrier = new CyclicBarrier(int parties);
cyclicBarrier.await();

ReadAndWriteLock:

读写锁,读的时候是共享锁,写的时候是排他锁

ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock read = readWriteLock.readLock();
Lock write = readWriteLock.writeLock();

以下两种锁暂不学习

Phaser

Semaphore

synchronized和Lock区别

synchronizedLock
存在层次Java关键字一个接口
锁的获取假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待视情况而定,Lock有多种锁获取的方式,其中tryLock()方式可以让线程不用一直等待
锁的释放无论是正常执行完还是发生异常,jvm都会自动释放锁必须在finally中释放锁,不然容易造成线程死锁
锁的类型可重入、不可中断、非公平可重入、可判断、可公平、可中断
性能少量同步适用于大量同步
支持锁的场景独占锁公平锁与非公平锁

可重入锁: 可以重复获得自己已获得的锁。举个例子就是一个类K中有两个同步方法a,b,在a中有调用b的动作,那么现在某个线程调用了a方法,也就是说调用方法的类K对象(后面简述为类K对象)被锁住。当运行到a方法中调b方法的时候,因为b方法也是需要类K对象这把锁(后面用锁X表示),所以该线程去获取这把锁X,如果是不可重入的话,现在锁X是已经被占用的状态,不可获取,那么就形成了死锁。这显然不合理,所以可重入就是可以重复获得自己已获得的锁。

Condition 类和Object 类锁方法区别

  1. Condition 类的 awiat 方法和 Object 类的 wait 方法等效
  2. Condition 类的 signal 方法和 Object 类的 notify 方法等效
  3. Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效
  4. ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的

多线程的可见性

一个线程改了一个变量以后,另一个线程是否能看到最新的被修改的值。默认的情况下一个线程改了一个变量,另一个线程是看不见的,因为CPU缓存的存在。

解决方法:volatile

如何预防死锁?

首先需要将死锁发生的是个必要条件讲出来:

  1. 互斥条件,同一时间只能有一个线程获取资源。
  2. 不可剥夺条件,一个线程已经占有的资源,在释放之前不会被其它线程抢占
  3. 请求和保持条件,线程等待过程中不会释放已占有的资源
  4. 循环等待条件,多个线程互相等待对方释放资源

死锁预防,那么就是需要破坏这四个必要条件

  1. 由于资源互斥是资源使用的固有特性,无法改变,我们不讨论
  2. 破坏不可剥夺条件,一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将 被隐式的释放重新加入到系统的资源列表中,可以被其他的进程使用,而等待的进 程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行
  3. 破坏请求与保持条件,第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源,第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源
  4. 破坏循环等待条件,采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的资源。

wait和sleep的区别与联系

  1. wait是Object类中的普通方法,sleep是Thread类中的静态方法
  2. sleep等待一定的时间之后,自动醒来进入到可运行状态,wait需要被notify或notifyAll来唤醒,在此期间两者都可以被打断
  3. sleep期间不会释放锁,wait会将锁释放
  4. sleep可以在任何地方使用,wait只能在同步代码块中使用

Java线程的生命周期

新建 就绪 运行 阻塞 销毁

多线程之间是如何通信的?

  1. 通过共享变量,变量需要volatile修饰。
  2. 使用wait()和notifyAll()方法,但是由于需要使用同一把锁,所以必须通知线程释放锁,被通知线程才能获取到锁,这样导致通知不及时。还有如果通知线程先运行,被通知线程后运行,被通知线程可能永远无法被唤醒。
  3. 使用CountDownLatch、CyclicBarrier实现,通知线程到指定条件,被通知线程进行await()。
  4. 使用Condition的await()和signalAll()方法,这是ReentrantLock中的使用。

Executors提供了几种线程池(通过Executors 点操作符 以下的方法来创建)

  • newCachedThreadPool()(工作队列使用的是 SynchronousQueue) 创建一个线程池,如果线程池中的线程数量过大,它可以有效的回收多余的线程,如果线程数不足,那么它可 以创建新的线程。 不足:这种方式虽然可以根据业务场景自动的扩展线程数来处理我们的业务,但是最多需要多少个线程同时处 理却是我们无法控制的。 优点:如果当第二个任务开始,第一个任务已经执行结束,那么第二个任务会复用第一个任务创建的线程,并 不会重新创建新的线程,提高了线程的复用率。 作用:该方法返回一个可以根据实际情况调整线程池中线程的数量的线程池。即该线程池中的线程数量不确定,是根据实际情况动态调整的。
  • newFixedThreadPool()(工作队列使用的是 LinkedBlockingQueue) 这种方式可以指定线程池中的线程数。如果满了后又来了新任务,此时只能排队等待。 优点:newFixedThreadPool 的线程数是可以进行控制的,因此我们可以通过控制最大线程来使我们的服务器达到最大的使用率,同时又可以保证即使流量突然增大也不会占用服务器过多的资源。 作用:该方法返回一个固定线程数量的线程池,该线程池中的线程数量始终不变,即不会再创建新的线程,也不会销毁已经创建好的线程,自始自终都是那几个固定的线程在工作,所以该线程池可以控制线程的最大并发数。
  • newScheduledThreadPool() 该线程池支持定时,以及周期性的任务执行,我们可以延迟任务的执行时间,也可以设置一个周期性的时间让任务重复执行。该线程池中有以下两种延迟的方法。 scheduleAtFixedRate 不同的地方是任务的执行时间,如果间隔时间大于任务的执行时间,任务不受执行 时间的影响。如果间隔时间小于任务的执行时间,那么任务执行结束之后,会立马执行,至此间隔时间就会被 打乱。 scheduleWithFixedDelay 的间隔时间不会受任务执行时间长短的影响。 作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。
  • newSingleThreadExecutor() 这是一个单线程池,至始至终都由一个线程来执行。 作用:该方法返回一个只有一个线程的线程池,即每次只能执行一个线程任务,多余的任务会保存到一个任务 队列中,等待这一个线程空闲,当这个线程空闲了再按 FIFO 方式顺序执行任务队列中的任务。
  • newSingleThreadScheduledExecutor() 只有一个线程,用来调度任务在指定时间执行。 作用:该方法返回一个可以控制线程池内线程定时或周期性执行某任务的线程池。只不过和上面newScheduledThreadPool()的区别是该线程池大小为 1,而上面的可以指定线程池的大小。

SpringBoot 中使用多线程

配置一个线程池Bean

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

@Configuration
@EnableAsync
@Slf4j
public class ExecutorConfig {
    @Value("${async.executor.thread.core_pool_size}")
    private int corePoolSize;

    @Value("${async.executor.thread.max_pool_size}")
    private int maxPoolSize;

    @Value("${async.executor.thread.keep_alive_time_seconds}")
    private long keepAliveTime;

    @Bean(name = "asyncServiceExecutor")
    public Executor asyncServiceExecutor() {
        log.info("start asyncServiceExecutor");
        return new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
    }
}

编写异步的service层

import java.util.concurrent.CompletableFuture;

public interface AsyncService {
    /**
     * 异步返回 输入参数的值
     * @param num
     * @return
     */
    CompletableFuture<String> getInputNum(int num);
}
import com.example.uploadfile.service.AsyncService;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;

@Service
public class AsyncServiceImpl implements AsyncService {
    @Async("asyncExecutor")
    @Override
    public CompletableFuture<String> getInputNum(int num) {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return CompletableFuture.completedFuture(Integer.toString(num));
    }
}

编写实际的业务

import java.util.concurrent.ExecutionException;

public interface TestService {
    /**
     * 调用多个异步方法
     * @return
     * @throws ExecutionException
     * @throws InterruptedException
     */
    Boolean useAsyncMethod() throws ExecutionException, InterruptedException;
}
import com.example.uploadfile.service.AsyncService;
import com.example.uploadfile.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

@Service
public class TestServiceImpl implements TestService {
    @Autowired
    private AsyncService asyncService;

    @Override
    public Boolean useAsyncMethod() throws ExecutionException, InterruptedException {
        CompletableFuture<String> inputNum1 = asyncService.getInputNum(1);
        CompletableFuture<String> inputNum2 = asyncService.getInputNum(2);
        CompletableFuture<String> inputNum3 = asyncService.getInputNum(3);
        CompletableFuture<String> inputNum4 = asyncService.getInputNum(4);
        // 这里会等待上面4个异步方法全部执行完
        CompletableFuture.allOf(inputNum1, inputNum2, inputNum3, inputNum4).join();
        // 这个get方法也是阻塞的,会等调用它的这个线程的任务有了返回以后才会继续执行后续的代码
        System.out.println("inputNum1:" + inputNum1.get());
        System.out.println("inputNum2:" + inputNum2.get());
        System.out.println("inputNum3:" + inputNum3.get());
        System.out.println("inputNum4:" + inputNum4.get());
        return true;
    }
}

注意事项

如下方式会使@Async失效:

  • 异步方法使用static修饰
  • 异步类没有使用@Component注解(或其他注解)导致spring无法扫描到异步类
  • 异步方法不能与调用异步方法的方法在同一个类中
  • 类中需要使用@Autowired或@Resource等注解自动注入,不能自己手动new对象
  • 如果使用SpringBoot框架必须在启动类中增加@EnableAsync注解

BIO,NIO,AIO

BIO是一个请求对应一个线程,这样这个线程会有一部分时间在等待数据到达,数据不到达就一直阻塞。而NIO是一个线程对应多个请求,在这些请求中轮询,有数据就的去处理,不会一直等待某个数据的到达。这样既不阻塞还可以控制线程的数量。

b和n是读取是否卡住,多路复用器增加系统调用,把获取状态,读取 分开,获取状态可以用一个调用查多个io,读取,还得一个一个独立读取

redis多路复用篇

NIO初期(还没有select系统调用的时候)是一个线程通过read轮询多个文件描述符,找到某个文件描述符有数据了,再将数据读取到内存;但是如果文件描述符(socket)过多,会发生多次系统调用,性能太差,于是有了select系统调用,这样,调用一次select就能把已经准备好数据的文件描述符给读出来,然后再调用read去把他读到内存中,这就是多路复用的NIO;不过这还不是最优的,因为还是需要多次系统调用,所以共享空间出来了,他是用户空间和内核空间的一个共享区域,新建立的socket放在共享空间中用红黑树维护,当数据到达以后,放到一个链表中,这样用户程序直接调用链表中的数据即可。

Java NIO篇

其实相当于就是一个线程处理大量的客户端的请求,通过一个线程轮询大量的channel,每次就获取一批有事件的channel,然后对每个请求启动一个线程处理即可。

Buffer Channel Selector

错误的理解: 数据都是先读到内核区,再被读到用户空间。那么BIO他是每来一个请求,就新建一个线程,线程一旦被创建,他就需要占用cpu的时间,那么数据读到内核区的时间(也就是等数据到达网卡的时间,后面简称时间1),以及数据从内核区拷贝到用户空间的时间(后面简称时间2)里面cpu什么也没干,也就是阻塞的。所以说单独整一个线程将数据读到用户空间,等数据读到了用户空间,再创建处理业务的线程,这样CPU就不用去等时间1和时间2也就是NIO。

未解之谜: 然后我还有个一个疑问,如果读数据这里使用零拷贝,是不是说我只要写一个线程,判断哪些数据已经拷贝到用户空间,对已拷贝到用户空间的数据创建业务线程。这样也可以实现NIO。

终止线程的方式

  1. 自然结束

  2. 使用volatile配合退出标志退出线程,此方式的缺点就是无法精确控制

  3. Interrupt 方法结束线程

    具体来说,当对一个线程,调用 interrupt() 时

    • 如果线程处于被阻塞状态(例如处于sleep, wait, join等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。仅此而已。
    • 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。 interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行也就是说,一个线程如果有被中断的需求,那么就可以这样做
    • 在调用阻塞方法时正确处理InterruptedException异常。(例如,catch异常后就结束线程)。
    • 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。
  4. stop 方法终止线程(线程不安全,不推荐的)

Thread类中interrupt(),interrupted()和isInterrupted()方法详解

  1. interrupt()
    其作用是中断此线程(此线程不一定是当前线程,而是指调用该方法的Thread实例所代表的线程),但实际上只是给线程设置一个中断标志,线程仍会继续运行。
  2. interrupted()
    作用是测试当前线程是否被中断(检查中断标志),返回一个boolean并清除中断状态,第二次再调用时中断状态已经被清除,将返回一个false。
  3. isInterrupted()
    作用是只测试此线程是否被中断 ,不清除中断状态。