JAVA多线程以及volatile关键字详解

发布时间:2024年01月02日

初识线程

首先先介绍一下JAVA线程的基本概念。平时我们使用的一个一个的应用就可以表示为一个进程,进程下面又包括了很多线程来同时执行该进程的任务,和同时抢用CPU资源?

  • 并发: 指的是多个线程同时调用

多线程的创建1

  • 通过继承thread类
  • 重写其中的run方法
  • 调用该类实例的start方法(也算是一种回调函数)
package csdn;

public class CSDNThreadDemo1 extends Thread{
    @Override
    public void run() {
        for(int i = 0;i<50000;i++)
            System.out.println("我是第一个线程");
    }
}

上述是一个具体的实现同时下列代码为调用上述类的实现

package csdn;
public class CSDNThread {
    public static void main(String[] args) {
        CSDNThreadDemo1 csdnThreadDemo1 = new CSDNThreadDemo1();
        csdnThreadDemo1.start();
        for(int i = 0; i<50000;i++)
            System.out.println("hello world");
    }
}

可以看的最后的运行结果呢

两个线程互相抢占资源互相抢夺CPU就造成了上述的轮番打印

因为java中是单继承模式,不支持多继承所以如果通过方法一进行了线程的创建,后续该类就无法继承其他的父类,所以方法一也是存在一定缺陷的

多线程的创建2

  • 此时我们可以使该类实现Runnable接口?
  • 然后实现接口中的run方法
  • 其他类调用该类实例的start方法
package csdn;
public class CSDNThreadDemo2 implements Runnable{
    @Override
    public void run() {
        for(int i = 0;i<50000;i++)
            System.out.println("我是第二个线程");
    }
}

改方法的好处就是一个类可以实现多个接口 但只能继承一个其他类所以实现了Runable接口后不影响该类的后续使用

多线程的创建3

匿名内部类(如果只是一个代码块 或者代码比较简洁推荐使用该方法)

? 本来匿名内部类的写法,下面完整代码中使用了lambda表达式来简化代码

Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("我是匿名内部类线程");
            }
        });
package csdn;
public class CSDNThread {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
           for(int i = 0; i < 50000; i++){
               System.out.println("我是匿名内部类的线程");
           }
        });
        t.start();
        for(int i = 0; i<50000;i++)
            System.out.println("hello world");
    }
}

线程安全问题

1.多个线程同时操作一个变量

由上述过程我们看到了不同线程之间在执行过程中会互相抢占CPU。

由此可能会应发线程安全问题 本文列举一个详细的线程安全问题,并详细解析出现该问题的原因

package csdn;

public class CSDNThreadSafe {
    static int num = 0;
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            for(int i = 0;i<50000;i++)
                num++;
        });
        t.start();
        for(int j = 0;j<50000;j++){
            num++;
        }
        System.out.println(num);
    }
}

在上述代码中,我们定义了一个num变量,并且此时main线程和另外一个线程在同步操作num这个变量,按照常理可以得出理论答案程序结束后num的值应该是100000(10w)但是我们实际运行程序可以看到每次运行的结果都会小于10w,并且每一次运行的结果都是随机的,

为什么会产生上述情况呢?

原因在于其实站在计算机的角度来讲num++这个操作其实不是“原子的”

通俗一点来讲,就是num++这个操作站在CPU的角度来讲其实分为了三个板块因为num这个变量的值目前是在内存中的,但是实际完成num+1这个操作是在寄存器(CPU)里面完成的.所以在执行++操作之前寄存器会讲内存中的num给读取出来,然后寄存器上完成+1操作,完成之后又讲新的数据内容写到内存中。此时才完成了完整的++操作.

  1. 内存中的数据加载到寄存器中(load)
  2. 寄存器对数据进行+1操作(add)
  3. 寄存器将操作过后的数字写回到内存中变量完成+1(save)

由上述过程可见,对数据进行++操作并不是一次性完成的,而是分为了3个步骤.又因为线程之间会互相抢占CPU和资源,所以上诉三个步骤顺序被打乱之后,num就不能+1了,所以才会有上述的每次运行结果都不相同

这里在详细举一个例子? 下图为2个线程抢占CPU的情况之一其他情况也可以进行类推 本文解析一种情况即可 上述A和B分别代表一个进程后面的英文对应上述的1.2.3操作

  • A(Load)
  • B(Load)
  • A(add)
  • A(save)
  • B(add)
  • B(save)

