首页 > 编程语言 >【并发编程】线程安全性问题

【并发编程】线程安全性问题

时间:2023-01-18 15:06:08浏览次数:34  
标签:Thread start SyncDemo 编程 并发 线程 new public

文章目录

1.什么是线程安全性

  • 当多个线程访问某个类,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类为线程安全的。
  • 线程不安全的原因
    • 线程是抢先执行的
    • 原子性操作,当CPU执行一个线程过程时,调度器可能调走CPU,去执行另一个线程,此线程的操作可能还没有结束。
    • 多个线程尝试修改同一个变量。
    • 内存可变性
    • 指令重排

2.原子性操作

  • 一个操作或这多个操作,要么全部执行并且执行过程中不被任何因素打断,要么就都不执行。
  • 如何把非原子性操作变成原子性
    • volatile关键字仅仅保证可见性,并不保证原子性,synchronized关键字使得操作具有原子性。

3.深入理解synchronized

(1)内置锁

  • 每个java对象都可以做一个实现同步的锁,这些锁成为内置锁。线程进入同步代码块或者方法的时候会自动获得锁,在退出同步代码块或者方法的时候释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或者方法。

(2)互斥锁

  • 内置锁是一个互斥锁,这就意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,知道线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。

(3)synchronized修饰普通方法

  • synchronzied修饰普通方法锁住的是当前调用的对象,假如开两个线程两个实例去掉方法,那么两个实例各自持有一个锁,互相不干扰,但是如果是两个线程用同一实例去调用,那么就持有一个锁,第一个线程释放锁后,第二个才会拿到锁。
public class SyncDemo {

