写点什么

Java 并发编程基础(上)

作者:FunTester
  • 2024-02-18
    河北
  • 本文字数:4812 字

    阅读完需:约 16 分钟

介绍

Java 是一种功能强大、用途广泛的编程语言。Java 并发是指多个线程同时执行程序,共享资源和数据。通过 synchronized 关键字、Lock 接口等实现线程同步,避免竞态条件和数据不一致问题。并发编程提高系统性能和资源利用率,然而并发编程带来了同步、线程安全等挑战,以及避免死锁和竞争条件等常见陷阱。

基本概念

首先,为了理解和使用 Java 中的并发编程打下基础。并发编程对于利用现代多核处理器的强大功能以及创建能够同时并行执行任务的响应灵敏且高效的应用程序至关重要。


Thread 和 Runnable

Thread类是创建和管理线程的基础类。它允许使用者在应用程序中定义和运行并发任务或进程。线程代表独立的执行逻辑,可以同时执行任务,从而可以在程序中实现并行性。


/**   * 测试线程   */  class TestThread extends Thread {        /**      * 重写run方法      */      @Override      void run() {          for (int i = 1; i < 3; i++) {              println("线程: ${Thread.currentThread().getName()} 计数: $i")//输出线程名和计数          }      }        static void main(String[] args) {          //创建两个线程          TestThread thread1 = new TestThread()          TestThread thread2 = new TestThread()          //启动线程          thread1.start()          thread2.start()          println "主线程结束"      }  }
复制代码


控制台输出:


主线程结束线程: Thread-1 计数: 1线程: Thread-0 计数: 1线程: Thread-1 计数: 2线程: Thread-0 计数: 2
复制代码


Runnable 接口是一个函数式接口,表示可以由线程并发执行的任务或代码段。它提供了一种定义线程应运行的代码的方法,而无需显式扩展该类 Thread。实现 Runnable 接口可以更好地分离关注点并提高代码的可重用性。


/**   * 测试runnable接口类   */  class TestRunnable implements Runnable {        /**      * 重写run方法      */      @Override      void run() {          for (int i = 1; i < 3; i++) {              println("线程: ${Thread.currentThread().getName()} 计数: $i")//输出线程名和计数          }      }        static void main(String[] args) {          //创建两个接口实例          TestRunnable runnable1 = new TestRunnable()          TestRunnable runnable2 = new TestRunnable()          //创建两个线程          Thread thread1 = new Thread(runnable1)          Thread thread2 = new Thread(runnable2)          //启动线程          thread1.start()          thread2.start()          println "主线程结束"      }  }
复制代码


控制台输出:


主线程结束线程: Thread-1 计数: 1线程: Thread-0 计数: 1线程: Thread-0 计数: 2线程: Thread-1 计数: 2
复制代码


Thread 状态表示线程在其生命周期中可能处于的不同阶段或条件:



同步关键字

synchronized关键字用于创建同步代码块,确保Thread一次只有一个可以执行它们。它提供了一种控制对程序关键部分的访问的方法,防止多个线程同时访问它们。


要进入同步块,必须获取对象监视器上的锁。对象的监视器是一种同步机制,提供 Object 实例的锁定功能。执行此操作后,块中包含的所有代码都可以进行独占和原子操作。退出 synchronized 块后,锁将返回到对象的监视器以供其他线程获取。如果无法立即获取锁,则执行 Thread 会等待,直到获取到这个锁。


/**   * 测试线程,用于测试synchronize关键字   */  class TestThread extends Thread {        static int count = 0//计数器,用于统计循环次数,这里是共享资源        static Object lock = new Object()//锁对象,用于同步代码块        /**       * 重写run方法       */      @Override      void run() {          for (int i = 0; i < 10; i++) {              synchronized (lock) { // 同步代码块                  count++//计数器加1              }          }      }        static void main(String[] args) {          TestThread thread1 = new TestThread()//创建线程实例          TestThread thread2 = new TestThread()//创建线程实例          thread1.start()//启动线程          thread2.start()//启动线程          thread1.join()//等待线程1结束          thread2.join()//等待线程2结束          println "count = $count"//输出count的值          println "主线程结束"        }  }
复制代码


控制台输出:


count = 20主线程结束
复制代码


synchronized关键字也可以在方法级别上指定。对于非静态方法,锁是从该方法所属的对象实例的监视器中获取的;对于静态方法,则是从该方法所属类的 Class 对象的监视器中获取的。


/**   * 计数器加1   * @return   */  synchronized def increase() {      count++//计数器加1  }    /**   * 计数器减1   * @return   */  static synchronized decrease() {      count--//计数器减1  }
复制代码


锁是可重入的,因此如果线程已经持有锁,它可以再次成功获取锁。


/**   * 这个方法演示synchronize重入, 一个线程可以多次进入同一个对象的synchronized方法   * @return   */  synchronized def step() {      step1()      step2()  }    synchronized def step1() {    }    synchronized def step2() {    }
复制代码

wait/notify/notifyAll

wait()使用, notify(),方法同步访问功能/资源的最常见模式notifyAll()是条件循环。让我们看一个示例,演示如何使用wait()notify()协调两个线程来打印备用数字:


public class WaitNotifyExample {    private static final Object lock = new Object();    private static boolean isOddTurn = true;     public static void main(String[] args) {        Thread oddThread = new Thread(() -> {            for (int i = 1; i <= 10; i += 2) {                synchronized (lock) {                    while (!isOddTurn) {                        try {                            lock.wait(); // Wait until it's the odd thread's turn                        } catch (InterruptedException e) {                            Thread.currentThread().interrupt();                        }                    }                    System.out.println("Odd: " + i);                    isOddTurn = false; // Satisfy the waiting condition                    lock.notify(); // Notify the even thread                }            }        });         Thread evenThread = new Thread(() -> {            for (int i = 2; i <= 10; i += 2) {                synchronized (lock) {                    while (isOddTurn) {                        try {                            lock.wait(); // Wait until it's the even thread's turn                        } catch (InterruptedException e) {                            Thread.currentThread().interrupt();                        }                    }                    System.out.println("Even: " + i);                    isOddTurn = true; // Satisfy the waiting condition                    lock.notify(); // Notify the odd thread                }            }        });         oddThread.start();        evenThread.start();    }}
复制代码


控制台输出:


Odd: 1Even: 2Odd: 3Even: 4Odd: 5Even: 6Odd: 7Even: 8Odd: 9Even: 10
复制代码


注意事项:


  • 为了在一个对象上使用wait()notify()notifyAll(),需要首先获取该对象的锁——我们的两个线程在该lock对象上同步以获取其锁。

  • 始终在检查正在等待的条件的循环内等待。如果另一个线程在等待开始之前满足条件,这可以解决计时问题,并且还可以保护您的代码免受虚假唤醒 - 我们的两个线程都在由标志控制的循环内等待isOddTurn

  • notify()在调用/之前始终确保满足等待条件notifyAll()。如果不这样做将导致通知,但没有线程能够逃脱其等待循环——我们的两个线程都满足isOddTurn另一个线程继续的标志。

volatile

当一个变量被声明为 volatile 时,它保证对该变量的任何读写操作都直接在主内存上执行,确保原子更新和对所有线程的变化可见性。换句话说,JMM 对于事件“写入 volatile 变量”和任何后续“读取 volatile 变量”都应用了“happens-before”关系。因此,变量的任何后续读取都将看到最近写入的值。

ThreadLocal 类

ThreadLocal是一个提供线程局部变量的类。线程局部变量是每个线程唯一的变量,这意味着访问变量的每个线程ThreadLocal都会获得该变量自己的独立副本。当使用者有需要为每个线程隔离和单独维护的数据时,这非常有用,而且还可以减少对共享资源的争用,这通常会导致性能瓶颈。它通常用于存储用户会话、数据库连接和线程特定状态等值,而无需在方法之间显式传递它们。


ThreadLocal以下是如何使用存储和检索线程特定数据的简单示例:


public class ThreadLocalExample {/**   * 创建一个ThreadLocal对象,使用withInitial方法设置初始值   */  static ThreadLocal<Long> threadLocal = ThreadLocal.withInitial(() -> System.currentTimeMillis())    static void main(String[] args) {      for (i in 0..<3) {//创建3个线程          new Thread({//每个线程都会打印出threadLocal的值              println threadLocal.get()          }).start()          sleep(1000)//休眠1秒      }  }}
复制代码


控制台输出:


170815541049917081554115111708155412516
复制代码

不可变对象

不可变对象是指其状态在创建后无法修改的对象。一旦不可变对象被初始化,其内部状态在其整个生命周期中保持不变。此属性使不可变对象本质上是线程安全的,因为它们不能被多个线程同时修改,从而消除了同步的需要。


创建不可变对象涉及几个关键步骤:


  1. 创建类final:防止继承并确保该类不能被子类化。

  2. 将所有字段声明为final:将所有实例变量标记为,以final确保它们仅初始化一次,通常在构造函数中初始化。

  3. 无 setter 方法:不提供允许修改对象状态的 setter 方法。

  4. 安全发布this构建过程中引用不会逃逸。

  5. 没有可变对象:如果类包含对可变对象(可以更改其状态的对象)的引用,请确保这些引用不公开或允许外部修改。

  6. 将所有字段设为私有:通过将字段设为私有来封装字段以限制直接访问。

  7. 在修改状态的方法中返回一个新对象:不修改现有对象,而是创建一个具有所需更改的新对象并返回它。


  /**   * 不可变对象   */  final class ImmutablePerson {        final String name        final int age        final List<ImmutablePerson> family        ImmutablePerson(String name, int age, List<ImmutablePerson> family) {          this.name = name          this.age = age          List<ImmutablePerson> copy = new ArrayList<>(family)// 构造函数中的参数family是一个可变的集合,所以需要进行防御性复制          this.family = Collections.unmodifiableList(copy)// 通过unmodifiableList方法,返回一个不可变的集合  //        在构造方法中,“this”不会传递到任何地方      }        String getName() {          name      }        int getAge() {          age      }        // 没有set方法,所以对象的状态不会改变        // 通过返回一个新的对象来实现修改      ImmutablePerson withAge(int newAge) {          return new ImmutablePerson(this.name, newAge)      }        // 为简单起见,没有 toString、hashCode 和 equals 方法  }
复制代码


发布于: 刚刚阅读数: 4
用户头像

FunTester

关注

公众号:FunTester,800篇原创,欢迎关注 2020-10-20 加入

Fun·BUG挖掘机·性能征服者·头顶锅盖·Tester

评论

发布
暂无评论
Java并发编程基础(上)_FunTester_InfoQ写作社区