你好offer之--并发

并发

JVM和本地的操作系统的线程有着一种对应的关系

并发编程三要素(线程的安全性问题体现在)

线程切换–原子性:一个或多个操作要么全部执行成功要么全部执行失败。–Atomic开头的原子类、synchronized、lock,可以解决原子性问题
缓存–可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。–volatile、synchronized、lock
编译优化–有序性:程序执行的顺序按照代码的先后顺序执行,避免指令重排。–volatile、Happens-Before

创建线程的四种方法

  • Thread类(最终也是实现Runnable接口)
  • Runnbable接口 (通过run方法来开启它的生命周期。run方法必须是公有的不带任何参数,没有返回值,且不抛出异常)
  • Callable接口(有返回值,和Future混合使用,callable用来产生数据,future用来汇总数据)
  • 线程池

start() 方法用于启动线程,run() 方法用于执行线程任务

前两个是在run方法实现逻辑
callable是在call方法实现逻辑

什么是 Future 和 FutureTask?

Future 接口表示异步计算的任务,他提供了判断任务是否完成,中断任务,并可以通过get方法获取任务执行结果,该方法会阻塞直到任务返回结果。
因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask。

FutureTask 类间接实现了 Future 接口,可以看出RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值

线程的基本操作:

interrupted(中断)其他线程可以调用该线程的interrupt()方法对其进行中断操作
join 让一个线程去等待另一个线程,直到另一个线程全部执行完成后,才可以执行该线程
sleep让某个线程让出CPU一会
sleep方法没有释放锁Thread,而wait方法释放了锁Object
wait() 方法通常被用于线程间交互/通信,sleep() 通常被用于暂停线程执行。
wait得使用notify才可以被唤醒
yield(避让)只避让优先级比他高的线程
notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
notifyAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态

中断线程

interrupt()是给线程设置中断标志;
interrupted()是检测中断并清除中断状态;
isInterrupted()只检测中断。

还有重要的一点就是interrupted()作用于当前线程,interrupt()和isInterrupted()作用于此线程

线程池

在主线程中开启多个线程并发执行一个任务,然后收集各个线程执行返回的结果并最终汇总起来。

线程池的优点

  • 降低资源消耗
  • 提高响应速度
  • 提高线程的可管理性,进行统一分配、调优和监控

线程池的使用

  • Runnable方式
    1. 创建服务进行实现线程池
    2. 执行线程
    3. 关闭连接
public class C {
    public static void main(String[] args) {
        //创建服务进行实现线程池
        ExecutorService service = Executors.newFixedThreadPool(2);

        //进行执行线程
        service.execute(new MyThred());
        service.execute(new MyThred());
        service.execute(new MyThred());
        service.execute(new MyThred());

        //关闭连接
        service.shutdown();

    }

}
class MyThred implements Runnable{

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

  • Callable方式
    1. 创建服务进行实现线程池
    2. 执行线程
    3. 获取结果
    4. 关闭连接
public class C  implements Callable{
    @Override
    public String call() {
        // run方法线程体
        return  Thread.currentThread().getName();
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        C t1 = new C();
        C t2 = new C();
        C t3 = new C();
        
        //(1)创建执行服务:
        ExecutorService ser = Executors.newFixedThreadPool(3);
        
        //(2)提交执行
        Future<String> r1 = ser.submit(t1);
        Future<String> r2 = ser.submit(t2);
        Future<String> r3 = ser.submit(t3);
        
        //(3)获取结果
        System.out.println(r1.get());
        System.out.println(r2.get());
        System.out.println(r3.get());

        //(4)关闭服务
        ser.shutdown();

    }

}

Java线程池的的工作原理

  • JVM先根据用户参数创建一定数量的可运行的线程任务,并将其放入队列中,在线程创建后启动这些任务,如果线程数量超过了最大线程数量(用户设置的线程池大小),则超出数量的线程排队等候,在有任务执行完毕后,线程池调度器会返回现有可用的线程,进而再次从队列中取出任务并执行。
  • 线程池的主要作用是线程复用、线程资源管理、控制操作系统的最大并发数、以保证高效(通过线程资源复用实现)且安全(通过控制最大线程并发数实现)地运行。使用线程池启动线程:处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。

在这里插入图片描述

线程池的核心组件

