1、synchronized
synchronized是一种互斥锁,一次只允许一个线程进入被锁住的代码块,synchronized可以修饰实例方法(锁对象实例),静态方法锁(当前类的class对象),代码块(传入synchronized的对象),在内存中,对象由对象头(markword信息)、对象实际数据、对齐填充组成,JDK早期用的都是重量级锁依赖的是操作系统的mutex相关指令来实现,会有用户态和内核态的转换会有性能损耗,1.6后面改进了有了锁的升级的概念,在jvm层面实现加锁的逻辑。
锁升级
只有一个线程进入临界区,cas对比markword的ID,偏向锁
多个线程交替进入临界区,轻量级锁(自旋锁)10次取锁,没有的话进入等待队列
多个线程同时进入临界区,重量级锁
2、volatile关键字
- 保证线程可见性
AB线程都用到一个变量,java默认是一个A线程开始运行时,会把变量值从内存中读到线程 的工作区,在运行过程中都用这个copy的值,并不会每一次都读取堆内存,所以如果B线程 修改了变量的值的话,A线程并不会及时的感知到,使用了volatile关键字的话每一次对变量 的读写都是直接走堆内存
- 禁止指令重排序
volatile使一个变量在多个线程间可见,volatile关键字并不能保证原子性
3、CAS(compare and swap)
cas(value,expected,newValue)
{
if value=ecpected
value=newValue
otherwise try again or fail
}
由CPU原语支持
CAS的缺点:会带来ABA问题
A线程读到当前值是10,但是B线程已经把10改成了100,C线程又把100改成了10,虽然对A线程来说当前值是未被修改过的,但我们从上帝视角来看是被BC改过的,当然也可以加一个版本号来解决这个问题
4、JUC同步锁
- ReentrantLock
- CountDownLatch
- CyclicBarrier
- ReadWriteLock
- Semaphore
- LockSupport
- Exchanger
ReentrantLock(可重入的互斥锁)
可用于替代synchronized,但是比synchronized更加灵活,需要注意的是,必须要必须要必须要手动释放锁,使用syn锁定的话如果遇到异常,jvm会自动释放锁,但是lock必须手动释放锁,因此经常在finally中进行锁的释放;提供了trylock()方法尝试锁定,无法锁定或者在规定时间内无法锁定都不影响方法的继续执行,可以根据trylock的返回值来判定是否锁定
在new ReentrantLock(true),可以设置是公平锁还是不公平锁,true为公平锁,但默认是非公平锁
还可以调用interrupt()打断线程的等待,lockInterruptibly()可以对interrupt()方法做出响应
CountDownLatch
直译的话是倒数开门栓,是一个线程工具类,有一个门栓await()插在哪里使线程等待,等其他的一些线程完成之后调用countdown方法使计数器减1,当减为0的时候才继续执行。
final CountDownLatch latch = new CountDownLatch(3);
latch.countDown();
latch.await();
CyclicBarrier
循环栅栏,作用是让指定数量的线程完成之后才开始下一步行动
调用await方法就加一次,加到数量开始下一步之后又变成0开始下一步
public class T07_TestCyclicBarrier {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(20, new Runnable() {
@Override
public void run() {
System.out.println("开始下一步");
}
});
for(int i=0; i<100; i++) {
new Thread(()->{
try {
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
ReadWriteLock
是一个接口,管理一组锁,读锁和写锁
ReadLock,是这个共享锁,如果t1线程获取了读锁,t2线程来申请读锁的话可以直接访问,t1、t2共享资源,要是t2访问写锁的话就一直等待直到t1释放读锁。
WriteLock。是一个排它锁,只要有线程获取到了写锁,无论其他线程来申请读锁还是写锁都要等待t1释放写锁。
Semaphore
信号量,使用它可以限制同时访问资源的线程个数
举例:
- 可以把它简单的理解成我们停车场入口立着的那个显示屏
- 每有一辆车进入停车场显示屏就会显示剩余车位 减1
- 每有一辆车从停车场出去,显示屏上显示的剩余车辆就会 加1
- 当显示屏上的剩余车位为 0 时,停车场入口的栏杆就不会再打开,车辆就无法进入停车场了,直到有一辆车从停车场出去为止
Semaphore的几个方法:
acquire():从信号量获得一个许可,在获得之前线程阻塞,否则线程被中断,拿到就许可数1
release():释放一个许可,许可数+1
availablePermits():获得信号量中的可用的许可数
LockSupport
阻塞线程和叫醒线程,可以让线程在任何位置阻塞(park方法),也可以在任何位置唤醒(unpark方法),可以在park方法之前调用unpark方法放行线程。
locksupport与wait、notify的区别
wait和notify都是object中的方法,调用这两方法时必须获取到锁对象,park不需要对象就可以锁住线程(locksupport.lock)
notify只能随机唤醒一个线程,而unlock可以唤醒指定线程
Exchanger
exchanger,用于线程之间数据的交换。它提供了一个同步点,在这个同步点两线程之间可以交换数据,一个线程先调用exchange方法时,他会一直等待另一个线程也执行exchange方法
5、AQS
JUC包中的ReentrantLock,,Semaphore,ReentrantReadWriteLock,CountDownLatch 等等几乎所有的类都是基于AQS实现的。
lock时的方法调用
AQS的结构
AQS 中有两个重要的东西,一个以Node为节点实现的链表的队列(CHL队列),还有一个STATE标志,并且通过CAS来改变它的值
ReentrantLock的非公平锁的lock过程
- 首先调用tryAcquire()方法获取到当前线程,getState()为0的话,则拿到锁,使用CAS操作将State变为1并将将线程设置为当前锁的拥有者,如果判定getState不为0的话再判定当前线程是否为锁的拥有者,是则将state+1(可重入)。
- 若上面的都不满足,则调用acquireQueued(addWaiter(Node.Exclusive,ags))
addWaiter(Node.Exclusive,ags):有一个死循环把节点加到队列的尾巴那里(CAS(oldtail,node))这样避免了整个链表都加锁,Exclusive表示加锁的类型,排他锁。返回节点node
acquireQueued:判定node,如果node的前置节点是head则tryAcquire()尝试拿锁(与head抢锁)抢不到就阻塞,等待前一个节点唤醒,如果前置节点不是head则将前驱节点是waitState设置为SIGNAL,当前线程park挂起
ReentrantLock的非公平锁的unlock过程
- 调用unlock方法实际就是调用AQS的release方法,而release方法会调用子类的tryRelease方法tryRelease会一直把State减为0,说明当前线程已经把锁释放了
- 然后从队尾开始往前找State<0并且离head节点最近的节点进行唤醒
- 唤醒之后,被唤醒的节点会去CAS获取锁,假如获取到之后会把head节点干掉自己设置成head节点
6、ThreadLocal(线程局部变量)
set(value)方法:拿到当前线程,通过getMap方法获取当前线程的ThreadLocalmap容器,如果有就把ThreadLocal和对应的值value存进去,没有就在当前线程创建一个
get()方法:用了ThreadLocal保证了同一个线程获取一个Connection对象,从而保证一次事务的所有操作需要在同一个数据库连接上
ThreadLocalMap里面是有一个弱引用指向ThreadLocal的,只要调用垃圾回收gc就会被回收
7、java里面的四种引用类型(强、弱、软、虚)
强引用:M m=new M();这个是强引用,不会被垃圾回收
软引用:一个对象只有软引用指向他时,内存不够的时候才会回收(做缓存用,但一般用不上)
弱引用:WeakReference<M> m=new WeakReference<>(new M())
m.get()拿到M的值,弱引用只要调用到垃圾回收gc就会被回收
ThreadLocal里面的弱引用
ThreadLocal<M> t1=new ThreadLocal<>();
t1.set(new M());
t1.remove();
如果使用强引用,即使t1=null,但key的引用还是会指向ThreadLocal对象,所以会有内存泄漏,而使用弱引用则不会,t1=null之后ThreadLocal会被垃圾回收时回收,但这样还是会有内存泄漏问题,因为ThreadLocal被回收之后,key的值会变为null,则整个value再也不会被访问到,依然造成内存泄漏,所以不用了一定要调用remove方法。
虚引用
虚引用里面的这个M的值是取不到的,但当M被回收的时候queue会有值,虚引用一般用于管理堆外内存的,但M被回收的时候通过queue可以检测到,然后可以清理堆外内存