Java并发
并发三大特性:原子性、有序性、可见性。
请简要描述线程与进程的关系,区别及优缺点? 线程是比进程更小的一个执行单元,各个线程共用进程的方法区(Hotspot元空间)和堆,而线程的程序计数器、本地方法栈和虚拟机栈是私有的。各进程是独立的,而各线程极有可能会相互影响。线程执行切换开销小,但不利于资源的管理和保护;而进程正相反。 进程只是线程的容器,执行任何工作都依赖于线程。 线程是任务调度执行的最小单元,进程是资源分配的最小单元。进程之间相互隔离。 协程是线程内部的时分复用。
程序计数器为什么是私有的? 程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。 程序计数器主要有下面两个作用:
虚拟机栈和本地方法栈为什么是私有的?
所以,为了保证线程中的局部变量不被别的线程访问到
并发&并行? 前者是通过时间片轮转,在宏观上达到同时运行多个任务的效果。 后者是严格意义地同时运行多个任务,在CPU多个核心上运行多个任务。
为什么要使用多线程呢?
当然多线程技术也存在问题,主要是安全问题:内存泄漏 死锁 线程不安全等
线程在new之后会处于new状态,调用start()后进入runnable(首先进入ready状态,随后获取CPU 空闲的时间片后进入running,实际上一个线程一次仅仅占用几十毫秒时间,随后就会进入ready等待下一时间片,所以可以不区分这两种状态)。
什么是上下文切换? 就是一个线程在主动或被动退出运行状态,比如: 通过sleep或者wait,或者在阻塞状态中没获取到锁,或者当前时间片用完, 此时这个线程会保存自己当前的运行状态,如程序计数器、虚拟机栈、本地方法栈,这就是上下文,以备线程下次运行时恢复加载。 但因为每次切换都要使用CPU和内存保存或加载上下文,导致了额外的开销,所以频繁切换会导致性能下降。
线程死锁是什么? //同步监视器,俗称:锁。任何一个对象,都可以充当锁。 多个线程以不同的顺序请求对方的资源,导致全部在阻塞状态无限期等待某个资源被释放的状态。 死锁产生的条件:
如何避免死锁?
sleep() 方法和 wait() 方法区别和共同点? 二者都可以暂停进程,但sleep没有释放锁,而wait释放了锁。wait是Object类的方法,而sleep是Thread类的方法。 wait之后,该线程必须等到其他线程进行Notify后才会恢复运行。而sleep后,线程会在等待时长后自动恢复运行。 因此wait主要用于线程之间的通信。sleep主要用于暂停进程。
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法? 因为Run方法的本质是在本线程的内部对Runnable对象调用Run方法,直接调用Run方法相当于是在主线程中去调用一个普通方法,而并没有用到多线程技术,因为子线程还根本就没有Start。 而调用Start方法,是启动一个子线程,再去调用该对象的run方法。的因此我们要在Start中去启用子线程。
synchronized关键字synchronized的作用: 保证不同线程在使用同一资源的同步性,使得它所修饰的方法或代码块在同一时间仅仅有一个线程可以执行。 synchronized使用方法:
synchronized void method(),作用于当前的实例对象,方法执行前要获得当前对象的锁
synchronized static void method(), 作用于当前类的所有对象,方法执行前要获得当前类的锁。因为静态成员是不属于对象而属于类的,因此若A线程调用该类某对象非静态sync方法,而B线程调用该类静态sync方法,是不会发生阻塞的。因为二者占用的是不同的锁。
synchronized(Obj){//业务代码},作用于指定对象Obj,代码块执行前要获取该对象的锁,也可以synchronized(类名.class)作用于当前类 底层原理: synchronized修饰代码块:在JVM层面调用了monitorenter和monitorexit,这两个命令是基于锁计数器的。 monitorenter:若当前锁计数器为0,就获取锁,然后锁计数器加1,表示当前锁对其他线程不可用。 monitorexit:若当前进程为锁的持有者,尝试释放锁后锁计数器减1,表示当前锁已被释放。 synchronized修饰方法:在JVM层面读取一个方法是否被标志为同步方法,之后去获取对象的锁或者class的锁。
单例模式? 单例模式就是一个类只用来创建一个实例,并且该实例是该类的一个成员,添加成员getinstance方法,判断若该类不存在实例,调用构造函数,并且在构造之前,并且给创建外加上一个synchronized的代码框给该类上锁。 双重校验:
synchronized 和 ReentrantLock 的区别
volatile 保证数据可见性。JVM会将经常使用的变量保存在线程私有的缓存中,在多线程情况下, 这会产生数据不一致。volatile修饰的变量允许线程直接在共享内存中读取该变量,从而避免数据不一致。也禁止了JVM的指令重排,使多线程能够正常运行。
ThreadLocalThreadLocal是一个类,作用是表达线程内部的变量,不进行同步以实现线程安全,也就是数据隔离。创建一个ThreadLocal变量,那么访问这个变量得所有线程都会在线程本地内存中拥有一个该变量的副本,并仅仅对这个副本进行操作, 从而保证了数据隔离。 原理:每个线程保存了一个特殊的ThreadLocalMap,默认为null,调用get或set后,键是ThreadLocal实例,值是通过set设置的变量值。变量的实际值是被放在当前线程的ThreadLocalMap中。 内存泄露:在使用完一个ThreadLocal之后,最好手动调用其remove方法,因为Map的key是ThreadLocal的弱引用,而value是强引用,可能会出现key被垃圾回收,而value则没有被回收,出现泄露。
创建线程的几种方式:
Callable接口:call方法相比于Runnable的run方法,可以返回值、返回异常
线程池:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。 如何创建线程池? 通过ThreadPoolExectutor的构造函数或Executors类创建。 前者更规范更安全,支持相当多的参数。
ThreadPoolExecutor本质是线程的容器,用来执行任务(如Runnable或Callable):
拒绝策略:达到最大线程数量后,丢弃抛出/不抛出异常、喜新厌旧、由提交任务的线程执行该任务。 原理:通过Runnable接口的对象创建任务,并用线程池的execute方法执行任务,若满足刚才说得条件,则创建线程并启动。
原子类:具有不可中断性质的类。不需要人为加锁也可以实现线程安全。分为四类:
AtomicInteger: 常用方法:获取值、设置值、CAS、增加值等等 原理:使用CAS(比较并赋值,当内存值与期望值相同时,再更新赋值),调用了native方法,并且使用volatile,保证直接从内存中取值。不需要synchronized也可以线程安全。减少了sync的开销
CAS: 比较并赋值,当内存值与期望值相同时,再更新赋值 是一个本地方法,原理就是比如线程1想要修改某变量的值,但是线程2抢先修改了该变量,这就导致线程1在调用cas的时候,预期值与内存值不同,也就无法修改,实现了类似于线程阻塞的效果。比sync开销更小,CAS只能对变量保证原子性,而不能如sync一样对方法。
AQS 介绍 是用来自定义构建锁和同步器的工具,比如ReentrantLock。 原理:是相当自然的,如果某线程去请求一个未被占有的资源,则将该线程设置为有效线程,该资源设置为锁定。若请求一个已经锁定的资源,那么该线程会由CLH锁队列去进行一系列如阻塞、等待、唤醒等机制去处理,类似于一个自定义版本的synchronize机制。通过CLH队列可以保证各线程排队访问某上锁资源。 CountDownLatch:计数锁,当该计数锁的计数值下降为0时,因为调用该锁await方法而处于等待中的线程就会恢复运行。
CyclicBarrier: 给一组线程使用,线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有线程同时运行。相当于一组线程在等待到达同一个同步点后再同时运行。相当于赛跑,等运动员都各就位了到达同步点了,所有运动员再同时起跑。
Semaphore:信号量
java锁类型
|
|
|
|