2.4 JAVA并发编程核心知识汇总(线程同步和线程池)
volatile变量详细可参考敖丙的深层原理: https://mp.weixin.qq.com/s/Oa3tcfAFO9IgsbE22C5TEg
-
JAVA多线程
-
多线程
-
这个技术是什么
-
进程:每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。
-
线程:线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。
-
目的:进程是所有线程的集合,每一个线程是进程中的一条执行路径。多线程的好处是可以提高程序的效率。充分利用多核CPU的计算能力:通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升。
-
多线程的运行原理:cpu在线程中做时间片的切换(多线程可以提高程序的运行效率,但不能无限制的开线程)
-
相关术语
-
并发和并行
-
并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
-
并行的关键是你有同时处理多个任务的能力。(多个队列多台咖啡机)
-
并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。
-
并发的关键是你有处理多个任务的能力,不一定要同时。(几个队列交替使用咖啡机)
-
并发和并行的区别
-
并行是俩个队列同时去使用两台咖啡机,多台机器处理一块逻辑。是真正的同时。
-
并发是两个队列交替去使用一台咖啡机,(一个CPU切换执行多线程)
-
线程安全:经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果,如不加事务的转账代码:
-
线程安全问题多是由全局变量和静态变量引起的,当多个线程对共享数据只执行读操作,不执行写操作时,一般是线程安全的;当多个线程都执行写操作时,需要考虑线程同步来解决线程安全问题。
-
线程同步:同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。如上面的代码简单加入@synchronized关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能。
-
解决线程同步的方式、
-
自动锁Synchronized(同步)
-
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
-
Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
-
锁升级:synchronized 获取锁的方式1.6之后,JVM 使用了锁升级的优化方式,就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁。
-
* 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
-
* 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
-
* 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
-
手动同步锁(Lock)使用ReentrantLock()对象跟synchronized作用一样:
-
使用方式
-
private Lock lock = new ReentrantLock();
-
ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
-
ReenreantLock类的常用方法:
-
ReentrantLock() : 创建一个ReentrantLock实例
-
获得锁的方法和区别
-
lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
-
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
-
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
-
lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
-
unlock() : 释放锁
-
底层原理
-
AQS
-
AQS中 维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。
-
这里volatile能够保证多线程下的可见性,当state=1则代表当前对象锁已经被占有,其他线程来加锁时则会失败,加锁失败的线程会被放入一个FIFO的等待队列中,比列会被UNSAFE.park()操作挂起,等待其他获取锁的线程释放锁才能够被唤醒。
-
另外state的操作都是通过CAS来保证其并发修改的安全性。
-
使用特殊域变量(volatile)实现线程同步,Volatile 关键字的作用是变量在多个线程之间可见性,和顺序性。参考:https://mp.weixin.qq.com/s/Oa3tcfAFO9IgsbE22C5TEg
-
JMM:Java内存模型,是java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别。(CacheCoherence)
-
MESI(缓存一致性协议):注意JMM的缓存一致性问题用这个解决
-
禁止指令重排序
-
内存屏障
-
happens-before
-
无法保证原子性
-
解决办法使用Volatile关键字将解决线程之间可见性, 强制线程每次读取该值的时候都去“主内存”中取值
-
volatile与synchronized区别
-
仅靠volatile不能保证线程的安全性。(原子性)
-
volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法
-
volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。
-
synchronized不仅保证可见性,而且还保证原子性,因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时,会出现阻塞
-
使用局部变量ThreadLocal,,ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,如何防止自己的变量被其它线程篡改。
-
什么是ThreadLoca
-
ThreadLocal提高一个线程的局部变量,访问某个线程拥有自己局部变量。说白了,ThreadLocal就是想在多线程环境下去保证成员变量的安全。
-
对于ThreadLocal而言,常用的方法,就是get/set/initialValue/remove方法。
-
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
-
任何线程局部变量一旦在工作完成后没有释放,java应用就存在内存泄露的风险
-
怎样解决内存泄露
-
ThreadLocal提供了这个问题的解决方案。
-
每次操作set、get、remove操作时,会相应调用 ThreadLocalMap 的三个方法,ThreadLocalMap的三个方法在每次被调用时 都会直接或间接调用一个 expungeStaleEntry() 方法,这个方法会将key为null的 Entry 删除,从而避免内存泄漏。
-
成手动调用remove的习惯,从而避免内存泄漏。
- 底层实现
-
什么是ThreadLocal
-
ThreadLocal提高一个线程的局部变量,访问某个线程拥有自己局部变量。说白了,ThreadLocal就是想在多线程环境下去保证成员变量的安全。
-
对于ThreadLocal而言,常用的方法,就是get/set/initialValue/remove方法。
-
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
-
任何线程局部变量一旦在工作完成后没有释放,java应用就存在内存泄露的风险
-
怎样解决内存泄露
-
ThreadLocal提供了这个问题的解决方案。
-
每次操作set、get、remove操作时,会相应调用 ThreadLocalMap 的三个方法,ThreadLocalMap的三个方法在每次被调用时 都会直接或间接调用一个 expungeStaleEntry() 方法,这个方法会将key为null的 Entry 删除,从而避免内存泄漏。
-
成手动调用remove的习惯,从而避免内存泄漏。
-
底层原理
-
ThreadLocal数据隔离的真相,每个线程Thread都维护了自己的threadLocals变量
-
所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量里面的
-
当前线程存放的ThreadLocalMap里(定制化Map,数组实现)
-
ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。(记得remove把值清空)
-
应用场景
-
其实我第一时间想到的就是Spring实现事务隔离级别的源码,Spring框架里面就是用的ThreadLocal来实现这种隔离,主要是在TransactionSynchronizationManager这个类里面,Spring的事务主要是ThreadLocal和AOP去做实现的
-
-
使用原子类(AtomicInteger、AtomicBoolean……)
-
使用容器类(BlockingQueue、ConcurrentHashMap)
-
synchronized 与 lock 的区别?
-
首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
-
synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
-
synchronized会自动释放锁,Lock需在finally中手工释放锁(lock.unlock()方法释放锁),否则容易造成线程死锁;
-
用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
-
synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
-
Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
-
Lock锁可以设置等待时间,到了时间自动放弃获取锁
-
临界区、互斥量、事件、信号量四种方式
-
临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。
-
互斥量:采用互斥对象机制。 只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享
-
信号量:它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目
-
事 件: 通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作
-
这个技术的优缺点
-
优点:可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
-
缺点:
-
线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
-
多线程需要协调和管理,所以需要 CPU 时间跟踪线程;
-
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。
-
这个技术的应用场景
-
迅雷种子多个下载,买票系统,应用系统,Ajax异步上传。
-
这个技术的使用方式
-
创建多线程有哪几种方式?
-
第一种继承Thread类 重写run方法、范例代码:注意 开启线程不是调用run方法,而是start方法、
-
定义一个Thread类的子类,重写run方法,将相关逻辑实现,run()方法就是线程要执行的业务逻辑方法
-
创建自定义的线程子类对象
-
调用子类实例的star()方法来启动线程
-
实现Runnable接口,重写run方法,代码范例
-
定义Runnable接口实现类MyRunnable,并重写run()方法
-
创建MyRunnable实例myRunnable,以myRunnable作为target创建Thead对象,该Thread对象才是真正的线程对象
-
调用线程对象的start()方法
-
实现 Callable 接口;
-
创建实现Callable接口的类myCallable
-
以myCallable为参数创建FutureTask对象
-
将FutureTask作为参数创建Thread对象
-
调用线程对象的start()方法
-
使用 Executors 工具类创建线程池
-
多线程使用API方法
-
start()启动线程
-
currentThread()获取当前线程对象
-
getID()获取当前线程ID Thread-编号 该编号从0开始
-
getName()获取当前线程名称
-
sleep(long mill)休眠线程
-
Stop()停止线程,(不安全,已不使用)
-
线程方法
-
OBject下的方法
-
wait():使一个线程处于等待状态,并且释放所持有的对象OBJ的lock。
-
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
-
notityAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。
-
这三个方法最终调用的都是jvm级的native方法。随着jvm运行平台的不同可能有些许差异。
-
Thread
-
join作用是让其他线程变为等待,t1.join();// 让其他线程变为等待,直到当前t1线程执行完毕,才释放。
-
Thread.yield()方法的作用:使当前线程从执行状态(运行状态)变为可执行态(就绪状态)暂停当前正在执行的线程,并执行其他线程。(可能没有效果)
-
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
-
守护线程和用户线程有什么区别呢?
-
Java中有两种线程,一种是用户线程(非守护线程,用户线程是指用户自定义创建的线程,主线程停止,用户线程不会停止)
-
另一种是守护线程(和主线程一起销毁GC回收线程)。守护线程当进程不存在或主线程停止,守护线程也会被停止。
-
使用setDaemon(true)方法设置为守护线程
-
多线程状态
-
线程从创建、运行到结束总是处于下面五个状态之一:新建状态(new Thread(r))、就绪状态(start()方法)、运行状态(真正开始执行run()方法)
-
阻塞状态 线程运行过程中,可能由于各种原因进入阻塞状态:
-
线程通过调用sleep方法进入睡眠状态;
-
线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
-
线程试图得到一个锁,而该锁正被其他线程持有;
-
线程在等待某个触发条件;
-
死亡状态,需要使用isAlive方法
-
线程run()、main()方法执行结束正常退出而自然死亡,
-
一个未捕获的异常终止了run方法而使线程猝死
-
JAVA线程调度算法
-
计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU 的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得 CPU 的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权。
-
有两种调度模型:分时调度模型和抢占式调度模型。
-
分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。
-
Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。
-
请说出与线程同步以及线程调度相关的方法。
-
(1) wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
-
(2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;
-
(3)notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
-
(4)notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
-
sleep() 和 wait() 有什么区别?两者都可以暂停线程的执行
-
类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
-
是否释放锁:sleep() 不释放锁;wait() 释放锁。
-
用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
-
用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。
-
这个技术的常见问题
-
进程与线程的区别?
-
答:进程是所有线程的集合,每一个线程是进程中的一条执行路径,线程只是一条执行路径。
-
为什么要用多线程?
-
答:提高程序效率
-
多线程创建方式?
-
答:继承Thread或Runnable 接口。
-
是继承Thread类好还是实现Runnable接口好?
-
答:Runnable接口好,因为实现了接口还可以继续继承。继承Thread类不能再继承。
-
你在哪里用到了多线程?
-
答:主要能体现到多线程提高程序效率。
-
举例:分批发送短信、迅雷多线程下载等。
-
这个技术扩充知识
-
锁
-
为什么加锁
-
加锁机制为了保证并发情况下保证线程安全,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
-
还有那些保证线程安全的方法?
-
使用线程安全的类;
-
使用synchronized同步代码块,或者用Lock锁;
-
多线程并发情况下,线程共享的变量改为方法局部级变量ThreadLocal;
-
一些常用的锁
-
最底层的两种就是会「互斥锁和自旋锁」,有很多高级的锁都是基于它们实现的,你可以认为它们是各种锁的地基,所以我们必须清楚它俩之间的区别和应用。
-
* 互斥锁加锁失败后,线程会释放 CPU ,给其他线程;
-
互斥锁是一种「独占锁」,比如当线程 A 加锁成功后,此时互斥锁已经被线程 A 独占了,只要线程 A 没有释放手中的锁,线程 B 加锁就会失败,于是就会释放 CPU 让给其他线程,既然线程 B 释放掉了 CPU,自然线程 B 加锁的代码就会被阻塞。
-
开发过程中,最常见的就是互斥锁的了,互斥锁加锁失败时,会用「线程切换」来应对,当加锁失败的线程再次加锁成功后的这一过程,会有两次线程上下文切换的成本,性能损耗比较大。
-
* 自旋锁加锁失败后,线程会忙等待,直到它拿到锁;
-
自旋锁是最比较简单的一种锁,一直自旋,利用 CPU 周期,直到锁可用。需要注意,在单核 CPU 上,需要抢占式的调度器(即不断通过时钟中断一个线程,运行其他线程)。否则,自旋锁在单 CPU 上无法使用,因为一个自旋的线程永远不会放弃 CPU。
-
悲观锁和乐观锁
-
悲观锁
-
悲观锁和乐观锁:都是广义上一种思想。悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
-
前面提到的互斥锁、自旋锁、读写锁,都是属于悲观锁。
-
悲观锁(修改,插入数据):最坏的情况,每次修改后都锁表,悲观锁采取加锁的形式,传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。当进行修改时其他线程操作的不能改动。
-
乐观锁
-
乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
-
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁一般会使用版本号机制或CAS(Compare-and-Swap,即比较并替换)算法实现。
-
实现算法
-
版本号机制
-
数据表中加上一个数据版本号version字段控制。
-
CAS算法是一种有名的无锁算法。Compare And Swap(比较与交换)
-
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作,不断的尝试,直到成功为止。
-
CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)
-
CAS的缺点1.ABA问题(循环调用)。2.一直循环占用内存3.只能保证一个共享变量的原子操作。
-
可重入锁 VS 非可重入锁
-
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
-
独享锁 VS 共享锁
-
独享锁和共享锁同样是一种概念。
-
自旋锁 VS 适应性自旋锁
-
阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。
-
而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
-
无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁
-
无锁:无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
-
偏向锁:偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
-
轻量级锁:是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
-
重量级锁:升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。
-
公平锁 VS 非公平锁#
-
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
-
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
-
死锁问题
-
产生的四个条件
-
互斥条件:资源是独占的且排他使用,进程互斥使用资源,即任意时刻一个资源只能给一个进程使用,其他进程若申请一个资源,而该资源被另一进程占有时,则申请者等待直到资源被占有者释放。
-
不可剥夺条件:进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺,而只能由获得该资源的进程资源释放。
-
请求和保持条件:进程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源。
-
循环等待条件:在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请,也就是前一个进程占有后一个进程所深情地资源
-
如何避免死锁(我们只要破坏产生死锁的四个条件中的其中一个就可以了。)
-
破坏互斥条件
-
这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
-
破坏请求与保持条件
-
一次性申请所有的资源。
-
破坏不剥夺条件
-
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
-
破坏循环等待条件
-
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
-
并发编程
-
JAVA高并发
-
JAVA高级并发
-
为什么要使用并发编程(并发编程的优点)
-
充分利用多核CPU的计算能力:通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升
-
方便进行业务拆分,提升系统并发能力和性能:在特殊的业务场景下,先天的就适合于并发编程。现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。
-
并发编程有什么缺点
-
并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如**:内存泄漏、上下文切换、线程安全、死锁**等问题。
-
并发编程三要素(线程的安全性问题体现在):
-
原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
-
可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)
-
有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序
-
出现线程安全问题的原因:
-
线程切换带来的原子性问题
-
缓存导致的可见性问题
-
编译优化带来的有序性问题
-
解决办法:
-
JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
-
synchronized、volatile、LOCK,可以解决可见性问题
-
Happens-Before 规则可以解决有序性问题
-
什么是上下文切换
-
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
-
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
-
大多数的特性在java.util.concurrent 包中,是专门用于多线程发编程的,充分利用了现代多处理器和多核心系统的功能以编写大规模并发应用程序。主要包含原子量、并发集合、同步器、可重入锁,并对线程池的构造提供了强力的支持。
-
线程池
-
什么是线程池
-
在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在 Java 中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。
-
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。Java 5+中的 Executor 接口定义一个执行线程的工具。它的子类型即线程池接口是 ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类 Executors 面提供了一些静态工厂方法,生成一些常用的线程池,
-
线程池的原理(个人理解):配置核心线程数,然后按照队列顺序几个线程同时执行,剩下的任务线程还在队列中,核心执行后,队列的线程才可执行。然后队列饱和策略处理新来的线程。
-
线程池的5中创建方式
-
Single Thread Executor : 只有一个线程的线程池,因此所有提交的任务是顺序执行,单线程。处理不过来的任务会进入FIFO队列等待执行
-
代码: Executors.newSingleThreadExecutor()
-
Cached Thread Pool : 可变线程池,它犹如一个弹簧,如果没有任务需求时,它回收空闲线程,如果需求增加,则按需增加线程,不对池的大小做限制,线程池里有很多线程需要同时执行,老的可用线程将被新的任务触发重新执行,如果线程超过60秒内没执行,那么将被终止并从池中删除,
-
代码:Executors.newCachedThreadPool()
-
Fixed Thread Pool : 拥有固定线程数的线程池,如果没有任务执行,那么线程会一直等待,定长线程池,提交任务时创建线程,直到池的最大容量,如果有线程非预期结束,会补充新线程
-
代码: Executors.newFixedThreadPool(4)
-
在构造函数中的参数4是线程池的大小,你可以随意设置,也可以和cpu的核数量保持一致,获取cpu的核数量int cpuNums = Runtime.getRuntime().availableProcessors();
-
Scheduled Thread Pool : 周期性线程池。支持执行周期性线程任务,用来调度即将执行的任务的线程池,可能是不是直接执行, 每隔多久执行一次... 策略型的
-
代码:Executors.newScheduledThreadPool()
-
Thread Pool Executor创建,虽然Executors提供四种创建方式,但阿里巴巴规范建议使用Thread Pool Executor,避免内存泄露的问题(扩展怎么解决内存泄露,JVM那篇博客写了)
-
ThreadPoolExecutor构造函数重要参数分析,ThreadPoolExecutor 3 个最重要的参数:
-
corePoolSize :核心线程数,线程数定义了最小可以同时运行的线程数量。
-
maximumPoolSize :线程池中允许存在的工作线程的最大数量
-
workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中。
-
keepAliveTime:线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
-
unit :keepAliveTime 参数的时间单位。
-
threadFactory:为线程池提供创建新线程的线程工厂
-
handler :线程池任务队列超过 maxinumPoolSize 之后的拒绝策略
-
ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
-
ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
-
ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
-
ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
-
线程池的优点
-
降低资源消耗:重用存在的线程,减少对象创建销毁的开销。
-
提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
-
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
-
附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。
-
综上所述使用线程池框架 Executor 能更好的管理线程、提供系统资源使用率。
-
线程池的2种使用方式
-
提交 Runnable ,任务完成后 Future 对象返回 null调用excute,提交任务, 匿名Runable重写run方法, run方法里是业务逻辑
-
提交 Callable,该方法返回一个 Future 实例表示任务的状态调用submit提交任务, 匿名Callable,重写call方法, 有返回值, 获取返回值会阻塞,一直要等到线程任务返回结果
-
线程池都有哪些状态?
-
RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
-
SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
-
STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
-
TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。
-
TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。
-
线程池的饱和策略
-
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor 定义一些策略:
-
ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
-
ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
-
ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
-
ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
-
线程无依赖性
-
多线程任务设计上尽量使得各任务是独立无依赖的,所谓依赖性可两个方面:
-
线程之间的依赖性。如果线程有依赖可能会造成死锁或饥饿
-
调用者与线程的依赖性。调用者得监视线程的完成情况,影响可并发量
-
线程池之ScheduledThreadPoolExecutor详解
-
ThreadPoolExecutor构造函数重要参数分析
-
线程池之ThreadPoolExecutor详解
-
AQS(AbstractQueuedSynchronizer)详解与源码分析
努力不一定成功,但不努力一定会失败~