此时A进程先把num加载进了寄存器 然后B这个进程又会将num又一次加入寄存器,然后A线程在寄存器中对num进行++操作,然后将改写好的值save进内存 此时num这个变量就会加一,但此时num已经在内存中了 B这个进程调用add和save方法,无法对num产生影响,所以 这个2个进程互相调用num++的操作理论来讲num的大小会加2但是经过上述过程num的实际大小的值只会进行+1所以才出现了前面运行结果均小于10w的解释

上述便是常规的一种线程不安全的情况.

解决方法

针对于第一种的线程不安全问题,解决办法其实有很多种,synchronized加锁操作变可以解决上述问题,(synchronized关键字本文不打算展开描述 ,篇幅太长,后续会专门写一个板块来进行synchronized的讲解) 这里只做简单介绍 在解决办法的基础上正好对synchronized做出简单介绍。

1.关键字synchronized

在了解到上述过程之后,为什么会出现上述情况呢,其实主要原因还是在于num++这个操作是分三步进行的,两个线程互相抢占这三步资源所以导致了出现线程不安全的情况。为此,引入了一个关键字synchronized加锁操作来使上述的三步过程不能被分开

synchronized加锁操作可以锁住一个代码块,让只能一个线程进行访问。

package csdn;
public class CSDNThreadSafe {
    static volatile int num = 0;
    public static synchronized void add(){
        num++;
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for(int i = 0;i<50000;i++)
                CSDNThreadSafe.add();
        });
        t.start();
        for(int j = 0;j<50000;j++){
            CSDNThreadSafe.add();
        }
        t.join();
        System.out.println(num);
    }
}

上述代码中我,我们对这个类里面的静态方法add进行了synchronized加锁操作,加上synchronized关键字过后,假如此时A线程在访问该方法,b线程无法同时执行该方法 会进行等待状态,等A结束后,B才能访问该线程。所以最后结果为10w了。 大概了解完synchronized后,我们继续讲下一个关键字。

2.关键字volatile

首先我们先阅读一段代码

package csdn;
import java.util.Scanner;
public class CSDNThreadSafeDemo2 {
    public static  int quit = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (quit == 0){
            }
            System.out.println("线程t1结束");
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入0以外的数字关闭程序");
            quit = scanner.nextInt();
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

上述代码T1线程中当qiut等于0的时候一个线程在不停的循环,另外一个线程T2中写入了scanner来改变quit的值 按照理论来讲,如果我们此时输入0以外的数字,比如输入1,T1线程中拿到quit等于1然后就会跳出循环执行下面的打印方法.(此时只有一个线程在操作变量不需要使用synchronized)但是运行程序可知

输入了0以外的数,但是为什么程序并没有被终止呢?

原因是原来在我们编写程序的时候开发JVM虚拟机,和java等编译器的工作人员会对我们的代码,在不改变原有逻辑的基础上进行一部分的优化.以便提高代码的执行效率

通俗一点来讲就是在计算机中读取寄存器的操作相比如读取内存的操作,速度会提升很多的(内存读取会比硬盘读取快很多),这里的qiut == 0的这个操作,在计算机中实际上就是先把内存中的值读取到寄存器中,然后寄存器进行比较。

  • 读取内容到寄存器(load)
  • 寄存器进行比较(jcmp)

在上述代码中,编译器和JVM虚拟机看到,一个循环在反复读取同一个值而且·这个值的大小始终是不变的,于是编译器就做了一个大胆的决定,他把前面的load操作直接给优化掉了,反正你都不会改变,就直接使用寄存器中原本保留的值,就不会每次都去内容中读取数据了,所以在上述代码中我们可以看到另外一个线程就算改变了quit的值,也是改变了quit在内存中的值,但此时T1线程经过编译器优化,已经不会继续往内存中读取值了,而是一直使用开始在编译器中保存的0这个值。所以T2线程改变了quit的大小,T1线程也依然不会停下.

上述过程就是常见的一个编译器优化的过程 要是放在单线程中就不会出问题,可是在多线程中,就往往可能发生这种问题.

为了解决上述问题,在java中引入了一个关键字volatile,使用这个关键字可以有效的避免出现编译器优化的问题。让每次比较的时候编译器都可以去内存中读取值的大小。

package csdn;
import java.util.Scanner;
public class CSDNThreadSafeDemo2 {
    public static volatile int quit = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (quit == 0){
            }
            System.out.println("线程t1结束");
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入0以外的数字关闭程序");
            quit = scanner.nextInt();
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

下列代码为加上关键字后

可以看出 程序现在能正常结束了.

文章来源:https://blog.csdn.net/a512512a/article/details/135276723
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。