上一篇我们说到线程并发与并行在于我们看来都是多线程,多线程之间它们会共享当前进程的资源,在共享的过程中,会出现一系列的问题,如数据“脏读,死锁等问题。线程的原子性,有序性,可见性以及volatile关键字和synchronized。
Java内存模型
从上图我们知道Java内存模型规定了所有的变量都存储在主内存中。每条线程中有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
在这里就出现了一些问题,线程A改变了变量i,那么线程B会及时的获取到吗?那么A改变了i,会及时的写入主内存中吗?要回答这些问题,我们首先要了解几个概念:原子性,有序性,可见性。
原子性
1.定义
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
2.例子
就说银行转账的例子,A给B转1000,那么分这几步,从账户A减去1000元,往账户B加上1000元。试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。
3.java的原子性
在Java编程过程中。怎么样的表达式才有原子性,那怎么样才能保证原子性呢。平常来说就一次操作的都有原子性,多次操作的我们可以用锁保证原子性(synchronized和Lock来实现)
如:
x = 10; //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4
语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。
语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。
所以上面4个语句只有语句1的操作具备原子性。
也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,从而保证了原子性。
在以上红色选中的三个部分,线程都有可能进行切换,而A线程进来了,突然到B进程,那么B进程改变了num=0;按道理来说已经没有票了,但是到A时,他已经进来了,就输出买票了。如果在一个线程进入到if中之后,当cpu切换到其他线程上时,不让其他的线程进入if语句,那么就算线程继续执行当前其他的线程,也无法进入到if中,这样就不会造成错误数据的出现。于是出现了用锁保证原子性(synchronized和Lock来实现)
synchronized(任意的对象(锁) )
{
写要被同步的代码
}
//书写售票的示例
class Demo implements Runnable{
//定义变量记录剩余的票数
int num = 100;
//创建一个对象,用作同步中的锁对象
Object obj = new Object();
//实现run方法
public void run() {
//实现售票的过程
while( true ) {
// t1 t2 t3
//判断当前有没有线程正在if中操作num,如果有当前线程就在这里临时等待
//这种思想称为线程的同步
//当票数小等于0的时候,就不再售票了
//使用同步代码块把线程要执行的任务代码可以同步起来
synchronized( obj ) //t1 在进入同步之前线程要先获取锁
/*
当某个线程执行到synchronized关键字之后,这时JVM会判断当前
同步上的这个对象有没有已经被其他线程获取走了,如果这时没有其他
线程获取这个对象,这时就会把当前同步上的这个对象交给当前正要进入
同步的这个线程。
*/
{
if( num > 0 )
{
//t0
try{Thread.sleep(2);}catch( InterruptedException e ){}
System.out.println(Thread.currentThread().getName()+"....."+num);
num--;
}
}//线程执行完同步之后,那么这时当前这个线程就会把锁释放掉
}
}
}
class ThreadDemo {
public static void main(String[] args) {
//创建线程任务
Demo d = new Demo();
//创建线程对象
Thread t = new Thread( d );
Thread t2 = new Thread(d);
Thread t3 = new Thread(d);
Thread t4 = new Thread(d);
//开启线程
t.start();
t2.start();
t3.start();
t4.start();
}
}
可见性
当然只一个原子性,也是不能保证数据的正确,还要有可见性。
1.定义
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
2.例子
还是上面的例子,如果synchronized这个只有原子性的话,那么保证了同一时刻只有一个线程执行,假如num=1,但是A线程改变了num=0,没有写入到主内存中,B线程获取到的数据还是1,也会有数据问题。所以通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。但是本身就是原子性的表达式,我们可以用volatile关键字修饰,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
如:
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
很多人在中断线程时可能都会采用这种标记办法,但是这样会出现问题的,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。
那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。但是用volatile修饰之后就变得不一样了:
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。
那么线程1读取到的就是最新的正确的值。
注意:volatile不能保证原子性,但是又可见性和有序性。
volatile的应用场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中。
例子:
1.状态标记量
volatile boolean flag = false;
//线程1
while(!flag){
doSomething();
}
//线程2
public void setFlag() {
flag = true;
}
有序性
//x、y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句
由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
可能语句2会在语句1之前执行,那么就有可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。
这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕
原创来源:滴一盘技术