重学Java并发编程
代码GitHub地址 github.com/imyiren/con…
- 刨根问底搞懂创建线程到底有几种方法?
- 如何正确得启动和停止一个线程 最佳实践与源码分析
- 多案例理解Object的wait,notify,notifyAll与Thread的sleep,yield,join等方法
- 了解线程属性,如何处理子线程异常
- 多线程安全和性能问题
1. 线程安全
1.1 线程安全定义
-
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以或得正确的结果,那么这个对象时线程安全的。
-
也就是说,当我们使用多线程访问某个对象的属性和方法时,不需要专门去做额外处理,程序就可以正常运行,就可以称为线程安全。
1.2 线程不安全
- 由线程安全可知,如果我们在使用多线程访问对象时,对它的一些调用或者操作,需要加锁之类的额外操作,才可以正常运行,我们就可以称之为线程不安全。
1.3 为什么不把所有类都做成线程安全的?
- 在运行速度上有影响:如果我们要把所有的类都做成线程安全的,那么必然我们会对对象的操作做一些加锁,此时多个线程做这些操作的时候,就无法同时进行。也会产生额外的开销。
- 在设计上来说,也会增加设计上的成本,代码量也会增多,需要大量的人力去做线程安全开发的优化等。
- 如果一个类不会应用在多线程中,我们也就没有必要去设计并发处理,无需去过度设计。
2 如何避免线程不安全?
2.1 案例说明
- 不安全的index++
/**
* 第一种:运行结果出错。
* 计数不准确(减少),找出具体位置
*
* @author yiren
*/
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
Process finished with exit code 0
复制代码
- 注意上面的结果是不一定的
- 我们看一下index++在两个线程同时执行的时候发生的一种情况,箭头为执行顺序
- 由于线程调度,线程1和线程2会可能会有如上的执行顺序,也就是说,我们两个线程都在执行index++的时候会让index少加。
2.2 常见问题:死锁、活锁、饥饿
- 死锁案例
/**
* 死锁问题
*
* @author yiren
*/
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 对象的发布和初始化的安全
-
发布:使一个对象能够被当前范围之外的代码所使用。
-
逸出:一种错误的发布。
- 方法返回一个private对象(private本意是不让外部访问)
/** * 发布逸出 * @author yiren */ 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")); } } 复制代码
- 还未完成初始化就把对象提供给外界,如:
- 构造函数中为初始化完毕就this赋值
- 隐式逸出---注册监听器事件
- 构造函数中运行线程
/** * 还未初始化完成就发布对象 * @author yiren */ 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 复制代码
/** * 监听器模式 * @author yiren */ 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 复制代码
-
如何解决逸出
- 返回副本
// 上方代码加上这个方法就OK public Map<String, String> getStatesCopy() { return new HashMap<>(this.states); } 复制代码
- 工厂模式修复上面监听器
/** * 监听器模式 利用工厂模式 来修复一下 * @author yiren */ 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中表明,线程会有私有内存区域,如果我们多线程要确保最新数据就会去主存中同步最新数据,这也会带来性能开销。
4. 面试问题
-
一共有哪几类线程安全问题?
-
那些场景需要额外注意线程安全问题?
-
什么是多线程带来的上下文切换?
关于我
- 坐标杭州,普通本科高校计算机科学与技术专业。
- 20年毕业,主做Java技术栈后端开发。
- GitHub: github.com/imyiren
- Blog : imyi.ren