1. 线程安全
1.1 线程安全定义
● 当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以或得正确的结果,那么这个对象时线程安全的。
● 也就是说,当我们使用多线程访问某个对象的属性和方法时,而在编程这个业务逻辑的时候,不需要专门去做额外处理(也就是可以像单线程编程一样),程序就可以正常运行(不会因为多线程而出错),就可以称为线程安全。
● 相反,如果在编程的时候,需要考虑这些线程在运行时的调度和交替(例如在get()调用到期间不能调用set()),或者需要进行额外的同步(比如使用synchronized关键字等),那么就是线程不安全的
1.2 线程不安全
● 由线程安全可知,如果我们在使用多线程访问对象时,对它的一些调用或者操作,需要加锁之类的额外操作,才可以正常运行,我们就可以称之为线程不安全。
1.3 为什么不把所有类都做成线程安全的?
● 在运行速度上有影响:如果我们要把所有的类都做成线程安全的,那么必然我们会对对象的操作做一些加锁,此时多个线程做这些操作的时候,就无法同时进行。也会产生额外的开销。
● 在设计上来说,也会增加设计上的成本,代码量也会增多,需要大量的人力去做线程安全开发的优化等。
● 如果一个类不会应用在多线程中,我们也就没有必要去设计并发处理,无需去过度设计。
2 如何避免线程不安全?
2.1 案例说明
- 不安全的index++
public class MultiThreadError implements Runnable {
private static MultiThreadError multiThreadError = new MultiThreadError();
private int index = 0;
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
index++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(multiThreadError);
Thread thread2 = new Thread(multiThreadError);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(multiThreadError.index);
}
}
11852
● 注意上面的结果是不一定的
● 我们看一下index++在两个线程同时执行的时候发生的一种情况,箭头为执行顺序
● 由于线程调度,线程1和线程2会可能会有如上的执行顺序,也就是说,我们两个线程都在执行index++的时候会让index少加。
2.2 常见问题:死锁、活锁、饥饿
- 死锁案例
public class ThreadDeadlock {
private static Object object1 = new Object();
private static Object object2 = new Object();
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println(" in 1 run");
synchronized (object1) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object2) {
System.out.println("1");
}
}
});
Thread thread1 = new Thread(() -> {
System.out.println(" in 2 run");
synchronized (object2) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object1) {
System.out.println("2");
}
}
});
thread.start();
thread1.start();
}
}
in 1 run
in 2 run
● 这个程序会一直停不下来,就卡在了每个run方法的第二个synchronized块
2.3 对象的发布和初始化的安全
● 发布:使一个对象能够被当前范围之外的代码所使用。
● 逸出:一种错误的发布。
a. 方法返回一个private对象(private本意是不让外部访问)
public class ReleaseEffusion {
private Map<String, String> states;
public ReleaseEffusion() {
this.states = new HashMap<>();
this.states.put("1", "周一");
this.states.put("2", "周二");
this.states.put("3", "周三");
this.states.put("4", "周四");
this.states.put("5", "周五");
this.states.put("6", "周六");
this.states.put("7", "周日");
}
/**
* 假设提供星期服务。。。
* @return map
*/
public Map<String,String> getStates() {
return this.states;
}
public static void main(String[] args) {
ReleaseEffusion releaseEffusion = new ReleaseEffusion();
Map<String, String> states = releaseEffusion.getStates();
System.out.println(states.get("1"));
states.remove("1");
System.out.println(states.get("1"));
}
}
a. 还未完成初始化就把对象提供给外界,如:
■ 构造函数中为初始化完毕就this赋值
■ 隐式逸出—注册监听器事件
■ 构造函数中运行线程
/**
* 还未初始化完成就发布对象
*/
public class ReleaseEffusionInit {
private static Point point;
public static void main(String[] args) throws InterruptedException {
PointMaker pointMaker = new PointMaker();
pointMaker.start();
Thread.sleep(10);
if (null != point) {
System.out.println(point);
}
TimeUnit.SECONDS.sleep(1);
if (null != point) {
System.out.println(point);
}
}
private static class Point{
private final int x, y;
public Point(int x, int y) throws InterruptedException {
this.x = x;
ReleaseEffusionInit.point = this;
TimeUnit.SECONDS.sleep(1);
this.y = y;
}
@Override
public String toString() {
return "Point{x=" + x + ", y=" + y + '}';
}
}
private static class PointMaker extends Thread {
@Override
public void run() {
try {
new Point(1, 1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Point{x=1, y=0}
Point{x=1, y=1}
Process finished with exit code 0
/**
* 监听器模式
*/
public class ReleaseEffusionListener {
public static void main(String[] args) {
Source source = new Source();
new Thread(() -> {
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
source.eventCome(new Event() {
});
}).start();
new ReleaseEffusionListener(source);
}
int count;
public ReleaseEffusionListener(Source source) {
source.registerListener(event -> {
System.out.println("\n我得到数字:" + count);
});
for (int i = 0; i < 10000; i++) {
System.out.print(i);
}
count = 100;
}
private static class Source {
private EventListener listener;
void registerListener(EventListener eventListener) {
this.listener = eventListener;
}
void eventCome(Event e) {
if (null != listener) {
listener.onEvent(e);
} else {
System.out.println("未初始化完毕");
}
}
}
private interface EventListener {
void onEvent(Event e);
}
interface Event { }
}
0123456789.......
我得到数字:0
28532854285528...9999
Process finished with exit code 0
/**
* 构造函数起线程
* @author yiren
*/
public class ReleaseEffusionConstructorStartThread {
private Map<String, String> states;
public ReleaseEffusionConstructorStartThread() {
new Thread(() -> {
this.states = new HashMap<>();
this.states.put("1", "周一");
this.states.put("2", "周二");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.states.put("3", "周三");
this.states.put("4", "周四");
this.states.put("5", "周五");
this.states.put("6", "周六");
this.states.put("7", "周日");
}).start();
}
/**
* 假设提供星期服务。。。
*
* @return map
*/
public Map<String, String> getStates() {
return this.states;
}
public static void main(String[] args) {
ReleaseEffusionConstructorStartThread releaseEffusion = new ReleaseEffusionConstructorStartThread();
Map<String, String> states = releaseEffusion.getStates();
System.out.println(states.get("1"));
states.remove("1");
System.out.println(states.get("1"));
System.out.println(states.get("3"));
}
}
Exception in thread "main" java.lang.NullPointerException
at com.imyiren.concurrency.thread.safe.ReleaseEffusionConstructorStartThread.main(ReleaseEffusionConstructorStartThread.java:43)
Process finished with exit code 1
● 如何解决逸出
a. 返回副本
// 上方代码加上这个方法就OK
public Map<String, String> getStatesCopy() {
return new HashMap<>(this.states);
}
a. 工厂模式修复上面监听器
/**
* 监听器模式 利用工厂模式 来修复一下
*/
public class ReleaseEffusionListenerFix {
public static void main(String[] args) {
Source source = new Source();
new Thread(() -> {
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
source.eventCome(new Event() {
});
}).start();
ReleaseEffusionListenerFix.getInstance(source);
}
private int count;
private EventListener listener;
public static ReleaseEffusionListenerFix getInstance(Source source) {
ReleaseEffusionListenerFix releaseEffusionListenerFix = new ReleaseEffusionListenerFix(source);
source.registerListener(releaseEffusionListenerFix.listener);
return releaseEffusionListenerFix;
}
private ReleaseEffusionListenerFix(Source source) {
listener = event -> System.out.println("\n我得到数字:" + count);
for (int i = 0; i < 10000; i++) {
System.out.print(i);
}
count = 100;
}
private static class Source {
private EventListener listener;
void registerListener(EventListener eventListener) {
this.listener = eventListener;
}
void eventCome(Event e) {
if (null != listener) {
listener.onEvent(e);
} else {
System.out.println("未初始化完毕");
}
}
}
private interface EventListener {
void onEvent(Event e);
}
interface Event { }
}
2.3 需要考虑线程安全问题的一些情况
● 访问共享的变量或者资源,如:属性、静态变量、缓存、数据库等
● 需要顺序操作的,就算每步都是线程安全的,也可能会存在安全问题。如:先读取再修改,先检查再执行
● 不同数据间存在绑定关系,如:ip和端口号
● 使用其他人或者第三方提供的类的时候。核查对方是否声明线程安全。如:HashMap和ConcurrentHashMap。
3. 多线程的性能问题
3.1 性能问题的体现
● 最明显的体验就是慢!比如前端调用一个借口,很久才返回结果或者直接超时。
3.2 造成性能问题的原因
- 线程调度:上下文切换
○ 何为上下文?
■ 就是上下切换需要保存的线程状态或者说数据(比如:线程执行到了那里,各个 参与运算的寄存器是什么内容),以确保恢复线程的执行。
○ 缓存开销
■ 当一个线程在CPU运算时,有些是需要把数据放到CPU缓存中的,如果上下文切换,那么当前线程CPU的缓存就会失效了。那就CPU就需要重新对新的线程数据进行缓存。所以CPU在启动新线程的时候开始的时候回比较慢,这就是因为CPU之前的缓存大部分都失效了。
○ 怎么样会导致频繁的上下文切换?
■ 多个线程进行竞争锁,还有就是IO读写 - 多个线程协作:内存同步
○ 我们的程序运行,编译器和CPU都会对程序进行优化,如指令重排序以更大得利用缓存,但是如果多线程写作的时候,我们就会利用一些手段禁止指令重排序以确保线程安全。还有就是当我们多个线程运行时,JMM中表明,线程会有私有内存区域,如果我们多线程要确保最新数据就会去主存中同步最新数据,这也会带来性能开销。