可见性
可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。
对于串行程序来说,可见性是不存在的,因为我们在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个变量值,并且是修改过的新值。
但在多线程环境中可就不一定了,由于线程对共享变量的操作都是线程拷贝到各自的工作内存进行操作后才写回到主内存中的,这就可能存在一个线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题,另外指令重排以及编译器优化也可能导致可见性问题。
如何解决可见性问题?
- 解决方法1:加volatile关键字保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中读取新值
- 解决方法2:使用synchronized和Lock保证可见性。因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中
上下文切换
时间片决定了一个线程可以连续占用处理器运行的时长
当一个线程的时间片用完,或者因自身原因被迫暂停运行,此时另一个线程会被操作系统选中来占用处理器
- 上下文切换(Context Switch):一个线程被暂停剥夺使用权,另一个线程-被选中开始或者继续运行的过程
- 切出:一个线程被剥夺处理器的使用权而被暂停运行
- 切入:一个线程被选中占用处理器开始运行或者继续运行
- 切出切入的过程中,操作系统需要保存和恢复相应的进度信息,这个进度信息就是上下文
上下文切换的诱因
程序本身触发的自发性上下文切换、系统或虚拟机触发的非自发性上下文切换
- 自发性上下文切换
- sleep、wait、yield、join、park、synchronized、lock
- 非自发性上下文切换
- 线程被分配的时间片用完、JVM垃圾回收(STW、线程暂停)、线程执行优先级
/**
* ClassName: VolatileVisibilitySample
* Description: 可见性demo
*
* @author blade
* @version 1.0
* @Date 2020/12/20 21:37
*/
public class VolatileVisibilitySample {
/**
* 全局成员变量
*/
private boolean initFlag = false;
public void refresh(){
this.initFlag = true; //普通写操作.
String threadname = Thread.currentThread().getName();
System.out.println("线程:"+threadname+":修改共享变量initFlag");
}
public void load(){
String threadname = Thread.currentThread().getName();
int i = 0;
while (!initFlag){
i++;
}
System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变"+i+initFlag);
}
public static void main(String[] args){
VolatileVisibilitySample sample = new VolatileVisibilitySample();
Thread threadA = new Thread(()->{
sample.refresh();
},"threadA");
Thread threadB = new Thread(()->{
sample.load();
},"threadB");
threadB.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadA.start();
}
}
上述代码,只会打印:线程:threadA:修改共享变量initFlag
然后threadB的工作内存中的initFlag一直没有刷新,会一直循环。
问题1:为什么会一直循环,没有发生上下文切换?
答:死循环,在实时操作系统里,除非有高优先级抢占,否则无法释放CPU资源。
只更新load方法,增加成员变量,其他不变。
static Object object = new Object();
List<Integer> a = new ArrayList<>();
public void load(){
String threadname = Thread.currentThread().getName();
int i = 0;
while (!initFlag){
/**
方法1
*/
synchronized (object){
i++;
}
/**
方法2
*/
i++;
System.out.println(i);
/**
方法3
*/
a.add(i);
/**
方法4
*/
Object[] ww = new Object[10000];
//等等还有很多....
}
System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变"+i+initFlag);
}
上面的办法,都会导致java线程发生上下文切换,进而实现了工作内存和主内存的数据同步。
方法1和2本质是一样的,都是synchronized导致上下文切换,看sout代码可知底层一样加了synchronized。
public void println(int var1) {
synchronized(this) {
this.print(var1);
this.newLine();
}
}
方法3和方法4,本质也一致。存在大量内存分配,而cpu计算速度远大于内存存取速度,故cpu空闲,发生上下文切换。
问题2:保持load方法不变,只取消main方法中的睡眠,同样也会终止循环,线程会感知到initFlag发生改变。
public class VolatileVisibilitySample {
/**
* 全局成员变量
*/
private boolean initFlag = false;
public void refresh(){
this.initFlag = true; //普通写操作,(volatile写)
String threadname = Thread.currentThread().getName();
System.out.println("线程:"+threadname+":修改共享变量initFlag");
}
public void load(){
String threadname = Thread.currentThread().getName();
int i = 0;
while (!initFlag){
i++;
}
System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变"+i+initFlag);
}
public static void main(String[] args){
VolatileVisibilitySample sample = new VolatileVisibilitySample();
Thread threadA = new Thread(()->{
sample.refresh();
},"threadA");
Thread threadB = new Thread(()->{
sample.load();
},"threadB");
threadB.start();
threadA.start();
}
}
由打印可知:threadB已经进入了while循环,并把i的值加到了13882,但此时嗅探到了initFlag发生改变,结束了循环。证明此时threadB发生了上下文切换,更新了工作内存中的数据。
but我也不晓得具体什么情况,等大佬留言救救我👀