线程加锁关键字

目录

一.synchronized关键字

1.下面先介绍一下这个关键字的简单用法

2.synchronized的特性

二.volatile关键字


一.synchronized关键字

首先synchronized这个关键字十分重要,一定得了解这个锁

1.下面先介绍一下这个关键字的简单用法

直接修饰普通方法:锁Demo16的对象(直接修饰普通方法,就相当于把锁对象指定为this了)

public class Demo16 {
    public synchronized void method(){
        
    }
}

对静态方法加锁:锁Demo16的类对象(没有this对象)

public class Demo16 {
    public synchronized static void method1(){

    }
}

对代码块加锁:明确指定锁哪个对象(this为锁对象)

public class Demo16 {
    public void method2(){
        synchronized (this){
            
        }
    }
}

锁类对象(通过反射)

public class Demo16 {
    public void method3(){
        synchronized (Demo16.class){
            
        }
    }
}

我们要知道synchronized锁的是什么,其实synchronized的本质操作是修改了Object对象的"对象头"里面的一个标记,当两个线程同时对一个对象加锁,才会发生竞争,否则是不会的!

2.synchronized的特性

(1)互斥

这个是挺好理解的,给一个线程加了锁之后,其他线程就不能再加锁了,只能等待这个线程释放锁之后,其他线程才可以进行加锁

就类似于这样的操作:一个人取钱,门上锁之后,其他人就只能在外面等待(也就是进入阻塞状态) 

注意:上一个线程解锁之后下一个线程并不是直接就获得锁,而是需要操作系统来"唤醒",这也是操作系统线程调度的一部分

例如A,B,C三个线程,线程A先获得锁,然后B尝试获得锁,再C尝试获得锁,此时B和C就会进入阻塞排队等待状态,等A释放锁之后,并不是按来的先后顺序获得锁,而是需要B和C再次竞争去获得锁

(2)刷新内存

synchronized的工作过程:

*获得互斥锁

*从主内存获得变量到工作内存

*执行代码

*将更新后的变量刷新到主内存

*释放互斥锁

其实就和volatile关键字的作用是一样的,可以保证内存可见性问题不会出现,但是要保存内存可见性的话,还是使用volatile好一些,毕竟这个是专门解决这个问题的,而编译器的优化我们也不知道它是在哪加的优化,因此使用标准库提供的volatile关键字更好一些去解决这个问题,下面我会再介绍一下这个关键字

(3)可重入

直观来说,同一个进程针对同一个锁,连续加锁两次,如果出现了死锁,就是不可重入,没有死锁就是可重入

简单解释一下死锁:其实就是一个问题进入到了死循环当中,并且无从解决,例如一个段子:疫情期间最重要的一码通崩溃了,现在程序员需要去公司解决问题,到了门口,门卫需要程序员提供一码通,程序员表示一码通崩溃了我现在就是要去解决一码通的问题,等我修复了,我才能出示一码通,但保安表示你一定要出示一码通,才能进去......然后就陷入死锁当中了,当然这只是一个段子,现在都是可以远程操作解决的,主要还是理解死锁

简单分析一下连续加锁两次会怎么样?

synchronized public void method2(){
        synchronized (this){

        }
    }

外层锁:进入方法就开始加锁,能够加锁成功

里层锁:进入代码块,开始加锁,这次就不能加锁成功了,锁已经被外层占用了,需要等外层解锁之后才能加锁成功

外层锁要执行玩整个方法,才能释放锁,但是要执行完整个方法,里面锁就必须加锁成功才能继续下去......这不就死锁了嘛!!!

针对这样的问题,其实在实际的开发是很容易写出这样的代码的,那岂不是使用锁bug更多了,显然实现JVM的大佬们早已经注意到了这一点,因此就把synchronized实现成了可重入锁,对于可重入锁再进行上述操作就不会出现死锁问题了~

可重入锁内部会记录当前的锁被哪个线程占用,同时也会记录一个"加锁次数",假如对线程A进行第一次加锁的话,会加锁成功,锁内部就会记录当前占用的是线程A,同时加锁次数为1,再次对A进行加锁的话,就不是真正的加锁了,就只把加锁的次数增加一次,后面解锁的时候,先把加锁的次数进行减一,直至次数减到0为止,才会真正进行释放锁!

