栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

线程安全讲解

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

线程安全讲解

线程安全是整个多线程中的要点

什么是线程不安全:

就是在编写多线程代码的时候,如果当前代码中因为多线程随机的调度顺序,导致程序出现bug,就称为线程不安全,反之就安全。

一个常见的线程不安全的案例:

class Increase {
    public int t = 0;
    public void inc() {
        t++;
    }
}
public class demo9 {
    public static Increase increase = new Increase();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                increase.inc();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                increase.inc();
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(increase.t);
    }
}

 可以看到,这里的结果并不是我们所希望的10000,而是一个介于5000和10000之间的数。

这里我们需要知道t++的原理,这个操作分为三个步骤,首先是从内存中将数据加载到cpu上,然后在cpu上进行加1操作,然后再放回内存中,这里的三个步骤不具有原子性,且进程会抢占执行,就会导致进程不安全。

那这里如何使进程安全??

一个典型的方法就是加锁

通过加锁操作,就可以把上诉的”无序“变为有序,把上诉的”随机“ 变为确定。

java中给进程加锁的方式有很多,最常用的就是synchronized 这个关键字。

这个关键字的意思是同步,也就是互斥。

synchronized public void inc() {
    t++;
}

这里只需要在inc这个函数上加上synchronized这个关键字,程序在进入这个函数的时候就会自动加上锁。

此时结果就正确了。

原因:如果当前锁是未被占用的情况,有一个线程t1尝试进行lock操作,t1就可以立即获取到锁,并且继续执行后面的逻辑。

又有一个线程尝试进行lock操作,t2线程就只能等待(block状态),一直等待,直到t1线程释放,这个时候t2才能执行后续的操作

引入多线程,就是为了实现并发编程(提高速度),但是当加了锁之后,数据结果是对了,但是这里的并发性实际上就降低了,速度也就下降了。

线程不安全的原因:

1.线程的抢占式执行(根本原因) 2.两个进程在修改同一个变量 3.线程对变量的修改不是原子的(加锁的本质就是把这些不是原子的操作给打包到一起了) 4.内存可见性

例如有一个变量,一个线程快速读取这个变量的值,另一个线程会在一定时间之后修改这个变量 

import java.util.Scanner;

public class demo10 {
    private static int isQuit = 0;
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (isQuit == 0) {
                //不做
            }
            System.out.println("t线程结束");
        });
        t.start();
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入一个整数");
        isQuit = scanner.nextInt();
        System.out.println("main线程结束");
    }
}

 可以看到,当在main线程中修改了isQuit,但是t线程并没有随之退出

这显然是bug

这里的是由于java中的编译优化,在多线程中,编译器的判定优化可能出现误差,在此处,isQuit的值是可能发生改变的,但是编译器在进行优化的时候,没法对于多线程的代码做出过多的预判,只是看到了t线程内部没有地方在修改isQuit,并且这个t线程里,反复要执行太多次的load操作了,所以在t中,编译器的直观感受就是反复进行了太多次的load操作了,太慢了,同时这里的load的结果好想一直是不变的(编译器不能把多个线程联系到一起分析),因此编译器做出了一个大胆的优化操作,就是直接省略掉这里的load操作,只保留第一次,后续的操作都不再读取内存,而是直接从寄存器中度,这就是内存可见性(改了,但没看到)

解决方案:

(1)还是synchronized 就会禁止编译器在synchronized内部产生上述的优化

(2)还可以使用另一个关键字,volatile,只要用这个关键字修饰对应的变量,就会禁止这里的优化,每次都会去内存中读取数据

 此时就能正确结束了,所以volatile的作用就是保证了内存的可见性,禁止了编译器的相关优化

5.指令重排序

这个也可能引起线程的不安全,这也是和编译器优化相关的 

指令重排序就是保证代码原有逻辑不变,调整了程序指令的执行顺序,从而提高了效率

如果是单线程环境下,这里的判定是比较准的,但是如果是多线程下,这里的判定就不准了

也可以使用synchronized来解决问题。

synchronized的基本使用

起到的效果:

    互斥(最核心的要点)保证内存可见性禁止指令重排序

具体的使用方法:

直接加到一个普通方法上,进入方法就相当于加锁,出了方法就相当于解锁

加到一个代码块上,需要手动指定一个”锁对象“

synchronized (this){
    t++;
}

