Java并发

有哪些解决线程并发问题的方案?

*当只有一个线程写,其它线程都是读的时候,可以用volatile修饰变量

*当多个线程写,那么一般情况下并发不严重的话可以用Synchronized,Synchronized并不是一开始就是重量级锁,在并发不严重的时候,比如只有一个线程访问的时候,是偏向锁;当多个线程访问,但不是同时访问,这时候锁升级为轻量级锁;当多个线程同时访问,这时候升级为重量级锁。所以在并发不是很严重的情况下,使用Synchronized是可以的。不过Synchronized有局限性,比如不能设置锁超时,不能通过代码释放锁。ReentranLock 可以通过代码释放锁,可以设置锁超时。

*高并发下,Synchronized、ReentranLock 效率低,因为同一时刻只有一个线程能进入同步代码块,如果同时有很多线程访问,那么其它线程就都在等待锁。这个时候可以使用并发包下的数据结构,例如ConcurrentHashMap,LinkBlockingQueue,以及原子性的数据结构如:AtomicInteger。

悲观锁和乐观锁的区别。

*乐观锁:乐观锁假设多个事务之间很少发生冲突,因此在读取数据时不会加锁,而是在更新数据时检查数据的版本(如使用版本号或时间戳),如果版本匹配则执行更新操作,否则认为发生了冲突。

*悲观锁:悲观锁假设多个事务之间会频繁发生冲突,因此在读取数据时会加锁,防止其他事务对数据进行修改,直到当前事务完成操作后才释放锁。

悲观锁和乐观锁使用场景的差别是什么?

*乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总 是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

*悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总 是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像 synchronized,不管三七二十一,直接上了锁就操作资源了。

Java中想实现一个乐观锁,都有哪些方式?

*CAS(Compare and Swap)操作: CAS 是乐观锁的基础。Java 提供了 java.util.concurrent.atomic 包,包含各种原子变量类(如 AtomicInteger、AtomicLong),这些类使用 CAS 操作实现了线程安全的原子操作,可以用来实现乐观锁。

*版本号控制:增加一个版本号字段记录数据更新时候的版本,每次更新时递增版本号。在更新数据时,同时比较版本号,若当前版本号和更新前获取的版本号一致,则更新成功,否则失败。

*时间戳:使用时间戳记录数据的更新时间,在更新数据时,在比较时间戳。如果当前时间戳大于数据的时间戳,则说明数据已经被其他线程更新,更新失败。

使用时间戳会不会有可见性问题?

会有可见性的问题,可见性问题指的是当一个线程修改了共享变量的值后,其他线程可能无法立即看到这个变化。

如果一个线程在读取数据的时间戳之后,另一个线程修改了数据并更新了时间戳,第一个线程可能无法感知到第二个线程的修改。这导致第一个线程在接下来的操作中基于过时的时间戳做出了错误的判断,从而可能发生数据不一致的情况。

为了避免时间戳导致的可见性问题,可以考虑使用volatile关键字: 在时间戳字段上使用 volatile 关键字可以确保多线程之间的可见性,可以及时看到其他线程对共享变量的修改。

volatile能解决吗,就够了吗?

volatile 关键字仅仅保证了可见性,并没有提供原子性。在实现乐观锁时,如果仅仅依赖 volatile 关键字来保证时间戳的可见性,仍然可能面临以下问题:

复合操作可能存在问题: 如果对时间戳字段的更新是一个复合操作,如读取时间戳、计算新值、更新时间戳,这样的操作并不具备原子性。在这种情况下,仅仅使用 volatile 是无法保证操作的原子性和线程安全的。

可以通过加锁的方式来保证对时间戳读写操作的并发安全。

除了加锁还有没有别的解法,绕开加锁使性能更好?

可以改用 Atomic 类来保证原子性,这些原子类是使用 CAS (Compare and Swap) 操作来保证数据的原子性。如 AtomicInteger、AtomicLong、AtomicReference 等,通过这些原子类可以实现 CAS 操作,从而确保对共享变量的原子操作。

synchronized 和 ReentrantLock 的区别

synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比 synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock 比 synchronized 的扩展性体现在几点上:

*ReentrantLock 可以对获取锁的等待时间进行设置,这样就避免了死锁
*ReentrantLock 可以获取各种锁的信息
*ReentrantLock 可以灵活地实现多路通知

讲一讲ThreadLocal使用的时候需要注意哪些点。

ThreadLocal 是线程共享变量。ThreadLoacl 有一个静态内部类 ThreadLocalMap,其 Key 是 ThreadLocal 对象,值是 Entry 对象,ThreadLocalMap是每个线程私有的。

*set 给ThreadLocalMap设置值。
*get 获取ThreadLocalMap。
*remove 删除ThreadLocalMap类型的对象。

