线程安全问题及如何解决!!!

线程安全问题

1.线程不安全:是指使用多线程执行任务时,得到的结果与预期不相符。

private static int count=0;
    public static void main(String[] args) {
        Thread t1=new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i <100000 ; i++) {
                    count++;
                }
            }
        });
        t1.start();
        Thread t2=new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++) {
                    count--;
                }
            }
        });
        t2.start();
        System.out.println(count);
    }

在这里插入图片描述
预期结果为0,这就发生了线程不安全问题

2.造成线程不安全的因素
(1)CPU是抢占式执行的(万恶之源)
(2)多个线程同时修改同一个变量
(3)可见性问题,即内存不可见
为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,这就会导致共享变量在多线程之间不能及时看到改变,这就是内存不可见。(共享的主内存的值修改了,而先操作的线程的工作内存的值没有修改)
(4)原子性问题

  • 原子性操作:一个或某几个操作只能在一个线程执行完之后,另一个线程才能开始执行该操作,也就是说这些操作是不可分割的,线程不能在这些操作上交替执行。
  • 比如:n++就不是原子性操作
    它是由三步操作组成的。从内存把数据读到CPU,进行数据操作,把数据写回CPU。
  • 不保证原子性会给多线程带来什么问题?
    在一个线程对一个变量操作时,会有别的线程插入进来,造成线程混乱

(5)编译器优化即指令重排序
在复杂的多线程中会出现混乱,从而导致线程不安全

如何解决线程不安全问题

线程不安全问题解决方案分析:
1.CPU抢占式执行的问题(不可控的)
2.多个线程同时修改同一个变量(让每个线程操作自己的私有变量,可能可以)
3.内存不可见问题(volatile关键字)
4.指令重排序问题(volatile关键字)
5.原子性问题(加锁)

详细说明两个关键字
1.volatile关键字(修饰操作的变量)

  • 可以解决内存不可见和指令重排序问题,但是不能解决原子性问题
  • 解决内存不可见:强制将线程自己的工作内存中的值清除,然后从共享的主内存中取值。

2.加锁(解决原子性问题)
java语言的加锁操作有两种:synchronized关键字和手动锁Lock

操作锁流程:尝试获取锁—>使用锁(具体业务)---->释放锁

synchronized关键字(监视器锁monitor lock)
使用方法:

 // 全局变量
    private static int number = 0;
    // 循环的最大次数
    private static final int maxSize = 100000;

    public static void main(String[] args) throws InterruptedException {
        // 声明锁对象
        Object lock = new Object();
        
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < maxSize; i++) {
                    // 实现加锁
                    synchronized (lock) {
                        // 代码1
                        number++;
                    }
                }
            }
        });
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < maxSize; i++) {
                    synchronized (lock) {
                        number--;
                    }
                }
            }
        });
        t2.start();

        // 等待两个线程执行完成
        t1.join();
        t2.join();

        System.out.println("最终执行结果:" + number);
    }

1.synchronized关键字三个维度

  • 是JVM层面锁的解决方案,它帮咱们实现了加锁和释放锁的过程,加锁monitorenter ,释放锁monitorexit
    在这里插入图片描述

  • 在操作系统层面使用的是mutex lock互斥锁来实现的

  • 针对Java语言来说,是将锁信息存放在对象头里(对象头里有两个重要的标识:标识锁状态,标识锁的拥有者

锁存放的地方:对象头
在这里插入图片描述

锁信息monitor
在这里插入图片描述
2.使用锁的注意事项:如果是同一业务的多线程执行,一定要使用同一把锁。

3.synchronized锁升级的过程

  • JDK1.6之前:重量级锁(用户态----->内核态),有特别大的性能消耗
  • JDK1.6之后
    在这里插入图片描述

手动锁Lock
使用方法:

// 全局变量
    private static int number = 0;
    // 循环的最大次数
    private static final int maxSize = 100000;

    public static void main(String[] args) throws InterruptedException {
        Lock lock=new ReentrantLock();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < maxSize; i++) {
                    // 加锁
                    lock.lock();
                    try {
                        //业务操作
                        number++;
                    }finally {
                        //释放锁
                        lock.unlock();
                    }
                }
            }
        });
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < maxSize; i++) {
                    // 加锁
                    lock.lock();
                    try {
                        //业务操作
                        number--;
                    }finally {
                        //释放锁
                        lock.unlock();
                    }
                }
            }
        });
        t2.start();

        // 等待两个线程执行完成
        t1.join();
        t2.join();

        System.out.println("最终执行结果:" + number);
    }

1.Lock(接口)手动锁的主要方法有
lock():加锁
trylock():在没有锁的时候可以尝试获取锁,不用死等
unlock():释放锁

2.注意事项:
(1)lock()操作一定要放在try外面
如果放在try里面可能会造成两个问题:

  • 如果try里面抛出异常了,还没加锁成功就执行finally里面的释放锁操作了(没得到锁就释放锁)
  • 在没有的到锁的情况下试图释放锁,这个时候产生的异常就会将业务代码产生的异常(try里面的异常)覆盖,增加了代码调试的难度。
    如果一定要放在try里面,一定要放在第一行
    (2)unlock()必须放在finally里面

公平锁和非公平锁

  • 公平锁调度
    1.一个线程释放锁
    2.(主动)唤醒“需要得到锁”的队列来得到锁(按序执行)
  • 非公平锁调度
    当一个线程释放锁之后,另一个线程刚好执行到获取锁的代码就可以直接获取锁。(抢占式执行)

1.非公平锁的性能更高
2.在Java语言中所有的锁的默认实现方式都是非公平锁
3.synchronized是非公平锁,ReentrantLock是默认是非公平锁,但也可以显式的声明为公平锁。
显式的声明公平锁:

 Lock lock=new ReentrantLock(true);

synchronized和Lock的区别
1.关键字不同
2.synchronized自动进行加锁和释放锁,而Lock需要手动加锁和释放锁。
3.Lock是Java层面的锁的实现,而synchronized是JVM层面的实现
4.synchronized和Lock适用范围不同,Lock只能用来修饰代码块,而synchronized既可以修饰代码块,又可以用来修饰静态方法和普通方法
5.synchronized锁的模式只有非公平锁模式,而Lock既可以使用公平锁的模式又可以使用非公平锁的模式
6.Lock的灵活性更高(trylock() )
7.Lock的粒度比较小,修饰的东西比较细致

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值