Java 多线程底层原理与实现

一、各种锁的概念

1.乐观锁与悲观锁

乐观锁

每次读取数据的时候都认为数据没有被修改过,读取数据的时候不加锁 , 但是在更新的时候会去对比一下原来的值,看有没有被别人更改过。适用于读多写少的场景

juc中atomic 使用的就是乐观锁,即CAS

悲观锁

每次读取数据的时候都认为数据已经被修改过,读取数据的时候也会加锁。别人想要拿到数据就要等待锁。适合写操作比较多的场景

synchronized 实现也是悲观锁

2.共享锁/独占锁

独占锁

独占锁指锁一次只能被一个线程占有

ReentrantLock 就是独占锁

共享锁

锁可以被多个线程持有

ReadWriteLock 中 Read 共享, write独占

3.可重入锁

如果当前线程持有obj对象的锁,而内部代码块中还需要获取obj锁,直接放行的方式就是可重入锁。

synchronized 和 ReentrantLock都是可重入锁

实现方式为为锁对象设置一个计数器和占有他的线程,每次获取锁的时候计数器加一,释放锁的时候计数器减一,当计数器为0的时候释放锁。

4.公平锁和非公平锁

公平锁

所有尝试获取锁的线程都会加入锁的等待队列,每次唤醒队列中的第一个线程。

常见于AQS

非公平锁

抢占锁时候判断锁是否被占有,没被占有直接抢占锁,如果被占有就加入等待队列

ReentrantLock使用非公平锁

非公平锁的性能比公平锁好,因为线程有机会不阻塞直接获得锁,公平锁需要唤醒阻塞队列中的线程,所以公平锁的CPU开销会比较大。

5.无锁、偏向锁、轻量级锁、重量级锁

偏向锁

仅有一个线程在使用锁,没有竞争线程,就是偏向锁,一旦有其他线程产生竞争,锁升级为轻量级锁

轻量级锁

当前有两个线程,一个持有锁,另一个会自旋等待锁;当再有一个线程(3个以上)同时竞争锁的时候,锁升级为重量级锁

重量级锁

其他线程试图获取锁的时候都会进入阻塞队列,只有当前线程释放锁的时候才会唤醒线程。

6.自旋锁

获取不到锁就一直循环试图获取锁。

7.互斥锁和读写锁

synchronized、ReentrantLock属于互斥锁;(都是独占锁)

ReadWriteLock 属于读写锁(读为共享锁,写为独占锁)

二、线程的实现方式

Java 中线程的实现方式主要有四种,利用Spring 注解@Asyn 也可以实现,这里不做详细讨论。它们分别是:

  • 继承Thread类,重写run 方法
  • 实现Runnable 接口,重写run 方法
  • 实现Callable 接口,重写call()方法,创建FutureTask 对象,指定Callable 对象
  • 利用线程池

1. 继承Thread 类

class TestThread extends Thread {
   
    @Override
    public void run() {
   
        // do something ....
    }
}

2. 实现Runnable 接口

class TestThread implements Runnable {
   
    @Override
    public void run() {
   
        // do something ....
    }
}

3. 实现Callable 接口,配合FutureTask

// 创建Callable 对象
Callable<String> stringCallable = () -> {
   
    System.out.println("do something");
    Thread.sleep(2000);
    return "ok";
}; 
// 根据Callable 对象创建FutureTask 对象
FutureTask<String> stringFutureTask = new FutureTask<>(stringCallable);
// 创建线程并启动
new Thread(stringFutureTask).start();
// 阻塞当前线程直到FutureTask返回处理结果
String result = stringFutureTask.get();

4. 使用线程池

三、线程池详解

1. 为什么使用线程池

在开发过程中不建议使用直接使用继承Thread类或者直接实现Runnable 的方式来管理线程,当每接收一个请求创建一个线程,线程执行完毕再销毁的这种模式会引发如下事实:

  • 频繁创建线程耗费资源
  • 线程上下文切换问题
  • 可能引发资源耗尽的风险

使用线程池后,优点如下:

  • 加快响应时间
  • 增加吞吐量

但是线程池使用不当也会有一些风险,比如:

  • 死锁:线程池中的线程持有其他线程的锁
  • 资源不足:假如不断向无限线程池中添加任务就会导致资源不足。
  • 并发错误:wait 和 notify 使用不当
  • 请求过载:QPS 极高的情况下,不可能为每个请求都分配一个线程,分配可能导致请求过载。

2. 线程池核心参数与工作原理

核心参数

定义:

public ThreadPoolExecutor(int corePoolSize,  
                           int maximumPoolSize,  
                           long keepAliveTime,  
                           TimeUnit unit,  
                           BlockingQueue<Runnable> workQueue,  
                           ThreadFactory threadFactory,  
                           RejectedExecutionHandler handler);

其中各个参数的含义如下:

  • corePoolSize:核心线程数目,核心线程没有最长存活时间,及时线程终止也不会被回收。
  • maximumPoolSize:线程池内 (核心线程+非核心线程)的数量
  • keepAliveTime:非核心线程的最大存活时间
  • unit:keepAliveTime的单位
  • workQueue:等待执行的任务队列
    • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排
    • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
    • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
    • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
  • threadFactory:线程工厂类
  • handler:拒绝策略

阻塞队列

  • ArrayBlockingQueue; //基于数组的先进先出队列,此队列创建时必须指定大小;
  • LinkedBlockingQueue; //基于链表的先进先出队列,如果创建时没有指定此队列大小ÿ
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值