Java中确保线程安全最经常使用的两种方式

上篇文章咱们简单聊了什么是多线程,我想你们对多线程已经有了一个初步的了解,没看的没有放下文章连接 什么是线程安全,你真的了解吗?安全


上篇咱们搞清楚了什么样的线程是安全的,咱们今天先来看段代码:bash

public void threadMethod(int j) {

    int i = 1;

    j = j + i;
}复制代码


你们以为这段代码是线程安全的吗?多线程


毫无疑问,它绝对是线程安全的,咱们来分析一下为何它是线程安全的?并发


咱们能够看到这段代码是没有任何状态的,什么意思,就是说咱们这段代码不包含任何的做用域,也没有去引用其余类中的域进行引用,它所执行的做用范围与执行结果只存在它这条线程的局部变量中,而且只能由正在执行的线程进行访问。当前线程的访问不会对另外一个访问同一个方法的线程形成任何的影响。ide


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


                                                        添加一个状态呢?

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


public class ThreadDemo {

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

   public void threadMethod(int j) {
       
       count++ ;

       int i = 1;

       j = j + i;
   }
}复制代码


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


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

能够发现,count的值并非正确的结果,当线程A读取到count的值,可是尚未进行修改的时候,线程B已经进来了,而后线程B读取到的仍是count为1的值,正由于如此因此咱们的count值已经出现了误差,那么这样的程序放在咱们的代码中是存在不少的隐患的。线程


二、如何确保线程安全?

既然存在线程安全的问题,那么确定得想办法解决这个问题,怎么解决?咱们说说常见的几种方式。


2.一、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(); // 获取锁对象

       // 若是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 {
       // lock.lock(); // 获取锁对象

       // 若是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来保证咱们线程安全的方式,其实Lock还有好多的方法来操做咱们的锁对象,这里咱们就很少说了,你们有兴趣能够看一下API。

PS:如今你能作到如何确保一个方法是线程安全的吗?

相关文章
相关标签/搜索