学习笔记-线程、多线程、线程池

目录

 一、JAVA线程

1.1 开启线程的方式

1.2 FutureTask

1.3 join()

1.4 yield

二、多线程问题

2.1 JVM在运行时候的内存分配过程

2.2 缓存不一致

2.3 并发多线程中三特性

2.4 指令重排

三、JAVA内存模型

3.1 原子性

3.2 可见性-volatile

3.3 有序性

四、volatile关键字

4.1 volatile不保证原子性

4.2 volatile原理

4.3 场景

4.5 MESI

 既然CPU有缓存一致性协议(MESI),为什么JMM还需要volatile关键字?

五、线程池

5.1 线程池的使用

5.2 JAVA线程池场景

1) BlockingQueue即阻塞队列

5.3 线程池的处理流程

5.4 ThreadPoolExecutor

5.5 Executors工厂类

两种提交任务的方法

七、高并发下库存加减问题

面试题

1、在 java 中守护线程和本地线程区别?

2、线程与进程的区别?

3、如何在Java中实现线程?如何选择?


 一、JAVA线程

线程状态:

1.1 开启线程的方式

JAVA开启线程的方式:继承Thread、实现Runnable、实现callable

注意:相比于runnablecallable可以有返回值,也可以抛出异常这点很关键.

某个页面需要三块信息,A-2秒;B-1秒;C-1秒  这里就4秒。通过callable就能2秒返回。

补充为什么run方法,一般会写while(true),这只是为了让线程一致执行,方便调试

public class ThreadTest extends Thread {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        ThreadTest test1 = new ThreadTest();
        test1.setName("test1");
        test1.start();
    }
}
public class RunnableTest implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        RunnableTest run1 = new RunnableTest();
        Thread thread1= new Thread(run1);
        thread1.setName("run1");
        thread1.start();
    }
}
public class CallableTest implements Callable {
    @Override
    public Object call() throws Exception {
        return "1234";
    }

