
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
mikechen睿哥,10年+大厂架构经验,资深技术专家,就职于阿里巴巴、淘宝、百度等一线互联网大厂。