重庆企业网站备案要多久时间,网上商城开发价格,滨州seo排名,域名注册网站搭建锁优化
Synchronized
在JDK1.6中引入了分级锁机制来优化Synchronized。当一个线程获取锁时
首先对象锁将成为一个偏向锁#xff0c;这样做是为了优化同一线程重复获取锁#xff0c;导致的用户态与内核态的切换问题#xff1b;其次如果有多个线程竞争锁资源#xff0c;锁…锁优化
Synchronized
在JDK1.6中引入了分级锁机制来优化Synchronized。当一个线程获取锁时
首先对象锁将成为一个偏向锁这样做是为了优化同一线程重复获取锁导致的用户态与内核态的切换问题其次如果有多个线程竞争锁资源锁将会升级为轻量级锁它适用于在短时间内持有锁且分锁有交替切换的场景轻量级锁还使用了自旋锁来避免线程用户态与内核态的频繁切换大大地提高了系统性能但如果锁竞争太激烈了那么同步锁将会升级为重量级锁。
减少锁竞争是优化Synchronized同步锁的关键。我们应该尽量使Synchronized同步锁处于轻量级锁或偏向锁这样才能提高Synchronized同步锁的性能。
通过减小锁粒度来降低锁竞争也是一种最常用的优化方法ConcurrentHashMap就很很巧妙地使用了分段锁Segment来降低锁资源竞争。另外我们还可以通过减少锁的持有时间来提高Synchronized同步锁在自旋时获取锁资源的成功率避免Synchronized同步锁升级为重量级锁。
Lock同步锁
Lock锁是基于Java实现的锁Lock是一个接口类常用的实现类有ReentrantLock、ReentrantReadWriteLockRRW它们都是依赖AbstractQueuedSynchronizerAQS类实现的。
从性能方面上来说在并发量不高、竞争不激烈的情况下Synchronized同步锁由于具有分级锁的优势性能上与Lock锁差不多但在高负载、高并发的情况下Synchronized同步锁由于竞争激烈会升级到重量级锁性能则没有Lock锁稳定。
ReentrantLock是一个独占锁同一时间只允许一个线程访问而RRW允许多个读线程同时访问但不允许写线程和读线程、写线程和写线程同时访问。读写锁内部维护了两个锁一个是用于读操作的ReadLock一个是用于写操作的WriteLock。
乐观锁
乐观锁顾名思义就是说在操作共享资源时它总是抱着乐观的态度进行它认为自己可以成功地完成操作。但实际上当多个线程同时操作一个共享资源时只有一个线程会成功那么失败的线程呢它们不会像悲观锁一样在操作系统中挂起而仅仅是返回并且系统允许失败的线程重试也允许自动放弃退出操作。
CAS是实现乐观锁的核心算法它包含了3个参数V需要更新的变量、E预期值和N最新值。 只有当需要更新的变量等于预期值时需要更新的变量才会被设置为最新值如果更新值和预期值不同则说明已经有其它线程更新了需要更新的变量此时当前线程不做操作返回V的真实值。 在JDK中的concurrent包中atomic路径下的类都是基于CAS实现的。AtomicInteger就是基于CAS实现的一个线程安全的整型类。AtomicInteger依赖于本地方法Unsafe类Unsafe类中的操作方法会调用CPU底层指令实现原子操作。 在数据库表中的内容时根据表记录的version进行的更新操作也是一种乐观锁机制。 锁优化总结
在读大于写的场景下读写锁RRW的性能最佳。在写大于读的场景下乐观锁的性能是最好的剩下的读写锁RRW、ReentrantLock、Synchronized的性能则相差不多。在读和写差不多的场景下读写锁RRW以及乐观锁的性能要优于Synchronized和ReentrantLock。
上下文切换优化
一个线程被剥夺处理器的使用权而被暂停运行就是“切出”一个线程被选中占用处理器开始或者继续运行就是“切入”。在这种切出切入的过程中操作系统需要保存和恢复相应的进度信息这个进度信息就是“上下文”了。 而切入、切出的过程需要进行上下文切换。 切入、切出过程中的进度信息它包括了寄存器的存储内容以及程序计数器存储的指令内容。 可以使用 Linux 内核提供的 vmstat 命令来监视 Java 程序运行过程中系统的上下文切换频率 system.cs。
[localhost /]
$vmstat
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----r b swpd free buff cache si so bi bo in cs us sy id wa st7 0 0 1870684 0 4104652 0 0 471 7420 0 0 0 0 100 0 0如果是监视某个应用的上下文切换就可以使用 pidstat命令监控指定进程的 Context Switch 上下文切换。 pidstat -w -l -p pid 1 100
# 212351 为进程的pid
[adminnui-sus033103101131.pre.na620 /]
$pidstat -w -l -p 212351 1 100
Linux 5.10.134-010.x86_64 (localhost.localdomain) 12/11/2024 _x86_64_ (4 CPU)03:47:43 PM UID PID cswch/s nvcswch/s Command
03:47:44 PM 2368 212351 0.00 0.00 java -server -Xms5g -Xmx8g -Djava.util.Arrays.useLegacyMergeSorttrue -Dproject.nametest -DLog4jContextSelectororg.apache.logg
03:47:45 PM 2368 212351 0.00 0.00 java -server -Xms5g -Xmx8g -Djava.util.Arrays.useLegacyMergeSorttrue -Dproject.nametest -DLog4jContextSelectororg.apache.logg
03:47:46 PM 2368 212351 0.00 0.00 java -server -Xms5g -Xmx8g -Djava.util.Arrays.useLegacyMergeSorttrue -Dproject.nametest -DLog4jContextSelectororg.apache.logg
03:47:47 PM 2368 212351 0.00 0.00 java -server -Xms5g -Xmx8g -Djava.util.Arrays.useLegacyMergeSorttrue -Dproject.nametest -DLog4jContextSelectororg.apache.logg
03:47:48 PM 2368 212351 0.00 0.00 java -server -Xms5g -Xmx8g -Djava.util.Arrays.useLegacyMergeSorttrue -Dproject.nametest -DLog4jContextSelectororg.apache.logg
03:47:49 PM 2368 212351 0.00 0.00 java -server -Xms5g -Xmx8g -Djava.util.Arrays.useLegacyMergeSorttrue -Dproject.nametest -DLog4jContextSelectororg.apache.logg
03:47:50 PM 2368 212351 0.00 0.00 java -server -Xms5g -Xmx8g -Djava.util.Arrays.useLegacyMergeSorttrue -Dproject.nametest -DLog4jContextSelectororg.apache.logg
03:47:51 PM 2368 212351 0.00 0.00 java -server -Xms5g -Xmx8g -Djava.util.Arrays.useLegacyMergeSorttrue -Dproject.nametest -DLog4jContextSelectororg.apache.logg我们可以分两种情况来分析一种是程序本身触发的切换这种我们称为自发性上下文切换另一种是由系统或者虚拟机诱发的非自发性上下文切换。
自发性上下文切换指线程由 Java 程序调用导致切出在多线程编程中执行调用以下方法或关键字常常就会引发自发性上下文切换。
sleep()wait()yield()join()park()synchronizedlock
非自发性上下文切换指线程由于调度器的原因被迫切出。常见的有线程被分配的时间片用完虚拟机垃圾回收导致或者执行优先级的问题导致。
上下文切换是多线程编程性能消耗的原因之一而竞争锁、线程间的通信以及过多地创建线程等多线程编程操作都会给系统带来上下文切换。除此之外I/O阻塞以及JVM的垃圾回收也会增加上下文切换。
所以多线程上下文切换优化包括以下几点建议 竞争锁优化 减少锁持有时间降低锁粒度。锁分离比如读写锁RRW锁分段比如 ConcurrentHashMap。非阻塞乐观锁替代竞争锁 wait/notify优化 首先可以使用 Object.notify() 替代 Object.notifyAll()。 因为Object.notify() 只会唤醒指定线程不会过早地唤醒其它未满足需求的阻塞线程所以可以减少相应的上下文切换。使用Lock锁结合Condition 接口替代Synchronized内部锁中的 wait / notify实现等待通知。 合理地设置线程池大小避免创建过多线程 使用协程实现非阻塞等待 减少Java虚拟机的垃圾回收 很多 JVM 垃圾回收器serial收集器、ParNew收集器在回收旧对象时会产生内存碎片从而需要进行内存整理在这个过程中就需要移动存活的对象。而移动内存对象就意味着这些对象所在的内存地址会发生变化因此在移动对象前需要暂停线程在移动完成后需要再次唤醒该线程。因此减少 JVM 垃圾回收的频率可以有效地减少上下文切换。
并发容器的使用
并发场景下的Map容器
如果对数据有强一致要求则需使用Hashtable在大部分场景通常都是弱一致性的情况下使用ConcurrentHashMap即可如果数据量在千万级别且存在大量增删改操作则可以考虑使用ConcurrentSkipListMap。
并发场景下的List容器
如果对数据有强一致要求则需使用VectorCopyOnWriteArrayList适用于读远大于写的操作场景。
线程池参数
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量int maximumPoolSize,//线程池的最大线程数long keepAliveTime,//当线程数大于核心线程数时多余的空闲线程存活的最长时间TimeUnit unit,//时间单位BlockingQueueRunnable workQueue,//任务队列用来储存等待执行任务的队列ThreadFactory threadFactory,//线程工厂用来创建线程一般默认即可RejectedExecutionHandler handler) //拒绝策略当提交的任务过多而不能及时处理时我们可以定制策略来处理任务下图描述了线程池参数的关系 在创建完线程池之后默认情况下线程池中并没有任何线程等到有任务来才创建线程去执行任务。但有一种情况排除在外就是调用prestartAllCoreThreads()或者prestartCoreThread()方法的话可以提前创建等于核心线程数的线程数量这种方式被称为预热。 当创建的线程数等于 corePoolSize 时提交的任务会被加入到设置的阻塞队列中。当队列满了会创建线程执行任务直到线程池中的数量等于maximumPoolSize。当线程数量已经等于maximumPoolSize时 新提交的任务无法加入到等待队列也无法创建非核心线程直接执行我们又没有为线程池设置拒绝策略这时线程池就会抛出RejectedExecutionException异常即线程池拒绝接受这个任务。当线程池中创建的线程数量超过设置的corePoolSize在某些线程处理完任务后如果等待keepAliveTime时间后仍然没有新的任务分配给它那么这个线程将会被回收。线程池回收线程时会对所谓的“核心线程”和“非核心线程”一视同仁直到线程池中线程的数量等于设置的corePoolSize参数回收过程才会停止。
即使是corePoolSize线程在一些非核心业务的线程池中如果长时间地占用线程数量也可能会影响到核心业务的线程池这个时候就需要把没有分配任务的线程回收掉。
通过allowCoreThreadTimeOut设置项要求线程池将包括“核心线程”在内的没有任务分配的所有线程在等待keepAliveTime时间后全部回收掉。
一般多线程执行的任务类型可以分为CPU密集型和I/O密集型根据不同的任务类型我们计算线程数的方法也不一样。
CPU密集型任务这种任务消耗的主要是CPU资源可以将线程数设置为NCPU核心数1比CPU核心数多出来的一个线程是为了防止线程偶发的缺页中断或者其它原因导致的任务暂停而带来的影响。一旦任务暂停CPU就会处于空闲状态而在这种情况下多出来的一个线程就可以充分利用CPU的空闲时间。I/O密集型任务这种任务应用起来系统会用大部分的时间来处理I/O交互而线程在处理I/O的时间段内不会占用CPU来处理这时就可以将CPU交出给其它线程使用。因此在I/O密集型任务的应用中我们可以多配置一些线程具体的计算方法是2N。碰上一些常规的业务操作比如通过一个线程池实现向用户定时推送消息的业务此时我们可以参考以下公式来计算线程数线程数NCPU核数*1WT线程等待时间/ST线程时间运行时间
综合来看我们可以根据自己的业务场景从“N1”和“2N”两个公式中选出一个适合的计算出一个大概的线程数量之后通过实际压测逐渐往“增大线程数量”和“减小线程数量”这两个方向调整然后观察整体的处理时间变化最终确定一个具体的线程数量。 线程池参数 - blockingQueue 多线程队列在Java多线程应用中特别是在线程池中队列的使用率非常高。Java提供的线程安全队列又分为了阻塞队列和非阻塞队列。 阻塞队列 ArrayBlockingQueue一个基于数组结构实现的有界阻塞队列按 FIFO先进先出原则对元素进行排序使用ReentrantLock、Condition来实现线程安全LinkedBlockingQueue一个基于链表结构实现的阻塞队列同样按FIFO 先进先出 原则对元素进行排序使用ReentrantLock、Condition来实现线程安全吞吐量通常要高于ArrayBlockingQueuePriorityBlockingQueue一个具有优先级的无限阻塞队列基于二叉堆结构实现的无界限最大值Integer.MAX_VALUE - 8阻塞队列队列没有实现排序但每当有数据变更时都会将最小或最大的数据放在堆最上面的节点上该队列也是使用了ReentrantLock、Condition实现的线程安全DelayQueue一个支持延时获取元素的无界阻塞队列基于PriorityBlockingQueue扩展实现与其不同的是实现了Delay延时接口SynchronousQueue一个不存储多个元素的阻塞队列每次进行放入数据时, 必须等待相应的消费者取走数据后才可以再次放入数据该队列使用了两种模式来管理元素一种是使用先进先出的队列一种是使用后进先出的栈使用哪种模式可以通过构造函数来指定。 非阻塞队列 ConcurrentLinkedQueue它是一种无界线程安全队列(FIFO)基于链表结构实现利用CAS乐观锁来保证线程安全。 使用协程来优化多线程业务
线程实现模型 1:1线程模型Linux操作系统编程中往往都是通过fork()函数创建一个子进程来代表一个内核中的线程这会产生大量冗余数据即占用大量内存空间又消耗大量CPU时间用来初始化内存空间以及复制数据。 这时候轻量级进程Light Weight Process即LWP出现了。相对于fork()系统调用创建的线程来说LWP使用clone()系统调用创建线程该函数是将部分父进程的资源的数据结构进行复制复制内容可选且没有被复制的资源可以通过指针共享给子进程。因此轻量级进程的运行单元更小运行速度更快。LWP是跟内核线程一对一映射的每个LWP都是由一个内核线程支持。 N:1线程模型1:1线程模型在线程创建、切换上都存在用户态和内核态的切换性能开销比较大。除此之外它还存在局限性主要就是指系统的资源有限不能支持创建大量的LWP。 N:1线程模型是在用户空间完成了线程的创建、同步、销毁和调度已经不需要内核的帮助了也就是说在线程创建、同步、销毁的过程中不会产生用户态和内核态的空间切换因此线程的操作非常快速且低消耗 N:M线程模型N:1线程模型的缺点在于操作系统不能感知用户态的线程因此容易造成某一个线程进行系统调用内核线程时被阻塞从而导致整个进程被阻塞。 N:M线程模型是基于上述两种线程模型实现的一种混合线程管理模型即支持用户态线程通过LWP与内核线程连接用户态的线程数量和内核态的LWP数量是N:M的映射关系。 JDK 1.8 Thread.java 中 Thread#start 方法的实现实际上是通过Native调用start0方法实现的在Linux下 JVM Thread的实现是基于pthread_create实现的而pthread_create实际上是调用了clone()完成系统调用创建线程的。所以目前Java在Linux操作系统下采用的是用户线程加轻量级线程一个用户线程映射到一个内核线程即1:1线程模型。Go语言是使用了N:M线程模型实现了自己的调度器它在N个内核线程上多路复用或调度M个协程协程的上下文切换是在用户态由协程调度器完成的因此不需要陷入内核相比之下这个代价就很小了。 目前Kilim协程框架在Java中应用得比较多通过这个框架开发人员就可以低成本地在Java中使用协程了。
感兴趣的可以通过Kilim官方资料学写下。
在有严重阻塞的场景下协程的性能更胜一筹。其实I/O阻塞型场景也就是协程在Java中的主要应用。但话说回来协程还是在Go语言中的应用较为成熟在Java中的协程目前还不是很稳定重点是缺乏大型项目的验证可以说Java的协程设计还有很长的路要走。
什么是数据的强、弱一致性
严格一致性强一致性所有的读写操作都按照全局时钟下的顺序执行且任何时刻线程读取到的缓存数据都是一样的Hashtable就是严格一致性 顺序一致性多个线程的整体执行可能是无序的但对于单个线程而言执行是有序的要保证任何一次读都能读到最近一次写入的数据volatile可以阻止指令重排序所以修饰的变量的程序属于顺序一致性
弱一致性不能保证任何一次读都能读到最近一次写入的数据但能保证最终可以读到写入的数据单个写锁无锁读就是弱一致性的一种实现。