    public static void main(String[] args) {
        FutureTask futureTask1 = new FutureTask(new CallableTest());
        Thread thread1 = new Thread(futureTask1);
        thread1.start();
        try {
            System.out.println(futureTask1.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

1.2 FutureTask

FutureTask包含了取消与启动计算的方法,查询计算是否完成以及检索计算结果的方法。只有在计算完成才能检索到结果,调用get()方法时如果任务还没有完成将会阻塞调用线程至到任务完成。一旦计算完成就不能重新开始与取消计算,但可以调用runAndReset()重置状态后再重新计算。

当FutureTask处于未启动或者已启动的状态时,调用FutureTask对象的get方法会将导致调用线程阻塞。当FutureTask处于已完成的状态时,调用FutureTask的get方法会立即放回调用结果或者抛出异常。

当FutureTask处于未启动状态时,调用FutureTask对象的cancel方法将导致线程永远不会被执行;当FutureTask处于已启动状态时,调用FutureTask对象cancel(true)方法将以中断执行此任务的线程的方式来试图停止此任务;当FutureTask处于已启动状态时,调用FutureTask对象cancel(false)方法将不会对正在进行的任务产生任何影响;当FutureTask处于已完成状态时,调用FutureTask对象cancel方法将返回false;


 

1.3 join()

其实就是挂起的用线程wait,等待被调用线程执行完

public class ThreadJoinTest extends Thread{
    @Override
    public void run() {
        System.out.println( "run start: "+LocalDateTime.now());
        try {
            Thread.sleep(5000); // 等待
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println( "run finish: "+LocalDateTime.now());
    }
    public static void main(String[] args) throws InterruptedException {
        System.out.println(Thread.currentThread().getName()+":"+LocalDateTime.now());
        ThreadJoinTest threadJoinTest = new ThreadJoinTest();
        threadJoinTest.start();
        threadJoinTest.join(); //有没有这行结果不一样
        System.out.println( "结束! "+LocalDateTime.now());

    }
}

也可以保证线程执行顺序

/**
 * @author 10450
 * @description t1 t2 t3 顺序执行
 * @date 2022/10/13 21:27
 */
public class OrderThread {
    public static void main(String[] args) {
        final Thread t3 = new Thread(){
            @Override
            public void run() {
                System.out.println(1);
            }
        };

        final Thread t2 = new Thread(){
            @Override
            public void run() {
                System.out.println(2);
                try {
                    t3.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        final Thread t1 = new Thread(){
            @Override
            public void run() {
                System.out.println(3);
                try {
                    t2.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        t3.start();
        t2.start();
        t1.start();
    }
}

1.4 yield

yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。

二、多线程问题

一个变量在内存中,被多个线程使用,这个变量叫:共享变量

2.1 JVM在运行时候的内存分配过程

线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这也是JVM为了提供性能而做的优化。

2.2 缓存不一致

每个线程有自己的内存栈,将内存中变量拷贝到自己的内存栈中。这就有可能造成缓存不一致。

从计算机层面层面上解决缓存不一致问题:

1、总线锁 LOCK  :有可能造成CPU阻塞

2、缓存一致性协议 MESI :保证缓存中使用的变量副本是一致的

 

2.3 并发多线程中三特性

原子性、可见性、有序性

原子性:要么全部成功,要么全部失败

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

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

2.4 指令重排

为了程序的运行效率,会对代码进行优化,不保证执行的顺序与代码的顺序一样,但是保证保证程序最终执行结果和代码顺序执行的结果是一致的。

注意:指令重排只能保证单线程的最终执行结果和代码顺序执行结果一直。多线程无法保证!

三、JAVA内存模型

JMM(Java Memory Model) JAVA内存模型是共享内存的并发模型,线程之间主要通过读-写共享变量(堆内存中的实例域,静态域和数组元素)来完成隐式通信。JMM 控制 Java 线程之间的通信,决定一个线程对共享变量的写入何时对另一个线程可见。

Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量(线程共享的变量)存储到内存从内存中取出变量这样底层细节。

PS:这里讲的主内存、工作内存与 Java 内存区域中的 Java 堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来,那从变量、主内存、工作内存的定义来看,主内存主要对应于 Java 堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。
 

3.1 原子性

int  i = 10;  //原子操作
int j = i ; // 3步, 读取i 赋值j j写入内存
i++;//  等同于i=i+1; 3步, 读取i 赋值i+1 i写入内存  

3.2 可见性-volatile

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

PS:通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

3.3 有序性

java内存模型中,由于JVM优化性能,会造成编译器或者处理器对指令进行重排。同样,指令重排能保证单线程执行结果与顺序执行结果一直。

  volatile保证一定的“有序性”。

PS:另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

四、volatile关键字

volatile修饰的共享变量,有两个特点:可见性和有序性。

即1、每次修改变量是,能被其他线程读取到。2、防止指令重排

参考:volatile和Cache一致性协议之MESI_jjavaboy的专栏-CSDN博客_volatile和缓存一致性协议

4.1 volatile不保证原子性

下面代码结果不一定是5000,因为i++不是原子性。

public class AutomaticTest {
    public volatile int i = 0 ;
    public void increaseI(){
        i++;
    }
    public static void main(String[] args) throws InterruptedException {
        final AutomaticTest test = new AutomaticTest();
        for (int p=0;p<5;p++) {
            new Thread() {
                public void run() {
                    for (int k = 0; k < 1000; k++) {
                        test.increaseI();
                    }
                }
            }.start();
        }
       Thread.sleep(2000);
        System.out.println(test.i);
    }
}

可以用Lock或者Synchronized进行完善。

public synchronized  void increaseI(){
        i++;
}
Lock lock = new ReentrantLock();
    
    public  void increase() {
        lock.lock();
        try {
            inc++;
        } finally{
            lock.unlock();
        }
    }

4.2 volatile原理

编译成汇编指令的时候,volatile修饰的变量前面会有Lock修饰。

lock前缀会形成“内存屏障”,防止指令重排、缓存修改后立即写入内存、当有内存写入后,其他读取该变量会失效。如下图。

4.3 场景

volatile性能比synchronized好,但是不保证原子性,所以可用于以下情况:

1、标记状态

2、double lock 

单例模式与双重检测

public class MySingleton {
    private volatile static MySingleton instance=  null;

    private MySingleton(){
    }

    public static MySingleton getInstance(){
        if(instance==null){
            synchronized (MySingleton.class) {
                if(instance==null)
                    instance = new MySingleton();
            }
        }
        return instance;
    }
}

4.5 MESI

MESI协议:缓存一致性协议。每个Cache line有4个状态

Modified 修改、exclusive 独占、share 共享、invalid 失效

 既然CPU有缓存一致性协议(MESI),为什么JMM还需要volatile关键字?

首先,volatile是java语言层面给出的保证,MSEI协议是多核cpu保证cache一致性(后面会细说这个一致性)的一种方法

五、线程池

5.1 线程池的使用

1、降低资源消耗。减少了创建和销毁线程的次数,每次工作线程都可以被重复利用,可执行多个任务。

2、提高响应速度。当任务到达时,省去了创建新线程的时间

3、提高了线程的可管理性。可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而把服务器累趴下,每个线程大约需要1MB内存,线程数量越多,消耗的内存也越大,最后可能导致死机。

5.2 JAVA线程池场景

1)任务数多但资源占用不大

2)任务数不多但资源占用大

举例:ABS项目资产导入校验,Excel导入后解析可能上万条数据。每行数据需要校验。

举例:产品系统,更新完产品数据需要统计,这个产品分类

3)极端场景情况

场景解读:如遇任务资源较大、任务数较多同时处理效率不高的场景下,首先需要考虑任务的产生发起需要限流,理论上讲为保障系统的可用性及稳定运行,任务的发起能力应当略小于任务处理能力,其次对于类似场景可以采用以时间换取空间的思想,充分利用系统计算资源,当遇到任务处理能力不足的情况下,任务发起方的作业将被阻塞,从而充分保护系统的资源开销边界,但可能会导致CPU核心态的使用率高,如

BlockingQueue queue = newSynchronousQueue<>();

ThreadPoolExecutor executor = newThreadPoolExecutor(64, 64, 0, TimeUnit.SECONDS, queue);

1) BlockingQueue即阻塞队列

什么是阻塞队列
1)支持阻塞的插入put方法:意思是当队列满时,队列会阻塞插入元素的线程,
直到队列不满。
2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队
列变为非空。

可以很好的解决生产与消费者的问题

常用阻塞队列
·ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
·LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
·PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
·DelayQueue:一个使用优先级队列实现的无界阻塞队列。
·SynchronousQueue:一个不存储元素的阻塞队列。
·LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
·LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列

最重要的参数:ReentrantLock重入锁

5.3 线程池的处理流程

第一步: 首先判断核心线程池是否已满:
  如果没有满,创建新线程并执行任务
  如果已满,进入第二步

第二步: 判断当前工作队列是否已满:
  如果没有满,放入工作队列中
  如果已满,进入第三步

第三步: 判断当前线程数是否超过最大线程数:
  如果没有超过,新建线程并执行任务
  如果超过,进入第四步

第四步:交给饱和策略来处理无法执行的任务。

PS: 线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了 4 种策略:
(1)AbortPolicy:直接抛出异常,默认策略;
(2)CallerRunsPolicy:用调用者所在的线程来执行任务;
(3)DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
(4)DiscardPolicy:直接丢弃任务;
 

注意

1.执行第一步和第三步需要获取全局锁。

2.核心池还没满的时候是还在预热过程。

3.工作队列会循环的从工作队列中获取任务来执行。

这样设计可以在执行execute()方法的时候,尽量避免获取全局锁。因为当线程池预热之后(线程数大于等于核心线程数corePoolSize),基本上所有方法都会执行步骤二,而步骤二不需要获取全局锁。

5.4 ThreadPoolExecutor

ThreadPoolExecutor的完整构造方法的签名是:ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) .

corePoolSize - 池中所保存的线程数,包括空闲线程。核心线程会一直存活,即使没有任务需要执行的空闲线程、当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭

maxiPoolSize-池中允许的最大线程数。

keepAliveTime - 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize

allowCoreThreadTimeout:允许核心线程超时

queueCapacity:任务队列容量(阻塞队列)

unit - keepAliveTime 参数的时间单位。

workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute方法提交的 Runnable任务。

threadFactory - 执行程序创建新线程时使用的工厂。

rejectedExecutionHandler- 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。

rejectedExecutionHandler:任务拒绝处理器
        * 两种情况会拒绝处理任务:
            - 当线程数已经达到maxPoolSize,且队列已满,会拒绝新任务
            - 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务
        * 线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常
        * ThreadPoolExecutor类有几个内部实现类来处理这类情况:
            - AbortPolicy 丢弃任务,抛运行时异常
            - CallerRunsPolicy 执行任务
            - DiscardPolicy 忽视,什么都不会发生
            - DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务
        * 实现RejectedExecutionHandler接口,可自定义处理器
 

ThreadPoolExecutor是Executors类的底层实现。

在JDK帮助文档中,有如此一段话:

“强烈建议程序员使用较为方便的Executors工厂方法Executors.newCachedThreadPool()(无界线程池,可以进行自动线程回收)、Executors.newFixedThreadPool(int)(固定大小线程池)Executors.newSingleThreadExecutor()(单个后台线程)

它们均为大多数使用场景预定义了设置。

5.5 Executors工厂类

四种线程池

1、newCachedThreadPool :创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,否则新建线程。(线程最大并发数不可控制)

CachedThreadPool 用于并发执行大量短期的小任务,或者是负载较轻的服务器
2、newFixedThreadPool:创建一个固定大小的线程池,可控制线程最大并发数,超出的线程会在队列中等待。
3、newScheduledThreadPool : 创建一个定时线程池,支持定时及周期性任务执行。

ScheduledThreadPoolExecutor 用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景
4、newSingleThreadExecutor :创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

两种提交任务的方法

ExecutorService 提供了两种提交任务的方法:

  • execute():提交不需要返回值的任务
  • submit():提交需要返回值的任务

七、高并发下库存加减问题

背景:互联网的秒杀场景,高并发下分布式集群服务。JVM中的Lock、Synchronized就不适合了。

思路:将多线程编程单线程。要么加分布式锁、要么用消息队列。

1、并发量不高的情况下,添加分布式锁。

2、为了提高性能,可以redis内存数据库增加性能,在通过MQ将同步到数据库中。

3、以上两种单线程无法满足并发量高的场景。并行异步减库存。

库存协调器就需要业务功能去实现。

补充:前端

面对高并发的抢购活动,前端【扩容】【静态化】【限流】

(1)扩容:,通过增加前端池的整体承载量来抗峰值。

(2)静态化:将活动页面上的所有可以静态的元素全部静态化,并尽量减少动态元素。通过CDN来抗峰值。

(3)限流:一般都会采用IP级别的限流,即针对某一个IP,限制单位时间内发起请求数量。或者活动入口的时候增加游戏或者问题环节进行消峰操作。

(4)有损服务:最后一招,在接近前端池承载能力的水位上限的时候,随机拒绝部分请求来保护活动整体的可用性。

面试题

1、在 java 中守护线程和本地线程区别?

2、线程与进程的区别?

一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用。而线程是在进程中执行的一个任务,线程是操作系统能够进行运算调度的最小单位

3、如何在Java中实现线程?如何选择?

继承Thread、实现Runnable、实现callable

JAVA是继承只能单继承场景,但是可以有实现多个接口

而 Runnable和Callable区别在在于Callable call() 方法可以返回值和抛出异常

4、

3、什么是多线程中的上下文切换?

4、死锁与活锁的区别,死锁与饥饿的区别?

5、Java 中用到的线程调度算法是什么?

6、什么是线程组,为什么在 Java 中不推荐使用?

7、为什么使用 Executor 框架?

8、在 Java 中 Executor 和 Executors 的区别?

9、什么是原子操作?在 Java Concurrency API 中有哪些原子类(atomic classes)?

10、Java Concurrency API 中的 Lock 接口(Lock interface)是什么?对比同步它有什么优势?

11、什么是 Executors 框架?

12、什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?

13、什么是 Callable 和 Future?

14、什么是 FutureTask?使用 ExecutorService 启动任务。

15、什么是并发容器的实现?

16、多线程同步和互斥有几种实现方法,都是什么?

17、什么是竞争条件?你怎样发现和解决竞争?

18、你将如何使用 thread dump?你将如何分析 Thread dump?

19、为什么我们调用 start()方法时会执行 run()方法,为什么我们不能直接调用 run()方法?

20、Java 中你怎样唤醒一个阻塞的线程?

21、什么是可重入锁(ReentrantLock)?

22、volatile 有什么用?能否用一句话说明下 volatile 的应用场景?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值