	public synchronized void exampleOut(){
        try {
            Thread.sleep(5000L);
            RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
            long uptime = runtimeMXBean.getUptime();
            System.out.println(Thread.currentThread().getName()+"-线程运行时长:"+uptime+"ms");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 两个线程调用两个实例案例
public static void main(String[] args) {
    SyncDemo syncDemo1 = new SyncDemo();
    SyncDemo syncDemo2 = new SyncDemo();

    new Thread(() -> {
        syncDemo1.exampleOut();
    }).start();

    new Thread(() -> {
        syncDemo2.exampleOut();
    }).start();
}

【并发编程】线程安全性问题_代码块

  • 两个线程调用一个实例案例
public static void main(String[] args) {
    SyncDemo syncDemo1 = new SyncDemo();
    SyncDemo syncDemo2 = new SyncDemo();

    new Thread(() -> {
        syncDemo1.exampleOut();
    }).start();

    new Thread(() -> {
        syncDemo1.exampleOut();
    }).start();
}

【并发编程】线程安全性问题_java_02

(4)synchronized修饰静态方法

  • synchronized修饰静态方法锁住的是整个类的对象,无论有多少个实例,只要是当前类的实例,都持有一个锁,一般生产不建议用静态同步方法,可能会导致程序运行阻塞。
public class SyncDemo {

    public static synchronized void staticOut(){
        try {
            Thread.sleep(5000L);
            RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
            long uptime = runtimeMXBean.getUptime();
            System.out.println(Thread.currentThread().getName()+"-线程运行时长:"+uptime+"ms");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public static void main(String[] args) {
    SyncDemo syncDemo1 = new SyncDemo();
    SyncDemo syncDemo2 = new SyncDemo();

    new Thread(() -> {
        syncDemo1.staticOut();
    }).start();

    new Thread(() -> {
        syncDemo2.staticOut();
    }).start();
}

【并发编程】线程安全性问题_java_03

(5)synchronized修饰代码块

  • synchronized修饰代码块锁住的是当前对象,用法和synchronized修饰普通方法一样,但是更细粒度确定锁的位置,比synchronized修饰普通方法的效率要高。
public class SyncDemo {
	private Object lock = new Object();
    public void blockOut(){

        try {
            Thread.sleep(5000L);
            RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
            long uptime = runtimeMXBean.getUptime();
            System.out.println(Thread.currentThread().getName()+"-线程运行时长:"+uptime+"ms");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 两个线程调用两个实例案例
public static void main(String[] args) {
    SyncDemo syncDemo1 = new SyncDemo();
    SyncDemo syncDemo2 = new SyncDemo();

    new Thread(() -> {
        syncDemo1.blockOut();
    }).start();

    new Thread(() -> {
        syncDemo2.blockOut();
    }).start();
}

【并发编程】线程安全性问题_System_04

  • 两个线程调用一个实例案例
public static void main(String[] args) {
    SyncDemo syncDemo1 = new SyncDemo();
    SyncDemo syncDemo2 = new SyncDemo();

    new Thread(() -> {
        syncDemo1.blockOut();
    }).start();

    new Thread(() -> {
        syncDemo1.blockOut();
    }).start();
}

【并发编程】线程安全性问题_后端_05

3.4.volatile关键字

(1)volatile关键字的作用

  • 保证了变量的可见性(visibility)。被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象。

(2)为什么会出现脏读

  • Java内存模型规定所有的变量都是存在主存当中,每个线程都有自己的工作内存。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。变量的值何时从线程的工作内存写回主存,无法确定。

(3)volatile案例实战

public class VolatileDemo {
    private volatile boolean flag = false;

    public void work(){
        while (!flag) {
            System.out.println("线程开始工作");
        }
    }

    public void down(){
        flag = true;
        System.out.println("线程停止工作");
    }

    public static void main(String[] args) {
        VolatileDemo work = new VolatileDemo();
        new Thread(work::work).start();
        new Thread(work::work).start();
        new Thread(work::down).start();
        new Thread(work::work).start();
        new Thread(work::work).start();
    }
}
  • 不加volatile关键字的运行结果

【并发编程】线程安全性问题_System_06

  • 加volatile关键字的运行结果

【并发编程】线程安全性问题_代码块_07

(4)volatile只能保证变量的可见性,不能保证对volatile变量操作的原子性

  • 案例实战,分别对num进行+1000,+2000,+3000的操作
public class VolatileDemo {

    private volatile int num = 0;

    //执行方法加上synchronized
    public synchronized void addNum(){
        num++;
    }
	//执行方法加上synchronized
    public synchronized int getNum(){
        return num;
    }

    public static void main(String[] args) {

        VolatileDemo volatileDemo = new VolatileDemo();

        new Thread(()->{
            for (int i = 0; i < 1000; i++) {
                volatileDemo.addNum();
            }
            System.out.println(volatileDemo.getNum());
        }).start();

        new Thread(()->{
            for (int i = 0; i < 2000; i++) {
                volatileDemo.addNum();
            }
            System.out.println(volatileDemo.getNum());
        }).start();

        new Thread(()->{
            for (int i = 0; i < 3000; i++) {
                volatileDemo.addNum();
            }
            System.out.println(volatileDemo.getNum());
        }).start();

    }
}

【并发编程】线程安全性问题_后端_08

  • 加上synchronized之后

【并发编程】线程安全性问题_后端_09

3.5.happens-before规则

(1)理解happens-before

  • 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作执行顺序排在第二个操作之前。
  • 两个操作之间存在happens-before关系,并不意味着java平台的具体实现必须按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么JMM也允许这样的重排序。
  • 如果操作A happens-before操作B,那么操作A在内存上所做的所有操作对于操作B都是可见的,不管它们在不在同一个线程。
  • happens-before关系保证正确同步的多线程程序执行的结果不被重排序改变。

(2)happens-before六大规则

  • 前三个规则用这个例子来看
class VolatileExample{
    int a=0;
    volatile boolean flag=false;
    public void writer(){
        a=1;                                  // 操作1
        flag=true;                            // 操作2
    }
    public void reader(){
        if(flag){                             // 操作3
            int i=a;                          // 操作4
            //这里i会是多少呢?
        }
    }
}
  • 程序顺序规则

    • 一个线程中的每一个操作,happens-before于该线程中的任意后续操作可见。
    • 程序前面对某个变量的修改一定是对后续操作可见的。
    • 例如上面代码块,按照程序顺序执行规则,1 happens-before 2,3 happens-before 4。
  • volatile变量规则

    • 对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
    • 例如上面代码块,按照volatile变量规则,2 happens-before 3。
  • 传递性规则

    • 如果A appens-before B,B happens-before C,那么A happens-before C。
    • 例如上面代码块,按照传递性规则,1 happens-before 4。
  • 管程中锁的规则

    • 对一个锁的解锁,happens-before于随后对这个锁的加锁。
    • 管程是一种通用的同步原语,在java中指的就是synchronized,synchronized是java里对管程的实现。
synchronized(this){
    if(this.x < 12){
        this.x = 12;
    }
}

//根据管程中锁的规则,线程A执行完成后x会变成12,执行完释放锁,线程B进入代码块的时候,能够看到线程A对x的操作,也就睡说B看到的x值为12。
  • 线程start规则
    • 父线程A启动后,启动子线程B,子线程B能够看到主线程在启动B前的所有操作(指共享变量的操作)。
public class StartDemo {
    private static int num = 0;
    public static void main(String[] args) {

        Thread A = new Thread(()->{
            Thread B = new Thread(()->{
                System.out.println("B线程中读取num:"+num); //操作2
            });
            num = 1;   //操作1
            B.start();
        });
        A.start();
    }
}

//根据线程start规则,1 happens-before 2,线程A对共享变量a=1的操作对于线程B是可见的。

【并发编程】线程安全性问题_System_10

  • 线程join规则
    • 父线程A等待子线程B完成, 当子线程B完成后 ,父线程A能够看到子线程B的操作(指的是对共享变量的操作)。
public class JoinDemo {

    private static int num = 0;

    public static void main(String[] args) {

        Thread A = new Thread(()->{
            Thread B = new Thread(()->{
                num = 2;
            });
            num = 1;
            B.start();
            try {
                B.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A线程中读取num:"+num);
        });
        A.start();
    }
}

【并发编程】线程安全性问题_java_11

3.6.如何避免线程安全性问题

(1)线程安全性问题成因

  • 多线程环境
  • 多个线程操作同意共享资源
  • 对该共享资源进行了非原子性操作

(2)如何避免线程安全性问题

  • 多线程环境–将多线程改为单线程(加锁)
  • 多个线程操作同一共享资源–让其资源不进行共享(ThreadLocal、资源不可变、操作无状态化)
  • 对该共享资源进行了非原子性操作–将非原子性的操作改成原子性的操作(加锁)

标签:Thread,start,SyncDemo,编程,并发,线程,new,public
From: https://blog.51cto.com/u_15646271/6019228

相关文章

  • 【并发编程】线程的基础知识篇
    文章目录1.进程与线程的区别2.线程的状态相互转换3.创建线程的方式4.线程的挂起和恢复5.线程的中断操作6.线程的优先级7.守护线程1.进程与线程的区别(1)什么是......
  • 场景编程集锦 - 考试打假
    场景描述考试历来都是人才选拔高效而重要的手段,也是彰显社会公平的重要方面。无论是中国古代的科举考试,还是当今的全国高考,或者是出国留学的雅思托福考试,古今中外概莫能外。......
  • go并发的坑
    1、闭包的坑案例1:packagemainimport( "fmt" "sync" "time")funcmain(){ wg:=sync.WaitGroup{} wg.Add(2) fori:=0;i<2;i++{ gofunc(){ ......
  • Golang第六章:协程、管道、反射、网络编程
    goroutine调度模型 管道 管道基本使用packagemainimport("fmt")funcmain(){varintChanchanintintChan=make(chanint,3)......
  • jmeter添加全局变量,跨线程组传递参数
    在软件测试中,当我们想把某个变量值想设置为全局变量,也就是在任何一个线程组都可以使用该变量时,我们就要用到BeanShell取样器,示例如下:1.拿到某个接口的变量值2.通过BeanSh......
  • SQLServer 编程总结
    case的用途 case语句有四个关键字,缺一不可,分别是:casewhenthenend,另外还有一个else。case后面跟字段名(当在when后面出现字段名时,case里不能写出来),when后面跟判断语......
  • 十年学会编程
    著者:PeterNorvig翻译:DaiYuwen为何人人都这么着急?信步走进任何一家书店,你会看到名为《如何在7天内学会Java》的书,还有各种各样类似的书:在几天内或几小时内学会Vis......
  • Java并发编程
    1.并发与并行?并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。并发(concurrency):指在同一时刻只能有......
  • 小满Vue3第三十八章(函数式编程,h函数)
    之前跟大家介绍了两种vue编写风格分别是template模板方式,和JSX方式感觉JSX被大家吐槽的很厉害,其实用习惯还挺好用的今天介绍第三种函数式编程主要会用到h函数​​h​​ 接......
  • socket编程相似对象、函数、概念的区别于联系
    socketaddr、sockaddr_in与addr_insocketaddr与socketaddr_in的关系类似于基类和派生类的关系。addr_in是socketaddr_in中一个成员变量。structso......