多线程基础知识(中)

简介: 多线程基础知识(中)

正文


9.锁对象


注:通过lock()获取锁,如果锁同时被另一个线程拥有则发生阻塞


任何时刻只有一个线程进入临界区,一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句,当其他线程调用lock时,它们被阻塞,直到第一个线程释放锁对象


把解锁操作括在finally之内至关重要,如果临界区的代码抛出异常,锁必须被释放,否则,其他线程将永远阻塞


package io.laokou.test.concurrent;
import java.util.Arrays;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author Kou Shenhai
* @version 1.0
* @date 2022/4/19 0019 上午 8:30
*/
public class SyncBankTest {
  private static final int NACCOUNTS = 100;
  private static final double INITIAL_BALANCE = 1000;
  private static final double MAX_AMOUNT = 1000;
  private static final int DELAY = 10;
  public static void main(String[] args) {
    SyncBank bank = new SyncBank(NACCOUNTS,INITIAL_BALANCE);
    for (int i = 0; i < NACCOUNTS; i++) {
      int fromAccount = i;
      Runnable runnable = () -> {
        try {
          while (true) {
            int toAccount = (int)(bank.size() * Math.random());
            double amount = MAX_AMOUNT * Math.random();
            bank.transfer(fromAccount,toAccount, amount);
            Thread.sleep((int)(DELAY * Math.random()));
          }
        } catch (Exception e) {}
      };
      new Thread(runnable).start();
    }
  }
}
class SyncBank {
  private final double[] accounts;
  private Lock bankLock = new ReentrantLock();
  public SyncBank(int n,double initialBalance) {
    this.accounts = new double[n];
    Arrays.fill(accounts, initialBalance);
  }
  public void transfer(int from,int to,double amount) {
    //获取锁
    bankLock.lock();
    try {
      if (accounts[from] < amount) {
        return;
      }
      System.out.print(Thread.currentThread());
      accounts[from] -= amount;
      System.out.printf("%10.2f from %d to %d", amount, from, to);
      accounts[to] += amount;
      System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
    } finally {
      //释放锁
      bankLock.unlock();
    }
  }
  public double getTotalBalance() {
      bankLock.lock();
      try {
          double sum = 0;
          for (int i = 0; i < accounts.length; i++) {
              sum += accounts[i];
          }
          return sum;
      } finally {
          bankLock.unlock();
      }
  }
  public int size() {
    return accounts.length;
  }
}
/**
* Thread[Thread-30,5,main]    429.99 from 30 to 70 Total Balance:  100000.00
* Thread[Thread-70,5,main]    723.57 from 70 to 68 Total Balance:  100000.00
* Thread[Thread-40,5,main]    193.84 from 40 to 1 Total Balance:  100000.00
* Thread[Thread-11,5,main]    287.24 from 11 to 69 Total Balance:  100000.00
* Thread[Thread-67,5,main]     88.12 from 67 to 96 Total Balance:  100000.00
* Thread[Thread-16,5,main]    162.57 from 16 to 99 Total Balance:  100000.00
* Thread[Thread-69,5,main]    715.54 from 69 to 53 Total Balance:  100000.00
* Thread[Thread-61,5,main]    543.64 from 61 to 3 Total Balance:  100000.00
* Thread[Thread-76,5,main]    312.71 from 76 to 43 Total Balance:  100000.00
* Thread[Thread-71,5,main]     89.88 from 71 to 41 Total Balance:  100000.00
* Thread[Thread-40,5,main]     53.28 from 40 to 8 Total Balance:  100000.00
 */


假定一个线程调用transfer,在执行结束前被剥夺运行权,假定第二个线程也调用transfer,由于第二个线程没有获得锁,将在调用lock方法时阻塞,它必须等待第一个线程完成transfer()执行之后才能再度被激活,当第一个线程释放锁时,那么第二个线程才能开始运行


222.png


注意:每一个Bank对象有自己的ReentrantLock对象,如果两个线程试图访问同一个Bank对象,那么锁以串行方式提供服务,但是,如果两个线程访问不同的Bank对象,每一个线程得到不同的锁对象,两个线程都不会发生阻塞,线程在操纵不同的Bank实例的时候,线程之间不会相互影响


锁是可重入的,因为线程可以重复地获得已经持续的锁。锁保持一个持有计数来跟踪对lock()的嵌套调用,线程在每一个调用lock都要调用unlock来释放锁,被一个锁保护的代码可以调用另一个使用相同的锁的方法,例如,transfer方法调用getTotalBalance(),这也会封锁bankLock对象,此时bankLock对象持有计数为2,当getTotalBalance()退出时,持有计数变回1,当transfer退出时,持有计数变为0,线程释放锁


