线程共享内存及可能存在的问题
每个线程表示一条单独的执行流,有自己的程序计数器,有自己的栈,但线程之间可以共享内存,他们可以访问和操作相同的对象。
package com.claa.javabasic.Thread;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: claa
* @Date: 2020/03/29 21:21
* @Description:
*/
public class ShareMemoryDemo {
private static int shared = 0;
private static void incrShared() {
shared ++;
}
static class ChildThread extends Thread {
List<String> list;
public ChildThread(List<String> list) {
this.list=list;
}
@Override
public void run() {
incrShared();
list.add(Thread.currentThread().getName());
}
}
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();
Thread t1 = new ChildThread(list);
Thread t2 = new ChildThread(list);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(shared);
System.out.println(list);
}
}
输出结果
2
[Thread-0, Thread-1]
通过这个例子,想强调说明执行流、内存、程序代码之间的关系。
(1)有两条执行流,一条执行main方法,另外两条执行ChildThread的run方法
(2)不同执行流可以访问和操作相同的变量,如本例中的shared和list变量。
(3)不同的执行流可以执行相同的程序代码,如本例中incrShared方法,ChildThread的run方法,被两条ChildThread执行流执行,incrShared方法是在外部定义的,但被ChildThread 的执行流执行。在分析代码执行过程时,理解代码在哪个线程执行时很重要的。
(4)当多条执行流执行相同的程序代码时,每条执行流都有单独的栈,方法中的参数和局部变量都有自己的一份。
当多条执行流可以操作相同的变量时,可能会出现一些意料之外的结果,包括竞态条件和内存可见性问题。
1.竞态条件
所谓竞态条件是指多个线程访问和操作同一个对象时,最终执行结果与执行时序有关,可能正确也可能不正确。
package com.claa.javabasic.Thread;
/**
* @Author: claa
* @Date: 2020/03/29 22:10
* @Description:
*/
public class CountThread extends Thread{
private static int counter = 0;
@Override
public void run() {
for(int i=0; i< 1000;i++) {
counter++;
}
}
public static void main(String[] args) throws InterruptedException {
int num =1000;
Thread[] threads = new Thread[num];
for(int i = 0; i < num; i++) {
threads[i] = new CountThread();
threads[i].start();
}
for(int i =0; i < num; i++) {
threads[i].join();
}
System.out.println(counter);
}
}
期望的结是100万,但实际执行,发现每次输出的结果都不一样,一般都不是100万,经常99万,为什么呢?
因为counter++ 这个操作不是原子操作,它分为三步:
(1)取counter的当前值;
(2)在当前值基础上加1;
(3)将新值重新赋值给counter。
两个线程可能同时执行第一步,取到了相同的counter值,比如都取到了100,第一个线程执行完后counter变为101,而第二个线程执行完后还101,最终结果就是与期望不符。
怎么解决呢?
使用synchronized关键字;
使用显示锁;
使用原子变量。
2.内存可见性
多个线程可以共享访问和操作相同的变量,但一个线程对一个共享变量的修改,另一个线程不一定马上就可以看到,甚至永远也看不到。
package com.claa.javabasic.Thread;
/**
* @Author: claa
* @Date: 2020/03/29 22:35
* @Description:内存可见性
*/
public class VisibilityDemo {
private static boolean shutdown = false;
static class HelloThread extends Thread{
@Override
public void run() {
while(! shutdown) {
// do nothing
}
System.out.println("exit hello");
}
}
public static void main(String[] args) throws InterruptedException{
new HelloThread().start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
shutdown = true;
System.out.println("exit main");
}
}
输出结果
exit main
期望的结果是两个线程都退出,但实际执行时,很可能发现HelloThread 永远都不退出,也就是说在HelloThread执行流看来shutdown永远为false,即使main线程已经更改为true。
这是因为内存可见性问题。在计算机系统中,除了内存,数据还会被缓冲存在cpu的寄存器以及各级缓冲中,当访问一个变量时,可能直接从寄存器或cpu中获取,不一定到内存中去取,当修改为一个变量时,也可能是先写到缓冲中,稍后才同步更新到内存中。在单线程的程序中,这一般不是问题,但在多线程的程序中,尤其是在多cpu 的情况下,这就是严重的问题。一个线程对内存的修改,另一个线程看不到,一是修改没有及时同步到内存中,二是另外一个线程根本没有从内存读。
怎么解决呢?
使用volatile 关键字
使用synchronzied 关键字或显示同步锁。