【多线程】线程安全以及synchronized锁的总结

在进行多线程编程过程中,避免不了一个重要的问题:线程安全问题,下面就线程安全问题进行阐述,以及有关线程安全的锁进行总结。

线程安全:

<1>我们为啥要进行多线程编程?

原因有几个

一:一个进程的创建和销毁所消耗的开销比较大,而线程是操作系统调度的最小单位,执行一些普通任务的时候并不需要用到进程这样的任务开销。

二:CPU一般是多核的,利用多线程进行并发编程,可以高效地利用CPU的处理器进行任务,提高任务执行效率。

三:在一些多IO操作的任务时候,不需要占用很多CPU的资源,于是CPU约等于被闲置不用,那么这个时候就可以进行并发编程,进行CPU的高效利用。

<2>线程如何创建?

在关于线程如何创建,我在之前写过了一篇关于Thread类的创建线程运用:(22条消息) 【多线程】Thread类的基本用法_SPMAX的博客-CSDN博客

<3>线程安全问题如何出现?

进行多线程编程的时候,由于此时有多个线程对内存中的资源进行读取和存放,此时会出现很多问题,当我们用单线程的思维去理解我们多线程编程任务的执行的时候,得到的多线程任务执行的结果和我们按照单线程的思维去执行的结果不一致的时候,那就是发生了线程安全问题。

原子性:

在我们在编译器执行一行命令或者一条语句的时候,在我们看来只是一条命令,并不会发生什么线程安全问题,但是在进行编译和汇编的时候,编译器所执行的一条指令可能在底层中是两三条机器语言组合而成的。例如说我们执行一条c=a+b的指令的时候,在底层中其实是,CPU从内存中读取地址a和地址b上的数值,放在CPU寄存器中,然后在CPU的寄存器中执行加法算数运算,然后将结果放回内存中的c的地址上,将c地址上的数值更新为a+b的值,因此在底层中,一条C指令或者是Java指令其实有很多句机器指令组成。

在知道了一句Java指令和C指令有可能有很多句机器指令组成以后,在谈回多线程的时候,我们平常在编写代码程序的时候,只有main这一个线程进行操作,因此并不会发生线程安全问题,在有多线程执行任务的时候,我们想象成多条main线程同步执行,但是这个同步执行是也有先后执行顺序的,可能差个几微秒等等,这要涉及到操作系统中线程的调度,在宏观上我们可以看作是同步执行,在微观上其实是有先后顺序执行的,然后在微观上有着先后执行顺序,但是内存只有一个,内存上的地址上的数值也只有一份,但是线程有很多个,这多个线程的执行顺序又有先后,在执行的时候可能会有线程A先读取内存的数值,然后进行修改后放回内存,此时线程B又再去读取内存中的数值进行运算后放回内存,这个时候线程B读取的内存数值已经被修改了,但是再线程B的指令中内存中数值还是线程A修改前的数据,此时线程B获取到内存数据就不对了,此时运算得到的结果在线程B中也不正确了。

因此这类问题一般叫做原子性问题,即如何避免多线程中在修改同一个内存上的数值的时候发生同时进行,而执行的机器操作数量却有很多条的时候,发生线程间指令执行顺序错乱导致线程获取到的数值是错误的问题等等。

内存可见性:

在考虑了在底层的机器指令有多条的问题的原子性之后,还需要考虑编译器优化的问题。

编译器优化一般发生在我们写的程序进行编译和汇编的时候,编译器自动帮助我们优化。

优化一般发生在我们写的程序执行一些重复的指令,例如一个while循环里面一直有一条指令被执行,然后一直循环的时候,编译器就会自动帮我们优化,比如说,在while循环里面有a=c这种赋值指令的时候,c的数值在内存上,然后读取c的数值的时候需要从CPU向内存读取数值,但是由于编译器发现这条读取指令在while循环里面一直重复,然后从CPU到内存读取的时间比较久,在编译的时候编译器帮我们进行优化成从内存读取c数值后存放在CPU的缓存中,下次进行读取的时候直接从CPU内部的缓存中直接读取c的数值就行,不用到内存中读取。

但是在进行多线程编程的时候,当有另一个线程执行指令的时候将上述c的数值进行修改了,但是上述有while执行的线程执行的获取c的数值是从内存获取一次然后放在CPU缓存中,然后就从缓存中读取数值c了,于是另一个线程对c进行修改了,但是上述被编译器优化的线程并不能拿到最新被修改的数值c,此时两个线程并发执行得到的数值就跟我们想象的不一样了。

还有另一种编译器优化的情况,学名叫做指令重排序,就是编译器将多条机器指令进行重新排序进行优化,导致另一个线程获取同一个资源的时候得到的结果是错误的情况,内存可见性和指令重排序都是编译器优化带来的问题。

synchronized锁

synchronized锁是一个解决线程安全问题的非常常见和重要的锁

它是一个关键字,可以对一个对象进行加锁,也可以对一个共享资源属性进行加锁。

