Java线程
进程与线程
进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。所以程序计数器是私有的
虚拟机栈和本地方法栈为什么是私有的
虚拟机栈: 每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
并行和并发区别
并发:两个及两个以上的作业在同一 时间段 内执行。
并行:两个及两个以上的作业在同一 时刻 执行。
最关键的点是:是否是 同时 执行。
创建线程
严格来说,Java 就只有一种方式可以创建线程,那就是通过new Thread().start()创建
继承Thread类
重写run方法后new出Thread对象调用start方法启动线程
实现Runnable接口
实现Runnable接口,一样重写run方法,new出对象还是调用start方法
实现Callable接口
实现Callable接口指定泛型后实现call方法时可以返回指定的数据(这点与run方法不同),使用FutureTask封装任务
FutureTask 的状态可以是 未开始、正在执行、已完成(成功或失败)。
结果获取:通过调用 get() 方法,你可以获取执行的结果。如果任务还未完成,调用 get() 会使当前线程等待,直到任务完成并返回结果。如果任务执行过程中抛出了异常,get() 会重新抛出该异常。
run和start的区别
直接调用 run 是在主线程中执行了 run,没有启动新的线程
使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
线程的状态
NEW: 初始状态,线程被创建出来但没有被调用 start() 。
RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。有执行资格没有执行权
BLOCKED:阻塞状态,需要等待锁释放。
WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
TERMINATED:终止状态,表示该线程已经运行完毕。
状态变化
为什么要使用多线程?
多核 CPU 意味着多个线程可以同时运行,这减少了线程上下文切换的开销
多线程是高并发的基础
单核cpu多线程运行效率
CPU 密集型:CPU 密集型的线程主要进行计算和逻辑处理,需要占用大量的 CPU 资源。IO 密集型:IO 密集型的线程主要进行输入输出操作,如读写文件、网络通信等,需要等待 IO 设备的响应,而不占用太多的 CPU 资源。
如果线程是 CPU 密集型的,那么多个线程同时运行会导致频繁的线程切换,增加了系统的开销,降低了效率
活跃性问题
线程死锁
线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
检测死锁
可以使用 jconsole工具(可以直接进行检测死锁)
或者使用 jps 定位进程 id,再用 jstack 定位死锁
jps查看当前所有线程以及id
jstack会显示详细的信息
预防死锁
一次性申请所有的资源。如果申请不到资源主动释放其占有的资源,按照顺序获取资源 释放资源则反序
活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束
比如一个线程count-- 一个线程count++
饥饿
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题
线程常用方法
1.5.4wait/sleep 的区别
共同点
wait0),wait(long)和 sleep(long)的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态
不同点
sleep(long)是 Thread 的静态方法,而 wait(),wait(long)都是 Object 的成员方法,每个对象都有
wait(long)和 wait() 还可以被 notify 唤醒,唤醒后继续执行,wait 如果不唤醒就一直等下去
它们都可以被打断唤醒
wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用).而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)sleep本身执行是不获取锁的
保证线程的执行顺序
使用join
如需使用sleep需要使用try-catch和throws
sleep 方法声明了 InterruptedException,这是一个受检查异常
涉及到lambada表达式
sleep与yield
调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态, 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException,在声明sleep方法时就需要捕获和抛出异常
使用Thread.sleep()方法时需要使用try-catch进行捕获,否则会报错,
使用sleep还需要注意在哪调用,如果在main线程中即使使用额外的线程t1.sleep实际上也是让主线程休眠
yield方法是一个静态方法,它属于Thread类。调用yield方法会让当前线程让出CPU执行权,对于多核cpu不一定会成功
wait 或 条件变量达到类似的效果不同的是,后两种都需要加锁是针对对象的(而sleep是针对线程的也就没有加锁而言),并且需要相应的唤醒操作,
interrupt以及isInterrupted()和interrupted的区别
interrupt()方法用于中断线程。调用这个方法会设置线程的中断状态,并在某些情况下引发线程中的 InterruptedException 异常。
如果线程正在被阻塞,如调用了 Thread.sleep()、Object.wait() 或 Thread.join() 方法,那么调用 interrupt() 会使这些方法抛出 InterruptedException 异常。
如果线程正在运行使用interrupt()那么只会影响打断标记,打断标记为true,如果打断阻塞线程打断标记为false
interrupt() 定义在Thread类中。
isInterrupted()方法用于检查线程是否被中断
isInterrupted()是一个实例方法,定义在Thread类中。调用isInterrupted()方法可以检查线程的中断状态。如果线程的中断标志位为true,则返回true;否则返回false。调用isInterrupted()方法不会修改线程的中断状态。
interrupted()是一个静态方法,定义在Thread类中。与isInterrupted()方法相似,interrupted()方法也可以用于检查线程的中断状态。不同的是,调用interrupted()方法会同时清除线程的中断状态,将中断标志位重置为false
park
如果打断标记已经是 true, 则 park 会失效
park()方法是java.util.concurrency.LockSupport类中的一个静态方法,用于阻塞当前线程。该方法会让当前线程进入等待状态,直到被其他线程调用unpark()方法或者被中断。
wait & notify 相比wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,
park & unpark 可以先 unpark,而 wait & notify 不能先 notify
先调用unpark就是让干粮多一份,最多只能有一份干粮(下次park就没有效果)
每次调用park都会查看是不是还有干粮
1.6、如何停止一个正在运行的线程
1 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
2 使用stop方法强行终止(不推荐,方法已作废)
3使用interrupt方法中断线程
打断阻塞的线程(sleep,wait,join)的线程,线程会抛出InterruptedException异常
打断正常的线程,可以根据打断状态来标记是否退出线程
二、共享模型之管程
2.1、Synchronized原理
2.1.1、对象的结构
对象存储在堆中,主要分为三部分内容:对象头、对象实例数据、对齐 填充。
对象头:Mark word和klasspointer两部分组成,如果是数组,还包括数组长度
对齐填充:JVM要求对象占用的空间必须是8的倍数,方便内存分配,因此这部分是为了填满不够的空间凑数用的。
Mark word结构
后续使用Synchronized加上重量级锁后Mark Word 中就被设置指向 Monitor 的Owner
使用javap -v xx.class的字节码信息
底层类比try catch finally 语句 第一个解锁的相当于finally,第二个相当于catch保证出现异常也会释放锁
2.1.2、Monitor结构
Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】
它的底层由monitor实现的,monitor是jvm级别的对象(C++实现),线程获得锁需要使用对象(锁)关联monitor
在monitor内部有三个属性,分别是owner、entrylist、waitset其中owner是关联的获得锁的线程关联的是其锁对象的Markword,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程(竞争锁失败的线程);,waitset关联的是处于Waiting状态的线程
2.1.4、锁升级
轻量级锁
同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。
线程会有一个Lock Record,他的Object reference指向Synchronized的obj,obj的markword会与锁lock record通过CAS交换
如果cas失败有两种可能
第一其他线程已经持有该轻量级锁,有竞争进入锁膨胀过程(升级为重量级锁)
第二种情况 自己执行了锁重入
就再从锁记录中添加一条LockRecord作为重入的计数(此时就有两条,每次都需要CAS)
退出逻辑
当退出 synchronized 代码块(解锁时)如果Lock Record有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给obj
针对重量级的加锁过程
刚开始 Monitor 中 Owner 为 null
当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner
在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED
Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
WaitSet中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程比如使用了wait,
锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功(或是撤销轻量锁时遇见,或是别的线程加锁时无法交换),
这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
然后为锁对象申请Monitor锁,(注意此时不再是lockRecord和markword交换了)
让 Object (就是对象锁 Object就是前者存储markword的结构)指向重量级锁地址(指向Montior对象)
然后竞争锁的线程进入 Monitor 的 EntryList BLOCKED,当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头会失败(因为Object的那个值已经指向了Monitor对象)。这时会进入重量级解锁流程,
此时原线程指向的地方变成了Monitor的Owner(交换两者)因为原线程已经变成重量级锁
Monitor 结构见上面图片
即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
2.1.6、自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放锁),这时当前线程就可以避免阻塞,即先进行几次循环,如果能够获取到锁,就不进行阻塞
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。Java 7 之后不能控制是否开启自旋功能
2.1.7、偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,
之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
2.2、synchronized 使用
当需要在方法上面使用synchronized 可以直接加载public后面,或者在方法内部使用 只不过需要传入参数,普通方法使用this,静态方法使用类名.class
如果一个用在静态方法一个用在普通方法上实际上他们不是同一个对象不是同一个锁
线程之间如何有效的使用 synchronized 取决于是不是加的同一个对象
2.3、变量的线程安全分析
局部变量如果是基本类型就是线程安全的,如果是引用类型那么可能发生线程安全问题
在法一里面调用法二和法三,此时是线程安全的,但如果现在有一个类继承这个类并且重写了法 3(子类重写父类的方法时访问修饰符可以变大 ),此时别的线程就可以直接调用法三而不用经过法一,这样就会发生线程安全问题
2.4、常见的线程安全类
String、Integer、StringBuffer、Random、Vector、Hashtable
三、共享模型之内存
3.1、JMM(Java内存模型)
定义了共享内存中多线程程序读写操作的行为规范,通过这些规范对内存的读写操作从而保证指令的正确性
JMM把内存分为两块,一块是私有线程的工作区域(工作内存)一块是所有线程的共享区域(主内存)
线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存
补充:堆在主内存中,每个线程私有的工作内存在栈中
3.2、CAS
比较并交换
Compare And Swap 体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数CAS的全称是
AbstractQueuedSynchronizer(AQS框架)、AtomicXXX类都使用到了
CAS数据交换的流程
每个线程在要修改某个变量时都会查看工作内存的该值和主内存的值是否相同,相同就可以改变主内存的这个值
自旋优化
如果修改值失败了 就会自选优化频繁的使用CAS检查工作内存和主内存的值
CAS底层依赖于Unsafe类来直接调用操作系统底层的CAS指令
3.3、volatile保证可见性
不保证原子性 保证可见性 也会防止指令重排序的发生
用 volatile 修饰共享变量,能够防止编译器优化发生,让一个线程对共享变量的修改对另一个线程可见
JVM虚拟机中有一个JIT(即时编译器)给代码做了优化,某个线程一直使用某个变量就把变量加载到该线程的工作内存,坏处是如果别的线程修改了值,这个线程不知道
Synchronized语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized 是属于重量级操作,性能相对更低
3.4、指令重排序问题
对于下面代码如果先执行actor1中第二行代码,然后执行actor2获取结果结果就是1,0
这是个不合常理的结果原因是发生了指令重排
下面代码因为y被volatile修饰,对于写操作,屏障上方的其他操作无法越过屏障到volatile之下
这样就不会发生上面的情况,所以给x加volatile还是会发生1,0的情况
3.5、AQS
AQS是多线程中的队列同步器 是一种锁机制 作为一个基础框架使用,ReentrantLock,Semaphore
线程放在FIFO队列中队列中放的是线程,AQS内部有个状态叫做state 0表示无锁 1表示有锁
首个线程请求锁 将state改为1,如果又有线程请求锁失败了 就去队列中等待
对列中有个head 进入队列最久的元素,tail 最后一个元素
多线程抢占资源如何保证,利用cas,如果state为1就是有锁,如果这个state为零那么那么该线程就到FIFO队列的tail等待
公平锁和非公平锁的实现
因为没有竞争到锁的对象都会在队列当中,而且是先进先出的,所以根据新来的线程是直接和队列中的head竞争还是放入到队列的末尾
这就决定了公平还是非公平
ReentrantLock
使用方法
ReentrantLock lock = new ReentrantLock();
void lock() // 如果锁可用就获得锁,如果锁不可用就阻塞直到锁释放
void lockInterruptibly() // 和lock()方法相似, 但阻塞的线程可中断 , 抛出java.lang.InterruptedException 异常
//关于lockInterruptibly的补充 interrupt() 方法本身不会影响锁的释放,因为锁的释放是由 unlock() 方法完成的。但是如果线程没有成功获得锁使用interrupt()就会抛出异常然后在finally中使用unlock()释放锁避免阻塞
boolean tryLock() // 非阻塞获取锁;尝试获取锁,如果成功返回 true 常与if结合使用
boolean tryLock(long timeout, TimeUnit timeUnit) //带有超时时间的获取锁方法
void unlock() // 释放锁
可中断
没有获取到锁被阻塞时被打断,就会抛出异常,需要被catch捕获,记得在finally中写上释放锁操作
需要使用可打断模式,才能打断阻塞的线程(即使用lock.lockInterruptibly();上锁)
使用线程名.interrupt打断正在等待锁的线程,可以减少死锁的发生
可以设置公平锁(默认是使用非公平锁,构造器参数为true就是公平锁)
非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好。
支持多个条件变量(有多个waitSet)
Condition 维护了一个等待队列,调用 await() 方法的线程会被放入这个等待队列中,当使用singnall()就会唤醒该队列上面的所有线程去竞争锁
与synchronized一样,都支持重入
实际用处就是如果method1调用method2调用method3,因为要避免多线程安全问题,所以需要将三个方法都加锁
锁超时
lock.tryLock()可设置为立即失效,或者带参数设置为超时失败
尝试获得锁。获取成功返回true
实现原理
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。
可以发现底层用到的就是AQS
synchronized 和 Lock 有什么区别?
线程池
Executors的使用方法
ExecutorService executorService = Executors.newFixedThreadPool(4);
ThreadPoolExecutor使用方式
该线程池使用的是链表结构的阻塞队列
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExecutorDemo {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
//使用阿里巴巴推荐的创建线程池的方式
//通过ThreadPoolExecutor构造函数自定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
//创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
Runnable worker = new MyRunnable("" + i);
//执行Runnable
executor.execute(worker);
}
//终止线程池
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
ThreadPoolExecutor线程池的核心参数
执行原理
ThreadPoolExecutor线程池常见的阻塞队列
FIFO 先进先出
LinkedBlockingQueue因为是基于链表 所以大小可变(队列的容量通常是一个很大的值(默认为 Integer.MAX_VALUE)也可指定大小)
ArrayBlockingQueue的LinkedBlockingQueue区别
两把锁的效率更高,两边都能运行
如何确定核心线程数
因为CPU密集型需要一直计算,设置过多的核心线程数就会频繁的切换
线程池的种类
Executors类提供了大量创建连接池的静态方法(该类都是基于ThreadExcetor实现的)
创建使用固定线程数的线程池
适用于任务量已知,且任务比较耗时的操作
单线程化的线程池,会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO)执行
可缓存线程池
新来的任务看当前有没有临时线程,有的话就使用这个临时线程,没有的话就创建一个,设置的临时线程过期时间是60s
适合任务密集,而且每个任务执行时间较短
延迟和周期执行功能的线程池
newScheduledThreadPool 支持定时及周期性任务执行
为什么不建议用Executors创建线程池
Runnable vs CallableRunnable自 Java 1.0 以来一直存在,但Callable仅在 Java 1.5 中引入,目的就是为了来处理Runnable不支持的用例。Runnable 接口不会返回结果或抛出检查异常,但是 Callable 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。工具类 Executors 可以实现将 Runnable 对象转换成 Callable 对象。(Executors.callable(Runnable task) 或 Executors.callable(Runnable task, Object result))。
execute() vs submit()execute() 和 submit()是两种提交任务到线程池的方法,有一些区别:返回值:execute() 方法用于提交不需要返回值的任务。通常用于执行 Runnable 任务,无法判断任务是否被线程池成功执行。submit() 方法用于提交需要返回值的任务。可以提交 Runnable 或 Callable 任务。submit() 方法返回一个 Future 对象,通过这个 Future 对象可以判断任务是否执行成功,并获取任务的返回值(get()方法会阻塞当前线程直到任务完成, get(long timeout,TimeUnit unit)多了一个超时时间,如果在 timeout 时间内任务还没有执行完,就会抛出 java.util.concurrent.TimeoutException)。异常处理:在使用 submit() 方法时,可以通过 Future 对象处理任务执行过程中抛出的异常;而在使用 execute() 方法时,异常处理需要通过自定义的 ThreadFactory (在线程工厂创建线程的时候设置UncaughtExceptionHandler对象来 处理异常)或 ThreadPoolExecutor 的 afterExecute() 方法来处理
shutdown()VSshutdownNow()shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。shutdownNow() :关闭线程池,线程池的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。isTerminated() VS isShutdown()isShutDown 当调用 shutdown() 方法后返回为 true。isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true几种常见的内置线程池
查看进程线程的方法
线程与栈和栈帧的关系
每个线程启动后,虚拟机就会为其分配一块栈内存。
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法,其他栈代表的方法没有执行
线程上下文切换
当线程的上下文切换发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,
程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
切换的原因
线程的 cpu 时间片用完,垃圾回收有更高优先级的线程需要运行线程
自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
各种锁机制
公平锁、非公平锁(默认) --ReentrantLock
可重入锁 ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
自旋锁 AtomicReference<Thread> atomicReference = new AtomicReference<>();
AtomicReference 是一种使用 CAS(Compare-And-Swap)操作来实现原子更新的类。
通过 compareAndSet 方法实现无锁(lock-free)的同步机制。
//自旋锁
public class SpinLockDemo {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
//加锁
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"==> myLock");
//自旋锁
while(!atomicReference.compareAndSet(null,thread)){ //获取不到锁就自旋等待!!!
}
}
//解锁
public void myUnlock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"==> myUnlock");
atomicReference.compareAndSet(thread,null);
}
}
第一个参数,期望的当前值。 第二个参数想要设置的新值
死锁 两个线程持有自己的锁,并试图获取对方的锁!
乐观锁,悲观锁
悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁
乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据
CAS算法
版本号机制Version
读写锁
ReentrantReadWriteLock 无论读还是写都必须获取锁
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
StampedLock
ThreadLocal
ThreadLocal的数据结构
Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。
每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。但是ThreadLocalMap没有链表结构
可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用
ThreadLocal是否被GC 取决于是强引用还是弱引用,如果是弱引用的话就会导致Key被回收 而value还会存在就导致了垃圾回收
ThreadLocal.set()方法源码详解
看图能够知道,如果ThreadLocalMap不存在那么就先创建ThreadLocalMap再set值
ThreadLocalMap的Hash算法
既然是map结构但是没有链表就一定有自己的hash算法解决数组冲突问题
ThreadLocal中有一个属性为HASH_INCREMENT = 0x61c88647,每当创建一个ThreadLocal对象,这个ThreadLocal.nextHashCode 这个值就会增长 0x61c8864,这个值很特殊,它是斐波那契数 也叫 黄金分割数。hash增量为 这个数字,带来的好处就是 hash 分布非常均匀。
由于没有采用链表结构,所以当有hash冲突的时候,需要插入的元素就后移到为null的位置