线程安全(重点)

Posted 钊z

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程安全(重点)相关的知识,希望对你有一定的参考价值。

文章目录

一.线程安全的概念

先来看一段代码

class Counter
    public int count = 0;
    public void add()
      count++;
    


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


可以看到结果是不确定的


1.1 线程安全的概念

先来说一下非线程安全的概念:非线程安全主要是指多个线程对同一个对象中的同一个实例变量进行操作时会出现值被更改、值不同步的情况,进而影响程序的执行流程。
线程安全:如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

1.2 线程不安全的原因

先解释上述线代码程不安全的原因:

如果两个线程并发执行count++,此时就相当于两组load add save进行执行,此时不同的线程调度顺序就可能会产生一些结果上的差异

由于线程的抢占执行,导致当前执行到任意一个指令,线程都可能bei调度走,CPU让别的线程来执行
如下图:

导致下面的结果:

线程安全问题的原因:
1.抢占式执行,随机调度(根本原因)
2.代码结构:多个线程同时修改同一个变量
3.原子性(操作是非原子性,容易出现问题)
4.内存可见性问题(如一个线程读,一个线程改)
5.指令重排序

1.3 解决线程不安全

从原子性入手,通过加锁,把非原子的,转成"原子"的

加了synchronized之后,进入方法就会加锁,出了方法就会解锁,如果两个线程同时尝试加锁,此时一个能获取锁成功,另一个只能阻塞等待(BLOCKED),一直阻塞到刚才的线程解锁,当前线程才能加锁成功

二.synchronized-monitor lock(监视器锁)

2.1 synchronized的特性

(1)互斥

  • 进入sychronized修饰的代码块,相当于加锁
  • 退出sychronizde修饰的代码块,相当于解锁

(2)刷新内存

synchronized的工作过程:

1.获得互斥锁
2.从内存拷贝变量的最新副本到工作的内存
3.执行代码
4.将更改后的共享变量的值刷新到主内存
5.释放互斥锁

(3)可重入

synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题(自己可以再次获取自己的内部锁)
理解"把自己锁死"
一个线程没有释放锁,然后又尝试再次加锁

按照之前对于锁的设定,第二次加锁的时候,就会阻塞等待,而获取不到第一次的锁,就把自己锁死

2.2 synchronied使用方法

1.直接修饰普通方法:

锁的SynchronizedDemo1对象

public class SynchronizedDemo1 
    public synchronized void methond() 
    

2.修饰静态方法:

锁SynchronizedDemo2对象

public class SynchronizedDemo2 
    public synchronized void methond() 
    

3.修饰代码块:

明确指定锁哪个对象

public class SychronizedDemo
    public void method()
         sychronized(this)
          
          
    

锁类对象

public class SynchronizedDemo 
     public void method() 
        synchronized (SynchronizedDemo.class) 
          
         
     

三.死锁

3.1死锁的情况

1.一个线程,连续加锁两次,如果锁是不可重入锁,就会死锁
2.两个线程,两把锁,t1和t2各自先针对锁A和锁B加锁,在获取对方的锁

