既然导致可见性和有序性的问题分别为 缓存和优化,那么最简单的解决方案便是禁用缓存 禁用编译优化
但是永久禁用必然会带来性能降低.故需要按需进行禁用.
简单说几个按需禁用缓存和编译优化的方法有 valatile synchronized final
1.何为volatile
直接来说就是禁用CPU缓存,使用这个修饰符修饰的变量,就是告诉编译器,这个变量的读写,不使用CPU的缓存,从内存中直接读写
2.final的使用
final关键字就是让编译器知道,这个变量不会发生改变,可以使劲的对其进行优化,现在来说,在开发中,对于不会发生改变的变量,尽量使用final来进行修饰
3.对于互斥锁的使用
这也是为了解决原子性问题,原子性问题其主要原因是因为线程切换的问题,如果可以按需禁用线程切换就能解决这个问题
在单核的时代,那么不会出现原子性的问题,对于多核的时代中,在同一时刻,有两个线程执行
保证单个核上线程连续的执行对于并发的问题并没有实质的解决
那么如何保证在多核的状态下,如何保证同一时刻只有一个线程执行
这就是互斥锁的引入
简单来说一个锁的状态,就是在线程进入临界区之前,先进行加锁,如果加锁成功进行临界区内
如果失败,则尝试等待,直到锁的线程解锁后成功加锁
完整的模型如下
Java自带的锁技术 Synchronized
Synchronized可以用来修饰代码块,修饰方法
常见于
class X {
// 修饰非静态方法 synchronized void foo() { // 临界区 } // 修饰静态方法 synchronized static void bar() { // 临界区 } // 修饰代码块 Object obj = new Object(); void baz() { synchronized(obj) { // 临界区 } } } |
Synchronized隐式的进行了加锁和解锁操作,并不像lock和unlock,再忘记解锁的时候回出现无限等待的情况,直接在代码块中书写,出了代码块自然解锁
锁和受保护资源的关系
常见的关系就是 N个受保护的资源对应一个锁
所以,多个对象,可以利用同一把锁进行保存起来
那么如何利用一把锁来保护多个资源呢
1.保护没有关联关系的资源
假设有过一个账户类 ,类中有两个成员变量,账号余额balance 账号密码password
常见一个final对象来作为锁
这一把锁对这两个成员变量其携带的所有方法加上相同的锁,这样就可以对其利用同一把锁来进行管理,但这种操作就变成了串行化了,
故建议对于不同的资源用不同的锁进行细粒度的管理
将余额分配一把锁,password分配一把锁,这种锁叫细粒度锁
1.保护没有关联关系的资源
假设有过一个账户类 ,类中有两个成员变量,账号余额balance 账号密码password
常见一个final对象来作为锁
这一把锁对这两个成员变量其携带的所有方法加上相同的锁,这样就可以对其利用同一把锁来进行管理,但这种操作就变成了串行化了,
故建议对于不同的资源用不同的锁进行细粒度的管理
将余额分配一把锁,password分配一把锁,这种锁叫细粒度锁
2.对于有关联关系的资源,如何上锁
对于有关联关系的资源,比如转账,有一个是本账户,有一个是转到的账户(即目标账户)
这样的话,由于我们直接加锁的话,只会加载this.balance,并不是目标账户的balance
导致在目标账户同时进行增加金钱的时候,事务A增加的被事务B覆盖了
如何避免并解决这种问题呢
就是让我们的锁可以覆盖所有受保护的资源就可以了
一种效率低下但是可以保证的方案就是
使用这个类 Account.class作为这个锁
这个类是所有Account实例对象共享的,是放在方法区的,大可不必担心唯一性,
使用Account作为锁的话,不用担心唯一性,代码虽然简单可用了,但效率低了
于是,可以总结导出,何为原子性 就是不可分割,不可分割只是外在表现,本质是多个资源之间具有一致性的要求,操作的中间状态不可见,
解决原子性问题的最好解决方案,就是保证中间状态对外不可见
如何应对这个问题,直接使用类做锁的话,虽然安全简单,但是效率极其低下
为此我们需要进行相关的优化
比如,将两个对象作为锁,一个转入对象,一个转出对象,在整个转账方法中,尝试同时获取到两个对象的锁
只有两者都拿到了,才会进行转账操作
那么逻辑的执行顺序就是
如果能获取到两个对象的锁,直接进行执行
如果只有其中之一,则先进行获取,然后等待另个一对象被释放
如果都没有,就持续尝试获取
这样使用了两道锁进行实现我们的处理,看起来很好,但是会带来一个问题,可能出现死锁的情况
就是,线程A去获取到了对象A,等待对象B的释放,但是线程B获取到了对象B,等待着A的释放
两者都不释放导致了死锁的诞生
关于死锁的具体解释,可以为 一组互相竞争的资源的线程因为互相等待,导致的”永久”阻塞的问题
关于如何预防死锁,就首先了解到死锁产生的四种必备条件
1.共享资源被多个线程占用
2,占有且等待 线程T1已经取得共享资源A,在等待共享资源B的时候,不释放A
3.不可抢占,强行抢占线程A的资源
4.循环等待,线程T1等待着T2的资源,T2等待着T1的资源
对于上面给出的防止死锁,只需要破坏这四种条件的其中之一就可以了
对于1,无法进行修改
对于占有且等待,则可以一次性的申请所有的资源
可以采用一个管理类Manager类,做到在申请的时候,由管理类来进行查看
只有两个资源都空闲的时候才会给与资源,不然就等待吧
对于不可抢占,也是采用主动释放的机制
这个实现很简单,但是不要直接使用Synchronized来去做到,如果直接使用Synchronized,那么会进行阻塞
但是Lock就可以完美的解决这个问题
对于循环等待,可以采用按序号来申请资源来做到预防,防止循环等待
在通常遇到多线程问题的时候,利用现实世界的模型来构思解决的方案
通常来说细粒度的锁都会伴随着死锁的问题
既然可能出现死锁的问题,那么就需要进行防止死锁的问题,而且要根据防止死锁的方案中选择成本最小的方案,保证性能