CAS
- CAS(Compare And Swap),处理器会比较当前内存地址上的值 和 线程中变量的值(预期值)是否相等,如果相等则替换内存地址上的值为新值,如果不相等则不更新,硬件层面上支持使用一条指令来实现上面的操作,x86中是
cmpxchgl指令 - CAS操作不涉及线程上下文切换,并且不使用锁,会提高系统的并发性

-
ABA问题:一个线程将变量A改为了变量B,之后又改为了变量A,另一个线程在执行CAS操作时会认为该变量没有发生变化,可能会导致错误。解决方案:添加版本号进行对比,可以使用AtomicStampedReference类javaprivate static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 66); public static void main(String[] args) throws InterruptedException { // 获取当前的引用值和时间戳 int stamp = atomicStampedReference.getStamp(); Integer value = atomicStampedReference.getReference(); // 修改引用值 boolean result = atomicStampedReference.compareAndSet(value, value + 10, stamp, stamp + 1); System.out.println("result = " + result); // 输出最终的值 System.out.println("Final value = " + atomicStampedReference.getReference()); System.out.println("Final stamp = " + atomicStampedReference.getStamp()); }
Synchorized
synchorized修饰实例方法时,锁对象是当前的实例对象;synchorized修饰静态方法时,锁对象是当前的class类对象;synchorized作用于代码块时,锁对象是括号里面的对象synchorized是可重入锁,一个线程可以重复获取到该锁,每获取一次锁,计数器加一,释放锁时,计数器减一,直到计数器为0,锁才会真正释放synchronized修饰方法时,会在方法的访问标志位中增加一个ACC_SYNCHORIZED标志,当线程访问该方法时,如果包含该标志位,就要获得该方法对应的对象的监视器锁才能执行该方法synchronized修饰代码块时,会在代码块的前后插入monitorenter和monitorexit字节码指令
锁升级过程:
- 在没有锁竞争的情况下,一个线程来加锁,JVM会向锁对象的对象头中写入该线程id (这时候的锁叫
偏向锁),后续该线程再进入该锁时,无需进行额外的同步操作 - 在锁轻度竞争的时候,会升级为
轻量级锁,JVM会在当前线程中创建一个栈帧,通过CAS操作将这个栈帧的地址写入对象头中,如果成功表示该线程获取到了锁,如果失败则表示其他线程已经持有该锁,此时会升级为重量级锁 - 当锁竞争激烈时,JVM会升级为重量级锁,
synchorized修改代码块时,线程会尝试获取monitor对象的所有权,抢到了就是成功获取到锁,抢不到线程就会阻塞,等待重新获取锁



Synchorized和ReentrantLock
synchorized和ReentrantLock都是可重入锁synchorized是非公平锁,不支持设置超时时间,不支持条件判断,不需要手动释放锁ReentrantLock既可以是非公平锁也可以是公平锁(默认是非公平锁),允许设置获取锁的超时时间,可以通过lock.Condition对象将不满足条件的线程加入到阻塞队列中
ThreadLocal
-
基本使用
javapublic class UserContext { private static ThreadLocal<User> CONTEXT = new ThreadLocal<User>(); public static User getUser() { return CONTEXT.get(); } public static void setUser(User user) { CONTEXT.set(user); } public static void remove() { CONTEXT.remove(); } } -
每个线程内部维护了一个
ThreadLocalMap对象,ThreadLocalMap以ThreadLocal为key,线程变量副本为value -
ThreadLocalMap中的key是弱引用,之所以不是强引用是因为所使用的线程往往是线程池中的线程,线程一般不会被销毁,对ThreadLocal的引用就会一直存在,导致内存释放不掉 -
最佳实践:
- 存储用户登录信息,让每个线程有独立的用户上下文
- 管理数据库链接,将一个数据库连接放入ThreadLocal中,在需要使用数据库连接的地方直接从ThreadLocal获取

Volatile
-
volatile可以保证可见性,当一个线程修改了volatile变量的值后,新值会立即被刷新到主内存中,其他线程在读取该变量时可以立即获取到最新的值,避免由于缓存一致性问题导致 "看见"旧值的现象 -
volatile底层会加一个lock前缀指令进行总线锁定,总线监听到修改了共享数据后,会让其他处理器中的缓存更新或失效 -
volatile可以禁止 jvm 指令重排,保证在多线程环境下也能正常运行正常情况:1. 为对象分配内存,2. 初始化,3. 将对象引用指向对应内存地址
指令重排可能变成1 3 2,这样可能会发生问题

AQS
- AQS是一个抽象类,内部封装了线程 加锁、解锁、入队、出队的一些方法,提供给其他juc锁使用
- 它内部通过维护一个共享变量
state和先进先出FIFO等待队列来控制并发线程,在独占锁中,state为0表示未被占用,1表示已被占用,当线程获取资源失败时,会被加入到AQS的等待队列中 - 基于AQS实现的类有
ReentrantLock,CountDownLatch,Semaphore等
其他
死锁:两个线程都想获取对方已经拥有的锁,避免死锁的方法:
- 避免在一个锁的代码块中再次尝试获取另一个锁
- 使用尝试获取锁的方法,比如
ReentrantLock的trylock方法,Semaphore的tryAcquire方法,如果获取不到使用其他方案 - 减小锁的控制范围
保证线程安全的方式
- 同步锁:
synchorized、ReentrantLock - 原子操作类:
AtomicInteger、AtomicReference - 线程安全的容器:
ConcurrentHashMap等,避免手动加锁 - 使用局部变量
一个线程被启动两次,会发生什么:
- 线程一旦运行就不会回到初始状态,会抛出
IllegalThreadStateException异常
上下文切换:
- 暂停一个线程的处理,并将CPU寄存器中该线程的数据存储在内存中
- 从内存中获取下一个线程的上下文,并在CPU的寄存器中恢复它
- 返回到程序计数器指示的位置 继续执行
并发和并行:
- 并发:线程轮流使用cpu的做法称为并发
- 并行:多个cpu同时在调度线程执行任务
原子性、可见性、有序性
- 原子性:一组操作要么全部执行成功,要么全部不执行
- 可见性:一个线程修改共享变量的值后,其他线程能够立即看到这个修改后的值
- 有序性:程序的执行顺序和代码的先后顺序一致,但是多线程环境下,编译器会对指令进行重排序,进而出现并发问题
指令重排:
- 编译器和处理器为了优化性能,对指令执行顺序进行调整,指令重排类型包括:编译器重排和cpu重排。为了避免多线程之间操作出现不同步现象,可以使用
volatile和synchorized等来限制这种行为
相关文章
- CAS:note.youdao.com/s/AJECfbdA
- ThreadLocal:note.youdao.com/s/7pvkie8N
- CPU缓存架构:note.youdao.com/s/ALicPbhV
- JUC并发工具类:note.youdao.com/s/dbBcYl4a