警告:留心临界区的代码,不要因为异常的抛出而跳出临界区,如果在临界区代码结束之前抛出了异常,finally语句将释放锁,单会使对象可能处于一种受损状态


ReetrantLock():构建一个可以被用来保护临界区的可重入锁

ReetrantLock(boolean fair):构建一个带有公平策略的锁,一个公平锁偏爱等待时间最长的线程,这一公平的保证将大大降低性能,默认情况下,锁没有被强制为公平

注意:使用公平锁比使用常规锁要慢很多,使用公平锁,也无法确保线程调度器是公平的,如果线程调度器选择忽略一个线程,而该线程为了这个锁已经等待很长时间,那么就没有机会公平处理这个锁


10.条件对象


调用await(),当前线程被阻塞,线程放弃锁。等待获得锁的线程和调用await()的线程存在本质上的不同,一旦一个线程调用await(),它进入该条件的等待集,当锁可用时,该线程不能马上解除阻塞,相反,它处于阻塞状态,直到另一个线程调用同一个条件上的signalAll()为止,这一调用重新激活因为await()而等待的所有线程,当这些线程从等待集当中移出时,它们再次成为可运行的,调度器将再次激活它们,同时,它们将试图重新进入该对象,一旦锁成为可用的,它们中的某个将从await调用返回,获得该锁并从被阻塞的地方继续执行(调用signalAll()不会立即激活一个等待线程,它仅仅解除等待线程的阻塞,以便这些线程可以在当前线程退出同步方法之后,通过竞争实现对对象的访问)

一个线程调用await(),它没有办法激活自身,它寄希望于其他线程,如果没有其他线程来重新激活等待的线程,它将永远不再运行了,这将导致令人不快的死锁现象,如果所有其他的线程被阻塞,最后一个活动线程在接触其他线程的阻塞状态之前调用await(),那它也被阻塞,没有任何线程可以解除其他线程的阻塞,那么该程序就挂起啦

signal()会随机解除等待集中某个线程的阻塞状态,这比解除所有线程的阻塞更加有效,但也存在危险,随机选择的线程发现自己仍然不能运行,那么它再次被阻塞,如果没有其他的线程再次调用signal(),那么系统就死锁

警告:一个线程拥有某个条件的锁时,它仅仅可以在该条件上调用await()、signalAll()或signal()


await():将该线程放到条件的等待集中

signalAll():解除该条件的等待集中的所有线程阻塞状态

signal():从该条件的等待集中随机地选择一个线程,解除其阻塞状态


package io.laokou.test.concurrent;
import java.util.Arrays;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author Kou Shenhai
* @version 1.0
* @date 2022/4/19 0019 上午 8:30
*/
public class SyncConditionBankTest {
  private static final int NACCOUNTS = 100;
  private static final double INITIAL_BALANCE = 1000;
  private static final double MAX_AMOUNT = 1000;
  private static final int DELAY = 10;
  public static void main(String[] args) {
    SyncConditionBank bank = new SyncConditionBank(NACCOUNTS,INITIAL_BALANCE);
    for (int i = 0; i < NACCOUNTS; i++) {
      int fromAccount = i;
      Runnable runnable = () -> {
        try {
          while (true) {
            int toAccount = (int)(bank.size() * Math.random());
            double amount = MAX_AMOUNT * Math.random();
            bank.transfer(fromAccount,toAccount, amount);
            Thread.sleep((int)(DELAY * Math.random()));
          }
        } catch (Exception e) {}
      };
      new Thread(runnable).start();
    }
  }
}
class SyncConditionBank {
  private Condition sufficientFunds;
  private final double[] accounts;
  private Lock bankLock = new ReentrantLock();
  public SyncConditionBank(int n,double initialBalance) {
    sufficientFunds = bankLock.newCondition();
    this.accounts = new double[n];
    Arrays.fill(accounts, initialBalance);
  }
  public void transfer(int from,int to,double amount) {
    //获取锁
    bankLock.lock();
    try {
      if (accounts[from] < amount) {
        sufficientFunds.await();
      }
      System.out.print(Thread.currentThread());
      accounts[from] -= amount;
      System.out.printf("%10.2f from %d to %d", amount, from, to);
      accounts[to] += amount;
      System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
      sufficientFunds.signalAll();
    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      //释放锁
      bankLock.unlock();
    }
  }
  public double getTotalBalance() {
      bankLock.lock();
      try {
          double sum = 0;
          for (int i = 0; i < accounts.length; i++) {
              sum += accounts[i];
          }
          return sum;
      } finally {
          bankLock.unlock();
      }
  }
  public int size() {
    return accounts.length;
  }
}
/**
* Thread[Thread-30,5,main]    429.99 from 30 to 70 Total Balance:  100000.00
* Thread[Thread-70,5,main]    723.57 from 70 to 68 Total Balance:  100000.00
* Thread[Thread-40,5,main]    193.84 from 40 to 1 Total Balance:  100000.00
* Thread[Thread-11,5,main]    287.24 from 11 to 69 Total Balance:  100000.00
* Thread[Thread-67,5,main]     88.12 from 67 to 96 Total Balance:  100000.00
* Thread[Thread-16,5,main]    162.57 from 16 to 99 Total Balance:  100000.00
* Thread[Thread-69,5,main]    715.54 from 69 to 53 Total Balance:  100000.00
* Thread[Thread-61,5,main]    543.64 from 61 to 3 Total Balance:  100000.00
* Thread[Thread-76,5,main]    312.71 from 76 to 43 Total Balance:  100000.00
* Thread[Thread-71,5,main]     89.88 from 71 to 41 Total Balance:  100000.00
* Thread[Thread-40,5,main]     53.28 from 40 to 8 Total Balance:  100000.00
 */


