到底什么是线程安全

前言

说到线程,想必肯定是离不开对于进程的介绍,进程是程序的一次执行,而什么是线程,线程可以理解为进程中的执行的一段程序片段。在一个多任务环境中下面的概念可以帮助我们理解两者间的差别:进程间是独立的,这表现在内存空间,上下文环境;线程运行在进程空间内。 一般来讲(不使用特殊技术)进程是无法突破进程边界存取其他进程内的存储空间;而线程由于处于进程空间内,所以同一进程所产生的线程共享同一内存空间。同一进程中的两段代码不能够同时执行,除非引入线程。线程是属于进程的,当进程退出时该进程所产生的线程都会被强制退出并清除。线程占用的资源要少于进程所占用的资源。进程和线程都可以有优先级。在线程系统中进程也是一个线程。可以将进程理解为一个程序的第一个线程。

什么是多线程

提到多线程这里要说两个概念,就是串行和并行,搞清楚这个我们才能更好的理解多线程。

所谓串行其实是相对于单条线程来执行多个任务来说的,我们就拿下载文件来举个例子,我们下载多个文件,在串行中它是按照一定的顺序去进行下载的,也就是说必须等下载完A之后,才能开始下载B,它们在时间上是不可能发生重叠的。

并行:下载多个文件,开启多条线程,多个文件同时进行下载,这里是严格意义上的在同一时刻发生的,并行在时间上是重叠的。

了解了这两个概念之后我们再来说说什么是多线程,举个例子,我们打开腾讯管家,腾讯管家本身就是一个程序也就是说它就是一个进程,它里面有很多的功能,例如查杀病毒、清理垃圾、电脑加速等,按照单线程来说,你想要清理垃圾还要病毒查杀,那么你必须先做完其中的一件事才能做下一件事,是有一个执行的顺序的,如果是多线程的话,我们其实在清理垃圾的时候还可以查杀病毒、电脑加速等等其他的操作,这个是严格意义上的同一时刻发生的,没有执行的先后顺序。

一句话总结一下:一个进程运行时产生了多个线程

什么是线程安全?

既然是线程安全问题,那么毫无疑问所有的隐患都是出现在多个线程访问的情况下产生的,也就是我们要确保在多条线程访问的时候,我们的程序还能按照我们预期的行为去执行,我们看一下下面的代码。

   Integer count = 0;
   
   public void getCount() {
       
       count ++;
       System.out.println(count);
   }

很简单的一段代码,我们就来统计一下这个方法的访问次数,多个线程同时访问会不会出现什么问题,我开启的3条线程每个线程循环10次,得到以下结果

我们可以看到,这里出现了两个26,为什么会出现这种情况,出现这种情况显然表明我们这个方法根本就不是线程安全的,出现这种问题的原因有很多,我们说最常见的一种,就是我们A线程在进入方法后,拿到了count的值,刚把这个值读取出来还没有改变count的值的时候,结果线程B也进来的,那么导致线程A和线程B拿到的count值是一样的。

那么由此我们可以了解这确实不是一个线程安全的类,因为他们都需要操作这个共享的变量,其实要对线程安全问题给出一个明确的定义还是蛮复杂的,我们根据我们这个程序来总结下什么是线程安全。

当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。

 如何保证线程安全

1、synchronized

synchronized关键字,就是用来控制线程同步的,保证我们的线程在多线程环境下,不被多个线程同时执行,确保我们数据的完整性,使用方法一般是加在方法上。

public class ThreadDemo {
  int count = 0; // 记录方法的命中次数
  public synchronized void threadMethod(int j) {
      count++ ;
      int i = 1;
      j = j + i;
  }
}

这样就可以确保我们的线程同步了,同时这里需要注意一个大家平时忽略的问题,首先synchronized锁的是括号里的对象,而不是代码,其次,对于非静态的synchronized方法,锁的是对象本身也就是this。

当synchronized锁住一个对象之后,别的线程如果想要获取锁对象,那么就必须等这个线程执行完释放锁对象之后才可以,否则一直处于等待状态。

注意点:虽然加synchronized关键字,可以让我们的线程变得安全,但是我们在用的时候,也要注意缩小synchronized的使用范围,如果随意使用时很影响程序的性能,别的对象想拿到锁,结果你没用锁还一直把锁占用,这样就有点浪费资源。

 2、Lock

先来说说它跟synchronized有什么区别吧,Lock是在Java1.6被引入进来的,Lock的引入让锁有了可操作性,什么意思?就是我们在需要的时候去手动的获取锁和释放锁,甚至我们还可以中断获取以及超时获取的同步特性,但是从使用上说Lock明显没有synchronized使用起来方便快捷。我们先来看下一般是如何使用的:

private Lock lock = new ReentrantLock(); // ReentrantLock是Lock的子类
  private void method(Thread thread){
      lock.lock(); // 获取锁对象
      try {
          System.out.println("线程名:"+thread.getName() + "获得了锁");
          // Thread.sleep(2000);
      }catch(Exception e){
          e.printStackTrace();
      } finally {
          System.out.println("线程名:"+thread.getName() + "释放了锁");
          lock.unlock(); // 释放锁对象
      }
}

进入方法我们首先要获取到锁,然后去执行我们业务代码,这里跟synchronized不同的是,Lock获取的所对象需要我们亲自去进行释放,为了防止我们代码出现异常,所以我们的释放锁操作放在finally中,因为finally中的代码无论如何都是会执行的。

写个主方法,开启两个线程测试一下我们的程序是否正常:

public static void main(String[] args) {
      LockTest lockTest = new LockTest();
      // 线程1
      Thread t1 = new Thread(new Runnable() {
          @Override
          public void run() {
              // Thread.currentThread()  返回当前线程的引用
              lockTest.method(Thread.currentThread());
          }
      }, "t1");
      // 线程2
      Thread t2 = new Thread(new Runnable() {
          @Override
          public void run() {
              lockTest.method(Thread.currentThread());
          }
      }, "t2");
      t1.start();
      t2.start();
  }

结果:

可以看出我们的执行,是没有任何问题的。

其实在Lock还有几种获取锁的方式,我们这里再说一种,就是tryLock()这个方法跟Lock()是有区别的,Lock在获取锁的时候,如果拿不到锁,就一直处于等待状态,直到拿到锁,但是tryLock()却不是这样的,tryLock是有一个Boolean的返回值的,如果没有拿到锁,直接返回false,停止等待,它不会像Lock()那样去一直等待获取锁。

我们来看下代码:

private void method(Thread thread){
      // lock.lock(); // 获取锁对象
      if (lock.tryLock()) {
          try {
              System.out.println("线程名:"+thread.getName() + "获得了锁");
              // Thread.sleep(2000);
          }catch(Exception e){
              e.printStackTrace();
          } finally {
              System.out.println("线程名:"+thread.getName() + "释放了锁");
              lock.unlock(); // 释放锁对象
          }
      }
  }

结果:我们继续使用刚才的两个线程进行测试可以发现,在线程t1获取到锁之后,线程t2立马进来,然后发现锁已经被占用,那么这个时候它也不在继续等待。

似乎这种方法,感觉不是很完美,如果我第一个线程,拿到锁的时间,比第二个线程进来的时间还要长,是不是也拿不到锁对象?

那我能不能,用一中方式来控制一下,让后面等待的线程,可以等待5秒,如果5秒之后,还获取不到锁,那么就停止等,其实tryLock()是可以进行设置等待的相应时间的。

private void method(Thread thread) throws InterruptedException {
      // 如果2秒内获取不到锁对象,那就不再等待
      if (lock.tryLock(2,TimeUnit.SECONDS)) {
          try {
              System.out.println("线程名:"+thread.getName() + "获得了锁");
              // 这里睡眠3秒
              Thread.sleep(3000);
          }catch(Exception e){
              e.printStackTrace();
          } finally {
              System.out.println("线程名:"+thread.getName() + "释放了锁");
              lock.unlock(); // 释放锁对象
          }
      }
}

结果:看上面的代码,我们可以发现,虽然我们获取锁对象的时候,可以等待2秒,但是我们线程t1在获取锁对象之后,执行任务缺花费了3秒,那么这个时候线程t2是不在等待的。

 我们再来改一下这个等待时间,改为5秒,再来看下结果:

private void method(Thread thread) throws InterruptedException {
      // 如果5秒内获取不到锁对象,那就不再等待
      if (lock.tryLock(5,TimeUnit.SECONDS)) {
          try {
              System.out.println("线程名:"+thread.getName() + "获得了锁");
          }catch(Exception e){
              e.printStackTrace();
          } finally {
              System.out.println("线程名:"+thread.getName() + "释放了锁");
              lock.unlock(); // 释放锁对象
          }
      }
}

结果:这个时候我们可以看到,线程t2等到5秒获取到了锁对象,执行了任务代码。

以上就是使用Lock,来保证我们线程安全的方式。

总结

上面简单介绍了线程的一些基本概念,什么是线程安全,以及在开发中保证线程安全的两种方式。

原文链接:什么是线程安全,你真的了解吗?

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值