保证线程安全的几个小技巧
前言
在软件编程中,多线程是个绕不开的话题。多线程的使用,能够提高程序的运行效率,但也带来新的问题:如何保证下面的线程安全呢?
无状态
例如:
publicclassTest{
publicvoidthreadMethod(intj){
inti1;
jji;
}
这个例子中,不存在全局变量,所以不存在线程安全问题。
官方解释:局部变量作用域仅限于函数内部,离开该函数的内部就是无效的。不可变
例如:
publicclassTest{
publicstaticfinalStringDEFAULTNAMEabc;
}
这个例子中,全局变量被final修饰,所以不存在线程安全问题。
官方解释:final被修饰的变量为常量一旦赋值不能修改,被修改的方法为最终方法不能被重写,被修饰的类是最终类,不能被继承。无修改权限
例如:
publicclassTest{
privateStringname;
publicStringgetName(){
returnname;
}
}
这个例子中,没有对外暴露修改name字段的入口,所以不存在线程安全问题。
官方解释:public表示共有:类的数据成员和函数可以被该类对象和派生类访问。private私有型:自己的类可以访问,但派生类不能访问。protected保护型:自身类和派生类可以访问相当于自身的private型成员,它同private的区别就是在对待派生类的区别上。synchronized
例如:
publicclassTest{
privateintage18;
publicsynchronizedintgetAge1(inti){
ageagei;
returnage;
}
publicintgetAge2(inti){
synchronized(this){
ageagei;
}
returnage;
}
publicintgetAge3(inti){
synchronized(Test。class){
ageagei;
}
returnage;
}
}这个例子中,使用到了synchronized关键字,所以不存在线程安全问题。
官方解释:Synchronized可保证同一时刻有且只有一条线程在操作共享数据,其他线程必须等待该线程处理完数据后再对共享数据进行操作。
注意:使用synchronized修饰非静态方法或者使用synchronized修饰代码块时指定的为实例对象时,同一个类的不同对象拥有自己的锁,因此不会相互阻塞。使用synchronized修饰类和对象时,由于类对象和实例对象分别拥有自己的监视器锁,因此不会相互阻塞。使用synchronized修饰实例对象时,如果一个线程正在访问实例对象的一个synchronized方法时,其它线程不仅不能访问该synchronized方法,该对象的其它synchronized方法也不能访问,因为一个对象只有一个监视器锁对象,但是其它线程可以访问该对象的非synchronized方法。线程A访问实例对象的非staticsynchronized方法时,线程B也可以同时访问实例对象的staticsynchronized方法,因为前者获取的是实例对象的监视器锁,而后者获取的是类对象的监视器锁,两者不存在互斥关系。Lock
例如:
publicclassTest{
privateReentrantLockreentrantLocknewReentrantLock();
privateintage18;
publicsynchronizedintgetAge(inti){
try{
reentrantLock。lock();
ageagei;
}catch(Exceptione){
e。printStackTrace();
}finally{
reentrantLock。unlock();
}
returnage;
}
}
这个例子中,使用到了ReentrantLock锁,所以不存在线程安全问题。
Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问。
Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。分布式锁
publicclassTest{
privateStringRedisTemplateredisTemplate;
privateintage18;
publicsynchronizedintgetAge(inti){
StringuuidUUID。randomUUID()。toString();
BooleanlockredisTemplate。opsForValue()。setIfAbsent(lock,uuid,300,TimeUnit。SECONDS);
if(lock){
ageagei;
}else{
getAge(i);模拟自旋
}
returnage;
}
}这个例子中,使用到了rerdis的setnx分布式锁,所以不存在线程安全问题。
如果你对redis分布式锁的用法和常见的坑,比较感兴趣的话,可以看看我的另一篇文章Redis缓存失效问题:缓存穿透缓存雪崩缓存击穿,有对分布式锁相关介绍说明volatile
例如:
publicclassTest{
privatevolatilebooleanrunningfalse;
privateThreadthread;
publicvoidhandle(){
连接canal
while(running){
业务处理
}
}
publicvoidstart(){
threadnewThread(this::handle,name);
runningtrue;
thread。start();
}
publicvoidstop(){
if(!running){
return;
}
runningfalse;
}
}这个例子中,全局变量使用volatile关键字,所以不存在线程安全问题。
官方解释:volatile是Java虚拟机提供的轻量级的同步机制,是基本上遵守了JMM的规范,主要是保证可见性和禁止指令重排,但是它并不保证原子性。
注意:volatile不能用于计数和统计等业务场景。因为volatile不能保证操作的原子性,可能会导致数据异常。ThreadLocal
例如:
publicclassTest{
privateThreadLocalthreadLocalnewThreadLocal();
publicvoidgetAge(inti){
try{
IntegerintegerthreadLocal。get();
threadLocal。set(integernull?0:integeri);
}catch(Exceptione){
e。printStackTrace();
}finally{
threadLocal。remove();
}
}
}这个例子中,使用了ThreadLocal,所以不存在线程安全问题。
官方解释:ThreadLoal变量,线程局部变量,同一个ThreadLocal所包含的对象,在不同的Thread中有不同的副本且其它Thread不可访问,那就不存在多线程间共享的问题。
注意:我们平常在使用ThreadLocal时,如果使用完之后,一定要记得在finally代码块中,调用它的remove方法清空数据,不然可能会出现内存泄露问题线程安全集合
例如:publicclassHashMapTest{privatestaticConcurrentHashMapString,ObjecthashMapnewConcurrentHashMap();publicstaticvoidmain(String〔〕args){newThread(newRunnable(){Overridepublicvoidrun(){hashMap。put(key1,value1);}})。start();newThread(newRunnable(){Overridepublicvoidrun(){hashMap。put(key2,value2);}})。start();try{Thread。sleep(50);}catch(InterruptedExceptione){e。printStackTrace();}System。out。println(hashMap);}}这个例子中,使用了ConcurrentHashMap,所以不存在线程安全问题。
常见的线程安全集合还有:Vector,Hashtable,CopyOnWriteArrayList,CopyOnWriteArraySet,ConcurrentSkipListMap,ConcurrentSkipListSet,ConcurrentLinkedQueue,ConcurrentLinkedDeque、使用Collections包装成线程安全等CAS
publicclassTest{
privateAtomicIntegeratomicIntegernewAtomicInteger();
publicintgetAge(inti){
returnatomicInteger。getAndAdd(i);
}
}这个例子中,使用了atomicInteger,所以不存在线程安全问题。
synchronized加锁,同一时间,只能有一个线程访问,一致性得到了保障,并发性下降。CAS用的dowhile,没有加锁,反复的通过CAS比较,直到成功,既保证了一致性,又提高了并发性。
逻辑上可以这么理解:java。util。concurrent。atomic这个包里面提供了一组原子类,其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入。