线程安全问题

 多线程带来的的风险-线程安全(重点)

1. 观察线程不安全

package Test.thread;
public class ThreadDemo {
    private static class Counter {
        private long n = 0;

        public void increment() {
            n++;
        }

        public void decrement() {
            n--;
        }

        public long value() {
            return n;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        final int COUNT = 1000_0000;
        Counter counter = new Counter();
        Thread thread = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                counter.increment();
            }
        }, "t1");
        thread.start();
        for (int i = 0; i < COUNT; i++) {
            counter.decrement();
        }
        thread.join();
        // 期望最终结果应该是 0
        System.out.println(counter.value());
    }
}

2.线程不安全的原因

  1. CPU是抢占式执行
  2. 线程共同操作同一变量
  3. 内存不可见问题
  4. 原子性
  5. 代码顺序性   编译器优化(会使代码顺序变乱,可能导致错误)

什么是原子性

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。

那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。

有时也把这个现象叫做同步互斥,表示操作是互相排斥的。

一条 java 语句不一定是原子的,也不一定只是一条指令

比如n++,其实是由三步操作组成的:

1. 从内存把数据读到 CPU

2. 进行数据更新

3. 把数据写回到 CPU

不保证原子性会给多线程带来什么问题

如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。

3. 解决线程不安全问题

3.1 线程不安全问题解决方案分析:

  1. CPU抢占执行的问题(不可控)
  2. 每个线程操作自己的私有变量(有可能可以)
  3. 只需要在关键步骤上排队执行【加锁】

3.2 volatile 关键字 

  • 修饰的共享变量,可以保证可见性,部分保证顺序性

  可以解决内存不可见和指令重排序问题,但是不能解决原子性问题。

3.3 加锁

操作锁的流程:

  1. 尝试获取锁
  2. 使用锁
  3. 释放锁

Java语言中加锁的操作有两种:

方法1. synchronized (给某一对象的对象头加锁)

实现分为:

1.synchronized  在JVM层面使用 monitor 来实现,它帮我们实现了加锁和释放锁的过程。

2.synchronized  在操作系统层面,它是依靠motex 互斥锁。

3.针对Java语言来说

         ①锁存放的位置:对象头

         ②头信息 monitor:Owner(拥有者),EntryQ,Rcthis,Next(使用次数),HashCode

注意事项:

如果是同一业务的多线程执行,一定要使用同一个锁

synchronized 的3种使用场景:

  1. 使用 synchronized 修饰代码块(可以给任意对象加锁)

  2. 使用 synchronized 修饰静态方法(可以对当前的类进行加锁)

  3. 使用 synchronized 修饰普通方法(对当前类实例进行加锁)

高频面试点:

synchronized 锁升级的过程(JDK 1.6):重量级:用户态 → 内核态(有特别大的性能消耗)

方法2. 手动式Lock方式 

Lock(重入锁) 只能用来修饰代码块

注意事项:

1. 使用 Lock 一定要释放锁

2. lock() 操作一定要放在try 外面。

如果放在 try 里面会造成两个问题:

    1.如果 try 里面抛出异常了,还没有加锁成功就执行 finally 里面释放锁的操作。因为还没有得到锁就释放锁。

    2.如果放在 try 里面,如果没有锁的情况下试图释放锁,这个时候产生的异常就会将业务代码的异常(也就是 try 里面的异常)吞噬掉,增加了代码调试的难度。

如果一定要把 lock 放在 try 里面的话,一定要放在第一行。

在Java 语言中所有锁的默认实现方式都是非公平锁

面试重点:

synchronized 和 Lock 的区别:

  1. 关键字不同。
  2. synchronized 是自动进行加锁和释放锁的过程;而 Lock 需要手动的加锁和解锁。
  3. Lock 是Java层面的锁的实现,synchronized 是JVM层的实现。
  4. synchronized 和 Lock 适用范围不同,Lock 只能用来修饰代码块,而 synchronized  既可以修饰代码块,又可以修饰静态方法和普通方法。
  5. synchronized 锁的模式只有非公平锁模式,而 Lock 既可以使用公平锁的模式又可以使用非公平锁的模式。
  6. Lock 的灵活性更高(tryLock)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值