11.锁与条件关键之处


锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码

锁可以管理试图进入被保护代码段的线程

锁可以拥有一个或多个相关的条件对象

每个条件对象管理那些已经进入被保护的代码段但还不能运行的


12.synchronized关键字


内部锁常用方法


wait() :等价于 await(),导致线程进入等待状态直到它被通知,该方法只能在一个同步方法中调用,如果当前线程不是对象锁的拥有者,该方法抛出一个IllegalMonitorStateException异常

notify() :等价于 signal(),随机选择一个在该对象上调用wait()的线程,解除其阻塞状态,该方法只能在一个同步方法或同步块中调用,如果当前线程不是对象锁的持有者,该方法抛出一个IllegalMonitorStateException异常

notifyAll(): 等价于 signalAll(),解除那些在该对象上调用wait()的线程的阻塞状态,该方法只能在同步方法或同步块内部调用,如果当前线程不是对象锁的持有者,该方法抛出一个IllegalMonitorStateException异常

补:wait与sleep区别


wait


只能在同步方式中调用

如果当前线程是对象锁的拥有者,则释放锁,然后进入等待状态,直到它被通知

没有时间限制,需要等待其他线程调用notify/notifyAll唤醒等待池的线程,才会进入就绪队列等待操作系统分配资源

同notify/notifyAll不需要捕获异常

sleep


Thread的静态方法,谁调用谁休眠

如果当前线程是对象锁的拥有者,不释放锁,进入线程休眠

可以用指定时间自动唤醒,时间没有到,则调用interrupt强行打断

必须捕获异常

内部锁和条件存在一些局限


不能中断一个正在试图获得锁的线程

试图获得锁时不能设定超时

每个锁仅有单一的条件,可能是不够的

使用Lock和Condition对象?还是同步方法?


最好既不适用Lock/Condition也不使用synchronized关键字,在许多情况下你可以使用java.util.concurrent包中的一种机制,它会为你处理所有的加锁

如果synchronized关键字适合你的程序,那么请尽量使用它,这样可以减少编写的代码数量,减少出错的概率

特别需要Lock/Condition结构提供的独有特性时,才使用Lock/Condition


