线程的使用为程序运行的速度带来了极大的提升,然而凡是都用两面性线程在提高运行速度的同时也不可避免的会给我们带来一些安全问题,我将从以下几个方面阐述线程的安全问题以及某些问题的解决策略。
1.线程的抢占式执行
说到线程的安全问题归根结底就是线程为了提高速度而使用的抢占式执行所带来的负面影响,由于多个线程是共享同一虚拟地址空间,即在同一进程内的不同线程所共享的是同一系统资源这也是为什么使用线程会更快的原因之一,当然我们这里不做讨论。因为多个线程抢占式使用同一内存空间执行任务,当时如果此时某一线程崩溃那么就会导致其他线程也无法正常运作,那么此时整个进程就会崩溃。
2.不同线程修改同一变量
我们想要修改一个变量的值,首先要从内存中取出,之后由cpu对其进行加减乘除的操作。但是当有多个线程对同一变量进行操作,由于线程并发的速度很快有可能此时不同的线程此时从内存中得到的是同一个值,这样就可能导致出现对变量值的错误修改
public class Demo2 {
static int count = 0;
static class MyRunnable implements Runnable{
@Override
public void run() {
for(int i = 0; i < 5000; i++){
count++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new MyRunnable();
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
此时可以看到,count的值并非我们预想中的10000,此时就是发生了不同线程在内存中取到相同的count值,假设此时count为1,则t1线程与t2线程就都拿到了count = 1,此时对其自增,count在t1与t2线程中都变成了2,在将其放会内存中,导致内存中的值为2而不是期望的3.
对于这种情况我们可以通过对其加锁来解决,代码如下:
static class MyRunnable implements Runnable{
@Override
public synchronized void run() {
for(int i = 0; i < 5000; i++){
count++;
}
}
}
只需要在重写的run方法上加上锁即可,此时不管是哪个线程先执行拿到count另一线程都会等待其执行完毕后在执行,此时count就会一直为期望的值。
3.操作的非原子性
讨论这个问题时我们先搞清楚什么是原子性,在过去的一段时间我们认为原子是世界上最小的物质,其不可再分(当然现在的原子不是最小的物质),所以我们对于一些不可在分割的操作认为其是原子性操作,其为不可再分的逻辑执行体。如果一个操作是原子性的那么其当然不会造成线程的不安全,当是非原子性的操作就极有可能造成线程安全问题。同时操作的非原子性也是多个线程修改同一变量导致线程不安全问题的前提。在上述提到的count自增中出现的问题,其原因之一就是自增操作是非原子性的操作。自增操作需要从内存中取数,让其自增,在放回内存,需要三个步骤才能完成,这就给不同线程在内存中能拿到相同的值提供了可能性。比如,线程1完成取数和自增操作还未完成放回操作时,此时线程2从内存中取出的依旧是未自增的值,这样就会导致count值少加一,可以通过加锁解决。
4.内存的可见性
内存可见性的存在准确来说是操作系统优化后的结果,当我们对内存中的某一个值进行反复的操作,操作系统为了提高速度,就会从缓冲区或寄存器中取值而不从内存中取值,此时就可能导致一些问题。
public class Demo3 {
static class Counter{
public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(){
@Override
public void run() {
while(counter.flag == 0){
}
System.out.println("t1结束");
}
};
Thread t2 = new Thread(){
public void run(){
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数");
counter.flag = scanner.nextInt();
}
};
t1.start();
t2.start();
}
}
代码如上,此时按照代码只有当flag == 0时循环才会继续,打印“t1结束”,但是此时当我们使flag = 1时,循环却不停止。
这就是因为此时内存中的flag被修改了,而编译器优化了取值操作从缓存或寄存器中直接取值(其速度远远快于从内存中取值)导致取出的值不是被修改过的flag。此时要解决这种问题,只需要用volatile修饰flag即可
5.指令的重排序
指令的重排序其实也是操作系统对于指令执行的一些优化操作,简单来说就是执行一个任务需要多条指令,操作系统根据执行的情况对于指令执行的顺序做出一些调整来优化程序的执行。当然打乱一些指令的执行顺序有可能会造成一些问题,就如内存的可见性中所说的问题也是因为cpu对指令进行了重排序优化了取值过程导致了线程的安全问题,所以说使用volatile关键字也可以保证指令的执行顺序。