乐观锁和悲观锁
乐观锁即认为不会发生并发竞争,直接去修改数据,知道发现存在竞争后再重试。悲观锁则是认为肯定会发生竞争,直接先上锁后修改数据。乐观锁的实现方式有CAS,但是需要跟版本号相结合使用,避免ABA问题。悲观锁可以通过各种独占锁来实现。
Violate
- 用来声明字段具有可见性(而非原子性):被violate声明过的字段可以保证被其他线程访问到。但是不能保证原子性:例如每个线程都能访问count,但是不能保证大家都按顺序执行count++, 有可能重复或者缺少++的次数。
- 禁止JVM重排序:JVM是有可能重排序我们的代码指令顺序的,violate关键字可以保证按顺序执行,例如new一个对象,第一步是为对象创建一个内存地址,第二步时初始化对象,第三步是将对象指向内存的地址。我们期望是1,2,3.但是有可能变成1,3,2.这在多线程时就可能存在问题:有可能第一个线程执行了1,3.然后第二个线程执行了2,导致认为对象是null。
Synchronize
- 用来修饰方法,代码块,保证同一时间只有一个线程能够执行对应的方法和代码块。
- 用来修饰静态方法时,锁的是当前的类。
- 用来修饰非静态方法时,锁的是当前的实例。
-用来修饰代码块时,取决于synchronize(xxx)括号里的内容。如果是object,则是锁的对应的对象,如果是xx.class则是锁的xx类。
- 不能用来修饰构造方法,但是可以修饰构造方法内的代码块。
- 原理:本质上都是通过Monitor对象来实现的,会维护一个owner来对应持有锁的线程,还有一个entityList来维护竞争锁的线程, 还有一个waiteSet来维护等待唤醒的线程,唤醒后会将线程从waiteSet移入entityList。
- 修饰代码块时,原理是使用了monitorenter和monitorexit来实现的,enter时锁计数器+1, exit时-1。若计数器不为0则不能enter。
- 修饰方法时,原理是使用了ACC_SYNCHRONIZED
标志
锁升级
- 无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态
- 第一次共享资源被访问时,会进入偏向锁状态,此时对象头会记录当前的线程ID,只有该线程能够访问共享资源。
- 如果偏向锁状态时存在另外一个线程尝试访问共享资源并产生了竞争,则会进入轻量级锁状态,会不断自旋尝试获取锁,此时会在当前线程的栈中创建一个锁记录,会尝试拷贝锁对象的头到自己的栈中。
- 如果自旋超过一定次数则会升级为重量锁,此时JVM会将线程阻塞,对象头指向monitor对象。
ReentrantLock
-原理:基于AQS实现,可以对队列中的线程进行精细的控制,所以可以实现公平锁。而synchronize是基于enittyList对用户不可见,无法保证FIFO,所以不能实现公平锁。
- ReentrantLock与Synchronize都是可重入锁,即可以重复获取同一个对象的锁。
- ReentrantLock可以实现公平锁,而Synchronize只能是非公平锁。(即不能保证按照顺序获取锁)
- ReentrantLock支持设置超时。但是synchronize不支持,因为Monitor对象没有该功能且无法修改。
- ReentrantLock是可中断锁,即在等待锁的过程中可以停止。但是Synchronize是不可中断锁,即等待过程中不能终止。
- ReentrantLock是独占锁,但是ReentrantReadLock是共享锁,即会阻塞写操作,不会阻塞其他线程的读操作
ThreadLocal
- 用来给每个线程维护一个独立的变量池子,单独读写,不会受到其他线程的影响
- ThreadLocal是ThreadLocalMap的封装,变量都存在map中以ThreadLocal为key,object微value的键值对,通过get和set方法读写。
- 如果没有调用remove方法,会造成内存泄漏。因为map中的value被entry强引用。
- 父子线程如何传递ThreadLocal的值? 使用InheritThreadLocal,会自动继承父线程的Threadlocal。但是缺点是只会在创建子线程是复制一次。如果要在之后再次通信,可以使用TransmittableThreadLocal
线程池
- 线程池与连接池等一样是将用完的线程不会马上销毁,而是保留在池中可以复用以提高效率。
- ThreadPoolExecutor参数:
- corePoolSize 线程池核心线程数量(即一直保持存活的线程数量)
- maximumPoolSize 最大数量 (即任务超过核心线程数量时,可以创建出来的最大线程数量)
- keepAliveTime 超过核心线程数的线程空闲时存活时间, unit 时间单位
- workQueue 用来储存等待执行的任务队列
- handler 用来定义拒绝策略,即队列满了之后如何处理
- 拒绝策略分为几种,默认是Abort,即抛出异常。还有discard,即直接丢弃。还有一种CallerRuns,即退还给调用execute的线程处理。但是有风险,即如果该任务耗时很长,而且是由主线程调用的execute,有可能造成主线程阻塞。
- 那么又没有一种既不丢弃任务,又不阻塞主线程的方法呢?
- 消息队列,redis缓存任务,或者将任务存入数据库中,等待空闲时再取出来用
- 线程池中的线程异常后,是销毁还是复用?如果是通过execute提交创建的线程会销毁重新创建,适用于调用方不在意抛出的异常。如果是submit则会将异常存在Future中供调用方处理异常后再处理。
- 如果线程池数量设置的太大,会导致上下文切换频繁。(线程数如果大于CPU核数,会导致CPU需要来回切换以处理每个线程,每次切换都会切换上下文)