package io.laokou.test.concurrent;
import java.util.Arrays;
/**
* @author Kou Shenhai
* @version 1.0
* @date 2022/4/19 0019 上午 8:30
*/
public class SynchronizedBankTest {
  private static final int NACCOUNTS = 100;
  private static final double INITIAL_BALANCE = 1000;
  private static final double MAX_AMOUNT = 1000;
  private static final int DELAY = 10;
  public static void main(String[] args) {
    SynchronizedBank bank = new SynchronizedBank(NACCOUNTS,INITIAL_BALANCE);
    for (int i = 0; i < NACCOUNTS; i++) {
      int fromAccount = i;
      Runnable runnable = () -> {
        try {
          while (true) {
            int toAccount = (int)(bank.size() * Math.random());
            double amount = MAX_AMOUNT * Math.random();
            bank.transfer(fromAccount,toAccount, amount);
            Thread.sleep((int)(DELAY * Math.random()));
          }
        } catch (Exception e) {}
      };
      new Thread(runnable).start();
    }
  }
}
class SynchronizedBank {
  private final double[] accounts;
  public SynchronizedBank(int n,double initialBalance) {
    this.accounts = new double[n];
    Arrays.fill(accounts, initialBalance);
  }
  public synchronized void transfer(int from,int to,double amount) throws InterruptedException {
    if (accounts[from] < amount) {
      wait();
    }
    System.out.print(Thread.currentThread());
    accounts[from] -= amount;
    System.out.printf("%10.2f from %d to %d", amount, from, to);
    accounts[to] += amount;
    System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
    notifyAll();
  }
  public synchronized double getTotalBalance() {
    double sum = 0;
    for (int i = 0; i < accounts.length; i++) {
      sum += accounts[i];
    }
    return sum;
  }
  public int size() {
    return accounts.length;
  }
}
/**
* Thread[Thread-30,5,main]    429.99 from 30 to 70 Total Balance:  100000.00
* Thread[Thread-70,5,main]    723.57 from 70 to 68 Total Balance:  100000.00
* Thread[Thread-40,5,main]    193.84 from 40 to 1 Total Balance:  100000.00
* Thread[Thread-11,5,main]    287.24 from 11 to 69 Total Balance:  100000.00
* Thread[Thread-67,5,main]     88.12 from 67 to 96 Total Balance:  100000.00
* Thread[Thread-16,5,main]    162.57 from 16 to 99 Total Balance:  100000.00
* Thread[Thread-69,5,main]    715.54 from 69 to 53 Total Balance:  100000.00
* Thread[Thread-61,5,main]    543.64 from 61 to 3 Total Balance:  100000.00
* Thread[Thread-76,5,main]    312.71 from 76 to 43 Total Balance:  100000.00
* Thread[Thread-71,5,main]     89.88 from 71 to 41 Total Balance:  100000.00
* Thread[Thread-40,5,main]     53.28 from 40 to 8 Total Balance:  100000.00
 */


13.同步阻塞


  • 使用一个对象的锁来实现额外的原子操作,实际上称为客户端锁定


package io.laokou.test.concurrent;
import java.util.Arrays;
/**
* @author Kou Shenhai
* @version 1.0
* @date 2022/4/19 0019 上午 8:30
*/
public class SynchronizedBlockBankTest {
  private static final int NACCOUNTS = 100;
  private static final double INITIAL_BALANCE = 1000;
  private static final double MAX_AMOUNT = 1000;
  private static final int DELAY = 10;
  public static void main(String[] args) {
    SynchronizedBlockBank bank = new SynchronizedBlockBank(NACCOUNTS,INITIAL_BALANCE);
    for (int i = 0; i < NACCOUNTS; i++) {
      int fromAccount = i;
      Runnable runnable = () -> {
        try {
          while (true) {
            int toAccount = (int)(bank.size() * Math.random());
            double amount = MAX_AMOUNT * Math.random();
            bank.transfer(fromAccount,toAccount, amount);
            Thread.sleep((int)(DELAY * Math.random()));
          }
        } catch (Exception e) {}
      };
      new Thread(runnable).start();
    }
  }
}
class SynchronizedBlockBank {
  private final double[] accounts;
  public SynchronizedBlockBank(int n,double initialBalance) {
    this.accounts = new double[n];
    Arrays.fill(accounts, initialBalance);
  }
  public void transfer(int from,int to,double amount) throws InterruptedException {
    synchronized (this) {
      if (accounts[from] < amount) {
        wait();
      }
      System.out.print(Thread.currentThread());
      accounts[from] -= amount;
      System.out.printf("%10.2f from %d to %d", amount, from, to);
      accounts[to] += amount;
      System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
      notifyAll();
    }
  }
  public double getTotalBalance() {
    synchronized (this) {
      double sum = 0;
      for (int i = 0; i < accounts.length; i++) {
        sum += accounts[i];
      }
      return sum;
    }
  }
  public int size() {
    return accounts.length;
  }
}
/**
* Thread[Thread-30,5,main]    429.99 from 30 to 70 Total Balance:  100000.00
* Thread[Thread-70,5,main]    723.57 from 70 to 68 Total Balance:  100000.00
* Thread[Thread-40,5,main]    193.84 from 40 to 1 Total Balance:  100000.00
* Thread[Thread-11,5,main]    287.24 from 11 to 69 Total Balance:  100000.00
* Thread[Thread-67,5,main]     88.12 from 67 to 96 Total Balance:  100000.00
* Thread[Thread-16,5,main]    162.57 from 16 to 99 Total Balance:  100000.00
* Thread[Thread-69,5,main]    715.54 from 69 to 53 Total Balance:  100000.00
* Thread[Thread-61,5,main]    543.64 from 61 to 3 Total Balance:  100000.00
* Thread[Thread-76,5,main]    312.71 from 76 to 43 Total Balance:  100000.00
* Thread[Thread-71,5,main]     89.88 from 71 to 41 Total Balance:  100000.00
* Thread[Thread-40,5,main]     53.28 from 40 to 8 Total Balance:  100000.00
 */


