原来这就是线程安全(一)

本文详细解释了线程不安全的概念,通过代码示例展示了并发编程中出现线程不安全问题的原因,包括随机调度、共享变量和非原子操作。文章还介绍了如何通过加锁机制(如`synchronized`关键字)来确保线程安全,并提供了多种加锁方法的示例。
摘要由CSDN通过智能技术生成

@TOC

一:什么是线程不安全??

先看一段代码:

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

在这里插入图片描述
上面的代码可能我们以为最终的count=100000;
但最终小于100000,这就是线程安全在搞鬼.
因为线程是随机调度 的,抢占式执行,就会使代码执行结果不可控,结果也就是随机的.
我们往往希望结果使我们所预期的,而这种不可控,随机的执行结果就称为bug.
多线程代码,引起了bug,这样的问题就是"线程安全问题",存在线程安全问题的代码,就称为"线程不安全".

二:为什么代码执行结果不是100000

count++操作其实是三个指令:
(1)把内存中的count中的数值,读到CPU寄存器中.(load)
(2)把寄存器中的值+1,(add).
(3)把寄存器上述计算后的值,写回到内存的count里.(save)
但因为不止一个线程,某个线程在执行指令的过程中,当它执行到任何一个指令的时候,由于随机调度,抢占式执行,就有可能被其他线程把它的CPU给抢占走.(操作系统把前一个线程调度走,后一个线程执行).
下面列举两个线程并发执行的时候,可能的执行指令的顺序,(不同的执行顺序,得到的结果可能存在差异)
在这里插入图片描述

在这里插入图片描述

我们知道count数据是存在内存中的,而同一个进程,不同的线程是共用一块内存资源的,也就是t1,t2线程操作的是同一块内存地址.
以下图为例:
在这里插入图片描述
1: t1线程 load操作拿到count的值,拿到了0,放到了A寄存器中;
2:t2线程 load 操作拿到count的值,拿到了0,放到了B寄存器中;
3:t1线程执行add操作,将A寄存器中的值+1,
4:t2线程执行add操作,将B寄存器中的值+1,
5:t1线程执行save 操作,将A寄存器中的值写回到内存中,内存中的值由0变为1.
6:t2线程执行save 操作,将B寄存器中的值写回到内存中,内存中的值由1变为1.

三:出现线程不安全的原因:

1:线程在系统中是随机调度,抢占式执行的,
2:当前代码中,多个线程同时修改同一个变量.

如果只有一个线程,那么修改操作没问题.
如果多个线程是读取操作,那么也没事.
如果多个线程修改不同的变量,也没事.
3:线程针对变量的修改操作不是"原子"的,而是三条指令.
但有的修改操作,是原子的,比如直接对变量进行赋值操作(CPU中就一个move指令).
4:内存可见性问题,引起的线程不安全
5:指令重排序,引起的线程不安全

四:解决线程不安全问题

线程不安全原因1:我们不能修改,因为硬件方面就是这样设计的(内核就是这样设计的 ).
原因2:不能修改,因为此时场景就是多线修改同一个变量.
原因3:可以通过"加锁"操作,将多个指令打包成一个整体.
通过"加锁",就达到了"互斥"的效果
互斥:t1线程在执行的时候,t2线程不能执行(阻塞等待),t2线程在执行的时候,t1线程不能执行(阻塞等待),互斥也称为锁竞争,锁冲突.

1:锁的操作:

(1)加锁:t1加上锁之后,t2也尝试加锁,就会阻塞等待(都是系统内核控制)(在Java中可以看到BLOCKED状态)
(2)解锁:直到t1解锁了之后,t2才有可能拿到锁(加锁成功).

2:编写代码

首先创建一个对象,使用这个对象作为锁:
在Java中可以使用任何对象作为加锁对象.
创建锁对象的意义:
锁对象的用途:有且只有一个,那就是用来区分,多个线程是否针对同一个对象(count)加锁,如果是,就会出现锁竞争/锁冲突/互斥,就会引起阻塞等待;如果不是,就不会出现锁竞争,也就不会阻塞等待.
通过设定不同的锁对象,来确定竞争关系.

public class Demo2 {
    public static int count=0;

    public static void main(String[] args) throws InterruptedException {
        //首先创建一个对象,使用这个对象作为锁
        Object locker = new Object();
        Thread t1=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
              synchronized (locker){
                  count++;
              }

            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                synchronized (locker){
                    count++;
                }

            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " +count);
    }
}

2.1 synchronized (){}

()里面写"锁对象"
{}:当进入代码块,就相当于对锁对象进行了加锁操作.
当出了代码块,就相当于对锁对象进行了解锁操作.

public class Demo2 {
    public static int count=0;

    public static void main(String[] args) throws InterruptedException {
        //首先创建一个对象,使用这个对象作为锁
        Object locker = new Object();
        Thread t1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
              synchronized (locker){
                  count++;
              }

            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (locker){
                    count++;
                }

            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " +count);
    }
}

上述代码就相当于两个线程,针对同一个锁对象加锁,就会产生互斥.
t1,t2做的事情就是:判断循环条件,加锁,load ,add, save ,解锁,i++;
假设:由于是线程是并发执行的,这里假设t1线程先执行到了加锁操作,并且t1还没解锁,那么t2线程就不能获得锁对象,就不能进行加锁操作,
假设t1刚解锁,t2 就加锁了,但t1线程并不是什么代码也不执行,而是继续执行i++,循环判断条件,当t1执行到了synchronized,发现不能获得锁对象,那么t1线程只好阻塞等待了.
因此:在t1,t2两个线程中,每次count++是存在锁竞争的,会变成"串行"执行,但是执行for 循环中的条件以及i++仍然是并发执行的.

public class Demo2 {
    public static int count=0;

    public static void main(String[] args) throws InterruptedException {
        //首先创建一个对象,使用这个对象作为锁
        Object locker = new Object();
        Object locker2=new Object();
        Thread t1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
              synchronized (locker){
                  count++;
              }

            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (locker2){
                    count++;
                }

            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " +count);
    }
}

上述代码就是两个线程针对不同对象加锁,就不会产生互斥,也就不会发生阻塞等待现象了.
操作系统中的加锁,解锁功能,核心还是CPU提供的指令(硬件提供了这样的能力,软件上才有对应的功能)

3:加锁的变种写法:

3.1:创建一个自定义类,并把这个类的引用作为加锁对象

class Count {
    public int count;
    public void add(){
        count++;
    }
    public int get(){
        return count;
    }
}
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Count count=new Count();
        Thread t1=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {
                synchronized (count){
                    count.add();
                }

            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {
                synchronized (count){
                    count.add();
                }

            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count= " +count.get());

    }
}

3.2: synchronized (this){}

class Count1 {
    public int count;
    public void add(){
        synchronized (this){
            count++;
        }

    }
    public int get(){
        return count;
    }

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

3.3: synchronized public void add(){}

class Count1 {
    public int count;
    synchronized public void add(){
            count++;
    }
    public int get(){
        return count;
    }

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

3.4:针对类对象加锁

class Count1 {
    public static int count;
   static void func(){
       synchronized (Count1.class){
           count++;
       }
   }
   public int get(){
       return count;
   }


public class Demo2 {

    }
    public static void main(String[] args) throws InterruptedException {
        Count1 count1=new Count1();
        Count1 count2=new Count1();
        Thread t1=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {

            count1.func();
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {
           count2.func();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = "+count1.get());
    }
}

  • 47
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十一.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值