线程和线程安全,你了解了吗?

什么是线程安全?

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

搞清楚了什么是线程安全,接下来我们看看Java中确保线程安全最常用的两种方式。先来看段代码。

public void threadMethod(int j) {

    int i = 1;

    j = j + i;
}

大家觉得这段代码是线程安全的吗?

毫无疑问,它绝对是线程安全的,我们来分析一下,为什么它是线程安全的?

我们可以看到这段代码是没有任何状态的,就是说我们这段代码,不包含任何的作用域,也没有去引用其他类中的域进行引用,它所执行的作用范围与执行结果只存在它这条线程的局部变量中,并且只能由正在执行的线程进行访问。当前线程的访问,不会对另一个访问同一个方法的线程造成任何的影响。

两个线程同时访问这个方法,因为没有共享的数据,所以他们之间的行为,并不会影响其他线程的操作和结果,所以说无状态的对象,也是线程安全的。

添加一个状态呢?

如果我们给这段代码添加一个状态,添加一个count,来记录这个方法并命中的次数,每请求一次count+1,那么这个时候这个线程还是安全的吗?

public class ThreadDemo {

   int count = 0; // 记录方法的命中次数

   public void threadMethod(int j) {

       count++ ;

       int i = 1;

       j = j + i;
   }
}

很明显已经不是了,单线程运行起来确实是没有任何问题的,但是当出现多条线程并发访问这个方法的时候,问题就出现了,我们先来分析下count+1这个操作。

扫描二维码关注公众号,回复: 5656884 查看本文章

进入这个方法之后首先要读取count的值,然后修改count的值,最后才把这把值赋值给count,总共包含了三步过程:“读取”一>“修改”一>“赋值”,既然这个过程是分步的,那么我们先来看下面这张图,看看你能不能看出问题:

可以发现,count的值并不是正确的结果,当线程A读取到count的值,但是还没有进行修改的时候,线程B已经进来了,然后线程B读取到的还是count为1的值,正因为如此所以我们的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 {
       // lock.lock(); 
       if (lock.tryLock(2,TimeUnit.SECONDS)) {
           try {
               System.out.println("线程名:"+thread.getName() + "获得了锁");  
               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 {
       // lock.lock(); 
       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,来保证我们线程安全的方式。

进群:可以领取免费的架构师学习资料;了解最新的学习动态;了解最新的阿里、京东招聘资讯;
获取更多的面试资料 Java高架构师、分布式架构、高可扩展、高性能、高并发、性能优化、Spring boot、
Redis、ActiveMQ、Nginx、Mycat、Netty、Jvm大型分布式项目实战学习架构师之路 
想要学习以上内容进群:739486042,进群获取架构师视频、笔记、源码、课件、等书籍资料

获取资料的方式:进群   739486042   即可领取

猜你喜欢

转载自blog.csdn.net/weixin_44699571/article/details/88553919