例如这段代码,就是在手动指定锁对象为this,当然也可以指定其他对象为锁对象,在java中,任何一个继承自Object的类的对象,都可以作为锁对象,这是因为synchornized加锁操作,本质上是在操作Object对象头中的一个标志位。

3.加到一个static方法上,此时就相当于指定当前的类对象为锁对象。

要注意,两个进程对同一个对象加锁才会产生竞争,两个进程对不同对象加锁,不会产生竞争

import java.util.Scanner;

public class demo11 {
    private static Object object1 = new Object();
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        Thread t1 = new Thread(() -> {
            while (true) {
                synchronized (object1) {
                    System.out.print("请输入一个整数:");
                    int a = scanner.nextInt();
                    System.out.println("a:" + a);
                }
            }
        });
        t1.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Thread t2 = new Thread(() ->  {
            synchronized (object1) {
                System.out.println("hello t2");
            }
        });
        t2.start();
    }
}

 这里的t1先拿到锁,然后t2就会进入阻塞状态。

可重入锁,这是synchronized 的一个特性,如果synchronized不是可重入的,就很容易出现”死锁“的情况如果连续针对同一个对象,下面是this,就是Increase对象加锁两次,并且如果锁不是可重入的话,就会出现死锁。

class Increase {
    public int t = 0;
    synchronized public void inc() {
        synchronized (this){
            t++;
        }
    }
}

就是说,一个进程,可以对一个锁进行多次加锁操作。

这里是java里面设置了可重入锁,但是在其他地方就不一定是可重入锁的,那么就会出现死锁。

除了上面说的一个进程一把锁会产生死锁的情况,还有其他情况会产生死锁。比如两个进程两把锁,互相竞争对方的锁。或者M个进程M把锁(哲学家就餐问题)

线程安全的类:

vectorHashTableConcurrentHashMapStringBuffer

这几个类是线程安全,核心方法上都带有synchronized关键字,可以在多线程环境下使用

 String是不可变对象,所以String也是安全的.

补充:JMM => Java Memory Model  Java存储模型(内存模型)

多线程程序,最麻烦的就是在调度过程中充满了随机性,这个是在系统层面上解决不了的

但是可以通过wait和notify机制对进程之间的调度做出一定的控制。

假设有两个线程t1和t2,希望先执行t1,t1执行了一些工作之后,再执行t2,就可以先让t2wait,然后t1执行了一些工作,就调用notify唤醒t2。

要注意,再wait这个方法里会做三件事:

    先针对o解锁进行等待当通知到来之后,就会被唤醒,同时尝试重新获取到锁,然后再继续执行
public class demo12 {
    private static Object o = new Object();
    public static void main(String[] args) throws InterruptedException {
        System.out.println("唤醒前");
        o.wait();
        System.out.println("唤醒后");
    }
}

 所以如果按照上面的来写对没加锁的再进行解锁就会出现报错。

所以需要修改代码:

public class demo12 {
    private static Object o = new Object();
    public static void main(String[] args) throws InterruptedException {
        System.out.println("唤醒前");
        synchronized (o) {
            o.wait();
        }
        System.out.println("唤醒后");
    }
}

 此时就能正常阻塞了

notify也是Object类的方法,哪个对象调用的wait,就需要哪个对象调用notify来唤醒

notify同样也要搭配synchronized来用

如果有多个线程都在等待,就只调用一个notify 

public class demo12 {
    private static Object o = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (true) {
                synchronized (o) {
                    System.out.println("wait 开始 ");
                    try {
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("wait 结束");
                }
            }
        });
        t1.start();
        Thread.sleep(3000);
        synchronized (o) {
            o.notify();
        }
        System.out.println("Main唤醒t1");
    }
}

这里Main线程就会唤醒t1线程。

 

public class demo12 {
    private static Object o = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (true) {
                synchronized (o) {
                    System.out.println("t1 wait 开始 ");
                    try {
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("t1 wait 结束");
                }
            }
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            while (true) {
                synchronized (o) {
                    System.out.println("t2 wait 开始 ");
                    try {
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("t2 wait 结束");
                }
            }
        });
        t2.start();

        Thread.sleep(3000);
        synchronized (o) {
            o.notify();
        }
        System.out.println("Main唤醒t");
    }
}

当有多个wait的时候,一个notify就只能随机唤醒一个线程。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/782319.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号