需要回顾之前博文《多线程的问题》
一、 CPU对于两个冒险的解决办法
- 结构冒险(CPU对某一个存储器读取资源为例):
使用同步,依赖硬件提供同步指令。 - 数据冒险:
二、多线程对问题的解决办法
- <b>2.1 安全问题的代码</b>
package com.tinygao.thread.safe;
public class UnSafe {
private int value;
public int getNext() {
return value++;
}
}
- <b>2.2 为什么不安全</b>
A/B代表两个不同线程,不安全执行错误情况
<b>!!因为它具备4个特性。永远记住这四点,绝大部分只要这4点!!</b>
- <b>有可变的状态(以下三个地方代表类是有状态的特征)</b>
1、有类变量
2 、有实例变量(本例子中的value属于这类)
3、有其他对象的引用(比如map.entry对象)
</br>
-
<b>复合操作(value++包含三个原子操作)</b>
1、读取value
2、value+1计算
3、将value写入主存
</br> -
<b>顺序没有控制</b>
B线程没有等待A操作完,就读取了value。导致执行两次加法,但最终结果都是10
</br> -
<b>状态不可见</b>
A线程中value值的改变,对B来说不可见。线程栈内存数据是互相隔离的,看不到! -
为什么不安全之提问
1、没有状态的类,是线程安全的 (√)
2、只要使用线程安全的类写出来的代码块一定是线程安全的(×)
==>多个安全类在一起成了复合操作了,加上没有控制顺序的手段,可能会出现不可预测的结果。
3、不可变对象一定是线程安全的(√)
==>什么是不可变对象?
1、对象创建之后其状态就不能修改。
2、对象的所有状态都是final类型
3、对象是正确创建的(this引用没有逸出)
↓对于第二问看concurrentMap是线程安全的,用了他的代码块可不是线程安全的,来举个栗子吧↓
package com.tinygao.thread.safe;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* Created by gsd on 2017/1/26.
*/
@Slf4j
public class UnSafe {
private int num;
private Map<String, String> map = Maps.newConcurrentMap();
public int getNumAdd() {
return num++;
}
public String getMapValue() {
if(!map.containsKey("tinygao")) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put("tinygao", Thread.currentThread().getName());
}
return map.get("tinygao");
}
public static void main(String[] args) {
UnSafe unSafe = new UnSafe();
ExecutorService es = Executors.newFixedThreadPool(
2,
new ThreadFactoryBuilder().setNameFormat("map-%d").build());
es.submit(()->{
log.info("map {}",unSafe.getMapValue());
});
es.submit(()->{
log.info("map {}",unSafe.getMapValue());
});
}
}
- 我们的本意:当第一个线程判断不存在mapkey的时候去填充这个key的值,之后的其他线程只要从map get出来这个key就可以了。
-
事实:两个线程都判断了mapkey不存在,都各自put了一把到map上。导致mapkey的值被覆盖了。打印结果(你的可能跟我不一样):↓
[map-1] INFO com.tinygao.thread.safe.UnSafe - map map-1
[map-0] INFO com.tinygao.thread.safe.UnSafe - map map-0 -
<b>2.3 解决安全问题</b>
对应上面的四个特性取反:
<b>1、去可变状态</b>
<b>2、复合操作改成原子操作(记住两个常见的复合操作)</b>
-- if-then操作(像上面map的例子)
-- 取-读-写(像上面value++的例子)
<b>3、控制执行顺序,即保证有序性</b>
<b>4、若有状态,则让状态在线程间互相可见</b>
</br>
<b>2.3.1、怎么去可变状态</b>
- 不再是成员变量、实例变量了。比如:把他移入到方法内部,这样就在自己的线程栈中了
public int getNumAdd() {
int num = 0;
return num++;
}
- 不在线程之间共享该状态(让状态封闭)
private ThreadLocal<Integer> num = new ThreadLocal<>();
public int getNumAdd() {
num.set(num.get()+1);
return num.get();
}
- 让状态做到不可变
final inti num = 1
再看一个不安全的
```
package com.tinygao.thread.safe;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
//这个是不安全的
@Slf4j
public class Unfinal {
private Integer lastNumber;
private Integer currentNumber;
public void setLastNumber(Integer i) {
this.lastNumber = i;
}
public Integer getCurrentNumber(Integer i) {
if(lastNumber == null || !lastNumber.equals(i)) {
return null;
}
else {
return 1;
}
}
public static void main(String[] args) {
Unfinal unfinal = new Unfinal();
ExecutorService es = Executors.newFixedThreadPool(2);
es.submit(()->{
unfinal.setLastNumber(1);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//预期打印是1,看看实际打印多少?
log.info("get first {}", unfinal.getCurrentNumber(1));
});
es.submit(()->{
unfinal.setLastNumber(2);
//预期打印是null,看看实际打印多少?
log.info("get seconde {}", unfinal.getCurrentNumber(1));
});
es.shutdown();
}
}
看一个final安全的,保证在构造函数中初始化一次状态后不可变了
package com.tinygao.thread.safe;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@Slf4j
public class FinalClass {
private final Integer lastNumber;
private final Integer currentNumber;
public FinalClass(Integer lastNumber, Integer currentNumber) {
this.lastNumber = lastNumber;
this.currentNumber = currentNumber;
}
public Integer getCurrentNumber(Integer i) {
if(lastNumber == null || !lastNumber.equals(i)) {
return null;
}
else {
return 1;
}
}
public static void main(String[] args) {
FinalClass finalclass = new FinalClass(1, 1);
ExecutorService es = Executors.newFixedThreadPool(2);
es.submit(()->{
log.info("get first {}", finalclass.getCurrentNumber(1));
});
es.submit(()->{
log.info("get seconde {}", finalclass.getCurrentNumber(1));
});
es.shutdown();
}
}
<b> 2.3.2、怎么将复合操作变成原子操作</b>
- "读-操作-写" 使用Atomic类
private AtomicInteger num2 = new AtomicInteger(0);
public int getNumAdd2() {
return num2.incrementAndGet();
}
- "if-then" 使用java自带的原子操作
private Map<String, String> map = Maps.newConcurrentMap();
public void safeMap() {
map.putIfAbsent("tinygao", Thread.currentThread().getName());
}
<b> 2.3.3、怎么控制执行顺序</b>
- 同步
1、synchronized
2、volatile类型的变量(但复合操作变量就有问题了)
3、显式锁
4、原子变量(long和double可能不是原子变量,看处理器架构)
<b> 2.3.4、怎么让状态在线程间可见</b>
- 同步
1、synchronized
2、volatile类型的变量(但复合操作变量就有问题了)
3、显式锁
4、原子变量(long和double可能不是原子变量,看处理器架构)
采用互斥。对于共享资源访问的<b>代码片段</b>叫做临界区
有多种方法,比如锁变量(0/1)、严格轮换法
概念:管程、信号量、互斥量、同步、阻塞、协作、互斥。
https://zh.wikipedia.org/wiki/%E7%9B%A3%E8%A6%96%E5%99%A8_(%E7%A8%8B%E5%BA%8F%E5%90%8C%E6%AD%A5%E5%8C%96)
198.35.26.96 zh.wikipedia.org
待续~
网友评论