做招聘网站需要多少钱,网页升级访问中每天正常,上海关键词优化排名软件,阿里云做网站需要些什么条件对于从事后端开发的同学来说#xff0c;线程安全问题是我们每天都需要考虑的问题。
线程安全问题通俗的讲#xff1a;主要是在多线程的环境下#xff0c;不同线程同时读和写公共资源#xff08;临界资源#xff09;#xff0c;导致的数据异常问题。
比如#xff1a;变…对于从事后端开发的同学来说线程安全问题是我们每天都需要考虑的问题。
线程安全问题通俗的讲主要是在多线程的环境下不同线程同时读和写公共资源临界资源导致的数据异常问题。
比如变量a0线程1给该变量1线程2也给该变量1。此时线程3获取a的值有可能不是2而是1。线程3这不就获取了错误的数据
线程安全问题会直接导致数据异常从而影响业务功能的正常使用所以这个问题还是非常严重的。
那么如何解决线程安全问题呢
今天跟大家一起聊聊保证线程安全的10个小技巧希望对你有所帮助。 1. 无状态
我们都知道只有多个线程访问公共资源的时候才可能出现数据安全问题那么如果我们没有公共资源是不是就没有这个问题呢
例如
public class NoStatusService {public void add(String status) {System.out.println(add status: status);}public void update(String status) {System.out.println(update status: status);}
}这个例子中NoStatusService没有定义公共资源换句话说是无状态的。
这种场景中NoStatusService类肯定是线程安全的。
2. 不可变
如果多个线程访问的公共资源是不可变的也不会出现数据的安全性问题。
例如
public class NoChangeService {public static final String DEFAULT_NAME abc;public void add(String status) {System.out.println(DEFAULT_NAME);}
}DEFAULT_NAME被定义成了static final的常量在多线程中环境中不会被修改所以这种情况也不会出现线程安全问题。
3. 无修改权限
有时候我们定义了公共资源但是该资源只暴露了读取的权限没有暴露修改的权限这样也是线程安全的。
例如
public class SafePublishService {private String name;public String getName() {return name;}public void add(String status) {System.out.println(add status: status);}
}这个例子中没有对外暴露修改name字段的入口所以不存在线程安全问题。
3. synchronized
使用JDK内部提供的同步机制这也是使用比较多的手段分为同步方法 和 同步代码块。
我们优先使用同步代码块因为同步方法的粒度是整个方法范围太大相对来说更消耗代码的性能。
其实每个对象内部都有一把锁只有抢到那把锁的线程才被允许进入对应的代码块执行相应的代码。
当代码块执行完之后JVM底层会自动释放那把锁。
例如
public class SyncService {private int age 1;private Object object new Object();//同步方法public synchronized void add(int i) {age age i; System.out.println(age: age);}public void update(int i) {//同步代码块对象锁synchronized (object) {age age i; System.out.println(age: age);} }public void update(int i) {//同步代码块类锁synchronized (SyncService.class) {age age i; System.out.println(age: age);} }
}4. Lock
除了使用synchronized关键字实现同步功能之外JDK还提供了Lock接口这种显示锁的方式。
通常我们会使用Lock接口的实现类ReentrantLock它包含了公平锁、非公平锁、可重入锁、读写锁 等更多更强大的功能。
例如
public class LockService {private ReentrantLock reentrantLock new ReentrantLock();public int age 1;public void add(int i) {try {reentrantLock.lock();age age i; System.out.println(age: age);} finally {reentrantLock.unlock(); } }
}但如果使用ReentrantLock它也带来了有个小问题就是需要在finally代码块中手动释放锁。
不过说句实话在使用Lock显示锁的方式解决线程安全问题给开发人员提供了更多的灵活性。
5. 分布式锁
如果是在单机的情况下使用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代码块中释放锁。
如果你对redis分布式锁的用法和常见的坑比较感兴趣的话可以看看我的另一篇文章《聊聊redis分布式锁的8大坑》里面有更详细的介绍。
6. volatile
有时候我们有这样的需求如果在多个线程中有任意一个线程把某个开关的状态设置为false则整个功能停止。
简单的需求分析之后发现只要求多个线程间的可见性不要求原子性。
如果一个线程修改了状态其他的所有线程都能获取到新的状态值。
这样一分析这就好办了使用volatile就能快速满足需求。
例如
Service
public CanalService {private volatile boolean running false;private Thread thread;Autowiredprivate CanalConnector canalConnector;public void handle() {//连接canalwhile(running) {//业务处理}}public void start() {thread new Thread(this::handle, name);running true;thread.start();}public void stop() {if(!running) {return;}running false;}
}需要特别注意的地方是volatile不能用于计数和统计等业务场景。因为volatile不能保证操作的原子性可能会导致数据异常。 7. ThreadLocal
除了上面几种解决思路之外JDK还提供了另外一种用空间换时间的新思路ThreadLocal。
当然ThreadLocal并不能完全取代锁特别是在一些秒杀更新库存中必须使用锁。
ThreadLocal的核心思想是共享变量在每个线程都有一个副本每个线程操作的都是自己的副本对另外的线程没有影响。 温馨提醒一下我们平常在使用ThreadLocal时如果使用完之后一定要记得在finally代码块中调用它的remove方法清空数据不然可能会出现内存泄露问题。 例如
public class ThreadLocalService {private ThreadLocalInteger threadLocal new ThreadLocal();public void add(int i) {Integer integer threadLocal.get();threadLocal.set(integer null ? : integer i);}
}如果对ThreadLocal感兴趣的小伙伴可以看看我的另一篇文章《ThreadLocal夺命11连问》里面有对ThreadLocal的原理、用法和坑有非常详细的介绍。
8. 线程安全集合
有时候我们需要使用的公共资源放在某个集合当中比如ArrayList、HashMap、HashSet等。
如果在多线程环境中有线程往这些集合中写数据另外的线程从集合中读数据就可能会出现线程安全问题。
为了解决集合的线程安全问题JDK专门给我们提供了能够保证线程安全的集合。
比如CopyOnWriteArrayList、ConcurrentHashMap、CopyOnWriteArraySet、ArrayBlockingQueue等等。
例如
public class HashMapTest {private static ConcurrentHashMapString, Object hashMap new ConcurrentHashMap();public static void main(String[] args) {new Thread(new Runnable() {Overridepublic void run() {hashMap.put(key1, value1);}}).start();new Thread(new Runnable() {Overridepublic void run() {hashMap.put(key2, value2);}}).start();try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(hashMap);}
}在JDK底层或者spring框架当中使用ConcurrentHashMap保存加载配置参数的场景非常多。
比较出名的是spring的refresh方法中会读取配置文件把配置放到很多的ConcurrentHashMap缓存起来。
9. CAS
JDK除了使用锁的机制解决多线程情况下数据安全问题之外还提供了CAS机制。
这种机制是使用CPU中比较和交换指令的原子性JDK里面是通过Unsafe类实现的。
CAS内部包含了四个值旧数据、期望数据、新数据 和 地址比较旧数据 和 期望的数据如果一样的话就把旧数据改成新数据。如果不一样的话当前线程不断自旋一直到成功为止。
不过使用CAS保证线程安全可能会出现ABA问题需要使用AtomicStampedReference增加版本号解决。
其实实际工作中很少直接使用Unsafe类的一般用atomic包下面的类即可。
public class AtomicService {private AtomicInteger atomicInteger new AtomicInteger();public int add(int i) {return atomicInteger.getAndAdd(i);}
}10. 数据隔离
有时候我们在操作集合数据时可以通过数据隔离来保证线程安全。
例如
public class ThreadPoolTest {public static void main(String[] args) {ExecutorService threadPool new ThreadPoolExecutor(8, //corePoolSize线程池中核心线程数10, //maximumPoolSize 线程池中大线程数60, //线程池中线程的大空闲时间超过这个时间空闲线程将被回收TimeUnit.SECONDS,//时间单位new ArrayBlockingQueue(500), //队列new ThreadPoolExecutor.CallerRunsPolicy()); //拒绝策略ListUser userList Lists.newArrayList(new User(1L, 苏三, 18, 成都),new User(2L, 苏三说技术, 20, 四川),new User(3L, 技术, 25, 云南));for (User user : userList) {threadPool.submit(new Work(user));}try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(userList);}static class Work implements Runnable {private User user;public Work(User user) {this.user user;}Overridepublic void run() {user.setName(user.getName() 测试);}}
}这个例子中使用线程池处理用户信息。
每个用户只被线程池中的一个线程处理不存在多个线程同时处理一个用户的情况。所以这种人为的数据隔离机制也能保证线程安全。
数据隔离还有另外一种场景kafka生产者把同一个订单的消息发送到同一个partion中。每一个partion都部署一个消费者在kafka消费者中使用单线程接收消息并且做业务处理。
这种场景下从整体上看不同的partion是用多线程处理数据的但同一个partion则是用单线程处理的所以也能解决线程安全问题。