在创建类的时候,在类里面对一个静态方法进行加锁,相当于对该类进行加锁,在编写代码的时候不需要new对象就可以直接拿到被加锁的类了;在创建类的时候,对类里面的一个非静态方法用synchronized进行加锁,在编写代码的时候,new一个对象的时候该对象才是被加锁的,而new不同的对象加的锁不一样,所以当需要对对象进行加锁然后使用该有锁的对象的时候,务必要多个线程都只是用同一个对象,因为只有该对象被加了这把锁,如果new另一个对象而另一个对象就是加了另一把锁。

synchronized可以解决原子性问题:锁的目的就是对共享的资源进行上锁,当一个线程遇到这个需要上锁的对象的时候,首先判断该锁是否被占有,如果没有被占有则获取该锁,并将该资源上锁,此时该共享资源如果被另外一个线程获取到了,判断这个资源的锁是否被占有的时候就会发现这个锁已经被之前那个线程占有,此时会发生阻塞等待,进入阻塞队列进行等待,此时第一个获取到锁的线程就会单一地得到这个共享资源,不会再有别的线程来干扰,等该线程执行完了指令后释放锁,此时另一个阻塞等待的线程就会被系统调度后唤醒,获取到这个锁再获取到共享资源继续执行,保证了多条机器指令只被一条线程执行,解决的了原子性问题。

synchronized锁的其他性质:

是一个非公平锁,公平锁的概念就是,多条线程获取公共资源的时候,是按照获取的时间先后顺序进行获取公关资源,而不遵循按获取时间先后顺序来获取锁的话就是非公平锁。

synchronized进行加锁的时候,不会考虑后序线程获取该共享资源的时间先后顺序,而是直接由线程调度决定,当多个线程获取到该共享资源的时候,除了第一个获取到共享资源,其他的线程发生阻塞等待,当锁被第一个线程释放了,此时后序阻塞的线程被系统调度唤醒是不确定的,于是后可能是后面获取该共享资源的锁先被唤醒然后发现锁没有被占用然后获取到锁,获取到共享资源,这就是非公平锁。

是可重入锁,可重入锁意味着synchronized可以自己锁自己,即不会发生死锁,当一个synchronized对一个资源进行上锁,在内部可以继续进行对该共享资源进行获取锁,并不会发生循环导致死锁。

synchronized会发生锁膨胀:

在对一个资源进行上锁的时候,过程大致是这样的:先进行无锁状态->偏向锁->轻量级锁->重量级锁

无锁:就是不加锁,此时判断该共享资源不会发生锁竞争,不进行加锁。

偏向锁:此时给该资源进行标记,但是不加锁,判断该资源可能发生锁竞争,但是概率不大,加上标记。

轻量级锁:此时判断会发生锁竞争,在用户态上对资源进行获取锁和加锁,当没有获取到锁的时候,会进入自适应循环获取锁的模式,调用CAS原子指令和while循环一直获取锁,此时如果锁被释放了,可以第一时间获取到锁,但是由于一直在尝试获取锁,占用 CPU的资源,导致资源消耗大。

重量级锁:此时判断锁竞争激烈,轻量级的自适应锁循环获取到达一定时间后获取不到锁就就从用户态转向内核态对锁进行获取和加锁,进入内核态的时候,判断锁是否被占有,若锁被占有,则放弃获取锁,即放弃CPU占用,进入阻塞队列等待系统调度唤醒后继续尝试获取锁,此时消耗的资源小,但是并不能在锁被释放后第一时间获取到锁,消耗时间长,效率低。

相关锁知识:

CAS:

全程Compare And Swap,即比较并交换,是硬件底层里面提供的一些原子指令,是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,后向上被操作系统封装等等然后得到的一系列具有原子性的操作。

例如类AtomicInteger 等等Atomic相关的原子类就是底层是CAS进行作用的,可以使得该对象是具有原子性的,不会因为由多个线程同时调用而发生线程安全问题。

还由轻量级锁的循环获取锁的实现,也是依靠CAS的。

ABA问题:

即当多个线程同时获取一个值的时候用CAS操作,但是在获取数值进行比较的时候,有可能该数值是被修改后得到的数值,此时数据跟用于比较的数值是一样的,但是是被修改过后得到一样的数据,类似于偷梁换柱,比如说CAS的数据是50,但是需要比较的数据是50然后被修改成了100后又被修改成了50,此时调用CAS的时候就获取到50进行比较然后符合后就继续执行Swap,但是这里被修改的50看似没什么问题,但是有一些业务可能会需要考虑到中间进行修改的100数值等等,此时该被修改过的50就不正确了,就不能获取该被修改过后的50,而是要获取被修改前的50;

借用版本号来进行可以解决ABA问题,即当50被修改前版本号是1,被修改成100后版本号变成2,然后被修改回50后版本号变成3,此时CAS获取该50的时候需要的版本号是1,此时版本号为3的50就不能被交换。

上述AtomicInteger不需要考虑数值是否被修改,只需要保证数值是对应于CAS锁需要比较的数值就行,因此不需要考虑ABA问题。

volatile关键字

由于是关键字,可以对一个属性进行修饰,但是不像synchronized可以对对象进行加锁,用于解决编译器优化问题,即上述讲到的内存可见性和指令重排序问题,取消编译器的优化,保证了多线程获取数据的时候数据是第一时间被修改后的最新数据。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值