当前位置:   article > 正文

Java并发编程入门_java并发入门

java并发入门

学好java并发编程,可以将并发抽象成以下三个问题:分发,同步,互斥

分发:

Java SDK 并发包里的 Executor、Fork/Join、Future 本质上都是一种分工方法

同步:

一个线程执行完了一个任务,如何通知执行后续任务的线程开工,线程之间相互协作,而解决协作的核心技术就是管程

互斥:

互斥用于解决线程安全问题,保障同一时间只允许有一个线程访问共享变量,实现互斥的核心技术就是锁

线程带来的原子性,可见性,有序性问题

可见性问题: (缓存导致)

在CPU多核场景下,两个线程同时操作内存中的值,会将内存值加载到cpu的缓存中,进行计算,最后再写回内存,两个cpu的缓存是不可见,导致最终的结果与预期是存在差距的

原子性问题 (使用互斥锁解决,也就是加锁解决)

count+1的指令可以被拆分为以下三个步骤

1、count加载进cpu寄存器

2、count+1

3、count写回内存

cpu保证的原子性是指令级别的,而不是高级操作符(count+1 可以)

有序性问题 (编译优化导致 使用volatile禁止指令重排以及遵循happen-before原则)

Demo实例

当我们需要操作不相关的资源,而不是使用单个锁

//如果使用Bank对象作为锁,会导致所有操作都是串行,性能效率差
//用不同的锁对受保护资源进行精细化管理,能够提升性能。这种锁叫细粒度锁。
  1. public class Bank {
  2. private int balance;
  3. private String password;
  4. //钱锁
  5. private final Object balanceLock = new Object();
  6. //密码锁
  7. private final Object passwordLock = new Object();
  8. private void saveMoney() {
  9. synchronized (balanceLock) {
  10. balance = +1;
  11. }
  12. }
  13. private int getMoney() {
  14. synchronized (balanceLock) {
  15. return balance;
  16. }
  17. }
  18. private void updatePass(String newPss) {
  19. synchronized (passwordLock) {
  20. password = newPss;
  21. }
  22. }
  23. }

使用一把锁保护多种资源 (性能较差,所有对Account的操作都是串行的)

  1. public class Account {
  2. private int balance;
  3. //Account在java加载过程中就已经被创建了,是所有account实例对象所共享的,
  4. // 在jvm中是唯一的,因此可以作为唯一共享的锁
  5. private void transfer(Account target, int mount) {
  6. synchronized (Account.class) {
  7. target.balance += mount;
  8. this.balance -= mount;
  9. }
  10. }
  11. }

优化 (但是会存在死锁问题)

死锁原因

账户A调用transfer,执行synchronized (target),此时账户A获取A的锁,等待B锁释放

账户B也调用了transfer,执行synchronized (target),此时账户B获取B的锁,等待A锁释放

此时账户A,B陷入无限等待的死循环,出现了死锁

  1. private void transfer(Account target, int mount) {
  2. synchronized (this) {
  3. synchronized (target) {
  4. this.balance = balance - mount;
  5. target.balance += mount;
  6. }
  7. }
  8. }

死锁产生的条件:

互斥,共享资源 X 和 Y 只能被一个线程占用;

占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;

不可抢占,其他线程不能强行抢占线程 T1 占有的资源;

循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。

破坏死锁:

1、破坏占有且等待

以账户的例子来说,当线程进行转账,需要同时获取A,B两个锁,才能进行操作,此时需要一个管理者来分配和释放资源

  1. class Allocator {
  2. private List<Object> als =
  3. new ArrayList<>();
  4. // 一次性申请所有资源
  5. synchronized boolean apply(
  6. Object from, Object to) {
  7. if (als.contains(from) ||
  8. als.contains(to)) {
  9. return false;
  10. } else {
  11. als.add(from);
  12. als.add(to);
  13. }
  14. return true;
  15. }
  16. // 归还资源
  17. synchronized void free(
  18. Object from, Object to) {
  19. als.remove(from);
  20. als.remove(to);
  21. }
  22. }
  23. class Account{
  24. private Allocator actr;
  25. private int balance;
  26. private void transfer2(Account target, int amt) {
  27. // 一次性申请转出账户和转入账户,直到成功
  28. while (!actr.apply(this, target)) {
  29. try {
  30. // 锁定转出账户
  31. synchronized (this) {
  32. // 锁定转入账户
  33. synchronized (target) {
  34. if (this.balance > amt) {
  35. this.balance -= amt;
  36. target.balance += amt;
  37. }
  38. }
  39. }
  40. } finally {
  41. actr.free(this, target);
  42. }
  43. }
  44. }
  45. }

2、破坏循环等待条件

获取锁的顺序按锁的ID从小到大排序

  1. class Account{
  2. private int balance
  3. private int id;
  4. private void transfer3(Account target, int amt) {
  5. //按锁的ID从小到大排,先获取的锁一定是最小的锁
  6. Account front = this;
  7. Account latter = target;
  8. if(front.id>latter.id){
  9. front = target;
  10. latter = this;
  11. }
  12. synchronized (front){
  13. synchronized (latter){
  14. if (this.balance > amt) {
  15. this.balance -= amt;
  16. target.balance += amt;
  17. }
  18. }
  19. }
  20. }
  21. }

创建多少线程是最合适的?

进行多线程开发,无疑是为了提高吞吐与降低延迟,设置最合适的线程,就是如果最大化提供内存和cpu的利用率

从两个场景出发: (可以通过Visual VM查看线程执行情况)

CPU密集型任务(即计算性任务),也非绝对,因为有些cpu核数本就比较少,也有任务开到几倍以上的核数(这块我也是很迷茫)

线程的数量 = CPU核数(lscpu查看cpu核数),一般在工程上可以设置为线程数=CPU核数+1

IO密集型任务

最佳线程数 =1 +(I/O 耗时 / CPU 耗时)

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/Guff_9hys/article/detail/926094
推荐阅读
相关标签
  

闽ICP备14008679号