Java线程安全问题怎么解决?4种常见方式详解!

Java线程安全问题怎么解决?4种常见方式详解!-mikechen

Java线程安全问题属于Java面试的高频话题,今天我们就来详解Java线程安全的详细四大解决方案@mikechen

什么是线程安全

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用,不会出现数据不一致或者数据污染。

线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。

 

出现线程不安全的原因

我们一起来看一个多线程访问的例子就很清楚了,代码示例如下:

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

我开启的3条线程每个线程循环10次,运行结果如下:

Java线程安全问题怎么解决?4种常见方式详解!-mikechen

我们可以看到这里出现了两个26,为什么会出现这种情况?

出现这种情况显然表明我们这个方法根本就不是线程安全的,比如:A线程在进入方法后,拿到了count的值,刚把这个值读取出来还没有改变count的值的时候,结果线程B也进来的,那么导致线程A和线程B拿到的count值是一样的。

由此我们知道了这个方式线程不安全的,那怎样来保证线程安全呢?具体有哪些方式呢?下面我接着详解。

 

保证线程安全的四种方式

第一种方式:synchronized

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

现在我就以售票问题来演示线程安全的问题,代码示例如下:

package com.bpan.spring.beans.thread;

import com.sun.org.apache.regexp.internal.recompile;

public class ThreadSynchronizedSecurity {
    
    static int tickets = 10;
    
    class SellTickets implements Runnable{

        @Override
        public void run() {
            // 同步代码块
            while(tickets > 0) {
                
                synchronized (this) {
                    
//                    System.out.println(this.getClass().getName().toString());
                    
                    if (tickets <= 0) {
                        
                        return;
                    }
                    
                    System.out.println(Thread.currentThread().getName()+"--->售出第:  "+tickets+" 票");
                    tickets--;
                    
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                
                if (tickets <= 0) {
                    
                    System.out.println(Thread.currentThread().getName()+"--->售票结束!");
                }
            }
        }
    }
    
    
    public static void main(String[] args) {
        
        
        SellTickets sell = new ThreadSynchronizedSecurity().new SellTickets();
        
        Thread thread1 = new Thread(sell, "1号窗口");
        Thread thread2 = new Thread(sell, "2号窗口");
        Thread thread3 = new Thread(sell, "3号窗口");
        Thread thread4 = new Thread(sell, "4号窗口");
        
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        
        
    }
    

}

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

这样我们就通过synchronized代码块来保证票(tickets)的线程安全。

注意:虽然加synchronized关键字,可以让我们的线程变得安全,但是我们在用的时候,也要注意缩小synchronized的使用范围,尽量保证锁的范围有线程安全的部分就可以了,否则造成性能的浪费。

 

第二种方式:Lock锁机制

先来说说它跟synchronized有什么区别:ReentrantLock是在Java1.6被引入进来的,ReentrantLock的引入让锁有了可操作性,什么意思?就是通过手动创建Lock对象,采用lock()加锁,unlock()解锁,来保护指定的代码块。

ReentrantLock,它包含了:公平锁、非公平锁、可重入锁、读写锁 等更多更强大的功能。

具体示例如下:

public class ThreadLockSecurity {
    
    static int tickets = 10;
    
    class SellTickets implements Runnable{
        
        Lock lock = new ReentrantLock();

        @Override
        public void run() {
            // Lock锁机制
            while(tickets > 0) {
                
                try {
                    lock.lock();
                    
                    if (tickets <= 0) {
                        
                        return;
                    }
                        
                    System.out.println(Thread.currentThread().getName()+"--->售出第:  "+tickets+" 票");
                    tickets--;
                } catch (Exception e1) {
                    // TODO Auto-generated catch block
                    e1.printStackTrace();
                }finally {
                    
                    lock.unlock();
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
                
            if (tickets <= 0) {
                
                System.out.println(Thread.currentThread().getName()+"--->售票结束!");
            }
            
        }
    }
    
    
    public static void main(String[] args) {
        
        
        SellTickets sell = new ThreadLockSecurity().new SellTickets();
        
        Thread thread1 = new Thread(sell, "1号窗口");
        Thread thread2 = new Thread(sell, "2号窗口");
        Thread thread3 = new Thread(sell, "3号窗口");
        Thread thread4 = new Thread(sell, "4号窗口");
        
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        
        
    }
    

}

但如果使用ReentrantLock,它也带来了有个小问题就是:需要在finally代码块中手动释放锁。

 

第三种方式:ThreadLocal

除了上面几种解决思路之外,JDK还提供了另外一种用空间换时间的新思路:ThreadLocal

ThreadLocal的核心思想是:共享变量在每个线程都有一个副本,每个线程操作的都是自己的副本,对另外的线程没有影响。

具体代码示例如下:

public class ThreadLocalService {
    private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public void add(int i) {
        Integer integer = threadLocal.get();
        threadLocal.set(integer == null ? 0 : integer + i);
    }
}

备注:我们平常在使用ThreadLocal时,如果使用完之后,一定要记得在finally代码块中,调用它的remove方法清空数据,不然可能会出现Java内存泄露问题。

 

第四种方式:分布式锁

上面谈到的都是在单机的情况下,使用synchronized和Lock保证线程安全是没有问题的,但如果在分布式的环境中,就会出现线程安全的问题。

比如:某个应用如果部署了多个节点,每一个节点使用可以synchronized和Lock保证线程安全,但不同的节点之间,没法保证线程安全,如下图所示:

Java线程安全问题怎么解决?4种常见方式详解!-mikechen

这个时候为了保证线程俺去,这就需要使用分布式锁了。

分布式锁有很多种,比如:数据库分布式锁,zookeeper分布式锁,redis分布式锁等,推荐使用redis分布式锁。

redis分布式锁的具体代码示例如下:

try{
  String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
  if ("OK".equals(result)) {
      return true;
  }
  return false;
} finally {
    unlock(lockKey);
}

备注:同样需要在finally代码块中释放锁。

 

线程安全总结

由于synchronized是在JVM层面实现的,因此系统可以监控锁的释放与否,而ReentrantLock是使用代码实现的,系统无法自动释放锁,需要在代码中的finally子句中显式释放锁lock.unlock()。

在并发量比较小的情况下,使用synchronized是个不错的选择,但是在并发量比较高的情况下,其性能下降会很严重,此时ReentrantLock是个不错的方案。

单机的情况下可以使用synchronized与ReentrantLockThreadLocal,在分布式多机的情况需要使用分布式锁来保证线程安全。

 

陈睿mikechen

10年+大厂架构经验,资深技术专家,就职于阿里巴巴、淘宝、百度等一线互联网大厂。

关注「mikechen」公众号,获取更多技术干货!

后台回复面试即可获取《史上最全阿里Java面试题总结》,后台回复架构,即可获取《阿里架构师进阶专题全部合集

评论交流
    说说你的看法