这样做的意义就降低了程序员的负担(使用成本,提高了开发效率),但是也带来了代价,程序中需要有更高的开销(维护锁属于哪个线程,并且记录加减次数,降低了运行效率)

死锁的其他场景:

上面介绍的是一个线程一把锁,还有其他的场景

*两个线程两把锁:

类似于每个线程占用着一把锁,然后相互请求对方的锁,但两个线程都是竞争,不愿意"妥协",都不愿意释放自己的锁,那么两个线程的请求都得不到回应,就陷入了死锁

*N个线程,M把锁:

这种情况就更复杂一点,这个在书上都有一个经典案例:哲学家就餐问题

要发生上面的死锁,下面四个条件是必不可少的:

①互斥使用:一个锁被一个线程占用,其他线程占用不了(这是锁的本质,保证原子性)

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

③请求和保持:当一个线程占据了多把锁之后,除非显式的释放锁,否则这些锁始终都是被该线程持有的!

④环路等待:等待关系,成环了~A等B,B等C,C等A,上面三个都是属于锁本身的特性,无法修改,那么要解决死锁问题,就得从最后一点出发了

那么如何避免环路等待呢?

只要约定好,针对多把锁加锁的时候,有固定的顺序就可以了,所有的线程都遵守同样的规则顺序,就不会出现环路等待了!   比如上面的哲学家就餐问题:可以给每根筷子都编个号,然后规定每个哲学家都只能先拿编号较小的筷子,再拿编号大的,然后同时开始拿筷子,第一个哲学家可以拿到一号筷子,第二个可以拿到二号筷子,第三个拿到三号筷子,第四个哲学家拿到四号筷子,然后第五个哲学家拿筷子的时候就会发现一号筷子被占用着,第五个哲学家就会陷入等待中,那么第四号哲学家就可以拿起五号筷子去吃面条了,吃完之后就会释放四号和五号筷子,这样前面的哲学家依次都可以拿到两根筷子,这样就解决了死锁问题了! 

java中也有很多现成的类,有些是线程安全的,有些是线程不安全的,在多线程的情况下,如果使用线程不安全的类的话,就需要注意了:

线程不安全的:

ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet,StringBuilder

线程安全的:

Vector(不推荐使用),HashTable(不推荐使用),ConcurrentHashMap,StringBuffer,String

前四个在一些关键的方法上都有synchronized,有了这个操作,就可以保证在多线程环境下,修改一个对象,就没啥问题了,而String是不变对象,无法在多个线程中同时修改String,单个线程也是不可以的!

二.volatile关键字

首先这个关键字的作用是,禁止编译器优化,保证内存可见性,这里会出现这样的问题,主要还是由于计算机的硬件结构所造成的的,计算机想要执行一些计算就需要把内存的数据读到CPU寄存器中,然后在寄存器中计算,在协会到内存中,CPU访问寄存器的速度要比访问内存的速度快很多,当多次访问内存,且结果一致的时候,CPU就会想偷懒了,然后就直接去访问寄存器,不访问内存了,然后如果修改了,写回到内存后,CPU也感知不到,就发生内存可见性了

java圈子里面经常见到的一个java内存模型JMM(java Memory Model),存储更好,JMM就是把上面讲的硬件结构在java中用专门的属于又重新封装了一遍,把CPU称为工作内存(work memory),内存称为主内存(main memory),工作流程,是先把数据从从主内存加载到工作内存,然后工作内存完成计算之后,再写回主内存!这就是JMM模型,那么为什么要这样做呢?一方面因为java作为一个跨平台的编程语言,要把硬件的细节封装起来,期望程序员感知不到CPU、内存等硬件设备,另一方面java社区也经常会造出一些"高大上"的概念,这也是很正常的!!!

另外,在CPU和内存中间其实还有一个缓存器cache,由于CPU从内存读数据,取的太慢了,尤其是频繁的取,就可以吧这样的数据直接放到寄存器中,但是寄存器又是十分的珍贵,其内存也非常小,又划算不来,于是CPU又另外搞了个存储空间,这个空间比寄存器大,比内存小,速度比寄存器慢,但是比内存快,这就是缓存器(cache),而且缓存器也是分等级的

 另外一定要注意volatile不能保证原子性,只能保证可见性,不要和synchronized搞混淆!

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

栋zzzz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值