关于并发编程的一些总结

本文概述了Java并发编程中的关键概念,包括synchronized的锁机制及其优化,ReentrantLock的使用与特性,以及Semaphore在控制并发线程数的应用。重点讲解了synchronized的进化、性能差异,以及如何在高并发场景下合理使用并发工具。
摘要由CSDN通过智能技术生成

并发编程的一些总结

1.synchronized是什么?

synchronized是Java中的一个关键字,主要是为了解决多个线程访问共享资源的同步性,可以保证被它修饰的代码块或方法在任何时间至多只有一个线程执行。

2.synchronized的进化史?

在早期Java版本中,synchronizd属于重量级锁,性能低下。

synchronized 在 JDK 1.6 之后引入了锁优化,可以随着多线程竞争激烈程度的不同而选择合适的锁策略。

  1. 当没有线程竞争的时候,是无锁的状态
  2. 当只有一个线程竞争的时候,是偏向锁
  3. 当有多个线程竞争的时候,撤销偏向锁,成为轻量级锁
  4. 当轻量级锁 CAS 次数太多的时候,就会撤销轻量级锁,称为重量级锁

轻量级锁和重量级锁的区别:

在轻量级锁下,线程是不进行阻塞的,线程拿不到锁会不断CAS自旋,直到拿到锁为止,这样会充分利用CPU资源,并在锁释放瞬间确保其他锁可以拿到,但是如果线程竞争激烈,线程就会不断CAS,这也是我们不想看到的。

重量级锁下,拿不到锁的线程会被阻塞,阻塞线程涉及到用户态到内核态的切换,这也是很大的消耗。

3.在高并发场景下,并发度肯定是比较高的,不建议使用 synchronized 的原因主要有以下几点:

  • 由于并发度比较高,因此 synchronized 一定会升级到重量级锁,但是重量级锁的性能是不太高的,因为线程要阻塞再唤醒,需要用户态和内核态之间切换
  • synchronized 没有读写锁优化
  • synchronized 不能对线程唤醒,也就是你线程如果获取不到锁的话会一直阻塞

4.synchronized怎么用?

  • 修饰实例方法,给当前对象实例加锁,要获得当前对象实例的锁
synchronized void method() {
    
}
  • 修饰静态方法

给当前类加锁,会作用于类的所有对象实例,进入同步代码块前要获得当前 class 的锁

这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。

synchronized static method() {
    
}
  • 修饰代码块(锁指定对象/类)
    • synchronized(object) 表示进入同步代码库前要获得 给定对象的锁
    • synchronized(类.class) 表示进入同步代码前要获得 给定 Class 的锁
synchronized(this) {
    // 业务代码
}

5.synchronized的实现原理

为什么说synchronized是基于对象实现的,先掌握Java对象在堆中的存储。

一个Java对象存储在堆上,分别是:对象头、对象实例数据、对齐填充。

对象头展开是:MarkWord、ClassMetadataAddress、数组长度

其中MarkWord记录了两部分信息:

  • 第一部分用于存储对象自身运行时数据,如哈希码、GC分代年龄
  • 另一部分存储了锁信息

进入同步代码块,调用monitorEnter方法,计数器加一,离开同步代码块,调用monitorExit方法,计数器减一

当计数器为0可以拿到锁,同时可以释放锁

6.线程中断与synchronized

//中断线程(实例方法)
public void Thread.interrupt();

//判断线程是否被中断(实例方法)
public boolean Thread.isInterrupted();

//判断是否被中断并清除当前中断状态(静态方法)
public static boolean Thread.interrupted();

7.volatile

在多线程并发编程中synchronized和volatile都扮演着重要的角色,volatile是轻量级的 synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程 修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。本文将深入分析在硬件层面上Intel处理器是如何实现volatile的,通过深入分析帮助我们正确地使用volatile变量。

volatile是Java当中的关键字