  • 线程池管理器:用于创建并管理线程
  • 工作线程:线程池中执行具体任务的线程
  • 任务接口:用于定义工作线程的调用和执行策略,只有线程执行了该接口,线程中的任务才能被线程池调度
  • 任务队列:存储待处理的任务,新的任务会不断增加到队列中去,执行完成的任务将被从队列中移除

线程池的拒绝策略

  • 直接抛出异常,阻止线程正常运行
  • 根据FIFO将最早的线程移除
  • 丢弃当前线程

常用的线程池

  • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
  • newCachedThreadPool创建了一个可缓存的线程池。当有新的任务提交时,有空闲线程则直接处理任务,没有空闲线程则创建新的线程处理任务
  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  • newWorkStealingPool是一个具有工作窃取功能的线程

线程间的通信

等待唤醒机制:

就是在一个线程进行了规定操作后,就进入等待状态(wait), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify);

Java的锁:

锁主要用于保障并发线程情况下数据的一致性

Java中每个对象都有一把锁,存放在对象头中。锁记录了当前对象被哪个线程占用
对象头由MarkWord(hashCode、锁标志位(无锁、偏向锁、轻量级锁和重量级锁))和ClassPoint(当前对象类型所在方法区中的类型数据)组成

锁的优化:
无锁–>偏向锁–>轻量级锁–>重量级锁
无锁:CAS
偏向锁:只有一个线程,该线程第二次访问就不需要在获取锁,可以直接执行。当有多个线程时,偏向锁直接升级为轻量级锁
轻量级锁:
重量级锁:需要通过monitor来进行控制

适应性自旋锁:其他线程也想获取对象时,会先自旋一定时间(上一次在同一个锁上帝自选时间和锁的状态来决定),规定时间内还没获得锁就会退出CPU。当自旋线程超过一个的时候就会升级重量级锁

AQS:

如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程并将共享资源设置为锁定状态,如果被请求的共享资源被占用则将他移入等候队列中。

AQS里面的核心方法有tryAcquire(尝试获取锁)和acquire(获取锁)愿意进入队列(FIFO)等候,直到获取

CAS:

准备要被更新的变量V,旧的预期值A,要修改的新值B
比较失败时不会被立即挂起,看持有锁的线程是否会很快释放锁

synchronized

可以用来同步线程,别编译后会生成monitorenter和monitorexit两给字节码指令来进行线程同步,一次只有一个进程进入monitor,其他线程就要等待。
一个线程进入monitor进入active状态,因为一些原因,该线程需要暂时让出执行权,此时这个线程就进入wait状态,此时另一个线程可以进行执行,执行完任务后。可以使用notify唤醒处于wait的线程

synchronized属于独占式的悲观锁,同时属于可重入锁。

synchronized作用于非静态方法–锁定的是方法的调用者
synchronized作用于静态方法–锁定的是类
synchronized作用于代码块–锁定的是传入的对象

ReentrantLock

可重入性指的是:一个线程可以不用释放锁来获取同一个锁n次,释放的时候也要相应的释放n次

  • 继承了lock接口,是可重入的互斥锁,虽然具有与synchronized相同功能,但是会比synchronized更加灵活
  • ReentrantLock通过AQS来实现锁的获取与释放

内部最核心的三个内部类:Sync、NonfailSync、FailSync

  • sync实现了AQS,内部的方法有 :非公平的尝试获取锁(非公平的尝试获取锁放在SYnc中说明:tryLuck一定是非公平的)、尝试释放锁(true代表完全释放,而不是是否被释放)
  • NonfailSync 非公平锁 。内部的方法有:尝试获取锁(一直尝试插队)和获取锁
  • FailSync 公平锁。内部的方法有:尝试获取锁(必须现在队列中排队)和获取锁

lock接口:
lock接口定义了6个方法:

  • lock():获取锁,当前锁被其他线程占用,那么它将会等待知道获取为止
  • lockInterruptibly():当前线程在等待锁的过程中被中断,将会推出等待
  • tryLock():尝试获取锁
  • tryLock(含参数):尝试在一段时间获取锁
  • unlock():释放锁
  • new Condition()准备阶段

volatile

可见性和禁止重排

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值