当前位置: 代码迷 >> 综合 >> 多线程——内存可见性问题及wait/notify
  详细解决方案

多线程——内存可见性问题及wait/notify

热度:14   发布时间:2023-12-01 22:01:23.0

synchronized的基本使用:

1、把synchronized加到普通的方法上:相当于把锁对象指定为this了

synchronized public void increase(){//加锁count ++;
}
//相当于进入方法就加锁,退出方法就解锁

2、把synchronized加到代码块上:锁对象自己指定

public void increase(){synchronized(this){//锁对象,如果要是针对某个代码块加锁,就需要手动指定锁对象是啥(针对哪个对象加锁)count ++;}
}
//相当于进入代码块就加锁,退出代码块就解锁

3、把synchronized加到静态方法上:针对类对象加锁(因为没有this)

synchronized public static void func(){}//同理也可以写成如下:
class Conter{public static void func(){synchronized(Conter.class){//Counter.class为类对象}}
}

synchronized的本质操作是修改了Object对象中的“对象头”里面的一个标记

当两个线程同时针对一个对象加锁,才会产生竞争

当两个线程针对不同对象加锁,就不会有竞争


synchronized也叫监视器锁monitor lock


synchronized的特性:

1、互斥:         

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待

进入 synchronized 修饰的代码块, 相当于 加锁

退出 synchronized 修饰的代码块, 相当于 解锁

synchronized用的锁是存在Java对象头里的。

2、刷新内存:起到和volatile相同的操作

3、可重入:同一个线程针对同一个锁连续加锁两次,如果出现了死锁就是不可重入,如果不会死锁,就是可重入

死锁:就像:一码通崩了,修复一码通的程序员要去公司修复程序,到了公司楼下,被保安拦住了,但是保安要求出示一码通才能进去,但是此时程序员进不去修补不了一码通就出示不了一码通,此时就是死锁 


关于死锁:

情况一:一个线程针对同一个锁连续锁两次     

一个线程针对同一个锁连续锁两次会怎样?   

synchronized public void increase(){synchronized(this){count ++;}
}

正常来说会导致死锁,因为外层锁是进入方法开始加锁的,当前锁没有被占用可以加锁成功;里层锁不能枷锁成功,因为当前锁被外层占用着,等外层锁释放之后,里层锁才能加锁成功。但是外层锁要执行完整个方法才能释放,要想执行整个方法,就得让里层锁加锁成功,此时互相矛盾,构成死锁

但是synchronized实现了可重入锁,所以上述加锁操作不会导致死锁                                   

关于可重入锁的内部原理:可重入锁的内部会记录当前的锁是被哪个线程占用的,同时也会记录一个“加锁次数”     

就像给线程a加锁,第一次加锁的时候,可以加锁成功,所内部记录了当前锁的占用者是a,同时记录加锁次数为1,后续再对a进行加锁,此时就不是真加锁了,只是给加锁次数自增,后续再解锁时,只是把次数减1,当锁的次数减到0的时候就真正解锁了       

情况二:两个线程,两把锁

就比如说姐妹俩出去看电影,姐姐买好了两个人的票,然后跟妹妹说你先把我的票钱给我,我再给你你的票,妹妹说你先把我的票给我,我再给你我都票钱,此时姐妹俩相当于两个线程,票和钱相当于两把锁,此时也构成了死锁

情况三:N个线程,M把锁       

//类似于这种代码
synchronized(a){synchronized(b){synchronized(c){}}
}

解决死锁问题(从解决环路等待入手) 

可以给每个筷子(筷子相当于锁)编号,约定让哲学家(哲学家相当于线程)拿筷子时先拿编号小的筷子,再拿编号大的筷子(相当于约定好顺序),此时当五个哲学家同时拿筷子就不会出现死锁了~


出现死锁的原因:

1、互斥使用:一个锁被一个线程占用了之后,其他线程占用不了(锁的本质,保证原子性)

2、不可抢占:一个锁被一个线程占用了之后,其他的线程不能把这个锁给抢走

3、请求和保持:当一个线程占据了多把锁之后,除非是显式的释放锁,否则这些锁始终都是被该线程持有的(就像哲学家拿筷子,先拿左手边的筷子,再拿右手边的筷子,但是当拿右手边筷子的时候拿不起来,右手边的筷子在被占用,此时虽然右手的筷子拿不起来,但也不把左手拿到的筷子放下

4、环路等待:A等B,B等C,C等A

要想避免死锁,关键要解决环路等待问题~


如何解决环路等待问题呢?

多把锁加锁的时候有固定的顺序即可~所有的线程都遵守同样的规定顺序,就不会出现环路等待


Java标准库中的线程安全的类:

Vector (不推荐使用)         HashTable (不推荐使用)         ConcurrentHashMap

StringBuffer         String
除了String外的线程安全的类之所以安全是因为:在一些关键的方法上都有synchronized,有了这个操作,就可以保证在多线程的环境下,修改同一个对象,没什么大问题
String之所以安全,是因为String是不可变对象(没有提供public的修改属性的操作),无法在多个线程中修改同一个String,就算是单线程都没法修改String

Java标准库中的线程不安全的类:

ArrayList         LinkedList         HashMap         TreeMap         HashSet         TreeSet
StringBuilder

关于volatile关键字: 

一、volatile可以禁止编译器进行优化,保证内存可见性


 上面模型也可以用JMM(Java Memory Model-》java内存模型)来表示

JMM其实就是把上述的硬件结构,在Java中用专门的术语又重新封装了一遍~具体的工作过程都是一样的,就是名字变了~

JMM就是把原来的CPU(包含cache)改叫成工作内存,把原来的内存改叫为主内存


当然,CPU和内存中间还有一个内存缓冲区cache

 之所以有这个内存缓冲区cache,是因为CPU从内存中取数据,取的太慢了,尤其是频繁取的时候,此时要想加快速度,可以将这些需要频繁取的数据直接放到寄存器里,后面直接从寄存器中读~但是存在一个问题,就是寄存器空间很小,而且很贵~

为了解决这个问题就出现了cache这个存储空间,这个空间比寄存器大,比内存小,速度比寄存器慢,但比内存快,此时刚刚那些需要频繁取出的数据就可以不用放在寄存器里了,直接可以放在cache缓存里

当然,缓存不止一个,有多级缓存

注意:L1、L2、L3都是CPU中的一个部分


二、volatile不保证原子性


关于wait和notify方法:用于处理线程调度的问题

wait和notify都是Object对象的方法,调用wait方法的线程,就会陷入阻塞,阻塞到有其他线程通过notify来通知


wait内部的三个功能:

1、先释放锁       2、等待其他线程的通知       3、收到通知后,重新获取锁,并继续往下执行


因此要想使用wait/notify就必须搭配synchronized(否则没有锁,怎么释放锁呢),并且wait哪个对象,就得针对哪个对象加锁

wait和notify的基本使用:

public static void main(String[] args) throws InterruptedException {Object object=new Object();synchronized (object){System.out.println("wait 前");object.wait();//代码走到这,就会阻塞System.out.println("wait 后");}}

  

  

 

 private static Object locker=new Object();//创建一个锁对象public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{//进行waitsynchronized (locker){System.out.println("wait 之前");try {locker.wait();//此时会阻塞} catch (InterruptedException e) {e.printStackTrace();}System.out.println("wait 之后");}});t1.start();Thread.sleep(3000);//等待3sThread t2=new Thread(()->{//进行notifysynchronized (locker){System.out.println("notify 之前");locker.notify();//此时结束等待System.out.println("notify 之后 ");}});t2.start();}

  



但是wait和notify都是针对同一个对象来操作的

例如:现在有一个对象o,有10个线程,都调用了o.wait,此时10个线程都是阻塞状态,如果调用了o.notify就会把10个其中的一个给唤醒,但唤醒哪个不确定

notifyAll,就会把所有的10个线程给唤醒

wait唤醒之后,会重新尝试获取到锁(会发生竞争) 

  相关解决方案