它的作用是禁止指令重排序和保证变量的可见性

但它不能保证原子性

  • 保证变量的可见性:当一个线程去修改被volatile修饰的变量时,可以保证修改之后的结果立刻刷新到主存上;当一个线程去读取被volatile修饰的变量时,必须强制从主存中读取,禁用缓存
  • 禁止指令重排序:防止JVM编译源码生成class时使用重排序

1)Lock前缀指令会引起处理器缓存回写到内存。Lock前缀指令导致在执行指令期间,声 言处理器的LOCK#信号。在多处理器环境中,LOCK#信号确保在声言该信号期间,处理器可以 独占任何共享内存[2]。但是,在最近的处理器里,LOCK#信号一般不锁总线,而是锁缓存,毕 竟锁总线开销的比较大。在8.1.4节有详细说明锁定操作对处理器缓存的影响,对于Intel486和 Pentium处理器,在锁操作时,总是在总线上声言LOCK#信号。但在P6和目前的处理器中,如果 访问的内存区域已经缓存在处理器内部,则不会声言LOCK#信号。相反,它会锁定这块内存区 域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁 定”,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。

2)一个处理器的缓存回写到内存会导致其他处理器的缓存无效。IA-32处理器和Intel 64处 理器使用MESI(修改、独占、共享、无效)控制协议去维护内部缓存和其他处理器缓存的一致 性。在多核处理器系统中进行操作的时候,IA-32和Intel 64处理器能嗅探其他处理器访问系统 内存和它们的内部缓存。处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的 缓存的数据在总线上保持一致。例如,在Pentium和P6 family处理器中,如果通过嗅探一个处理 器来检测其他处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理 器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充

8.ReentrantLock

ReentrantLock 是基于 AQS ,抽象同步队列实现的。

ReentrantLock基于AQS,在并发编程中它可以实现公平锁非公平锁来对共享资源进行同步

同时,和synchronized一样,ReentrantLock支持可重入,除此之外,ReentrantLock在调度上更灵活,支持更多丰富的功能

ReentrantLock的lock方法:

在进入lock方法后,发现内部调用了sync.lock()方法。

Sync这个类是一个抽象类,它有两个实现类,Sync继承了AQS

