1、一共有哪几类线程安全问题?
2、哪些场景需要额外注意线程安全问题
3、什么是多线程的上下文切换?
1、线程安全
我是红色
当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作(调度、交替、执行顺序),调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。 --《Java Concurrency In Practice》
1、线程不安全
比如读操作的同时完成写操作->不安全,需额外同步
但是否需要全都线程安全?--摇考虑运行速度、设计成本、如何平衡两者?
对于完全不用于多线程的程序,不需要考虑太多多线程相关的内容
2、什么情况下会出现线程安全问题,如何避免?
- 运行结果错误:i++多线程下出现消失的请求现象
public class MultiThreadError implements Runnable {
int index = 0;
static MultiThreadError instance = new MultiThreadError();
public static void main(String[] args) throws Exception {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("两个线程相加后结果是:" + instance.index);
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
index++;
}
}
}

i++不是原子性操作,它本身是i=1,i+1,i=2这三个操作的集合,当线程1还在前2个操作的时候,线程2读到的i依然是i=1,那么线程1最终得到i=2,线程2最终也得到i=2,两次i++相当于只自加了1次。
- 活跃性问题:死锁、活锁、饥饿
/**
* 测试死锁,两个线程同时需要lock1和lock2,却先后各拿了一把锁
*/
public class DeadLock implements Runnable {
public static void main(String[] args) throws InterruptedException {
String o1 = "lock1";
String o2 = "lock2";
Thread t1 = new Thread(new DeadLock(o1, o2));
Thread t2 = new Thread(new DeadLock(o2, o1));
t1.start();
t2.start();
t1.join();
t2.join();
}
private String lock1;
private Object lock2;
public DeadLock(String lock1, String lock2) {
this.lock1 = lock1;
this.lock2 = lock2;
}
@Override
public void run() {
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + " get " + lock1);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + " get " + lock2);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
可以看出来两个线程各拿了一个锁就等待了。


- 对象发布和初始化的时候的安全问题
发布:脱离了本类来到其他类就称为发布,比如一个对象被称为public,或者return一个对象,或者把一个对象作为一个参数传到了其他方法中。
逸出:被发布到了不该发布的地方。
1、方法返回一个private对象(private的本意是不让外部访问)
/**
* 方法返回一个private对象,这个对象可能被外界修改,产生坏影响
*/
public class returnPrivate {
private Map<Integer, String> theMap;
public returnPrivate() {
theMap = new HashMap<>();
theMap.put(1, "song");
theMap.put(2, "xin");
theMap.put(3, "ran");
}
public Map<Integer, String> getMap() {
return this.theMap;
}
public static void main(String[] args) {
returnPrivate rP = new returnPrivate();
Map<Integer, String> map = rP.getMap();
System.out.println(map.get(1));
map.remove(1);
Map<Integer, String> map2 = rP.getMap();
System.out.println(map2.get(1));
}
}
结果很尴尬,第一个人调用后对得到的map作了操作,第二个无法得到原来的正确map。

