这个系列的博客主要是对Java高级编程中多线程相关的知识点做一个梳理,内容主要包括程序、进程、线程、并行、并发等相关概念的理解,创建多线程的几种方式,Thread类,线程的同步机制,线程通信等几个大的模块。该系列博客的文章如下,推荐按顺序阅读:
- Java多线程大闯关–走进多线程.
- Java多线程大闯关–线程的调度、优先级、生命周期.
- Java多线程大闯关–线程的同步机制.
- Java多线程大闯关–线程通信.
- Java多线程大闯关–volatile关键字.
Java对于多线程的安全问题提供了专业的解决方式:同步机制。对多条操作共共享数据的语句,只能让一个线程执行完,在执行过程中,其他线程不可以参与执行。前面所说的synchronized就是一把锁,多个线程抢这把锁,谁抢到了谁就上,其它的线程只能等那个线程爽完了把锁释放了才能再抢一次,实际上这抢来抢去的不免失了和气,而且频繁的线程上下文切换和调度增加了使用和执行成本。volatile是轻量级的volatile,在多处理器开发中保证共享变量的“可见性”,意思就是CPU1中的某个线程把某个用volatile修饰了的变量修改了,其他CPU中的线程都能看读到这个被修改后的值。
volatile的定义与实现原理
程序在运行过程中,会将运算需要的数据从物理内存中复制一份到 CPU 的高速缓存当中,计算结束之后,再将高速缓存中的数据刷新到物理内存当中。
- 定义:Java编程语言允许线程访问共享变量,为了确保共享变量能够准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。
- 原理:volatile变量修饰的共享变量进行写操作时,当前处理器缓存行的数据会被写回到系统内存,这个写回内存的操作会使得其他在CPU里缓存了该内存地址的数据无效,当处理器对这个数据进行修改时,会重新从系统内存中把数据读到缓存里。
还是通过代码来直观的看一下volatile的效果,依然是三个窗口卖票的问题。
package com.learnjiawa.mutithread;
/**
* 例子:创建三个窗口卖票,总票数为100张.使用继承Thread类的方式
* 存在线程的安全问题,待解决。
*/
class Window extends Thread{
private static int ticket = 100;
@Override
public void run() {
while(true){
if(ticket > 0){
//睡一会,增加线程安全问题出现的机率
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":卖票,票号为:" + ticket);
ticket--;
}else{
break;
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
Window t1 = new Window();
Window t2 = new Window();
Window t3 = new Window();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
将ticket关键字用volatile修饰一哈,再运行
private static volatile int ticket = 100;
你以为这就可以保证线程安全了吗,too young too simple,volatile关键字只能保证变量的可见性,不能保证操作的原子性,比如A线程修改了该共享变量的值,之后B线程读取到的了A线程修改后的值,但是这时候这CPU没有选择紧接着执行B线程的写操作,而是安排C线程也读到了该值,这使得两个线程都已经完成了从系统内存中读取到缓存中的操作,然后分别直接进行了对该变量的写操作,出现了重票现象。所以必须将这个读写操作变成原子性的操作(即一个操作或者多个操作要么全部执行,并且执行的过程不会被任何因素打断,要么就都不执行)。
代码更改如下:
package com.learnjiawa.jihe;
import java.util.concurrent.atomic.AtomicInteger;
/**
*
* 例子:创建三个窗口卖票,总票数为100张.使用继承Thread类的方式
*
* 存在线程的安全问题,待解决。
*/
class Window extends Thread{
private static AtomicInteger ticket = new AtomicInteger(100);
@Override
public void run() {
while(true){
if(ticket.get() > 0){
//睡一会,增加线程安全问题出现的机率
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":卖票,票号为:" + ticket.getAndDecrement());
}else{
break;
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
Window t1 = new Window();
Window t2 = new Window();
Window t3 = new Window();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
哎哟,重票现象没有了,但是还有个小尾巴,在最后的时候,有一个线程前一个线程睡觉的时候悄悄跑了进去,等到前一个线程输出后,它也醒了,但这时它读到了前一个线程修改过后的值“0”,但是这个线程已经过了门禁了,所以输出了个-1。不过这里总算把线程安全问题的重票问题解决了,这个小尾巴解决起来应该简单,就不搞啦,这里主要是了解一下volatile关键字和原子操作类,哦,忘了说,原子操作类源码里面是依赖了volatile关键字的,具体的底层原理等以后写并发专题的时候再写把,这里先简单了解。
参考文献
[1]Bruce Eckel.Java编程思想(第4版)[M].机械工业出版社,2008:650-757.
更多
对我的文章感兴趣,持续更新中…