目录
1. 为什么引入同步机制
多线程为什么要采用同步机制,因为不同的线程有自己的栈,栈中可能引用了多个对象,而多个线程可能引用到了堆中的同一个或多个对象,而线程的栈内存当中的数据只是临时数据,最终都是要刷新到堆中的对象内存,这里的刷新并不是最终的状态一次性刷新,而是在程序执行的过程中随时刷新(肯定有固定的机制,暂不考虑),也许在一个线程中被应用对象中的某一个方法执行到一半的时候就将该对象的变量状态刷新到了堆的对象内存中,那么再从多线程角度来看,当多个线程对同一个对象中的同一个变量进行读写的时候,就会出现类似数据库中的并发问题。
假设银行里某一用户账户有1000元,线程A读取到1000,并想取出这1000元,并且在栈中修改成了0但还没有刷新到堆中,线程B也读取到1000,此时账户刷新到银行系统中,则账户的钱变成了0,这个时候也想去除1000,再次刷新到行系统中,账号的钱变成0,这个时候A,B都取出1000元,但是账户只有1000,显然出现了问题。针对上述问题,假设我们添加了同步机制,那么就可以很容易的解决。
怎样解决这种问题呢,在线程使用一个资源时为其加锁即可。访问资源的第一个线程为其加上锁以后,其他线程便不能再使用那个资源,除非被解锁。
2. 竞态条件和内存可见性
线程和线程之间是共享内存的,当多线程对共享内存进行操作的时候有几个问题是难以避免的,竞态条件和内存可见性。
2.1 竞态条件
当多线程访问和操作同一对象的时候计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件
最常见的竞态条件为:
- 先检测后执行。执行依赖于检测的结果,而检测结果依赖于多个线程的执行时序,而多个线程的执行时序通常情况下是不固定不可判断的,从而导致执行结果出现各种问题。
- 延迟初始化(最典型即为单例)
上文中说到的加锁就是为了解决这个问题,常见的解决方案有:
- 使用synchronized关键字
- 使用显式锁(Lock)
- 使用原子变量
2.2 内存可见性
关于内存可见性问题要先从内存和cpu的配合谈起,内存是一个硬件,执行速度比CPU慢几百倍,所以在计算机中,CPU在执行运算的时候,不会每次运算都和内存进行数据交互,而是先把一些数据写入CPU中的缓存区(寄存器和各级缓存),在结束之后写入内存。这个过程是及其快的,单线程下并没有任何问题。
但是在多线程下就出现了问题,一个线程对内存中的一个数据做出了修改,但是并没有及时写入内存(暂时存放在缓存中);这时候另一个线程对同样的数据进行修改的时候拿到的就是内存中还没有被修改的数据,也就是说一个线程对一个共享变量的修改,另一个线程不能马上看到,甚至永远看不到。
这就是内存的可见性问题。
解决这个问题的常见方法是:
- 使用volatile关键字
- 使用synchronized关键字或显式锁同步
3. 线程同步方法
3.1 synchronzied
同步代码块
每个java对象都有一个互斥锁标记,用来分配给线程,synchronized(o){ } 对o加锁的同步代码块,只有拿到锁标记的线程才能够进入对o加锁的同步代码块。
同步方法
synchronized作为方法修饰符修饰的方法被称为同步方法,表示对this加锁的同步代码块(整个方法都是一个代码块)。
3.2 JDK1.5的锁 Lock
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
ReenreantLock类的常用方法有:
//创建一个锁对象
Lock lock = new ReentrantLock();
//上锁(进入同步代码块)
lock.lock();
//解锁(出同步代码块)
lock.unlock();
//尝试拿到锁,如果有锁就拿到,没有拿到不会阻塞,返回false
tryLock();
注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用
ReentrantLock具有和synchronized相似的作用,但是更加的灵活和强大。
synchronized和ReentrantLock的区别
- 两者都是互斥锁,所谓互斥锁:同一时间只有一个拿到锁的线程才能够去访问加锁的共享资源,其他的线程只能阻塞
- 都是重入锁,用计数器实现;
所谓重入就是可以重复进入同一个函数
假设一种场景,一个递归函数,如果一个函数的锁只允许进入一次,那么线程在需要递归调用函数的时候,应该怎么办?退无可退,有不能重复进入加锁的函数,也就形成了一种新的死锁。
重入锁的出现就解决了这个问题,实现重入的方法也很简单,就是给锁添加一个计数器,一个线程拿到锁之后,每次拿锁都会计数器加1,每次释放减1,如果等于0那么就是真正的释放了锁。
- ReentrantLock独有特点
- ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁
- ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程
- ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制
3.3 volatile关键字
volatile 修饰符 用来保证可见性
当一个共享变量被volatile修饰的时候,他会保证变量被修改之后立马在内存中更新,另一线程在取值的时候需要去内存中读取新的值。
注意:尽管volatile 可以保证变量的内存可见性,但是不能够保存原子性,对于b++这个操作来说,并不是一步到位的,而是分为好几步的,读取变量,定义常量1,变量b加1,结果同步到内存。虽然在每一步中获取的都是变量的最新值,但是没有保证b++的原子性,自然无法做到线程安全