解决方法
用"副本"代替成员变量
public Map<Integer, String> getMapImproved() {
return new HashMap<>(this.theMap);
}
2、还未完成初始化(构造函数没完全执行完毕)就把对象提供给外界,比如: 在构造函数中未初始化完毕this赋值;隐式逸出-注册监听事件;构造函数中运行线程。
/**
* 构造函数中初始化还没完成就使用this进行赋值
*/
public class ThisInConstruct {
static Point point;
public static void main(String[] args) throws InterruptedException {
new Thread(new MakePoint()).start();
Thread.sleep(10);
// Thread.sleep(110);
System.out.println("Point is :" + point.toString());
}
}
class Point {
private int x, y;
public Point(int x, int y) throws InterruptedException {
this.x = x;
ThisInConstruct.point = this;
Thread.sleep(100);
this.y = y;
}
@Override
public String toString() {
return "Point{" +
"x=" + x +
", y=" + y +
'}';
}
}
class MakePoint implements Runnable {
@Override
public void run() {
try {
new Point(1, 1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
当在main中,只延时10ms时,结果为

延时为110ms时,结果为

用观察者模式来实现监听,看上去没有使用this赋值,但实际上已经将内部变量隐式逸出了。
/**
* 观察者模式来实现注册监听事件
*/
public class ObserverListen {
int count;
//构造方法中就订阅一个subject
public ObserverListen(Subject subject) {
Listner listner = new Listner() {
@Override
public void response() {
System.out.println("\n 得到的count = " + count);
}
};
subject.register(listner);
//一个类中,监听其实是非主要部分,应该有一段主逻辑,用for来模拟
for (int i = 0; i < 10000; i++) {
System.out.print(i);
}
count = 10;
}
public static void main(String[] args) {
Subject sub = new Subject();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
sub.eventCome(new Event(){});
}
}).start();
ObserverListen observerListen = new ObserverListen(sub);
}
}
class Subject {
private Listner listner;
void register(Listner listner) {
this.listner = listner;
}
void eventCome(Event e) {
if (null != this.listner) {
listner.response();
} else {
System.out.println("Listener初始化未完成!");
}
}
}
interface Listner {
void response();
}
interface Event {
}
表面上看上去,诶,我没有在构造方法中去做this赋值,但是监听的部分其实相当于有做了,所以看到count并不是10。

解决方法
分析代码其实容易看出来问题,就是注册监听器的时间不对,如果在初始化真正完成后,再去注册监听器就ok了,使用工厂模式进行修正。
public class ObserverListenUpdate {
private int count;
//把监听器定义为成员变量
private Listner listner;
//构造方法中就订阅一个subject
public ObserverListenUpdate(Subject subject) {
Listner listner = new Listner() {
@Override
public void response() {
System.out.println("\n 得到的count = " + count);
}
};
this.listner = listner;
//一个类中,监听其实是非主要部分,应该有一段主逻辑,用for来模拟
for (int i = 0; i < 10000; i++) {
System.out.print(i);
}
count = 10;
}
//用工厂方法去得到实例
public static ObserverListenUpdate getInstance(Subject subject) {
ObserverListenUpdate instanceSafe = new ObserverListenUpdate(subject);
subject.register(instanceSafe.listner);
return instanceSafe;
}
public static void main(String[] args) {
Subject sub = new Subject();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
sub.eventCome(new Event(){});
}
}).start();
ObserverListenUpdate observerListen = getInstance(sub);
}
}
第三类:
/**
* 在构造函数中运行新线程,在获取新线程中赋值的变量出现NPE
* 如果延时,就能获取到
* 这种因为时间不同造成结果不同的程序,非常不安全,也不稳定
*/
public class ConstructNewThread {
private Map<String, String> states;
public ConstructNewThread() {
new Thread(new Runnable() {
@Override
public void run() {
states = new HashMap<>();
states.put("1", "song");
states.put("2", "xin");
states.put("3", "ran");
}
}).start();
}
public Map<String, String> getStates() {
return states;
}
public static void main(String[] args) throws InterruptedException {
ConstructNewThread multiThreadsError6 = new ConstructNewThread();
// Thread.sleep(1000);
System.out.println(multiThreadsError6.getStates().get("1"));
}
}
当不在main中延时,获取不到变量

当使用了sleep进行延时

2、考虑线程安全的情况
1、访问共享的变量或资源,比如对象的属性、静态变量、共享缓存、数据库等
2、所有依赖时序的操作,即使每一步的操作都是线程安全的,还是存在并发问题。可以用synchronized锁,做原子操作
3、不同的数据之间存在捆绑关系的时候,比如ip和端口号,这两个结合在一起,才是正确的,所以要同时修改成功,或者同时修改失败
4、使用其他类的时候,如果对方没有生命自己是线程安全的。比如HashMap,如果要线程安全,使用ConcurrentHashMap
3、多线程会导致的问题
1、性能问题有哪些体现、什么是性能问题
- 服务响应慢、吞吐量低、资源消耗(比如内存)过高等
2、为什么多线程会带来性能问题
- 调度:上下文切换,缓存开销,频繁地竞争锁,或者由于IO读写等原因导致频繁阻塞
- 协作:内存同步->JAVA内存模型->为了数据的正确性,同步手段往往会使用禁止编译器优化,使CPU内的缓存失效。