多线程的三个特性
多线程要保证并发线程正确执行,必须要保证三个特性。
原子性(互斥性)
- 一个或多个操作不能被分割,要么全部执行,要么就都不执行。
可见性
- 多个线程访问同一个变量,一个线程修改了这个变量,别的线程能立即看到修改的值。
有序性
- 程序执行的顺序按照代码的先后顺序执行。但是处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行顺序和编写顺序一致。但最终结果是一致的。
synchronized可保证原子性和可见性。但不能保证有序性。
volatile可保证可见性和禁止指令重排。但不能保证原子性。
Lock接口间接借助了volatile关键字间接地实现了可见性和有序性。
volatile
初始flag = false
创建一个线程,循环判断flag标志,如果flag为true,则结束循环
在主线程中开启线程,然后休眠2秒,再修改falg为true
public class VisibilityTest implements Runnable{
public boolean flag = false;
@Override
public void run() {
while (true) {
if (flag) {
System.out.println("循环结束!");
break;
}
}
}
}
public class volatileTest {
public static void main(String[] args) {
VisibilityTest visibilityTest = new VisibilityTest();
new Thread(visibilityTest).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
visibilityTest.flag = true;
System.out.println("main线程结束");
}
}
如果falg被修改为true,此时线程中的循环应该被退出
但是结果如下
main线程结束
(程序未停止...)
分析
虽然flag被修改为true,但是在子线程中并未获取到修改后的值
因为程序存在主存中,为了提高效率,线程执行后将值存储到了自己的缓存中,这样每次就不必去操作主存,从而提高效率。
主线程修改的是自己缓存中的值,而有可能还没来得及更新到主存中,也有可能子线程没来得及更新主存中的值,所以子线程看到的还是自己缓存中的内容,所以在主线程中修改了值,在子线程中并未看到。
那么如何解决呢?
只需要增加volatile关键字即可
- volatile关键字保证了线程修改的变量值会立即被刷新到主存中,从而保证了变量在各个线程中都是可见的
public volatile boolean flag = false;
再次运行,程序就会立刻停止了
main线程结束
循环结束!
面试题:i++是原子操作吗?
i++是原子操作吗?
答:不是,因为 i++ 分三步执行:取值、修改、替换
测试
public class Add {
private int num;
public int addNum() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return this.num++;
}
}
public class AddAddTest {
public static void main(String[] args) {
Add add = new Add();
for(int i = 0; i < 10; i++) {
new Thread(()-> System.out.print(add.addNum() + " ")).start();
}
}
}
预期结果应该是输出0到9不同的值,但是不保证顺序
结果
1 2 3 4 5 0 0 0 0 5
出现了连续4个0,和预期结果不符
分析
i++不是原子性操作,分三步:取值、修改、替换
在多线程下有可能会出问题,比如在A取到i的值为0的时候,还没等到修改,B也获取到了i的值为0,然后修改、替换i的值变为了1,此时又轮到了A执行,然后A修改、替换,也从0修改为1,此时就造成了错误
解决:
第一种方法:加锁
添加synchronized关键字,以保证操作同步
public class Add {
private int num;
public int addNum() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this) {
return this.num++;
}
}
}
第二种方式:使用原子变量
public class Add {
private AtomicInteger num = new AtomicInteger(0);
public int addNum() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取值并自增一
return num.getAndIncrement();
}
}
结果
5 2 0 3 8 9 1 4 7 6
0到9十个不同的数字,结果正确,说明此方法保证了自增操作是原子操作