Java 线程在运行的生命周期中6 种状态
- NEW: 初始状态,线程被创建出来但没有被调用start() 。
- RUNNABLE: 运行状态。
- BLOCKED :阻塞状态,需要等待锁释放。
- WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作 (通知或中断)。
- TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等
待 。77777- TERMINATED:终止状态,表示该线程已经运行完毕
几个锁的概念
公平锁:
多个线程按照申请锁的顺序来获取锁,先来后到,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程等待队列的第一个,就占有锁,否则就会加入到等待队列,以后会按照FIFO先进先出的规则从队列中取。
非公平锁:
多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程获取锁。在高并发的情况下,哟可能会造成优先级反转或者饥饿现象。
悲观锁
悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。
乐观锁
乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。
CAS
CAS 全名 compare and swap (比较并交换)是一种基于 Java 实现的 计算机代数系统,用于多线程并发编程时数据在无锁的情况下保证线程安全安全运行。
CAS机制 主要用于对一个变量(操作)进行原子性的操作,它包含三个参数值:需要进行操作的变量A、变量的旧值B、即将要更改的新值C。会对当前内存中的 A 进行判断看是否等同于 B ,如果相等则把 A 值更改为 C 。(CAS 的原理是期望的值和原本的一个值作比较,如果相同则更新成新的值。)
Atomic 原子类:
Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。Atomic 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
AQS,AbstractQueuedSynchronizer
AQS 为构建锁和同步器提供了一些通用功能的是实现,因此,使用 AQS能简 单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock, SynchronousQueue等等皆是基于 AQS 的。
AQS 核心思想是,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制是用 CLH 队列 实现的,即将获取不到锁的线程加入到队列中。 CLH(Craig,Landin,and Hagersten) 是一个双向队列。AQS是将每条线程封装成一个 CLH 队列的一个结点来实现锁的分配。 在 CLH 队列中,一个节点表示一个线程,它保存着线程的引用、 当前节点在队列中的状态、前驱节点、 后继节点
AQS 使用 int 成员变量 state 表示同步状态,通过内置的线程等待队列来完成获取资源线程的排队工作。
state 变量由 volatile 修饰,用于展示当前临界资源的获锁情况。
// 共享变量,使用volatile修饰保证线程可⻅性
private volatile int state;
另外,状态信息 state 可以通过 protected 类型的getState()、setState()和 compareAndSetState() 进行操作。并且,这几个方法都是 final 修饰的,在子类中无法被重写。
可重入锁,也叫递归锁
指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。
JDK 提供的所有现成的 Lock 实现类,包括 synchronized
Volatile
Volatile标记的变量就是指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。volatile 的意义就是禁用 CPU 缓存。volatile 关键字能保证数据的可⻅性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
volatile 关键字除了可以保证变量的可⻅性,还有一个重要的作用是防止 JVM 的指令重排序。 如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的内存屏障的方式来禁止指令重排序
Synchronized
synchronized 的实现使用的是 monitorenter 和 monitorexit 指令。
当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。每个对象中都内置了一个monitor对象。 wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步块中才能调用wait/notify等方法。 在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。 对象锁的的拥有者线程才可以执行 monitorexit 指令来释放锁。在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取而代之的是 CC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否为同步方法。锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率
synchronized 和 volatile 有什么区别?
synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在。
volatile 关键字是线程同步的轻量级实现, volatile性能肯定比 synchronized关键字要好 。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。
volatile 关键字能保证数据的可⻅性,但不能保证数据的原子性。 synchronized 关键字两者都能保证。
volatile关键字主要用于解决变量在多个线程之间的可⻅性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性
synchronized 和 ReentrantLock 有什么区别
1 两者都是可重入锁。
2 synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API:
synchronized 是依赖于 JVM 实现的,虚拟机团队在 JDK6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的。
ReentrantLock 是 API 层面的,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成
3 ReentrantLock 比 synchronized 增加了一些高级功:
可中断 : ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lockInterruptibly 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
公平锁 : ReentrantLock可以指定是公平锁还是非公平锁。而 synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的。
选择性通知: synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口。
Condition具有很好的灵活性,可以在一个Lock对象中可以创建多个Condition实例,线程可以注册在指定的Condition中,从而可以有选择的进行线程通知。在使用notify()/ notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,ReentrantLock类结合Condition实例可以实现“选择性通知” 。而 synchronized关键字就相当于整个 Lock 对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话会通知所有处于等待状态的线程,这样会造成很大的效率问题。而Condition实例的signalAll()方法,只会唤醒注册在该Condition实例中的所有等待线程。本质是一个分组
Semaphore 信号量
synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,而 Semaphore(信号量)可以用来控制同时访问特定资源的线程数量。
Semaphore 的使用简单,我们这里假设有 N(N>5) 个线程来获取 Semaphore 中的共享资源,下面的代码表示同一时刻N 个线程中只有 5 个 线程能获取到共享资源,其他线程都会阻塞,只有获取到共享资源的线程才能执行。等到有线程释放了共享资源,其他阻塞的线程才能获取到。
// 初始共享资源数量
final Semaphore semaphore = new Semaphore(5);
// 获取1个许可
semaphore.acquire();
// 释放1个许可
semaphore.release();
当初始的资源个数为 1 的时候,Semaphore 退化为排他锁
CountDownLatch
CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。
CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch 使用完毕后,它不能再次被使用。
CountDownLatch 是共享锁的一种实现,它默认构造 AQS 的 state 值为 count。 当线程使用 countDown() 方法时,其实以CAS 的 操作来减少 state,直至 state 为 0 。当调用 await() 方法的时候,如果 state 不为 0,那就证明任务还没有执行完毕,await() 方法就会一直阻塞,也就是说 await() 方法之后的语句不会被执行。然后,CountDownLatch 会自旋 CAS 判断 state == 0,如果 state == 0 的话,就会释放所有等待的线程,await() 方法之后的语句得到执行。
CyclicBarrier
字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是:让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开⻔,所有被屏障拦截的线程才会继续干活
happens-before 规则
int userNum = getUserNum(); // 1
int teacherNum = getTeacherNum(); // 2
int totalNum = userNum + teacherNum; // 3
•1 happens-before 2
•2 happens-before 3
•1 happens-before 3
虽然 1 happens-before 2,但对 1 和 2 进行重排序不会影响代码的执行结果, 所以 JMM 是允许编译器和处理器执行这种重排序的。但 1 和 2 必须是在 3 执行之前,也就是说 1,2 happens-before 3 。
happens-before 表达的意义并不是一个操作发生在另外一个操作的前面,它想表达的意义是前一个操作的结果对于后一个操作是可⻅的,无论这两个操作是否在同一个线程里。
常⻅规则
1. 程序顺序规则 :一个线程内,按照代码顺序,书写在前面的操作 happens-before 于书写在后面的操作
2. volatile 规则 :对 volatile 变量的写操作的结果对于发生于其后的任何操作都是可⻅的
3. 传递规则 :如果 A happens-before B,且 B happens-before C,那么 A happens-before C