多线程

volatile关键字,起到了什么作用

volatile关键字来保证可见性,volatile让变量每次在使用的时候,都从主存中取。而不是从各个线程的“工作内存”。 volatile变量对于每次使用,线程都能得到当前volatile变量的最新值。但是volatile变量并不保证并发的正确性。
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。大家知道我们写的代码(尤其是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同。

Runnable与Thread,Callable(Future,关心线程最终的执行状态)

三种多线程的实现方式:

  • 继承Thread类
    继承Thread类,需要覆盖方法 run()方法,在创建Thread类的子类时需要重写 run(),加入线程所要执行的代即可。
  • 实现Runnable接口
  • 实现Callable接口
    可以返回一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
    创建callable接口的实现类,并实现call() 方法,该call() 方法将作为线程的执行体,且该call() 方法是有返回值的。
    区别
    Runnable与Callable接口的方式创建多线程的特点:
    访问当前线程,需要使用Thread.currentThread方法。
    Thread类的方式创建多线程的特点:
    访问当前线程,直接使用this即可。

什么是同步,什么是异步?

  • 同步: 就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去;
    发送一个请求,等待返回,然后再发送下一个请求,有个等待过程;同步就是一个个轮流去执行
    同步可以避免出现死锁,读脏数据的发生,一般共享某一资源的时候用,如果每个人都有修改权限,同时修改一个文件,有可能使一个人读取另一个人已经删除的内容,就会出错,同步就会按顺序来修改。
  • 异步: 指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。
    发送一个请求,不等待返回,随时可以再发送下一个请求,即不需要等待。异步就是一起同时并发
  • 区别:一个需要等待,一个不需要等待,在部分情况下,我们的项目开发中都会优先选择不需要等待的异步交互方式。
  • 多线程环境中的数据共享问题。即当多个线程需要访问同一个资源时,它们需要以某种顺序来确保该资源在某一特定时刻只能被一个线程所访问,如果使用异步,程序的运行结果将不可预料。因此,在这种情况下,就必须对数据进行同步,即限制只能有一个进程访问资源,其他线程必须等待。

串行与并行

  • 并行:线程同时被(多个)cpu执行,并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
  • 并发:多个线程被(一个)cpu 轮流切换着执行,并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。
  • “并行”概念是“并发”概念的一个子集
    串行:是指程序中的程序段必须按照先后顺序来执行,也就是只有前面的程序段执行完了,后面的程序段才能执行
    并发性(concurrency)和并行性(parallel)是两个概念,并行是指在同一时刻,有多条指令在多个处理器上同时执行;并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得宏观上具有多个进程同时执行的效果。

ThreadLocal

ThreadLocal 是线程的局部变量, 是每一个线程所单独持有的,其他线程不能对其进行访问, 通常是类中的 private static 字段,是对该字段初始值的一个拷贝,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

我们知道有时候一个对象的变量会被多个线程所访问,这时就会有线程安全问题,当然我们可以使用synchorinized 关键字来为此变量加锁,进行同步处理,从而限制只能有一个线程来使用此变量,但是加锁会大大影响程序执行效率,此外我们还可以使用ThreadLocal来解决对某一个变量的访问冲突问题。

当使用ThreadLocal维护变量的时候 为每一个使用该变量的线程提供一个独立的变量副本,即每个线程内部都会有一个该变量,这样同时多个线程访问该变量并不会彼此相互影响,因此他们使用的都是自己从内存中拷贝过来的变量的副本, 这样就不存在线程安全问题,也不会影响程序的执行性能。

但是要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。

Atomic相关数据结构的作用

java.util.concurrent.atomic
在Atomic包里一共有12个类,四种原子更新方式,分别是原子更新基本类型,原子更新数组,原子更新引用和原子更新字段.
这些类可以保证多线程环境下,当某个线程在执行atomic的方法时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个线程执行。
原子性:同一时间,一个被lock的区域只能被一个线程访问操作
原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何线程打断。

Lock:ReentrantLock,ReentrantReadWriteLock

java.util.concurrent.locks
CAS:Compare and Swap, 翻译成比较并交换。
非公平锁的加锁过程:大家同时获取锁,谁先拿到锁,谁先执行
公平锁的加锁过程::按照获取锁的顺序执行,谁先获取锁谁下执行

ReentrantLock:
ReentrantLock类中有三个内部类,Sync是另外两个类的父类,ReentrantLock的公平锁和非公平锁的实现就是通过Sync的两个子类NonfairSync和FairSync来完成的。
默认ReentrantLock实现的是非公平锁,非公平锁虽然失去了公平但是获得了更好地吞吐量。
加粗样式
ReentrantReadWriteLock:
ReentranrReaderWriterLock主类有三个重要的成员变量:读锁、写锁和同步器。从构造函数可以看出读写锁的同步器默认是非公平的(NonfaireSync)。
可以并发读,但写的时候不允许有任何操作
读锁的时候可以共享读,写需要等待,读锁释放才能写;
写锁不允许任何读写获取锁操作。

Executors创建线程池

Executors提供四种线程池,分别为:
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

Executors.newFixedThreadPool(3);在线程池中保持三个线程可以同时执行,但是注意,并不是说线程池中永远都是这三个线程,只是说可以同时存在的线程数,当某个线程执行结束后,会有新的线程进来。

ThreadPoolExecutor(创建自己的线程池)接收哪些参数,都有什么作用

什么时候扩容:当工作队列满的时候,需要增加线程池;

publicThreadPoolExecutor(
		int corePoolSize, //线程池中核心线程数的最大值
        int maximumPoolSize,//线程池中能拥有最多线程数
        long keepAliveTime,//表示空闲线程的存活时间。
        TimeUnit unit,//表示keepAliveTime的单位
        BlockingQueue<Runnable> workQueue,//用于缓存任务的阻塞队列
        ThreadFactory threadFactory, //
        RejectedExecutionHandler handler):线程池到达max,工作队列满的时候使用handler

handler:表示当workQueue已满,且池中的线程数达到maximumPoolSize时,线程池拒绝添加新任务时采取的策略。

RejectedExecutionHandler,如何管理线程池队列

  • AbortPolicy:

该策略是线程池的默认策略。使用该策略时,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {  
          //不做任何处理,直接抛出异常  
  throw new RejectedExecutionException("Task " + r.toString() +  
                  " rejected from " +  e.toString());  
       }  
  • DiscardPolicy:

这个策略和AbortPolicy的slient版本,如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常。

1.public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {  
2.        //就是一个空的方法  
3.     }  
  • DiscardOldestPolicy:

这个策略从字面上也很好理解,丢弃最老的。也就是说如果队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列。
因为队列是队尾进,队头出,所以队头元素是最老的,因此每次都是移除对头元素后再尝试入队。

1.public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {  
2.    if (!e.isShutdown()) {  
3.        //移除队头元素  
4.        e.getQueue().poll();  
5.        //再尝试入队  
6.        e.execute(r);  
7.    }  
8.}  
  • CallerRunsPolicy

用此策略,如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行。就像是个急脾气的人,我等不到别人来做这件事就干脆自己干。

1.public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {  
2.            if (!e.isShutdown()) {  
3.                //直接执行run方法  
4.                r.run();  
5.            }  
6.        }  
  • 自定义

如果以上策略都不符合业务场景,那么可以自己定义一个拒绝策略,只要实现RejectedExecutionHandler接口,并且实现rejectedExecution方法就可以了。具体的逻辑就在rejectedExecution方法里去定义就OK了。
例如:我定义了我的一个拒绝策略,叫做MyRejectPolicy,里面的逻辑就是打印处理被拒绝的任务内容
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值