public class Thread15 
    public static void main(String[] args) 
        Object lock1 = new Object();
        Object  lock2= new Object();
        Thread t1 = new Thread(()->
            synchronized (lock1)
                try 
                    Thread.sleep(1000);
                 catch (InterruptedException e) 
                     e.printStackTrace();
                
                synchronized (lock2)
                    System.out.println("t1把锁1和锁2都获得了");
                
            

        );
         Thread t2 = new Thread(()->
            synchronized (lock2)
                try 
                    Thread.sleep(1000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
                synchronized (lock1)
                    System.out.println("t2把锁1和锁2都获得了");
                
            
             ;
         );
         t1.start();
         t2.start();
    



3.多个线程,多把锁(相当于2的一般情况)

3.2 死锁的四个必要条件

1.互斥使用

线程1拿到了锁,线程2就须等着

2.不可抢占

线程1拿到锁A之后,必须是线程1主动释放

3.请求和保持

线程1拿到锁A之后,在尝试获取锁B,A这把锁还是保持的

4.循环等待

线程1尝试获取到锁A和锁B,线程2尝试获取锁B和锁A,线程1在获取B的时候等待线程2释放B,同时线程2 在获取A的时候等待线程1释放A

3.3解决死锁的办法

给锁编号,然后指定一个固定的顺序来加锁,任意线程加把锁,都让线程遵守上述顺序,此时循环等待自然破除

对于synchronied前三个条件都是锁的基本特性,我们只能对四修改

public class Thread15 
    public static void main(String[] args) 
        Object lock1 = new Object();
        Object  lock2= new Object();
        Thread t1 = new Thread(()->
            synchronized (lock1)
                try 
                    Thread.sleep(1000);
                 catch (InterruptedException e) 
                     e.printStackTrace();
                
                synchronized (lock2)
                    System.out.println("t1把锁1和锁2都获得了");
                
            

        );
         Thread t2 = new Thread(()->
            synchronized (lock1)
                try 
                    Thread.sleep(1000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
                synchronized (lock2)
                    System.out.println("t2把锁1和锁2都获得了");
                
            
             ;
         );
         t1.start();
         t2.start();
    


四.volatile 关键字

volatile 和内存可见性问题密切相关

一个线程针对一个变量进行读取操作,同时另一个线程针对这个变量进行修改,此时读取到值,不一定是修改之后的值(归根结底是编译器/jvm在多线程下优化时产生了误判)




使用汇编语言解释
1.load,把内存中flag的值,读取到寄存器
2.cmp把寄存器的值和0进行比较,根据比较结果,决定下一不执行.
由于load执行速度太慢(相比于cmp来说),再加上反复load的结果都一样,JVM就不在重复load判定没人改flag值,就只读取一次就好
而给flag加上volatile关键字,告诉编译器变量是"易变"的,不再进行优化

class MyCounter
     volatile public int flag = 0;

public class Thread16 
    public static void main(String[] args) 
        MyCounter myCounter = new MyCounter();
        Thread t1 = new Thread(() ->
            while (myCounter.flag == 0)
                //循环体空着

            
            System.out.println("t1循环结束");
        );
        Thread t2 = new Thread(() ->
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            myCounter.flag = scanner.nextInt();
        );
        t1.start();
        t2.start();
    


结果:

五. wait和notify

wait和notify可以协调线程之间的先后顺序

完成这个协调工作, 主要涉及到三个方法

  • wait() / wait(long timeout): 让当前线程进入等待状态.
  • notify() / notifyAll():唤醒在当前对象上等待的线程.

注意: wait, notify, notifyAll 都是 Object 类的方法

5.1 wait()方法

wait的操作
1.先释放锁
2.在阻塞等待
3.收到通知之后,重新获取锁,并且在获取锁后,继续往下执行

wait操作需要搭配synchorized来使用

public class Thread17 
    public static void main(String[] args) throws InterruptedException 
        Object object = new Object();

            System.out.println("wait之前");
            object.wait();
            System.out.println("wait之后");

    

无synchorized的情况

wait无参数版本,就是死等
wait带参数版本,指定了等待的最大时间

5.2 notify()方法

notify()方法是唤醒等待线程

  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)

  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

notfiyAll()方法可以一次唤醒所有的等待线程

线程安全问题重点

多线程带来的的风险 - 线程安全

线程安全

线程安全问题是多线程所涉及到的最重要的,也是最复杂的问题

观察线程不安全

public class ThreadDemo14 
    static class Counter
        public int count = 0;
        public void increase()
            count++;
        
    
    public static void main(String[] args) throws InterruptedException 
        Counter counter = new Counter();
        Thread t1 = new Thread()
            @Override
            public void run()
                for (int i = 0; i < 50000; i++) 
                    counter.increase();
                
            
        ;
        t1.start();

        Thread t2 = new Thread()
            @Override
            public void run()
                for (int i = 0; i < 50000; i++) 
                    counter.increase();
                
            
        ;
        t2.start();
        t1.join();
        t2.join();
        // 两个线程各自自增5000次,最终预期结果,应该是10w
        System.out.println(counter.count);
    

注意:

运行结果:


多运行几次你会发现,结果并不是10w,而且每次运行的结果都不一样
上述现象:则是线程不安全
线程不安全: 多线程并发执行某个代码时,产生了逻辑上的错误,就是"线程不安全"

线程安全的概念

和线程不安全对应,线程安全就是 多线程并发执行某个代码,没有逻辑上的错误,就是"线程安全"

线程不安全的原因

思考: 为啥会出现上述情况???

原因:

  • 线程是抢占式执行的 (线程不安全的万恶之源)
    抢占执行:线程之间的调度完全由内核负责,用户代码中感知不到,也无法控制
    线程之间谁先执行,谁后执行,谁执行到哪里从CPU上下来,这样的过程都是用户无法控制的,也是无法感知的
  • 自增操作不是原子的
    每次++,都能拆分成三个步骤:
    1.把内存中的数据读取到CPU中 — load
    2.在CPU中,把数据+1 — increase
    3.把计算结束的数据写回到内存中 — save
    当CPU执行到上边三个步骤的任意一个步骤时,都可能被调度器调度走,让给其他线程来执行

画图表示:
上述代码的执行结果在范围 [5w,10w] 之间
极端情况下,
t1 和 t2 每次++ 都是纯并行的,结果就是 5w
t1 和 t2 每次++ 都是纯串行的,结果就是 10w
实际情况,一般不会这么极端,调度过程中有时候是并行,有时候是串行(多少次并行,多少次串行,这个不清楚),因此导致最终的结果是在 [5w,10w] 之间

  • 多个线程尝试修改同一个变量
    若一个线程修改一个变量,线程安全
    若多个线程尝试读取同一个变量,线程安全
    若多个线程尝试修改不同的变量,线程安全
  • 内存可见性
  • 指令重排序
    Java 的编译器在编译代码时,会针对指令进行优化 (优化:调整指令的先后顺序,保证原有逻辑不变的情况下, 来提高程序的运行效率)

如何解决线程不安全问题?

1.抢占式执行 — (这个没法解决,操作系统内核解决)
2.自增操作非原子 — (这个有办法,可以给自增操作加上锁) 适用范围最广
3.多个线程同时修改同一个变量 — (这个不一定有办法解决,得看具体的需求)

转下篇:

以上是关于线程安全(重点)的主要内容,如果未能解决你的问题,请参考以下文章

Java中的线程安全问题(多线程重点)

Java中的线程安全问题(多线程重点)

Java中的线程安全问题(多线程重点)

异步编程之 EventLoop

异步编程之 EventLoop

面试问题总结(一)Golang