Java线程安全问题属于Java面试的高频话题,今天我们就来详解Java线程安全的详细四大解决方案@mikechen
什么是线程安全
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用,不会出现数据不一致或者数据污染。
线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
出现线程不安全的原因
我们一起来看一个多线程访问的例子就很清楚了,代码示例如下:
Integer count = 0; public void getCount() { count ++; System.out.println(count); }
我开启的3条线程每个线程循环10次,运行结果如下:
我们可以看到这里出现了两个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保证线程安全,但不同的节点之间,没法保证线程安全,如下图所示:
这个时候为了保证线程俺去,这就需要使用分布式锁了。
分布式锁有很多种,比如:数据库分布式锁,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与ReentrantLock、ThreadLocal,在分布式多机的情况需要使用分布式锁来保证线程安全。
陈睿mikechen
10年+大厂架构经验,资深技术专家,就职于阿里巴巴、淘宝、百度等一线互联网大厂。
关注「mikechen」公众号,获取更多技术干货!
后台回复【面试】即可获取《史上最全阿里Java面试题总结》,后台回复【架构】,即可获取《阿里架构师进阶专题全部合集》