Sync类:
    abstract static class Sync extends AbstractQueuedSynchronizer {
        
NonFairSyncstatic final class NonfairSync extends Sync {
     
FairSyncstatic final class FairSync extends Sync {

使用公平锁,参数加true,非公平锁直接无参构造就行了:
        public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

公平锁:

获取锁时,看有没有线程排队,有排队就不去竞争,线程获取锁的顺序与入阻塞队列的顺序一致,不会有"插队"行为。

非公平锁:

不管有没有线程排队,先尝试获取锁资源,获取不到再去"排队",但是有"插队行为"。

分析AQS

Sync继承了AQS,我们来分析一下AQS

AQS的数据结构是一个双向链表/队列,它里面有个Node类

abstract static class Node {
        volatile Node prev;       // initially attached via casTail
        volatile Node next;       // visibly nonnull when signallable
        Thread waiter;            // visibly nonnull when enqueued
        volatile int status;      // written by owner, atomic bit ops by others

        // methods for atomic operations
        final boolean casPrev(Node c, Node v) {  // for cleanQueue
            return U.weakCompareAndSetReference(this, PREV, c, v);
        }
        final boolean casNext(Node c, Node v) {  // for cleanQueue
            return U.weakCompareAndSetReference(this, NEXT, c, v);
        }
        final int getAndUnsetStatus(int v) {     // for signalling
            return U.getAndBitwiseAndInt(this, STATUS, ~v);
        }
        final void setPrevRelaxed(Node p) {      // for off-queue assignment
            U.putReference(this, PREV, p);
        }
        final void setStatusRelaxed(int s) {     // for off-queue assignment
            U.putInt(this, STATUS, s);
        }
        final void clearStatus() {               // for reducing unneeded signals
            U.putIntOpaque(this, STATUS, 0);
        }

        private static final long STATUS
            = U.objectFieldOffset(Node.class, "status");
        private static final long NEXT
            = U.objectFieldOffset(Node.class, "next");
        private static final long PREV
            = U.objectFieldOffset(Node.class, "prev");
    }

在这里插入图片描述

ReentrantLock是如何拿到锁的?无论是公平还是非公平:

线程通过CAS的方式能够把AQS中的state变量从0变为1,就代表拿到锁了

9.让线程进入阻塞有哪些方法?

  1. 使用 Thread.sleep(long millis):通过调用 Thread.sleep() 方法,线程可以暂时挂起一段时间,进入阻塞状态。在指定的时间过后,线程会重新进入就绪状态。
  2. 使用 Object.wait():线程可以通过调用 Object.wait() 方法进入等待状态,等待其他线程调用相同对象的 notify()notifyAll() 方法来唤醒它。
  3. 使用 Thread.join():在一个线程中调用另一个线程的 join() 方法可以使调用线程等待被调用线程执行完毕,进入阻塞状态,直到被调用线程执行完毕。
  4. 调用 Lock 接口中的 lock 方法:通过 Lock 接口获取锁时,若锁已经被其他线程持有,线程将进入阻塞状态,直到获取到锁。
  5. 使用 BlockingQueueBlockingQueue 是一个用于线程间通信的阻塞队列,当队列为空或满时,线程在插入或获取元素时会被阻塞。
  6. I/O 操作:执行阻塞式的 I/O 操作(如读取文件、网络通信等)会使线程进入阻塞状态,直到操作完成。

这些方法允许线程进入阻塞状态,并且在满足特定条件或时间后,线程可以重新进入就绪状态,等待调度器再次分配执行。

10.控制并发线程数的Semaphore

《[中文]Java并发编程的艺术》

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。

Semaphore可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。假如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程 并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有10个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,就可以使用Semaphore来做流量控制,如代码所示。

public class SemaphoreTest {
private static final int THREAD_COUNT = 30;
private static ExecutorServicethreadPool = Executors.newFixedThreadPool(THREAD_COUNT);
private static Semaphore s = new Semaphore(10);
public static void main(String[] args) {
	for (inti = 0; i< THREAD_COUNT; i++) {
		threadPool.execute(new Runnable() {
			@Override
			public void run() {
			try {
				s.acquire();
				System.out.println("save data");
				s.release();
				} catch (InterruptedException e) {
					}
						}
			});
		}
		threadPool.shutdown();
	}
}

在代码中,虽然有30个线程在执行,但是只允许10个并发执行。Semaphore的构造方法 Semaphore(int permits)接受一个整型的数字,表示可用的许可证数量。Semaphore(10)表示允 许10个线程获取许可证,也就是最大并发数是10。Semaphore的用法也很简单,首先线程使用 Semaphore的acquire()方法获取一个许可证,使用完之后调用release()方法归还许可证。还可以用tryAcquire()方法尝试获取许可证。

Semaphore还提供一些其他方法,具体如下。

**·intavailablePermits():**返回此信号量中当前可用的许可证数。

**·intgetQueueLength():**返回正在等待获取许可证的线程数。

**·booleanhasQueuedThreads():**是否有线程正在等待获取许可证。

**·void reducePermits(int reduction):**减少reduction个许可证,是个protected方法。

**·Collection getQueuedThreads():**返回所有等待获取许可证的线程集合,是个protected方 法

中当前可用的许可证数。

**·intgetQueueLength():**返回正在等待获取许可证的线程数。

**·booleanhasQueuedThreads():**是否有线程正在等待获取许可证。

**·void reducePermits(int reduction):**减少reduction个许可证,是个protected方法。

**·Collection getQueuedThreads():**返回所有等待获取许可证的线程集合,是个protected方 法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值