java并发编程之线程安全问题

       首先我们上次在--java多线程基础--中说到,线程是独立的,可以是多个线程并行执行的并且不会影响其他线程的一条执行路径。

这个其实很让人误解,但是说是不会影响到其他线程也对,这里有必要解释一下多线程到底是为了什么?在干什么?

        多线程是为了什么:多线程就是为了提高程序运行效率。

        多线程在干什么:多线程就是在充分压榨CPU,让CPU不停的为我们工作,充分利用每一个线程来为我们做事情。

        所以在--java多线程基础--所说的独立的不影响别的线程,指的是在运行期间,每个线程都是独立的,可以并行运行的一条执行路径。

那么进入正题:

首先第一个点,什么是线程安全?

       答:我们上面也说到了,多线会充分的压榨UPC,来为我们做事情,现在硬件技术越来越好,我们CPU处理的速度也越来越快,当然大家知道什么太快了也不好~~咳咳,回到正题,线程安全问题,就是当多个线程同时共享同一个对象(全局变量,静态变量等),并且对该对象做操作的时候,那么就会发生线程安全问题

       总结:线程安全就指的是,在一个线程操作该对象的过程中,没有其他线程对该对象做操作,通俗一点说,一次只能是一个线程对该对象做操作就是线程安全的,如果是操作过程中有其他线程也在操作该对象,那么就是线程不安全,也称之为线程安全问题,如果是多个线程进行读取该对象是不会发生线程安全问题,只有对该对象进行操作才会发生线程安全问题

我们举个多线程买火车票的例子:

//火车票
@Data
public class TrainTickets {

    //表示我们有100个火车票
    private static Integer number = 100;

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
               try{
                   while (TrainTickets.number > 0){
                       Thread.sleep(50);
                       System.out.println(Thread.currentThread().getName()+"拿到了:"+TrainTickets.number--+" 张火车票");
                   }
               }catch (Exception e){
                   e.printStackTrace();
               }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    while (TrainTickets.number > 0){
                        Thread.sleep(50);
                        System.out.println(Thread.currentThread().getName()+"拿到了:"+TrainTickets.number--+" 张火车票");
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }).start();
    }

}

上面的例子就是,两个窗口同时抢火车票,也就是我们说的--操作--那么我们看看结果:

       我们可以很清晰的看到我们的火车票卖重复了,这是不可取的,怎么可能存在两张相同火车票,而且后面的顺序也不对,我们希望看到的是一个降序的输出,因为现在硬件很好,其实我们要模拟线程安全问题还是比较难的,计算机处理速度现在普遍很快,我们可以借助Thread.sleep()进行让线程休眠,在同时执行,就可以看到线程安全问题。

那么这里就先基于Java并发包(java.util.concurrent)的三种解决方式:

       首先我们需要知道什么是Java的内置锁:Java提供了一种内置的锁机制来支持原子性每一个Java对象都可以用作一个实现同步的锁,称为内置锁,就相当于一根网线,当一个电脑插着该网线的时候,别的网线就需要等待它使用完毕之后才能拿到网线进行使用