存在的问题:

1、对于线程池,由于线程池会重用 Thread 对象,因此与 Thread 绑定的 ThreadLocal 也会被重用,造成一系列问题。
2、内存泄漏。由于 ThreadLocal 是弱引用,但 Entry 的 value 是强引用,因此当 ThreadLocal 被垃圾回收后,value 依旧不会被释放,产生内存泄漏。

使用完ThreadLocal后,及时调用remove()方法释放内存空间,这样就能避免内存泄漏。

线程并发还有别的问题吗?

Java的线程安全在三个方面体现

*原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,在Java中使用了atomic和synchronized这两个关键字来确保原子性;

*可见性:一个线程对主内存的修改可以及时地被其他线程看到,在Java中使用了synchronized和volatile这两个关键字确保可见性;

*有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,在Java中使用了happens-before原则来确保有序性。

常用的线程池有哪些呢?

*ScheduledThreadPool:可以设置定期的执行任务,它支持定时或周期性执行任务,比如每隔 10 秒钟执行一次任务,我通过这个实现类设置定期执行任务的策略。

*FixedThreadPool:它的核心线程数和最大线程数是一样的,所以可以把它看作是固定线程数的线程池,它的特点是线程池中的线程数除了初始阶段需要从 0 开始增加外,之后的线程数量就是固定的,就算任务数超过线程数,线程池也不会再创建更多的线程来处理任务,而是会把超出线程处理能力的任务放到任务队列中进行等待。而且就算任务队列满了,到了本该继续增加线程数的时候,由于它的最大线程数和核心线程数是一样的,所以也无法再增加新的线程了。

*CachedThreadPool:可以称作可缓存线程池,它的特点在于线程数是几乎可以无限增加的(实际最大可以达到 Integer.MAX_VALUE,为 2^31-1,这个数非常大,所以基本不可能达到),而当线程闲置时还可以对线程进行回收。也就是说该线程池的线程数量不是固定不变的,当然它也有一个用于存储提交任务的队列,但这个队列是 SynchronousQueue,队列的容量为0,实际不存储任何任务,它只负责对任务进行中转和传递,所以效率比较高。

*SingleThreadExecutor:它会使用唯一的线程去执行任务,原理和 FixedThreadPool 是一样的,只不过这里线程只有一个,如果线程在执行任务的过程中发生异常,线程池也会重新创建一个线程来执行后续的任务。这种线程池由于只有一个线程,所以非常适合用于所有任务都需要按被提交的顺序依次执行的场景,而前几种线程池不一定能够保障任务的执行顺序等于被提交的顺序,因为它们是多线程并行执行的。

*SingleThreadScheduledExecutor:它实际和 ScheduledThreadPool 线程池非常相似,它只是 ScheduledThreadPool 的一个特例,内部只有一个线程。

简单讲一讲线程的生命周期

ThreadPool
线程池的五个状态如下

*RUNNING: 接收新的任务,并能继续处理 workQueue 中的任务

*SHUTDOWN: 不再接收新的任务,不过能继续处理 workQueue 中的任务

*STOP: 不再接收新的任务,也不再处理 workQueue 中的任务,并且会中断正在处理任务的线程

*TIDYING: 所有的任务都完结了,并且线程数量(workCount)为 0 时即为此状态,进入此状态后会调用 terminated() 这个钩子方法进入 TERMINATED 状态

*TERMINATED: 调用 terminated() 方法后即为此状态

有什么办法能够提升Java线程的并发能力呢?

*使用线程池:合理配置线程池大小,避免频繁创建和销毁线程,提高线程的复用率,减少资源开销。

*使用并发集合:Java 并发包中提供了诸如 ConcurrentHashMap、ConcurrentLinkedQueue 等线程安全的集合类,使用这些并发集合可以减少在多线程环境下的锁竞争,提升并发性能

*避免锁粒度过细:过细的锁粒度会增加锁的竞争,影响并发性能,尽量使用合适的锁粒度来避免锁的争用。

*减少同步区域代码量:在同步代码块中尽量减少耗时操作,保持同步区域的代码量尽量少,以减少线程等待时间,提高并发性能。

*使用乐观锁:乐观锁机制可以提高并发性能,通过版本号或时间戳等方式,避免使用传统的悲观锁机制,减少不必要的阻塞。

*使用无锁算法:无锁的数据结构和算法可以减少锁的开销和线程之间的竞争,如CAS操作、原子类等。

标题: Java并发

链接: http://example.com/2024/05/14/Java%E5%B9%B6%E5%8F%91/

版权声明: 若无特殊标注皆为 鹏少 原创版权, 转载请以链接形式注明作者及原始出处

最后编辑时间: 2024-05-22