14.监视器


监视器特性


监视器是只包含私有域的类

每个监视器类的对象有一个相关的锁

使用该锁对所有的方法进行加锁,如果客户端调用obj.method(),那么obj对象的锁是在方法调用开始时自动获得,并且当方法返回时自动释放该锁,因为所有的域是私有的,这样的安排可以确保一个线程在对象操作时,没有其他线程能访问该域

该锁可以有任意多个相关条件


java设计者以不是很精准的方式采用了监视器概念,Java中的每一个对象有一个内部的锁和内部的条件,如果一个方法用synchronized关键字声明,那么它表现就像是一个监视器方法,通过调用wait/notify/notifyAll来访问条件变量


java对象不同于监视器,线程安全性下降


域不要求必须是private

方法不要求必须是synchronized

内部锁对客户是可用的


15.volatile域


仅仅为了读写一个或两个实例域就使用同步,开销过大,现代的处理器与编译器,出错的可能性很大


多处理器的计算机能够暂时在寄存器或本地内存缓冲区中保存内存中的值,结果是,运行在不同的处理器上的线程可能在同一个内存位置获取到不同的值

编译器可以改变指令执行的顺序以使吞吐量最大化,这种顺序上的变化不会改变代码语义,但是编译器假定内存的值仅仅在代码中有显示的修改指令时才会改变,然而,内存的值可以被另一个线程改变

注:如果向一个变量写入值,而这个变量接下来可能会被另一个线程读取,或者,从一个变量读值,而这个变量可能是之前被另一个线程写入的,此时必须使用同步


volatile关键字为实例域的同步访问提供了一种免锁机制,如果声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的


假定一个对象有一个变量,它的值被一个线程设置却被另一个线程查询,使用内部锁,如果另一个线程已经对该对象加锁,setValue()和getValue()可能被阻塞


注:volatile是java提供的轻量级的同步机制,保证了可见性,不保证原子性,不能保证读取,翻转和写入不被中断


16.final变量


使用final保证能够安全地访问一个共享域,不使用final,就不能保证其他线程看到的是更新后的值,获取到的值可能是空的


目录
相关文章
|
存储 Linux 调度
Linux系统编程 多线程基础
Linux系统编程 多线程基础
62 1
|
Java API 调度
并发编程系列教程(01) - 多线程基础
并发编程系列教程(01) - 多线程基础
70 0
|
6月前
|
存储 安全 Java
10分钟巩固多线程基础
10分钟巩固多线程基础
|
Java 程序员 调度
多线程(初阶)——多线程基础
多线程(初阶)——多线程基础
88 0
|
Java API 调度
并发编程之多线程基础
每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。
并发编程之多线程基础
|
缓存 安全 Java
6. 多线程基础
对一个程序的运行状态, 以及在运行中所占用的资源(内存, CPU)的描述; 一个进程可以理解为一个程序; 但是反之, 一个程序就是一个进程, 这句话是错的。
92 0
6. 多线程基础
|
安全 Java 编译器
多线程基础(上)
多线程基础(上)
67 0
多线程基础(上)
|
Java 编译器 程序员
多线程基础(下)
多线程基础(下)
103 0
多线程基础(下)
|
SpringCloudAlibaba 安全 前端开发
JUC系列(一) 多线程基础复习
问:如何学习JUC? 答: 源码 + Java帮助文档 面试高频, juc 其实就是 Java.util 包下的线程分类的工具
JUC系列(一) 多线程基础复习
多线程编程之线程扫盲
Java有这么多的线程池, 但是底层原理只有一个。其他都是对其进行的封装。不用死背面试题, 核心知识点很少,一篇文章征服面试官。
137 0