我们使用多线程通常是为了提高程序执行效率, 充分调度处理器性能. 但是由于多线程的种种特性,使得假如使用不当可能会导致程序执行结果偏离我们的预期, 这就是线程不安全. 下面就列举一些常见的问题产生原因和解决办法.

线程的"抢占式执行"

        内核调度线程时, 当一个进程中有多个线程时, 线程的执行几乎是无序的, 无序所以不可控, 就有可能导致程序不按照预期运行. 这属于是CPU的运行逻辑, 我们无法从代码层面干预.

线程操作存在多个指令

        CPU是以指令为单位执行的, 假如线程的操作只有一个指令需要执行, 那么就不会产生问题, 因为一个指令从开始到结束不会被干预, 如int的赋值操作就是单指令的. 相反的假如存在多个指令需要被执行, 就有可能在所有指令都执行结束之前, 线程就被调度走了. 

        就像人要喝水的话, 假如桌面上有一瓶水, 就可以直接拿起来喝. 但假如没有水, 就要穿好衣服裤子去超市买水了才有水喝, 而这中间可能又被别人叫去干啥导致最后没喝着水. 

解决方法: 通常是想办法通过特殊手段将多个操作打包为一个"原子操作", 这样在执行结束前都不会被干预.

原子操作通常是add(写入)  + load(操作/修改) + save(保存)

内存可见性问题

        硬件的速度比较: CPU( > 缓存) > 内存 > 硬盘. 每个层级都差了3~4个数量级. 所以假设有一个程序需要频繁地在内存中读取数据, 但是这个数据本身又没有改变, 就相当于重复读取内存的相同数据, 导致效率很低. 编译器可能会自动优化成将该数据暂时保存在CPU中防止重复内存读取, 加快执行效率. 但是此时假如中间穿插了一个修改内存中该数据的操作, 就会导致与CPU中的数据有出入, 导致程序执行出错. 如

int m = 1;
int n = 2;
for(int i = 0; i < 1_0000; i++)
    boolean b = (n == m);

解决方法: 使用volatile关键字, 告诉编译器这里不要进行优化, 数据可能会随时改动.

指令重排序

        

多个线程访问修改一个公共变量

public class Main {

    static class Counter {
        public int counter;
        public synchronized void increase () {
            counter++;
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Counter c = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1_0000; i++) {
                c.increase();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1_0000; i++) {
                c.increase();
            }
        });

        t1.start();
        t2.start();
        t2.join();

        System.out.println(c.counter);
    }
}

输出随机数

java date 线程不安全 java线程不安全的原因_java date 线程不安全

解决方法就是加锁!!!

用synchronized关键字

一个线程针对一个锁对象加锁, 加锁后别的线程想对该对象进行操作(可能是加锁), 就只能阻塞等待. 只有当锁内的代码执行完了才会被解锁. 因为加锁就是为了解决线程安全为题, 防止同时修改导致数据错误.

这个加锁的顺序取决于系统调度和代码写法. 比如写了两个线程的加锁, 就由系统调度随机加锁了.但是一个一个加锁后跟了一个几秒钟的sleep, 肯定就是前面的先被加锁.

加锁方法

方法内对当前对象加锁: synchronized (this \ 特定对象 \ 特定静态对象 \ 类对象) 

class Counter {   
    public int counter = 0;

    this表示对创建的Counter对象加锁
    多个Counter对象之间加锁不冲突
    public void increment () {
        synchronized (this) {
            counter++;
        }
    }

}

class Counter {   
    public int counter = 0;

    表示对创建的Counter对象中的object对象加锁
    多个Counter对象之间加锁不冲突
    private Object ob = new Object();
    public void increment () {
        synchronized (ob) {
            counter++;
        }
    }
}

class Counter {   
    public int counter = 0;

    表示对创建的Counter对象中的obj对象加锁
    但是此时由于obj是静态的, 所以创建多个Counter也是针对同一个obj加锁, 涉及到了锁竞争  
    static private Object obj = new Object();
    public void increment () {
        synchronized (obj) {
            counter++;
        }
    }
}

increment1是对Counter对象加锁
increment2是对ob对象加锁
不会产生锁竞争!!!!
class Counter {   
    public int counter = 0;

    public void increment1 () {
        synchronized (this) {
            counter++;
        }
    }

    private Object ob = new Object();
    public void increment2 () {
        synchronized (ob) {
            counter++;
        }
    }
}

通过Counter.class反射得到Counter的类对象
对一个类对象进行加锁
所以之后每一个Counter.increment锁的都是同一个对象, 涉及锁竞争
class Counter {
    public int counter = 0;
    
    public void increment () {
        synchronized (Counter.class) {
            counter++;
        }
    }
}

ps:Java对象中的各种信息如有什么属性, 多少个属性,参数有多少个等等的信息, 都存储在该对象的类对象中, 类对象保存于.class文件(.java源文件)里, 通过反射就是套出来存有这些信息的类对象, 让这个类的信息变得可见. 

锁对象

Java中所有对象都可以成为锁对象, 针对同一个锁对象进行加锁就会产生互斥. 

所以在判断锁是否存在竞争互斥的时候, 判断锁是否加在同一个对象上是最核心的判断原则.

package threading;

public class Demo {

    static class IT{
        public int iterator = 0;

        public void increment () {
            synchronized (this) {
                iterator++;
            }
        }
    }

    public static void main(String[] args) {

        IT it1 = new IT();
        IT it2 = new IT();

        Thread t1 = new Thread(() -> {
            it1.increment();
        });

        Thread t2 = new Thread(() -> {
            it2.increment();
        });
        t1.start();
        t2.start();

    }
}
//this表示锁当前对象, 两个对象it1和it2都不一样, 所以不存在锁竞争

可重入锁

假如一个锁里面再有一个锁, 就会造成死循环. 因为前一个锁需要等锁的内容执行完才会开锁, 而后一个锁要等前一个锁执行才会加锁, 前面等后面, 后面等前面, 死循环. Java为了避免这样情况的发生, 将synchronized设置为了可重入锁.

假如使用synchronized之后, 发现里面还准备加锁, 就会直接忽略那个锁, 相当于后面那个锁不存在.

实现原理大概是通过计数器的形式实现, 锁的数量为0可加, 不为0就放行.

ps:一个程序在锁中发生异常后, 会脱离当前代码块, 实现锁-1, 防止死锁(永远无法解开的锁)的发生.