多线程的同步
为什么引入同步机制
多线程为什么要采用同步机制,因为不同的线程有自己的栈,栈中可能引用了多个对象,而多个线程可能引用到了堆中的同一个或多个对象,而线程的栈内存当中的数据只是临时数据,最终都是要刷新到堆中的对象内存,这里的刷新并不是最终的状态一次性刷新,而是在程序执行的过程中随时刷新(肯定有固定的机制,暂不考虑),也许在一个线程中被应用对象中的某一个方法执行到一半的时候就将该对象的变量状态刷新到了堆的对象内存中,那么再从多线程角度来看,当多个线程对同一个对象中的同一个变量进行读写的时候,就会出现类似数据库中的并发问题。
假设银行里某一用户账户有1000元,线程A读取到1000,并想取出这1000元,并且在栈中修改成了0但还没有刷新到堆中,线程B也读取到1000,此时账户刷新到银行系统中,则账户的钱变成了0,这个时候也想去除1000,再次刷新到行系统中,账号的钱变成0,这个时候A,B都取出1000元,但是账户只有1000,显然出现了问题。针对上述问题,假设我们添加了同步机制,那么就可以很容易的解决。
怎样解决这种问题呢,在线程使用一个资源时为其加锁即可。访问资源的第一个线程为其加上锁以后,其他线程便不能再使用那个资源,除非被解锁。
代码:
package com.java.test;
/**
* Created by xiaofandiy03 on 2018/4/14.
*/
public class DrawMoneyTest {
public static void main(String[] args)
{
Bank bank = new Bank();
Thread t1 = new MoneyThread(bank);// 从银行取钱
Thread t2 = new MoneyThread(bank);// 从取款机取钱
t1.start();
t2.start();
}
}
class Bank{
private int money =1000;
public int getMoney(int number)
{
if(number <0)
{
return -1;
}else if(number >money) {
return -2;
}else if(money <0)
{
return -3;
}else {
try {
Thread.sleep(1000);
}catch (InterruptedException e)
{
e.printStackTrace();
}
}
money-=number;
System.out.println("Left Money :" +money);
return number;
}
}
class MoneyThread extends Thread
{
private Bank bank;
public MoneyThread(Bank bank)
{
this.bank = bank;
}
@Override
public void run()
{
System.out.println(bank.getMoney(1000));
}
}复制代码
怎么解决这种问题呢,解决的方案是加锁。
你想要进行对一组加锁的代码进行操作吗?想的话就先拿到锁,拿到锁之后就可以操作被加锁的代码,倘若拿不到锁的话就只能等着,因为等的线程太多了,这就是线程的阻塞。
竞态条件和内存可见性
竞态条件
当多线程访问和操作同一对象的时候计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件
最常见的竞态条件为:
- 先检测后执行。执行依赖于检测的结果,而检测结果依赖于多个线程的执行时序,而多个线程的执行时序通常情况下是不固定不可判断的,从而导致执行结果出现各种问题。
- 延迟初始化(最典型即为单例)
上文中说到的加锁就是为了解决这个问题,常见的解决方案有:
- 使用synchronized关键字
- 使用显式锁(Lock)
- 使用原子变量
内存可见性
关于内存可见性问题要先从内存和cpu的配合谈起,内存是一个硬件,执行速度比CPU慢几百倍,所以在计算机中,CPU在执行运算的时候,不会每次运算都和内存进行数据交互,而是先把一些数据写入CPU中的缓存区(寄存器和各级缓存),在结束之后写入内存。这个过程是及其快的,单线程下并没有任何问题。
但是在多线程下就出现了问题,一个线程对内存中的一个数据做出了修改,但是并没有及时写入内存(暂时存放在缓存中);这时候另一个线程对同样的数据进行修改的时候拿到的就是内存中还没有被修改的数据,也就是说一个线程对一个共享变量的修改,另一个线程不能马上看到,甚至永远看不到。
这就是内存的可见性问题。
解决这个问题的常见方法是:
- 使用volatile关键字
- 使用synchronized关键字或显式锁同步
线程同步方法
同步方法:
即有synchronized关键字修饰方法。悠悠java每个对象都有一个内置锁,放用关键字修饰方法时,内置所会保护整个方法。在调用该方法钱,获得内置锁,否则就处于阻塞状态。
class Bank{
private int money =1000;
public synchronized int getMoney(int number)
{
if(number <0)
{
return -1;
}else if(number >money) {
return -2;
}else if(money <0)
{
return -3;
}else {
try {
Thread.sleep(1000);
}catch (InterruptedException e)
{
e.printStackTrace();
}
}
money-=number;
System.out.println("Left Money :" +money);
return number;
}
}复制代码
synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类
同步代码块
即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
class Bank{
private int money =1000;
public int getMoney(int number)
{
synchronized (this) {
if (number < 0) {
return -1;
} else if (number > money) {
return -2;
} else if (money < 0) {
return -3;
} else {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
money -= number;
}
System.out.println("Left Money :" +money);
return number;
}
}
复制代码
同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
使用重入锁实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用
eentrantLock具有和synchronized相似的作用,但是更加的灵活和强大。
它是一个重入锁(synchronized也是),所谓重入就是可以重复进入同一个函数,这有什么用呢?
假设一种场景,一个递归函数,如果一个函数的锁只允许进入一次,那么线程在需要递归调用函数的时候,应该怎么办?退无可退,有不能重复进入加锁的函数,也就形成了一种新的死锁。
重入锁的出现就解决了这个问题,实现重入的方法也很简单,就是给锁添加一个计数器,一个线程拿到锁之后,每次拿锁都会计数器加1,每次释放减1,如果等于0那么就是真正的释放了锁。
volatile 关键字
当一个共享变量被volatile修饰的时候,他会保证变量被修改之后立马在内存中更新,另一线程在取值时候需要去内存中读取新的值。
volatile可以保证变量的内存可见性,但是不能保证原子性,对于b++这个操作来说,并不是一步到位的,而是分好几步的,读取白那两,定义常量1,变量b加1,结果同步到内存。虽然在每一步中获取的都是变量的最新值,但是没有保证b++的原子性,自然无法做到线程安全。
使用局部变量实现线程同步
如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。现在明白了吧,原来每个线程运行的都是一个副本,也就是说存钱和取钱是两个账户,知识名字相同而已。所以就会发生上面的效果。
a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题