不可变对象
不可变对象(Immutable Objects)是指对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,任何对它的改变都应该产生一个新的对象。
不可变对象需要满足的条件:
除了使用final自行封装不可变对象之外,还可以通过以下两种方式定义不可变对象
线程封闭
当访问共享的可变数据时,通常需要同步。一种避免同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步,这种技术称为线程封闭。
常见线程封闭手段:
spring中一定要在拦截器afterCompletion中,执行threadlocal的remove函数,线程池中使用同理。
同步容器
stringbuilder:线程不安全(可以在函数中定义,利用堆栈封闭避免了线程不安全,同时节省了加锁的消耗,性能更好)
stringbuffer:线程安全(每个函数都是用synchronized修饰),可以做全局变量。
SimpleDateFormat:JDK中的工具类,线程不安全。使用方法可以参考stringbuilder。
JodaTime:线程安全,功能更丰富。
ArrayList/HashSet/HashMap等Collections:都是线程不安全的
Vector/Stack/HashTable:都是线程安全的
先检查再执行:if(condition(a)){handle(a)},这种形式如果没有加锁的话,就不是原子性,也是线程不安全的
并发容器
线程安全的容器除了上文提到的同步容器一些外,在Java的J.U.C(java.utils.concurrent的缩写)下,同样提供了线程安全的并发容器。
注意:并发容器的批量操作都不是线程安全的,例如调用removeAll,containsAll等,需要自行加锁。
CopyOnWriteArrayList、CopyOnWriteArraySet,这种利用cow特性的数据结构,需要copy消耗内存,可能引发gc。
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
死锁的必要条件
死锁示例代码:
@Slf4j public class DeadLock implements Runnable { public int flag = 1; //静态对象是类的所有对象共享的 private static Object o1 = new Object(), o2 = new Object(); @Override public void run() { log.info("flag:{}", flag); if (flag == 1) { synchronized (o1) { try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } synchronized (o2) { log.info("1"); } } } if (flag == 0) { synchronized (o2) { try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } synchronized (o1) { log.info("0"); } } } } public static void main(String[] args) { DeadLock td1 = new DeadLock(); DeadLock td2 = new DeadLock(); td1.flag = 1; td2.flag = 0; //td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。 //td2的run()可能在td1的run()之前运行 new Thread(td1).start(); new Thread(td2).start(); } }
避免死锁的方法
死锁排查方法
虽然造成死锁的原因是因为我们设计得不够好,但是可能写代码的时候不知道哪里发生了死锁。
JDK提供了两种方式来给我们检测:
检测出死锁时的解决方案
一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(编者注:原因同超时类似,不能从根本上减轻竞争)。
一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。
1. 使用本地变量
尽量使用本地变量,而不是创建一个类或实例的变量。
class concurrentTask { private static List temp = new ArrayList<>(); public void execute(Message message) { // 使用本地变量保证线程安全 // List temp = new ArrayList<>(); temp.add(message.getId()); temp.add(message.getCode()); // ...省略各种业务逻辑 temp.clear(); } }
2. 使用不可变类
不可变类比如String 、Integer等一旦创建,不再改变,不可变类可以降低代码中需要的同步数量。
3. 最小化锁的作用域范围
"阿姆达尔定律",又称"安达尔定理": S=1/(1-a+a/n)
a:并行计算部分所占比例
n:并行处理结点个数
S:加速比
当1-a等于0时,没有串行只有并行,最大加速比 S=n
当a=0时,只有串行没有并行,最小加速比 S = 1
当n→∞时,极限加速比 s→ 1/(1-a)
例如,若串行代码占整个代码的25%,则并行处理的总体性能不可能超过4。
4. 使用线程池,而不是直接使用new Thread执行
避免new Thread创建线程。通过线程池的管理,可提高线程的复用性(避免新建线程的昂贵的资源消耗),简化线程生命周期的管理。JDK提供了各种ThreadPool线程池和Executor。
5. 宁可使用同步工具类也不要使用线程的wait和notify
同步工具类包括:countdownlaunch/Semaphore/Semaphore。应当优先使用这些同步工具,而不是去思考如何使用线程的wait和notify。此外,使用BlockingQueue实现生产消费的设计比使用wait和notify要好。
6. 使用blockingqueue实现生产消费模式
阻塞队列是生产者-消费者模式的最好的实现方式,不仅包括单个生产者单个消费者,还支持多个生产者多个消费者情况。
7. 使用并发集合而不是加了锁的同步集合
JDK提供了ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet、BlockingQueue中的Deque和BlockingDeque五大并发集合,他们有着较好性能;尽量使用该并发集合,而避免使用synchronizedXXX的锁同步集合。
8. 使用semaphore创建有界的访问
为了建立稳定可靠的系统,对于数据库、文件系统和socket等资源必须要做有界的访问,Semaphone可以限制这些资源开销的选择,Semaphone可以以最低的代价阻塞线程等待,可以通过Semaphone来控制同时访问指定资源的线程数。
9. 宁可使用同步代码块,也不使用同步的方法
主要针对synchronized关键字。使用synchronized关键字同步代码块只会锁定一个对象,而不会将整个方法锁定(当类不是单例的时候)。如果更改共同的变量或类的字段,首先应该选择的是原子型变量,然后使用volatile。如果需要互斥锁,可以考虑使用ReentrantLock。
10. 避免使用静态变量
静态变量在多线程并发环境中会造成较多的问题。当使用静态变量时,优先将其指定为final变量,若用其来保存集合Collection变量,则考虑使用只读集合。详见上文的不可变对象,同步容器和并发容器。