介绍锁之前,先介绍一下JUC(java util concurrent)。它是java提供的一个工具包,里面有我们常用的各种锁,它分为3个包
- java.util.concurrent //如:volatile,CountDownLatch,CyclicBarrier,Semaphore
- java.util.concurrent.atomic //原子操作类对象:AtomicInteger...
- java.util.concurrent.locks //锁:ReentrantLock,ReentrantReadWriteLock...
一:公平锁/非公平锁
- 公平锁:加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得。
- 非公平锁:加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待。
- 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列
- ReentrantLock 默认的 lock()方法采用的是非公平锁,可以通过new ReentrantLock(true)设置为公平锁
- 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列
二.可重入锁(递归锁)
- 指一个线程获取外层函数锁之后,内层递归函数也能仍然获得该锁的代码 (就像进入自己的家的防盗门后,也同样可以进卧室,卫生间...)
- ReentrantLock和synchronized 都是可重入锁(递归锁)
比如:
public synchronized void method1(){ ... method2(); } public synchronized void method2(){ ... }
作用:可重入锁最大的作用就是避免死锁。
三.自旋锁
- 指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁。
- 这样的好处是减少线程上下文切换的消耗(因为线程阻塞/唤醒代价很大),缺点是循环会消耗CPU。
- 应用:原子性操作类AtomicXXX就是采用自旋锁+CAS使用。
public class SpinLockDemo { // 原子引用线程, 没写参数,引用类型默认为null AtomicReference<Thread> atomicReference = new AtomicReference<>(); //上锁 public void myLock() { Thread thread = Thread.currentThread(); System.out.println(Thread.currentThread().getName() + "==>mylock"); // 自旋 while (!atomicReference.compareAndSet(null, thread)) { } //执行业务代码 ... System.out.println(Thread.currentThread().getName() + "开始执行业务代码"); } //解锁 public void myUnlock() { //执行业务代码 ... System.out.println(Thread.currentThread().getName() + "结束执行业务代码"); Thread thread = Thread.currentThread(); atomicReference.compareAndSet(thread, null); System.out.println(Thread.currentThread().getName() + "==>myUnlock"); } // 测试 public static void main(String[] args) { SpinLockDemo spinLockDemo = new SpinLockDemo(); new Thread(() -> { spinLockDemo.myLock(); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } spinLockDemo.myUnlock(); }, "T1").start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> { spinLockDemo.myLock(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } spinLockDemo.myUnlock(); }, "T2").start(); } }
四.读写锁
- 读锁(共享锁):该锁可被多个线程所持有。
- 写锁(独占锁): 指该锁一次只能被一个线程锁持有
- 对于ReentranrLock和Synchronized而言都是独占锁。
疑问:读锁和不加锁有啥区别?
- 读写锁是互斥的,共享的读锁是为了锁住写线程,也就是说在读的时候不能写 。
好处:
- 读写锁保证:
- 读-读:能共存
- 读-写:不能共存
- 写-写:不能共存
- 提高并发的效率
使用
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); //读锁 readWriteLock.readLock().lock(); readWriteLock.readLock().unlock(); //写锁 readWriteLock.writeLock().lock(); readWriteLock.writeLock().unlock();
五.悲观锁和乐观锁?
- 悲观锁:很悲观,每次操作都加锁。比如sync和lock.
- 乐观锁:很乐观,每次都不加锁。但是更新时会判断一个条件。
- 一般采用version机制,和cas机制。
- update t_goods set status=2,version=version+1 where id=#{id} and version=#{version};
六.互斥锁/同步锁
- 互斥锁:互斥是通过竞争对资源的独占使用,彼此没有什么关系,也没有固定的执行顺序。
- 就像加sync,lock锁的时候就是互斥锁。
- 同步锁:同步是线程通过一定的逻辑顺序占有资源,有一定的合作关系去完成任务。
- 就像Barrier,Semphore这样的机制。执行完某些线程才能执行下一个线程。
七.synchronized
synchronized 和 lock 区别: 最关键的就是 lock定制化比较高
- 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
- synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放 锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1 阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
- synchronized的锁可重入、不可中断、非公平;而Lock锁可重入、可判断、可公平(两者皆可)
- lock锁可以唤醒指定线程
- 无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源
- 偏向锁:偏向第一个访问Thread,非常的乐观,从始至终只有一个线程请求某一把锁
- 加锁:
- 当线程第一次访问时,在对象头的相关位置(锁状态,持有的锁,偏向锁id记录threadID)进行记录。
- 后来这个线程再次进入时,比对ThreadID进行操作。
- 解锁:
- 偏向锁使用了一种等待竞争出现才释放锁的机制,只有别的线程也访问该资源失败时, 升级为轻量级锁。
- 过程:在全局安全点,暂停拥有偏向锁的线程,判断偏向锁线程是否存活,存活就升级为轻量级锁,不存活就先变为无锁状态,再把抢占的线程变为偏向锁。
- 加锁:
- 轻量级锁:多个线程在不同的时间段请求同一把锁,也就是说没有锁竞争
- 加锁:
- 在访问线程的栈中创建一个锁记录空间,将对象头的信息放在这个空间,然后使用CAS将 对象头转变为一种指针,指向锁记录空间
- 解锁:
- 使用CAS将锁记录空间的信息替换到对象头中。
- 当存在多个线程竞争锁的时候,就会进行自旋,自旋到一定数量,转变为重量级锁
- 加锁:
- 重量级锁:当一个线程拿到锁时候,其他线程会进入阻塞状态,当锁被释放的时候唤醒其他线程
sync的其他优化:
- 锁粗化:将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成为一个范围更大的锁。 比如stringBuffer.append方法.
- 锁消除:根据逃逸技术,如果认为这段代码是线程安全的,删除没有必要的加锁操作
synchronized 的实现原理:
- 同步代码块是通过monitorEnter 和 monitorExit指令(字节码指令)获取线程的执行权。
- 同步方法是通过acc_synchronized标志实现线程的执行权的控制,一旦执行到同步方法, 就会先判断是否有标志位,才会去隐式的调用上面两个指令。
- 具体过程:monitor对象存在于每个对象的对象头,进入一个同步代码块,就会执行monitorEnter,就会获取当前对象的一个持有权,这个时候monitor的计数器为1,当前线程就是这个monitor 的持有者,如果你已经是这个monitor的owner,再次进入,monitor就会 +1,同理当他执行 完monitorExit,对应的计数器就会减一,直到计数器为0,就会释放持有权。
八.JUC的工具类(同步锁)
- CountDownLatch:线程计数器(递减)
//定义指定数量一个线程计数器 CountDownLatch count = new CountDownLatch(6); //计数器-1 count.countDown(); //线程阻塞,等待计数器归零 count.await();
- CyclicBarrier:栅栏(七颗龙珠召唤神龙)(递加)
//定义一个类似"召唤神龙"的对象,所有线程执行完再执行该线程 CyclicBarrier cyclicBarrier = new CyclicBarrier(8,()->{}); //所有线程进入等待,等待线程全部执行完 cyclicBarrier.await();
小结:上述两个方法虽然都是为了等待前面所有的线程执行完再执行后续的线程 ,但是CountDownLatc的后续线程只能是主线程,不能是分线程; 而CyclicBarrier的后续线程可以是分线程(自定义一个线程)
- Semaphore:信号量 (类似于阻塞队列)
- 用于并发线程数的控制
(比如固定的几个停车位,每个线程就是一个车,抢停车位) //定义一个停车位的对象 Semaphore semaphore = new Semaphore(8); //线程拿到停车位 semaphore.acquire(); //线程释放停车位 semaphore.release();
寄语:做颗星星,有棱有角,还会发光