【Java】线程安全问题

线程安全问题

 

运行下面代码

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();
        //两个线程各自自增 50000 次,预期结果应该是 100000
        System.out.println(counter.count);
    }
}

运行效果

这样的代码就是线程不安全的代码。

 

这就是多线程并发编程所涉及到最重要最复杂的问题。

  • 线程不安全:多线程并发执行某个代码时,产生了逻辑上的错误,就是“线程不安全”。
  • 线程安全:多线程并发执行某个代码,没有逻辑上的错误,就是“线程安全”。

 

线程不安全的原因是什么?

  1. 线程是抢占式执行的(线程不安全的万恶之源)。【抢占式:线程之间的调度完全由内核负责,用户代码中感知不到,也无法控制。线程之间谁先执行,谁后执行,谁执行到哪里从 CPU 上下来,这样的过程都是用户无法控制也无法感知的】
  2. 自增操作不是原子的。【每次 ++ 都能拆分成三个步骤:1、把内存中的数据读取到 CPU 中(load);2、在 CPU 中把数据 +1(increase);3、把计算结束的数据写回到内存中(save)。当 CPU 执行到上面三个步骤中的任何一步的时候,都可能会被调度器调度走,执行其他的线程】
  3. 多个线程尝试修改同一个变量,如果有多个线程,一个线程读取数据,一个线程修改数据,此时也是可能导致线程不安全【如果是一个线程修改一个变量,线程安全;如果多个线程尝试读取同一个变量,线程安全;如果多个线程尝试修改不同的变量,线程安全】
  4. 内存可见性导致的线程安全问题
  5. 指令重排序(Java 的编译器在编译代码时,会针对指令进行优化,调整这领的先后顺序,保证原有逻辑不变的情况下,提高程序的运行效率)

如果两个线程是串行执行的,运行后计算结果是正确的,并发执行的效果,线程1 进行 ++ 了一半的时候,线程 2 也在公式进行 ++

 

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

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

 

什么是锁?

与生活中的锁类似

锁的特点:

互斥的。同一时刻只有一个线程能获取到锁。其他线程如果也尝试获取锁,就会发生阻塞等待,一直等到刚才的线程释放锁,此时剩下的线程再重新竞争锁 

 

锁的基本操作:

  1. 加锁(获取锁)lock
  2. 解锁(释放锁)unlock

Java 中使用锁要借助 synchronized 关键字,用法可以灵活的指定某个对象来加锁,而不仅仅是把锁加到某个方法上,如果把 synchronized 关键字写到方法内部,英文原意为同步,在这里代表互斥

 

在刚刚的代码中,自增方法前加上 synchronized 关键字

 

此时的运行结果就是正确的 100000

 

锁是如何解决线程安全问题的?

线程1 释放锁之后,线程2 才可能获取到锁

此时线程1 哪怕被执行了一半被调度走了也没关系,其他的线程想尝试 ++ 操作也不会对线程1 的修改产生任何负面影响,这样的话线程1 的自增操作就能被一鼓作气的执行完,中间不会受到干扰,也就相当于保证了原子性
 

synchronized 的几种常见用法:

  1. 加到普通方法前:表示锁 this
  2. 加到静态方法前:表示锁当前类对象
  3. 加到某个代码块之前,显示指定给某个对象加锁

 

使用锁时要注意:

  1. 使用的时候一定要注意按照正确的方式来使用,否则就容易出现各种各样的问题
  2. 一旦使用锁,这个代码基本上就和“高性能”无缘了,锁的等待时间可能会很久

因为锁的等待时间是不可控的,可能会等很久,也有可能会出现死锁的问题,一旦程序出现死锁的问题,程序就凉凉~了

 

看下面对锁的使用

当前这个代码中,两个线程尝试获取同一把锁

import java.util.Scanner;


public class ThreadDemo1 {
    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(){
            @Override
            public void run(){
                Scanner sc = new Scanner(System.in);
                System.out.println("请输入一个整数");
                synchronized (locker) {
                    //如果用户不输入,锁救护一直阻塞在 nextInt 中
                    int num = sc.nextInt();
                    System.out.println("num = " + num);
                }
            }
        };
        t1.start();
        Thread t2 = new Thread(){
            @Override
            public void run(){
               while (true){
                   synchronized (locker){
                       System.out.println("线程2获取到锁");
                       try {
                           Thread.sleep(1000);
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                       }
                   }
               }
            }
        };
        t2.start();
    }
}

执行效果

一旦线程1 获取到锁,并且没有被释放的话,线程2 就会一直在锁这里被阻塞等待 

在 jconsole 中查看一下,线程1 就被阻塞在了 nextInt 方法,等待用户输入

 

再看看线程2 的调用栈,阻塞在了我代码的 32 行,并且进入了 BLOCKED 状态

 

在锁这里被阻塞

 

当我输入了一个整数后,线程1 的锁就被释放了,线程2 就可以继续运行了

 

当两个线程对两个对象来加锁后,两个线程就不再互相竞争了

package com.Test0821;

import java.util.Scanner;


public class ThreadDemo1 {
    public static void main(String[] args) {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(){
            @Override
            public void run(){
                Scanner sc = new Scanner(System.in);
                System.out.println("请输入一个整数");
                synchronized (locker1) {
                    //如果用户不输入,锁救护一直阻塞在 nextInt 中
                    int num = sc.nextInt();
                    System.out.println("num = " + num);
                }
            }
        };
        t1.start();
        Thread t2 = new Thread(){
            @Override
            public void run(){
               while (true){
                   synchronized (locker2){
                       System.out.println("线程2获取到锁");
                       try {
                           Thread.sleep(1000);
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                       }
                   }
               }
            }
        };
        t2.start();
    }
}

 

运行效果

线程1 没有释放锁,线程2 也可以正常获取到锁,因为两个线程获取的不是同一个对象的锁

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值