多线程
synchronized和Lock的区别
1、Lock是java中的一个接口 synchronized是内置的关键字
2、synchronized发生异常会自动释放锁,因此不会出现死锁的现象Lock需要我们手动的加锁和解锁,如果没有解锁,会出现死锁现象 因此需 要再finally中进行解锁
3、synchronized非公平锁 Lock锁默认也是非公平锁 但是传入参数 可以让其变成公平锁
4、Lock可以提高多线程进行读操作的效率
5、Lock的tryLock可以尝试获取锁,获取不到进行锁中断,而synchronized做不到 会一直等下去
6、Lock锁可以通过条件变量进行精确唤醒,而synchronized做不到
线程之间通信
多个线程之间相互协作完成一些事情
synchronized实现线程间的通信
wait() 等待 当监视器对象/锁对象调用wait方法时会导致当前的线程阻塞,停止运行,放弃cpu使用权,会释放锁
notify() 唤醒 当监视器对象/锁对象调用notify方法时,会从当前因为调用wait()方法而导致阻塞的线程中随即唤醒一个,让其回到就绪 状态,得到CPU后继续执行
notifyAll() 当监视器对象/锁对象调用notifyAll方法时,会唤醒所有因调用wait方法而阻塞的线程,回到就绪状态,得到CPU后继续执行
package day01;
public class M11 extends Thread{
private static int num = 0;
private static Object o = new Object();
@Override
public void run() {
while(num<=10){
synchronized (o){
o.notify();
System.out.println(Thread.currentThread().getName()+"输出:"+num);
num++;
if(num<11){
try {
o.wait(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
注意点:
1、上述的三个方法必须放在synchronized修饰的代码块或者方法里
2、上述的三个方法必须使用同一个锁对象
3、这三个方法是0bject中的方法
4、wait(参数) 是毫秒值 在规定的时间内 如过有线程将其唤醒 则回到就绪状态 等待获取cpu 资源开始执行
如果超过规定时间没有任何线程将其唤醒 则过了时间自动唤醒 回到就绪状态 等待获取cpu 资源开始执行
Lock实现线程间的通信
Lock:实现线程间的通信
1、需要多个线程共享同一个锁对象
2、创建Condition类型的对象 使用该对象实现线程间的通信 等待和唤醒
3、await()方法实现等待功能 唤醒 signal =>notify signalAll ===> notifyAll
4、在使用上述方法之前需要进行加锁 使用完成后需要解锁
5、等待和唤醒 必须使用同一个Condition对象 才能实现线程间的通信
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class M12 extends Thread{
private static int num = 1;
private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
@Override
public void run() {
while(num<=10){
lock.lock();
condition.signal();
System.out.println(Thread.currentThread().getName()+"输出:"+num);
num++;
if(num<11){
try {
condition.await();
}catch (Exception e){
}
}
lock.unlock();
}
}
}
如何使用Lock实现精确唤醒
假设有了个线程 a,b,c,要求三个线程一起进入到就绪态,执行时一定要按照 a–>b–>c的顺序执行
public class MyThread {
public static void main(String[] args) throws InterruptedException {
int num = 1;
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
new Thread(){ // num = 3
@Override
public void run() {
try{
lock.lock();
if(num!=3) {
condition3.await();
}
System.out.println("c");
}catch (Exception e){
}finally {
lock.unlock();
}
}
}.start();
new Thread(){ // num = 2
@Override
public void run() {
try{
lock.lock();
if(num!=2) {
condition2.await();
}
System.out.println("b");
condition3.signal();
}catch (Exception e){
}finally {
lock.unlock();
}
}
}.start();
new Thread(){ // num = 1
@Override
public void run() {
try{
lock.lock();
if(num!=1) {
condition1.await();
}
System.out.println("a");
condition2.signal();
}catch (Exception e){
}finally {
lock.unlock();
}
}
}.start();
}
}
阻塞队列
public class MyThread {
public static void main(String[] args) throws InterruptedException {
//构造方法中的参数指定队列的大小
ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(1);
//放数据
arrayBlockingQueue.put("1");
//再次向队列放数据,放不进去,会阻塞
// arrayBlockingQueue.put("2");
arrayBlockingQueue.take();
//取不到数据,也会阻塞
arrayBlockingQueue.take();
System.out.println("程序执行完毕");
}
}
public class MyThread {
public static void main(String[] args) throws InterruptedException {
ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(1);
new Thread(){
@Override
public void run() {
while (true){
try {
arrayBlockingQueue.put("炸鸡");
System.out.println("放了一只炸鸡");
Thread.sleep(1000);
}catch (Exception e){
}
}
}
}.start();
new Thread(){
@Override
public void run() {
while (true){
try {
System.out.println("取了"+arrayBlockingQueue.take());
Thread.sleep(1000);
}catch (Exception e){
}
}
}
}.start();
}
}
源码:
ArrayBlockingQueue<String > arrayBlockingQueue = new ArrayBlockingQueue<>(1);
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity]; 创建长度为1的数组用来保存元素
lock = new ReentrantLock(fair); 创建锁
notEmpty = lock.newCondition(); 创建两个条件变量 进行线程的阻塞和唤醒
notFull = lock.newCondition();
}
arrayBlockingQueue.put("abc")源码:
public void put(E e) throws InterruptedException {
checkNotNull(e); 检查传入的元素不能为null 如果为null 会抛出异常
final ReentrantLock lock = this.lock; 创建锁
lock.lockInterruptibly();如果线程没有中断,则线程获取锁
try {
count数组中元素的个数 items.length数组的长度
相等说明数组满了,对应线程进行等待notFull.await(),释放锁
不相等,则将添加的元素放到数组中,会对取数据的线程进行唤醒 notEmpty.signal()
唤醒那些被notEmpty.await()阻塞的线程
enqueue(e)该方法做的事情
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
System.out.println(arrayBlockingQueue.take());
arrayBlockingQueue.take()源码:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); //当前获取数据的线程获取锁
try {
如果元素的个数为0,数组中没有元素,对应的线程阻塞
如果元素不等于0,数组中有元素,获取元素dequeue()
1、从数组中拿数据
2、唤醒那些添加数据的线程 notFull.signal()
唤醒那些被notFull.await()阻塞的线程
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
防止虚假唤醒
在Java中,当使用wait()
和notify()
或notifyAll()
方法进行线程间通信时,可能会遇到“虚假唤醒”的情况。虚假唤醒指的是,即使没有其他线程调用notify()
或notifyAll()
来唤醒等待在条件上的线程,等待的线程也可能被意外地唤醒。这可能是由于操作系统级别的一些原因,或者 JVM 的内部调度策略导致的。
为了避免因虚假唤醒而导致的错误执行流程,通常推荐在等待条件的代码块中使用while
循环而不是if
语句来检查条件。这样做有以下几个关键原因:
- 循环检查条件:当使用
while
循环时,即使线程被唤醒,它也会立即重新检查条件是否仍然满足。如果条件不满足(即可能是由于虚假唤醒),线程会再次调用wait()
方法进入等待状态,而不是错误地继续执行后续代码。 - 确保正确性:通过持续循环并检查条件,可以确保只有当条件真正满足时,线程才继续执行其后的逻辑。这样可以维护程序的逻辑正确性和数据一致性。
内存可见性
一个线程对共享变量的修改对其他线程来说是可见的,volatile 可以保存内存的可见性
public class Money {
public volatile static int money = 1000;
}
public class T2 extends Thread{
@Override
public void run() {
Money.money=100;
System.out.println("money变成100了");
}
}
public class T1 extends Thread{
@Override
public void run() {
while (Money.money==1000){
}
System.out.println("t1线程执行完毕");
}
}
public class MyThread {
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1();
T2 t2 = new T2();
t1.start();
Thread.sleep(1000);
t2.start();
}
}
Volatile关键字
MESI缓存一致性协议
MESI缓存一致性协议: 多个CPU从主内存读取同一个数据到各自得高速缓存,当其中某个cpu修改了缓存里得数据,该数据会马上同步回主内存,其他cpu通过总线嗅探机制可以感知到数据得变化从而将自己缓存里得数据失效,从主内存在从新拉取一份新的数据到工作内存。volatile关键字会开启总线得MESI缓存一致性协议
**
**
线程2修改值以后,会经过总线,然后写回主内存。volatile开启总线MESI缓存一致性协议,每个cpu 都会监听总线,当知道其他cpu修改了变量值,立刻会失效自己工作内存中得值。重新去主内存取值。线程2修改值了以后,会在store之前加锁,锁住主内存得值,这期间其他线程的等锁释放后才能访问,主内存得值修改后会释放锁,这样保障了其他线程读取到得是最新得值。这里锁得颗粒度很小,所以影响忽略不计。这就是Volatile保障可见性得基本原理。
**
**
怎么保障读取是最新得值呢?
volatile缓存可见性实现原理底层实现主要是通过汇编LOCK前缀指令,它会锁定这块内存区域得缓存(缓存行锁定)并回写到主内存
volatile关键字的作用
1、volatile开启总线MESI缓存一致性协议,每个cpu 都会监听总线
2、将工作内存中的更改后的变量及时刷新回到主内存
3、通知其他线程对应工作内存中的变量失效,及时去主内存中拉取最新的值
synchronized关键字的作用
1、线程获得锁
2、清空变量副本
3、拷贝共享变量最新的值到变量副本中
4、执行代码
5、将修改后变量副本中的值赋值给共享数据===> 将工作内存中的值刷新回主内存
6、释放锁
可见性: 一个线程对共享变量的修改,对于其他线程是可见的
禁止指令重排序: 在对不改变最终的结果的情况下,jvm会对我们写的代码进行重新排序,达到优化的目的,synchronized修饰的方法或者代码块中的代码禁止指令重新排序的,就按照编写的顺序执行,volatile也是可以实现的.
原子性: synchronized可以保证原子性 volatile不能保证原子性
CAS
比较并交 Compare And Swap
需要三个值: 主内存中的值 v 期望值 a 要改变的值 b【新的值】
1、读取主内存中的值给期望值
2、再次获取主内存中的值和上一步获取到的期望值进行对比
3、如果对比的结果不相等 说明有别的线程将主内存中的值进行了改变 此时期望值不是最新的
此时我们不能使用新的值去替换主内存中的值
如果对比的结果相等,此时我们使用新的值去替换主内存中的值
原子类型: AtomicInteger 自旋锁+CAS
常用的方法: incrementAndGet() 先加1 然后在获取结果
getAndIncrement() 先获取结果 然后在加1
get() 获取最终的结果
getAndSet 返回旧的值 然后再用新的值覆盖旧的值
…
弊端:会产生性能的消耗 自旋锁 空转
import java.util.concurrent.atomic.AtomicInteger;
/**
AtomicInteger 原子类型
*/
public class T3 extends Thread{
public static AtomicInteger a = new AtomicInteger(0);
@Override
public void run() {
for(int i=1;i<=1000;i++){
a.incrementAndGet();
}
}
}
public class MyThread {
public static void main(String[] args) throws InterruptedException {
T3 t1 = new T3();
T3 t2 = new T3();
t2.start();
t1.start();
Thread.sleep(1000);
System.out.println(T3.a.get());
}
}
compareAndSwapInt
方法包含了四个参数,当前对象,偏移量,期望值,新值;如果当前对象对应偏移量的值与期望值相同,则把当前对象的值更改为新值,并返回true;否则返回false
compareAndSwapInt方法为原子操作,同一时刻只能有一个线程来操作,即使中断,其他线程也无法进入。在进行操作时,前面加了一个LOCK前缀指令,该指令的作用:
1、对内存的读-改-写操作原子执行,原因是:Lock指令可以锁总线,其他cpu对内存的读写请求都会被阻塞,直到锁释放。锁总线开销大,后来采用锁缓存替代锁总线
2、禁止该指令与之前和之后的读和写指令重排序。
3、把写缓冲区中的所有数据刷新到内存中,同时让其他cpu的缓存失效,从而重新从主内存拉取最新的数据
上面的第1点保证了CAS操作是一个原子操作,第2点和第3点保证了CAS同时具有volatile读和volatile写的特性
ABA问题
ABA问题是指在CAS操作时,其他线程将变量值A改为了B,但是又被改回了A,等到本线程使用期望值A与当前变量进行比较时,发现变量A没有变,于是CAS就将A值进行了交换操作,但是实际上该值已经被其他线程改变过,这与乐观锁的设计思想不符合。ABA问题的解决思路是,每次变量更新的时候把变量的版本号加1,那么A-B-A就会变成A1-B2-A3,只要变量被某一线程修改过,改变量对应的版本号就会发生递增变化,从而解决了ABA问题。在JDK的java.util.concurrent.atomic包中提供了AtomicStampedReference来解决ABA问题,该类的compareAndSet是该类的核心方法
AtomicInteger的源码流程
CAS+自旋锁
- 内部使用
volatile
修饰的 int 变量:AtomicInteger
内部有一个被volatile
修饰的 int 变量,用于存储整数值。volatile
关键字确保了变量的可见性,使得对该变量的读写操作具有原子性。 - 使用 CAS 操作进行原子更新:
AtomicInteger
中的原子操作方法都是基于 CAS 操作实现的。CAS 操作包括比较内存中的值与预期值是否相等,如果相等则更新为新值,否则操作失败。CAS 操作利用底层硬件提供的原子指令,确保操作的原子性。 - 循环重试:由于 CAS 操作可能在多线程环境下失败,因此
AtomicInteger
在实现中使用了循环重试的机制。如果 CAS 操作失败,它会不断尝试进行 CAS 操作,直到成功为止。
erruptedException {
T3 t1 = new T3();
T3 t2 = new T3();
t2.start();
t1.start();
Thread.sleep(1000);
System.out.println(T3.a.get());
}
}
**==compareAndSwapInt==**
**方法包含了四个参数,当前对象,偏移量,期望值,新值;如果当前对象对应偏移量的值与期望值相同,则把当前对象的值更改为新值,并返回true;否则返回false**
**==compareAndSwapInt方法为原子操作,同一时刻只能有一个线程来操作,即使中断,其他线程也无法进入。在进行操作时,前面加了一个LOCK前缀指令,该指令的作用:==**
**==1、对内存的读-改-写操作原子执行,原因是:Lock指令可以锁总线,其他cpu对内存的读写请求都会被阻塞,直到锁释放。锁总线开销大,后来采用锁缓存替代锁总线==**
**2、禁止该指令与之前和之后的读和写指令重排序。**
**3、把写缓冲区中的所有数据刷新到内存中,同时让其他cpu的缓存失效,从而重新从主内存拉取最新的数据**
**上面的第1点保证了CAS操作是一个原子操作,第2点和第3点保证了CAS同时具有volatile读和volatile写的特性**
#### ==ABA问题==
ABA问题是指在CAS操作时,其他线程将变量值A改为了B,但是又被改回了A,等到本线程使用期望值A与当前变量进行比较时,发现变量A没有变,于是CAS就将A值进行了交换操作,但是实际上该值已经被其他线程改变过,这与乐观锁的设计思想不符合。ABA问题的解决思路是,每次变量更新的时候把变量的版本号加1,那么A-B-A就会变成A1-B2-A3,只要变量被某一线程修改过,改变量对应的版本号就会发生递增变化,从而解决了ABA问题。在JDK的java.util.concurrent.atomic包中提供了AtomicStampedReference来解决ABA问题,该类的compareAndSet是该类的核心方法
#### AtomicInteger的源码流程
CAS+自旋锁
1. 内部使用 `volatile` 修饰的 int 变量:`AtomicInteger` 内部有一个被 `volatile` 修饰的 int 变量,用于存储整数值。`volatile` 关键字确保了变量的可见性,使得对该变量的读写操作具有原子性。
2. 使用 CAS 操作进行原子更新:`AtomicInteger` 中的原子操作方法都是基于 CAS 操作实现的。CAS 操作包括比较内存中的值与预期值是否相等,如果相等则更新为新值,否则操作失败。CAS 操作利用底层硬件提供的原子指令,确保操作的原子性。
3. 循环重试:由于 CAS 操作可能在多线程环境下失败,因此 `AtomicInteger` 在实现中使用了循环重试的机制。如果 CAS 操作失败,它会不断尝试进行 CAS 操作,直到成功为止。