1:使用synchronized关键字(内置锁

     1-1:同步代码块

//火车票
@Data
public class TrainTickets {

    //表示我们有100个火车票
    private static Integer number = 100;

    //出票操作
    public static Integer ticketIssue(){
        synchronized (number){
            return TrainTickets.number--;
        }
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    while (TrainTickets.number >0) {
                        Thread.sleep(50);
                        System.out.println(Thread.currentThread().getName() + "拿到第了:" + TrainTickets.ticketIssue() + " 张火车票");
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    while (TrainTickets.number >0) {
                        Thread.sleep(50);
                        System.out.println(Thread.currentThread().getName() + "拿到第了:" + TrainTickets.ticketIssue() + " 张火车票");
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });
        thread1.start();
        thread2.start();
    }

}

我们对我们上次的代码稍加进行修改,新增出票方法,然后在对票操作的地方加上 synchronized

       这次我们可以清晰的看到两个线程拿到的票是没有重复的,那么这就保证了我们的逻辑,从这里可以看出 synchronized 关键字是可以解决线程安全问题的。

下面对于synchrsyonized的使用介绍很关键

      1-1-1:synchronized 关键字不仅仅可以是代码块,也可以加在方法上,加在方法上就代表整个方法都每次只能有一个线程进行执行,但是我们不推荐这样做,推荐使用同步代码块,在可能发生线程安全问题的地方使用同步代码块即可。

      1-1-2:在同步代码块或者同步方法包裹的内容执行期间,我们可以理解为锁未释放,那么在这个期间的其他线程就算拿到了CPU的资源也是不可以进行运行,他们会进行等待,只有拿到了锁才可以进行运行,那么这样我们就保证了线程安全问题。

      1-1-3:关于同步代码块的参数问题,可以使用任意对象,记住一定是对象,这里为什么说我上面可以使用Integer类型,因为Integer是int的封装对象,基本数据类型的封装对象都可以作为锁对象,还有String类型,当然还可以传递自己这个类对象,比如:synchrsyonized(TrainTickets.class,这就是使用this锁

      1-1-4:死锁,什么是死锁:线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。我们知道了一个线程只能拿到了锁资源才能进行执行,如果两个线程互相拿到了对方的钥匙,那么这个时候两个线程就卡在哪里不能执行,这是一个很严重的后果,所以我们要避免这个情况

      1-1-5:如何避免死锁情况,那就是不要重复加锁,而且尽量使用同一把锁,也就是说不要在锁里面嵌套锁,而且就算在锁里面嵌套锁,也尽量使用同一个对象作为锁对象,其实说明白的,既然我们的同步代码块包裹的内容只有一个线程能执行,那么在里面继续枷锁的意义是啥,而且还容易出现死锁现象,因为当锁没有释放的时候其他线程拿不到锁,那么就永远执行不了,然而影响程序效率。

      1-1-6:拓展 synchronized 修饰方法使用锁是当前this锁。synchronized 修饰静态方法使用锁是当前类的字节码文件

2:使用lock锁 

 private static Lock lock = new ReentrantLock();


    //出票操作
    public static Integer ticketIssue(){
        Integer result = null;
        try {
            //上锁
            lock.lock();
            result = TrainTickets.number--;
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            //释放锁
            lock.unlock();
        }
        return result;
    }

这里我们使用lock锁进行实现synchronized的功能,我们还是使用TrainTickets这个类作为演示,main方法不变,我们只改变一下出票方法

运行结果:

从运行结果我们可以看到,lock锁可以以实现synchronized的功能

      2-1-1:lock锁也遵循一把锁的规则,如果我们把锁创建在出票的方法中的话,那么同样会发生线程安全问题,因为每一个线程进来都是一个新的锁,所以我们还是要提供一把锁,而不是多把锁。

      2-1-2:lock锁需要手动上锁,和释放锁,所以一般结合try-catch-finally进行使用,使用完之后必须释放锁

      2-1-3:lock锁和synchronized的主要区别:那就是处理机制不同,而且lock锁可以进行条件判断并中断锁,因为在finally中我们释放了锁,那么其他锁就可以拿到锁资源进行运行。如果想详细了解二者区别请点击我:深入研究 Java Synchronize 和 Lock 的区别与用法

      2-1-3:lock总结:需要手动上锁,解锁,可以在try中进行条件判断,如果发生某些情况可以抛出异常中断锁,让出资源。

       到了这里基本上常用的java内置的锁,我们就讲解完毕了,他们都是给可能发生线程安全问题的地方加锁,保证一个线程进行运行,而保证线程安全问题,同样的他们还有一个名称,叫做重入锁,也就是他们的锁对象是可以传递的,也就是同一把锁,外层的锁可以进行传递,内层的锁可以同样拿到该锁对象。

3:ThreadLocal接口

如果想详细了解ThreadLocal的可以点击我:彻底理解ThreadLocal

       这里我们直接说简单地说一下,ThreadLocal是怎么实现线程安全的,Threadlocal为每一个线程都创建了一个变量的副本,那么也就是说,每个线程访问的对象都是一个全新的,属于自己的对象,那么是属于自己一个人的,也就保证了只允许一个线程进行访问,那么只有一个线程进行访问,那么怎么会发生线程安全问题呢?至于他的原理,那就是使用的Map集合,把线程名称作为一个Key进行保存,下次如果存在key只需要取出保存的对象即可,如果可有该对象,那么就添加一个即可。

这是官方的一些源码,可以很清晰的看到,是保存在一个Map中。

threadLocal很简单,只有四个方法:

ThreadLocal关键方法
名称作用
initiaValue初始化数据使用
get得到保存的数据
set设置保存的数据
remove删除

 

那么我们还是使用示例,火车票进行使用ThreadLocal

public class MyThreadLocal implements Runnable {

    private static Integer number = 100;

    private static ThreadLocal<Integer> threadLocal =  new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return MyThreadLocal.number;
        }
    };

    public Integer get(){
        int number = threadLocal.get();
        threadLocal.set(--number);
        return number;
    }

    @Override
    public void run() {
        try{
            while (threadLocal.get() >0) {
                Thread.sleep(50);
                System.out.println(Thread.currentThread().getName() + "拿到第了:" + get() + " 张火车票");
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        MyThreadLocal myThreadLocal = new MyThreadLocal();
        Thread thread = new Thread(myThreadLocal);
        Thread thread1 = new Thread(myThreadLocal);
        thread.start();
        thread1.start();
    }
}

上面就是使用Threadlocal的实例,

那么我们从运行图上面可以看到,Thread-0和Thread-1都是拿到了一百张火车票,而且顺序是对的,那这说明了什么,说明了两个线程没有共享一个对象,而是每个线程都有自己的专属的一个对象,所以才没有发生共同消费的情况,哪有人说不对啊,你这每张票都卖重复了,这是不和逻辑的啊,其实这只是为了演示这个效果,为了证明ThreadLocal中存在一份独立的对象副本,具体的使用场景需要根据情况而变,如果是希望很多线程共享一个数据,而又不发生重复西奥菲,那么就可以使用上面的加锁,如果是希望这个数据不能被别人修改,只能被自己使用,那么就可以使用ThreadLocal。

       结语:到了这里就要和大家说再见了,本文适合小白学习,我也是一名小白,我希望可以和看到这篇文章的伙伴们共同进步,如果有哪些写的不对的,还希望大家指出,谢谢阅读~~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值