一、什么是线程安全?
在操作系统中,因为线程的调度是随机的(抢占式执行),正是因为这中随机性,才会让代码中产生很多bug 如果认为是因为这样的线程调度才导致代码产生了bug,则认为线程是不安全的, 如果这样的调度,并没有让代码产生bug,我们则认为线程是安全的。
二、线程安全问题产生的原因:
1.抢占式执行
多线程调度的过程,可以是认为“随机”的,没有规律;
2.多线程修改同一个变量
多个线程同时修改同一个变量,容易产生线程安全问题,如果是修改不同变量,那么多个线程之间的寄存器数据修改对内存中的数据修改影响不大。
3.变量不保证原子性
原子性也指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行。不保证原子性会给多线程带来的问题是:如果一个线程正对一个变量操作,由于线程调度是无序的(线程的抢占式调度现象),中途可能会让其他线程插进来了,如果这个操作被打断了,结果可能就是错误的。
4.指令重排序
在单线程环境下,指令重排序将代码的执行方式进行优化,而不会影响代码的结果。而在多线程环境下,由于多线程的代码执行复杂程度更高,编译器很难在编译阶段对代码的执行效果进行预测,因此很容易导致优化后的逻辑和之前的不等价。
三、解决线程的安全问题
加锁和解锁就是确保不是多个线程同时使用操作同一个数据,导致线程不安全。
1、synchronized(同步锁)
概念:
针对某个修改操作进行加锁,让修改操作变成原子的.就像我们学校的公共澡堂一样,大家去洗澡都是抢占式洗,谁先占到位置就可以把门儿锁了然后洗澡,其它人就进不去了,想洗多久就洗多久,只有我洗完其它人才可以到我的这个位置洗.一旦一个线程先拿到锁对某个修改操作加锁,那么其它线程就无法对该修改操作加锁,需要拿到锁的线程释放锁之后才能加锁.这里不要将join()和加锁混淆哦!join()是将多个线程进行串行,而加锁只会让多个线程间的某个步骤进行串行,其它步骤还是并行.
代码实现:
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
String s = "锁对象的外貌没有任何影响";
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
synchronized (s) {
count++;
}
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
synchronized (s) {
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count="+count);
}
count++操作就会被打包一个操作,也就是原子性,此时三条执行的执行顺序是一体的,也就是不会再产生上述的线程不安全问题了。
两个线程对同一个对象加锁之后,当t1先拿到锁,也就是先执行代码,即使t1线程执行到一半,被cpu调度走;此时t2线程也无法进行拿到锁。
加锁后,count++语句是串行执行的,而for循环语句是并行执行。
当t1释放锁之后,t1线程和t2线程还是会同时争夺这把锁,也就是说他们的拿到锁的顺序也是不确定的。
2、使用volatile关键字
在多线程情况下,某线程A没有休眠就会执行非常快,编译器会对某个变量flag的重复加载进行优化,让flag只被加载一次,那么当另外的线程对flag进行修改后,在线程A中对flag的值还是初始加载的值,就会对变量flag的值产生误判,从而引起bug.这种情况我们就可以使用volatile关键字修饰变量flag,让编译器暂停优化,保证内存可见性(synchronized关键字是否能保证内存可见性目前是存在争议的).
代码实现:
public static int flag = 0;
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
while (flag == 0) {
}
System.out.println("thread1结束");
}
});
Thread thread2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要修改flag的值:>");
flag = scanner.nextInt();
});
thread1.start();
thread2.start();
volvatile 关键字有如下两大作用:
禁止指令重排序:保证指令执行的顺序,防止 JVM 出于优化而修改指令执行顺序,引发线程安全问题。
保证内存可见性:也就是说,保证了我们读取到的数据是内存中的数据,而不是缓存,具体的,当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
3.wait-notify
为了线程能按照规定的顺序执行,使用wait-notify。这两个都是Object提供的方法。
wait在执行时:
- 解锁;
- 阻塞等待;
- 当被其他线程唤醒之后,尝试重新加锁,加锁成功,wait执行完毕,继续往下执行其他逻辑。
故我们的 wait 方法和 notify 方法都要在 synchronized 内部使用,并且和synchronized的对象一致,如:
如果 wait 没有搭配synchronized 使用,会直接抛出异常
注意事项:
- 要想让 notify 能顺利唤醒 wait ,需要确保 wait 和 notify 都是使用同一个对象调用的;
- wait 和 notify 都需要在 synchronized 内部执行,notify 在 synchronized 内部执行是 Java强制要求的;
- 如果进行 notify 时,另一个线程没有处于 wait 状态不会有任何影响。
当 wait 引起线程阻塞时,可以使用 interrupt 方法打断当前线程的阻塞状态。
4.wait 和 sleep 的区别
- wait 需要搭配synchronized 使用,sleep 不需要;
- wait 是 Object 的方法,sleep 是Thread 的静态方法。
- wait 是用于线程之间的通信的,sleep是让线程阻塞一段时间。
- wait和sleep都可以让线程放弃执行一段时间。