线程安全
什么是线程安全
不用考虑这些线程在运行时环境下的调度和交替执行(不用考虑多个线程执行顺序和交替执行的影响)
也不需要额外的同步(允许多个线程同时操作某块代码)
或者在调用方进行任何其他的协调操作(调用方不用为结果的获取添加额外的协调代码,(例如通过join等待某些线程的执行完成才获取结果)获取的时机并不影响结果)
不管业务遇到怎样的多个线程调用某个方法或者读写某个对象的情况,在编写这个业务逻辑时,不需要做额外的处理,程序也不会因为多线程出错,就可以称之为线程安全.
线程不安全
get的时候同时set:A线程get值后存在缓存进行后续判断,这时B线程set修改了值,A通过过期的值进行后续处理,得出错误结果,这是线程不安全的.
这时需要额外的同步来保证它的线程安全,也就是A执行完判断导致的后续逻辑前,B不能修改值
全部线程安全
限制了默认的编译优化,限制了执行线程数,大幅影响运行速度,增加代码设计成本
不用于多线程的
在确保只有单线程访问的情况,不用过度设计为线程安全的(同步块),因为本身就不会产生线程安全问题
什么情况下会出现线程安全问题,如何避免
线程不安全导致的问题
1.同时写:数据读写,由于同时写造成的数据覆盖和丢失.
2.提前读:由于提前读,在前一个写操作还没保存数据到主内存前,就已经读取数据
运行结果错误:a++多线程下出现消失的计算
static int num = 0;
public static void main(String[] args) {
new Thread(IntIncrement::increment).start();
new Thread(IntIncrement::increment).start();
}
public static void increment() {
for (int i = 0; i < 10000; i++) {
num++;
}
System.out.println(num);
}
测试结果1
11289
19401
测试结果2
10823
19579
原因
在这里a++看似一行代码,其实是四个指令
javap -c Test.class
static int num;
public static void main(String[] args) {
num++;
}
public static void main(java.lang.String[]);
Code:
//获取静态变量值压入栈顶
0: getstatic #2 // Field num:I
//将一个int常量1压入栈顶
3: iconst_1
//将栈顶两int相加,并将结果压入栈顶
4: iadd
//将栈顶数据赋值给静态变量
5: putstatic #2 // Field num:I
8: return
所以在多线程运行时,可能出现如下情况
a++具体在哪里消失,消失几个
static int index = 0;
static AtomicInteger realIndex = new AtomicInteger(0);
static AtomicInteger wrongIndex = new AtomicInteger(0);
static boolean[] mark = new boolean[20001];
static CyclicBarrier barrier = new CyclicBarrier(2);
static CyclicBarrier barrier1 = new CyclicBarrier(2);
static CountDownLatch countDownLatch = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
new Thread(IntIncrement::increment).start();
new Thread(IntIncrement::increment).start();
countDownLatch.await();
System.err.println(">>>" + index);
System.out.println("realIndex>>>" + realIndex.get());
System.out.println("wrongIndex>>>" + wrongIndex.get());
}
public static void increment() {
try {
mark[0] = true;
for (int i = 0; i < 10000; i++) {
//上下两个循环栅栏,确保index++同时有两个线程执行
//避免mark[index]在取值时,因为其它线程执行了index++
//跳过某个index
awaitAndReset(barrier, barrier1);
//放弃时间片,增大出现线程安全问题的几率
Thread.yield();
index++;
awaitAndReset(barrier1, barrier);
//原子递增,记录总执行次数
realIndex.incrementAndGet();
//同步锁,确保每次只有一个线程执行如下逻辑,
//避免在mark[index]赋值时,两个线程都执行过了if(index)判断
synchronized (IntIncrement.class) {
//mark[index]判断是否至少有个线程执行过了
//mark[index-1]判断是否已发生线程安全问题
//因为当index++正确执行两次为+2时,跳过了+1
//所以当前index-1没有赋值,应该为默认值false,为正常情况.
//当index++执行两次出现值覆盖情况时,应该只+1
//当前为true,前一个也为true(mark(0)默认为true,
//只要之前执行过,根据逻辑index-1一定为true),
//表明是连续执行,且为错误情况.
if (mark[index] && mark[index - 1]) {
//原子递增,记录错误index++次数
wrongIndex.incrementAndGet();
//打印出错时的值
System.out.println(index);
}
//确保index执行过,就一定为true
mark[index] = true;
}
}
} finally {
//每有一个线程结束减一
countDownLatch.countDown();
}
}
/**
* 让当前栅栏等待,并恢复下一个要等待的栅栏
* @param await 需要等待的栅栏
* @param reset 恢复的栅栏
*/
public static void awaitAndReset(CyclicBarrier await, CyclicBarrier reset) {
try {
reset.reset();
await.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
4989
6450
6491
11940
18849
>>>19995
realIndex>>>20000
wrongIndex>>>5
仿写一个cyclicBarrier
public class CustomBarrier {
Object lock = new Object();
int count;
int index;
public CustomBarrier(int count) {
this.count = count;
this.index = count;
}
public void await() {
synchronized (lock) {
// System.out.println(Thread.currentThread().getName() + ">>>await");
try {
if (--index == 0) notifyAll0();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void notifyAll0() {
new Thread(() -> {
synchronized (lock) {
lock.notifyAll();
}
}).start();
}
public void reset() {
synchronized (lock) {
index = count;
}
}
}
活跃性问题:死锁
死锁
public static void main(String[] args) {
new Thread(DeathLock::needOneAndTwo).start();
new Thread(DeathLock::needTwoAndOne).start();
}
public static void needOneAndTwo() {
synchronized (Object.class) {
sleep(1);
synchronized (DeathLock.class){
System.out.println("needOneAndTwo");
}
}
}
public static void needTwoAndOne() {
synchronized (DeathLock.class) {
sleep(1);
synchronized (Object.class){
System.out.println("needTwoAndOne");
}
}
}
//确保两者一定会争抢锁
public static void sleep(int second){
try {
TimeUnit.SECONDS.sleep(second);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
可以看到dump出来的数据,idea已经检测出是死锁(红色)了
对象发布和初始化时候的安全问题
发布
让一个方法,变量,实例可以在类自身以外使用(例如public),公开的发布
方法的return返回,将方法内部的变量发布出去了
调用一个方法的传参,将当前方法内的变量,发布到其它方法中去
逸出
1.方法返回一个private对象(本不能被外访问的)
public class PrivateEscapeTest {
public static void main(String[] args) {
PrivateEscape privateEscape = new PrivateEscape();
System.out.println(privateEscape.get("1"));
privateEscape.handle().put("1", null);
System.out.println(privateEscape.get("1"));
}
}
class PrivateEscape {
private Map<String, String> map = new HashMap<>();
PrivateEscape() {
map.put("1", "星期一");
map.put("2", "星期二");
}
public String get(String key) {
return map.get(key);
}
public Map handle() {
return map;
}
}
星期一
null
可以看到private不希望外部改变的变量,因为public handle方法返回导致的逸出,暴露给了外部,使得private变量的值不再安全
2.还未完成初始化(构造方法还未执行完毕),就把对象提供给外界,例如:
在构造函数中将this赋值给某个变量
public class ThisEscape {
static Point point;
public static void main(String[] args) {
new Thread(() -> {
new Point(1, 1);
}).start();
sleep(100);
System.out.println(point);
sleep(1000);
System.out.println(point);
}
public static void sleep(int millisecond) {
try {
Thread.sleep(millisecond);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Point {
private final int x, y;
private Object object = new Object();
public Point(int x, int y) {
this.x = x;
ThisEscape.point = this;
ThisEscape.sleep(1000);
this.y = y;
}
@Override
public String toString() {
return "Point{" +
"x=" + x +
", y=" + y +
'}';
}
}
Point{x=1, y=0}
Point{x=1, y=1}
在构造方法还没执行完前,就将对象暴露给了外界.不同的时间获取可以得到不同的值,这可能导致未知的计算错误且难以察觉
构造函数中运行线程
private Map<String, String> map = new HashMap<>();
NewThreadInConstruct() throws InterruptedException {
map.put("1", "星期六");
new Thread(() -> {
this.map = null;
}).start();
Thread.sleep(1000);
map.put("2", "星期日");
}
public static void main(String[] args) throws InterruptedException {
new NewThreadInConstruct();
}
Exception in thread "main" java.lang.NullPointerException
at com.example.threadCoreKnowledge.threadSecurity.NewThreadInConstruct.<init>(NewThreadInConstruct.java:16)
at com.example.threadCoreKnowledge.threadSecurity.NewThreadInConstruct.main(NewThreadInConstruct.java:20)
同样是将对象(this)提前暴露在外暴露在外,这个甚至可以在新线程中修改private变量的引用.
有时也会隐晦的调用新线程,例如在构造函数中通过一个连接池给一个连接变量赋值,连接池可能通过开启新线程的方式来处理初始化新建连接的工作,但执行完构造函数后,这边就以为连接已经创建好了,这时调用就会出错
特别是有时测试,可能有时新线程创建的快,在测试问题时还察觉不到这个问题,只有当生产时,因为机器繁忙起来,时间片的调度使得新线程的处理无法及时完成,才会暴露问题.
隐式逸出—注册监听事件
int count;
public RegisterListener(CustomSource source){
source.registerListener((event)->{
System.err.println();
System.err.println("我得到的数字>>>"+count);
});
for (int i = 0; i <10000 ; i++) {
System.out.print(1);
}
count=100;
}
public static void main(String[] args) {
CustomSource source = new CustomSource();
new Thread(()->{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
source.eventCome(new Event() {});
}).start();
RegisterListener registerListener = new RegisterListener(source);
}
static class CustomSource {
EventListener listener;
void registerListener(EventListener eventListener) {
this.listener = eventListener;
}
void eventCome(Event e) {
if (listener != null) {
listener.onHandle(e);
}else System.out.println("还未初始化");
}
}
interface EventListener {
void onHandle(Event event);
}
interface Event {
}
111111...
我得到的数字>>>0
111111111...
Process finished with exit code 0
在构造中注册监听器,在lambda表达式中直接使用了实例变量,也就是lambda表达式中持有了外部变量的引用
如果count=100赋值还没完成就直接调用依旧会出现线程安全问题
解决逸出
-
返回副本
public class PrivateEscapeTest { public static void main(String[] args) { PrivateEscape privateEscape = new PrivateEscape(); System.out.println(privateEscape.get("1")); privateEscape.handle().put("1", null); privateEscape.handleImprove().put("2", null); System.out.println(privateEscape.get("1")); System.out.println(privateEscape.get("2")); } } class PrivateEscape { private Map<String, String> map = new HashMap<>(); PrivateEscape() { map.put("1", "星期一"); map.put("2", "星期二"); } public String get(String key) { return map.get(key); } public Map handle() { return map; } public Map handleImprove() { return new HashMap(map); } }
星期一 null 星期二
-
工厂模式
public class RegisterListenerFactory { int count; private EventListener listener; private RegisterListenerFactory(CustomSource source){ listener=(event)->{ System.err.println(); System.err.println("我得到的数字>>>"+count); }; for (int i = 0; i <10000 ; i++) { System.out.print(1); } count=100; } public static RegisterListenerFactory getInstance(CustomSource source){ RegisterListenerFactory factory = new RegisterListenerFactory(source); source.registerListener(factory.listener); return factory; } public static void main(String[] args) { CustomSource source = new CustomSource(); new Thread(()->{ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } source.eventCome(new Event() {}); }).start(); RegisterListenerFactory registerListener = getInstance(source); } static class CustomSource { EventListener listener; void registerListener(EventListener eventListener) { this.listener = eventListener; } void eventCome(Event e) { if (listener != null) { listener.onHandle(e); }else { System.out.println(""); System.out.println("还未初始化"); }; } } interface EventListener { void onHandle(Event event); } interface Event { } }
11111... 还未初始化 1111111... Process finished with exit code 0
通过工厂模式,确保构造函数执行完毕后,才进行注册
各种需要考虑线程安全的情况
1.共享变量/资源的访问,会存在线程安全问题,比如对象的属性,静态变量,数据库,共享缓存等.
2.所有依赖时序的动作,即使每一步都是线程安全的,整套动作还是存在线程安全问题:read-modify-write,check-then-action
因为可能在read或者check后,另一个线程修改了变量,导致本来read的值变为脏数据,或者check无效
3.不同的数据之间存在绑定关系
例如访问一个account
updateUsername-updatePassword应该是原子的关系,参数要不都修改完成,要不都失败.
不然只修改了某一个变量对于访问一个新账号是没有意义的.
4.使用其它类时,如果对方没有声明自身为线程安全的
例如hashMap,它就不是线程安全的.如果需要相同功能的线程安全的类,应该使用ConcurrentHashMap
否则hashMap在多线程环境下,很容易出现线程安全问题(值的覆盖,并发修改报错)
多线程会导致的性能问题
性能问题有那些体现,什么是性能问题
服务响应慢,接口响应慢,吞吐量降低,资源消耗大(内存占用过多)
为什么多线程会带来性能问题
1.调度:上下文的切换
为什么会出现上下文切换
当可运行线程数超过CPU(处理器)的数量,OS就会开始调度线程,以便让每个线程都能得以执行.
上下文切换是什么
1.保存现场
例如A线程执行sleep之后,调度器会中断这个线程,保存现场(当前变量,执行到哪行代码),然后让另一个等待CPU资源的B线程得到时间片开始运行.
也就是当中断发生,从线程A切换到线程B去执行之前,OS首先要把线程A的上下文数据(方法调用栈中存储的各类信息,当前执行到哪行代码)妥善保管好,然后把寄存器、内存分页等恢复到线程B挂起时的状态,这样线程B被重新激活后,才能像仿佛从来没被挂起过一样.
这种保护和恢复现场的工作,避免不了一系列数据在各种寄存器、缓存中的来回拷贝,当然不可能是一种轻量级操作.
这种上下文切换的开销是非常到大的,有时比线程执行的时间更长.通常一次上下文切换会消耗5千到1万个CPU时钟周期大概是几微秒.
2.缓存失效
切换线程后,原本保存在CPU多级缓存中的数据(热点数据)也将失效,需要重新加载.这导致切换回来后,一开始的执行速度会由于热点数据的重新缓存而慢很多,因此如果频繁的发生上下文切换,程序性能将大大降低(虽然OS针对这种情况存在最小执行时间,两次切换之间不能小于这个时间,以此避免开销大于程序执行,但这是提升下限的操作,频繁切换带来的性能损耗依旧很大).
何时会导致密集的上下文切换*
频繁的抢锁、频繁的IO(input-output)、频繁的线程阻塞恢复
2.协作:内存同步
synchronized/volatile/Lock接口的锁,这些同步操作,会导致CPU放弃CPU高速多级缓存的使用,而是读写都从主内存操作.由于硬件上性能的差距,降低了执行的速度.
问题
1.一共有哪几类线程安全问题?
1.1 a++,虽然代码只有一行,但是字节码指令有三个.在多线程环境下无法确保原子性,使得其它线程可能读到过期的数据
1.2 重排序导致的线程安全问题.Java允许在确保程序运行结果符合严格串行环境下运行的结果时,编译器的重排序处理,优化程序执行速度.
但在多线程环境下因为其它线程执行时机的不确定性,和重排序导致的代码乱序,结果可能出现不符合预期的情况.