上一篇我们说到线程并发与并行在于我们看来都是多线程,多线程之间它们会共享当前进程的资源,在共享的过程中,会出现一系列的问题,如数据“脏读,死锁等问题。线程的原子性,有序性,可见性以及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能够保证任一时刻只有一个线程执行该代码块,从而保证了原子性。
[图片上传失败...(image-2a4716-1576676775299)]
在以上红色选中的三个部分,线程都有可能进行切换,而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已经初始化完毕
本文深入探讨Java并发编程的核心概念,包括线程间的资源共享、原子性、有序性与可见性等关键特性。通过具体实例,解析如何利用synchronized、Lock及volatile关键字确保数据一致性,避免脏读、死锁等问题。
1244

被折叠的 条评论
为什么被折叠?



