介绍
涉及IO读/写时,多线程可以提高应用程序的性能。不幸的是,共享资源(共享变量)在每个CPU缓存中可能有不同的版本。结果是应用程序的行为不可预测。Java提供了synchronized
关键字来保持CPU缓存中共享资源的一致性。不幸的是,synchronized
关键词减慢了应用程序的速度。
我使用JMH作为微型基准测试AverageTime
模式,这意味着基准测试结果是每个测试用例的平均运行时间,较低的输出效果更好。您可以在此链接找到更多关于微基准的信息。
为什么同步减速应用程序?
当线程被锁定并开始执行同步块中的指令时,所有其他线程将被阻塞并变为空闲状态。这些线程的执行上下文(CPU缓存,指令集,堆栈指针...)将被存储,其他活动线程的执行上下文将被恢复以恢复计算。它被称为上下文切换,需要系统的大量工作。任务计划程序也必须运行以选择将加载哪个线程。
易变关键字
volatile
关键字只是做了一些事情:告诉CPU从主内存中读取资源的值,而不是从CPU的缓存中读取资源的值; 在每次后续读取该字段之前,都会发生对易失性字段的写入。 易失性永远不会有比同步更高volatile
的开销,synchronized
如果synchronized
块只有一个操作,它将具有相同的开销。
volatile
如果只有一个写入线程,关键字可以很好地工作。如果有2个或更多的写入线程,就会出现竞争条件:所有写入线程获取最新版本的变量,在自己的CPU上修改值,然后写入主内存。结果是内存中的数据只是一个线程的输出,其他线程的修改被覆盖。
包java.util.concurrent
Doug Lea在创建和改进这个软件包时做了很棒的工作。这个包有很多用于管理线程的工具,还包含一些线程安全的数据结构。那些数据结构也可以使用synchronized
,volatile
但是以一种复杂的方式,你可以从编写你自己的代码中获得更好的性能。
ConcurrentHashMap
“遵循与”相同的功能规范Hashtable
“,并为您提供线程安全的优势。
public class TestHashMap { @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Threads(10) public void concurrentHashMap(BenchMarkState state){ Integer temp; for(int i = 0; i < 100000; i++){ temp = Integer.valueOf(i); state.chm.put(temp,temp); } } @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Threads(10) public void hashMap(BenchMarkState state){ Integer temp; for(int i = 0; i < 100000; i++){ temp = Integer.valueOf(i); synchronized (state.LOCK_1) { state.hm.put(temp,temp); } } } @State(Scope.Benchmark) public static class BenchMarkState { @Setup(Level.Trial) public void doSetup() { hm = new HashMap<>(100000); chm = new ConcurrentHashMap<>(100000); } @TearDown(Level.Trial) public void doTearDown() { hm = new HashMap<>(100000); chm = new ConcurrentHashMap<>(100000); } public HashMap<Integer, Integer> hm = new HashMap<>(100000); public ConcurrentHashMap<Integer, Integer> chm = new ConcurrentHashMap<>(100000); public final Object LOCK_1 = new Object(); }
隐藏 复制代码
Benchmark Mode Cnt Score Error Units TestHashMap.concurrentHashMap avgt 200 10740649.930 ± 351589.110 ns/op TestHashMap.hashMap avgt 200 60661584.668 ± 758157.651 ns/op
AtomicInteger
和其他类似的类使用volatile
和Unsafe.compareAndSwapInt
。AtomicInteger
可以称为忙等待,这意味着一个线程总是检查条件执行。这个线程什么都不做,但是任务调度程序不能检测到这个检查并且认为这个线程很忙,所以任务调度程序不能把CPU拿到另一个准备执行的线程。如果在几个CPU时钟之后条件可以归档,则忙等待效果很好。
public class TestCAS { @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Threads(10) public void atomic(BenchMarkState state){ while(state.atomic.get() < 100000) while(true){ int temp = state.atomic.get(); if(temp >= 100000 || state.atomic.compareAndSet(temp, temp + 1)) break; } } @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Threads(10) public void integer(BenchMarkState state){ while(state.integer < 100000){ synchronized (state.LOCK) { if(state.integer < 100000) state.integer += 1; } } } @State(Scope.Benchmark) public static class BenchMarkState { @Setup(Level.Trial) public void doSetup() { atomic.set(0); integer = 0; } public Object LOCK = new Object(); public AtomicInteger atomic = new AtomicInteger(0); public Integer integer = new Integer(0); }
Benchmark Mode Cnt Score Error Units TestCAS.atomic avgt 200 10.053 ± 0.985 ns/op TestCAS.integer avgt 200 12.666 ± 1.145 ns/op
锁
Lock
具有更多灵活的功能synchronized
,您可以使用tryLock()
特定的时间等待或确保最长的等待线程获得公平选项的锁定。但synchronized
关键字可以保证执行顺序和数据新鲜度,源代码synchronized
也很简单。Lock
@Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Threads(10) public void lock(BenchMarkState state){ while(state.intLock < 100000){ state.lock.lock(); if(state.intLock < 100000) state.intLock++; state.lock.unlock(); } } @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Threads(10) public void synchonized(BenchMarkState state){ while(state.intSync < 100000){ synchronized (state.LOCK) { if(state.intSync < 100000) state.intSync += 1; } } }
Benchmark Mode Cnt Score Error Units TestLock.lock avgt 200 1.960 ± 0.074 ns/op TestLock.synchonized avgt 200 2.394 ± 0.047 ns/op
不变的对象
这个想法很简单,如果一个对象从不改变值,它是线程安全的。但是有一个问题,每次你想改变一些值时你必须创建一个新的对象,因此GC过热。
结论
在synchronized
关键字之间共享线程之间的资源很容易,但它可能会导致世界各地的等待和放慢您的应用程序。其他简单的技术也可以归档线程安全,但速度比synchronized
。
针对上面的技术我特意整理了一下,有很多技术不是靠几句话能讲清楚,所以干脆找朋友录制了一些视频,很多问题其实答案很简单,但是背后的思考和逻辑不简单,要做到知其然还要知其所以然。如果想学习Java工程化、高性能及分布式、深入浅出。微服务、Spring,MyBatis,Netty源码分析的朋友可以加我的Java进阶群:744642380,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家。