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,就算是单线程都没法修改StringJava标准库中的线程不安全的类:
ArrayList LinkedList HashMap TreeMap HashSet TreeSetStringBuilder
关于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唤醒之后,会重新尝试获取到锁(会发生竞争)