【JavaEE】_线程安全

目录

1. 线程不安全问题

2. 线程不安全的原因

3. 解决线程不安全问题


1. 线程不安全问题

线程安全问题是多线程编程必须考虑的重要问题,也因为其难以理解与处理,故而程序员也尝试发明更多的编程模型来处理并发编程,如多进程、多线程、actor、csp等等;

我们知道,操作系统调度线程是抢占式执行,这样的随机性可能会导致程序执行出现一些bug,如果由于这样的调度的随机性使得代码出现了bug,则认为代码是不安全的,如果没有引入bug,则认为代码是安全的;

线程不安全的典型案例:使用两个线程对同一个整型变量进行自增操作,每个线程自增五万次:

class Counter{
    //保存两个线程要自增的变量
    public int count = 0;
    public void increase(){
        count++;
    }
}
public class Demo1 {
    private static Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
           for(int i=0;i<50000;i++){
               counter.increase();
           }
        });
        Thread t2 = new Thread(()->{
           for(int i=0;i<50000;i++){
               counter.increase();
           }
        });
        t1.start();
        t2.start();
        //在main线程中打印两个线程自增结束后得到的count结果
        //t1、t2执行结束后再打印count结果
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

运行三次,输出结果与预期并不相符且多次运行多次不同:

          

注:1.t1.join()与t2.join()谁先谁后均可:

线程是随机调度的,t1、t2线程的结束前后是未知的,

如果t1先结束,则先令main线程等待t1结束,待t1结束后再令main线程等待t2结束;

如果t2先结束,仍先令main等待t1结束,t2结束了t1还未结束,main线程仍然在等待t1结束,等t1结束后,t2已经结束了,则此时t2.jion()立即返回;

2.站在CPU角度来看,count++实际上是3个CPU指令:

第一步:将内存中count值加载到CPU寄存器中;(load)

第二步:寄存器中的值将其+1;(add)

第三步:把寄存器中的值写回到内存的count中;(save)

由于抢占式执行,两个线程同时执行这三个指令的时候顺序上充满了随机性,只有当两个线程的三条指令串型执行的时候才会符合预期,只要三条指令出现交错,就会出现错误,如:

2. 线程不安全的原因

(1)根本原因:线程是抢占式执行,线程间的调度充满随机性;

(2)修改共享数据:多个线程对同一个变量进行修改操作,才会导致线程不安全问题;

当多个线程分别对不同的多个变量进行操作,或是多个线程对同一个变量进行读操作,都不会导致线程不安全问题;

(3)操作的原子性问题:针对变量的操作不是原子性的,就会导致线程不安全问题,如上文示例中,自增操作其实是3条指令;

当操作是原子性的,如读取变量的值就只对应一条机器指令,就不会导致线程不安全问题;

(4)内存可见性问题:java编译器的优化操作使得在某些情况下线程之间出现信息不同步问题:

如线程t1一直在高速循环进行读操作,线程t2不定时进行修改操作,此时由于t1的高速访问可能无果,就会停止将数据从内存中读至寄存器中再进行读取,而直接从寄存器中读取,此时若t2线程进行修改操作,就会由于内存可见性问题而使两个线程信息不同步,出现安全问题,示例代码如下:

import java.util.Scanner;
public class Demo2 {
    private static int isQuit = 0;
    public static void main(String[] args) {
        Thread t = new Thread(()->{
           while(0 == isQuit){

           }
            System.out.println("Thread t has finished.");
        });
        t.start();
        Scanner scanner = new Scanner(System.in);
        System.out.println("Please input the value of isQuit: ");
        isQuit = scanner.nextInt();
        System.out.println("Thread main has finished.");
    }
}

输出结果为:

并未输出"Thread t has finished."说明t线程并未结束; 

(5)指令重排序问题:指令重排序也是编译器优化的一种操作,编译器在某些情况下可能调整代码的先后顺序来提高程序的效率,单线程通常不会出现问题,但在多线程代码中,可能就会误判导致线程安全问题;

3. 解决线程不安全问题

对应上文的线程不安全问题原因,思考解决线程不安全问题的方法:

(1)线程调度的随机性问题:无法从代码层面进行改进的;

(2)多线程修改同一变量问题:部分情况下可调整代码结构,使不同线程操作不同变量;

(3)变量操作的原子性问题:加锁操作将多个操作打包为一个原子性操作;

(4)内存可见性问题:

① 使用synchronized关键字可以保证内存可见性,被synchronied修饰的代码块,相当于手动禁止了编译器的优化;

② 使用volatile关键字可以保证内存可见性,禁止编译器做出上述优化:

import java.util.Scanner;
public class Demo2 {
    private static volatile int isQuit = 0;
    public static void main(String[] args) {
        Thread t = new Thread(()->{
           while(0 == isQuit){

           }
            System.out.println("Thread t has finished.");
        });
        t.start();
        Scanner scanner = new Scanner(System.in);
        System.out.println("Please input the value of isQuit: ");
        isQuit = scanner.nextInt();
        System.out.println("Thread main has finished.");
    }
}

此时输出结果为:

  

(5)指令重排序问题:synchronized关键字可以禁止指令重排序;

注:synchronized解决多线程修改同一变量问题代码示例:

使用锁后,就将线程间乱序的并发变成了一个串型操作,并发性降低但会更安全;

虽然效率有所降低但相较于单线程程序,还是能分担步骤压力,效率还是较高的;

java中加锁的方式有很多种,最常使用的是synchronized关键字:

class Counter{
    public int count=0;
    synchronized public void increase(){
        count++;
    }
}
public class Demo1 {
    private static Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
           for(int i=0;i<50000;i++){
               counter.increase();
           }
        });
        Thread t2 = new Thread(()->{
           for(int i=0;i<50000;i++){
               counter.increase();
           }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

输出结果为:

  

注:(1)在increase()方法前加上synchronized修饰,此时进入方法就会自动加锁,离开方法就会自动解锁;

(2)当给一个线程加锁成功时,其他线程尝试加锁就会触发阻塞等待,此时对应的线程就处于clocked状态;

(3)阻塞状态会一直持续到占用锁的线程解锁为止,时间轴缩略图如下:

 (3)synchronized可以保证操作的原子性,保证内存可见性,还可以禁止指令重排序;

你可能感兴趣的:(JavaEE,java,jvm,开发语言)