锁对象

用 ReentrantLock 保护代码块的基本结构如下:

 myLock.lock(); // a ReentrantLock object 
try { 
    critical section 
} 
finally {
    myLock.unlock();// make sure the lock is unlocked even if an exception is thrown 
} 

 

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

[警告]  把解锁操作括在 finally 子句之内是至关重要的。如果在临界区的代码抛出异常, 锁必须被释放。否则, 其他线程将永远阻塞。
[注]  : 如果使用锁, 就不能使用带资源的 try语句。首先, 解锁方法名不是 close。不过, 即使将它重命名, 带资源的 try语句也无法正常工作。它的首部希望声明一个新变量。但 是如果使用一个锁, 你可能想使用多个线程共享的那个变量(而不是新变量) 。

让我们使用一个锁来保护 Bank类的 transfer 方法。

public class Bank { 
    private Lock bankLock = new ReentrantLock();// ReentrantLock implements the Lock interface 
    ...
    public void transfer(int from, int to, int amount) {
        bankLock.lock(); 
        try { System.out.print(Thread.currentThread()); 
             accounts[from] -= amount; 
             System.out.printf(" X10.2f from %A to Xd", amount, from, to); 
             accounts[to] += amount; 
             System.out.printf(" Total Balance: X10.2fXn", getTotalBalance());
            }finally{
            banklock.unlock();
        }
    }
} 

 

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

锁是可重入的, 因为线程可以重复地获得已经持有的锁。锁保持一个持有计数(hold count) 来跟踪对 lock 方法的嵌套调用。线程在每一次调用 lock 都要调用 unlock 来释放锁。 由于这一特性, 被一个锁保护的代码可以调用另一个使用相同的锁的方法。

 

条件对象

通常, 线程进人临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对 象来管理那些已经获得了一个锁但是却不能做有用工作的线程。在这一节里, 我们介绍 Java 库中条件对象的实现。(由于历史的原因, 条件对象经常被称为条件变量(conditional variable)。 )

现在来细化银行的模拟程序。我们避免选择没有足够资金的账户作为转出账户。注意不 能使用下面这样的代码:

if (bank.getBalance(fron) >= amount)

bank.transfer(fro«, to, amount);

 

当前线程完全有可能在成功地完成测试,且在调用 transfer 方法之前将被中断。

if (bank.getBalance(from) >= amount) // thread night be deactivated at this point

bank.transfer(from, to, amount); 在线程再次运行前,账户余额可能已经低于提款金额。必须确保没有其他线程在本检査余额 与转账活动之间修改余额。通过使用锁来保护检査与转账动作来做到这一点:

public void transfer(int from, int to,int amount) { 
     bankLock.1ock(); 
     try { 
         while (accounts[from] < amount) { 
             // wait
             ...
         }
         // transfer funds
         ...
     }
     finally { 
         bankLock.unlock(); 
     }
} 

 

现在,当账户中没有足够的余额时, 应该做什么呢? 等待直到另一个线程向账户中注入 了资金。但是,这一线程刚刚获得了对 bankLock 的排它性访问, 因此别的线程没有进行存 款操作的机会。这就是为什么我们需要条件对象的原因。 一个锁对象可以有一个或多个相关的条件对象。你可以用 newCondition 方法获得一个条 件对象。习惯上给每一个条件对象命名为可以反映它所表达的条件的名字。例如,在此设置 一个条件对象来表达“ 余额充足” 条件。

class Bank { 
    private Condition sufficientFunds; 
    public Bank() { 
        sufficientFunds = bankLock.newCondition(); 
    } 
}

 

如果 transfer 方法发现余额不足,它调用

sufficientFunds.await();

当前线程现在被阻塞了,并放弃了锁。我们希望这样可以使得另一个线程可以进行增加 账户余额的操作。

等待获得锁的线程和调用 await 方法的线程存在本质上的不同。一旦一个线程调用 await 方法,它进人该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞 状态,直到另一个线程调用同一条件上的 signalAll 方法时为止。 当另一个线程转账时, 它应该调用

sufficientFunds,signalAll();

[注]  通常, 对 await 的调用应该在如下形式的循环体中 while (I(ok toproceed)) condition.await();
[警]  当一个线程拥有某个条件的锁时, 它仅仅可以在该条件上调用 await、signalAll 或 signal 方法。

实际上, 正确地使用条件是富有挑战性的。在开始实现自己的条件对象之前, 应该考虑 使用 14.10 节中描述的结构。

//程序清单 14-7 synch/Bank.java
package synch;
​
import java.util.concurrent.locks.*;
​
/**
 * A bank with a number of bank accounts that uses locks for serializing access.
 * @version 1.30 2004-08-01
 * @author Cay Horstmann
 */
public class Bank
{
   private final double[] accounts;
   private Lock bankLock;
   private Condition sufficientFunds;
​
   /**
    * Constructs the bank.
    * @param n the number of accounts
    * @param initialBalance the initial balance for each account
    */
   public Bank(int n, double initialBalance)
   {
      accounts = new double[n];
      for (int i = 0; i < accounts.length; i++)
         accounts[i] = initialBalance;
      bankLock = new ReentrantLock();
      sufficientFunds = bankLock.newCondition();
   }
​
   /**
    * Transfers money from one account to another.
    * @param from the account to transfer from
    * @param to the account to transfer to
    * @param amount the amount to transfer
    */
   public void transfer(int from, int to, double amount) throws InterruptedException
   {
      bankLock.lock();
      try
      {
         while (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();
      }
      finally
      {
         bankLock.unlock();
      }
   }
​
   /**
    * Gets the sum of all account balances.
    * @return the total balance
    */
   public double getTotalBalance()
   {
      bankLock.lock();
      try
      {
         double sum = 0;
​
         for (double a : accounts)
            sum += a;
​
         return sum;
      }
      finally
      {
         bankLock.unlock();
      }
   }
​
   /**
    * Gets the number of accounts in the bank.
    * @return the number of accounts
    */
   public int size()
   {
      return accounts.length;
   }
}

 


synchronized 关键字

线程同步synchronized

-使用synchronized关键字可以实现线程间的同步。

-lsynchronized可以同步

–实例方法

–类方法

–代码块

同步实例方法

使用synchronized修饰实例方法

语法:

synchronized <返回类型> <函数名>()

使用synchronized修饰实例方法的效果

同一个对象的该实例方法,在同一时刻只能被一个线程执行(举例计数)。

比如:线程A正在执行对象a的该实例方法,此时线程B如果也要运行对象a的该实例方法就必须等待,但是,如果线程B是要运行对象b的该实例方法,任然可以执行。

 

同步类方法

使用synchronized修饰类方法

语法:

static synchronized <返回类型> <函数名>()

使用synchronized修饰类方法的效果

该类方法,在同一时刻只能被一个线程执行(举例计数)。

 

同步代码块

可以让一个方法中部分代码被同步,多个线程不能同时访问代码块中,会阻塞等待。

语法:

synchronized (Object的对象) {代码块}

该使用方法和互斥锁很相似,当某一个线程执行synchronized (Object)时,就获取了Object对象的锁,l在代码块被执行完毕之前,其他线程都无法获取该Object对象的锁,因此,会被阻塞在代码块之外。

package com.ice.test;
​
public class MyThread extends Thread {
​
    private Entry entry;
    private String name;
​
    // 构造器:初始化成员变量
    public MyThread(Entry entry, String name) {
        this.entry = entry;
        this.name = name;
    }
​
    @Override
    public void run() {// 线程执行的方法
        super.run();
​
        // 通过对象调用方法
        entry.show(name);
​
    }
}
​
class Entry {
    // 线程同步实例方法:只对多个线程中使用同一个对象调用此方法时生效,如果使用的是不同对象,那么同步失效
    // //实例方法
    // public synchronized void show(String name) {//变量表示线程名称
    // for (int i = 0; i < 10; i++) {
    // try {
    // Thread.sleep(1000);
    //
    // System.out.println(name + "---" + i);
    // } catch (InterruptedException e) {
    // e.printStackTrace();
    // }
    // }
    // }
//  //线程同步类方法:在多个线程中无论使用同一个对象还是不同对象来调用此方法,同步都生效
//  // 类方法
//  public synchronized static void show(String name) {// 变量表示线程名称
//      for (int i = 0; i < 10; i++) {
//          try {
//              Thread.sleep(1000);
//
//              System.out.println(name + "---" + i);
//          } catch (InterruptedException e) {
//              e.printStackTrace();
//          }
//      }
//  }
    
    
    public void show(String name) {// 变量表示线程名称
        //线程同步代码块:在多个线程中使用同一个对象调用此方法的代码块时,同步才生效,使用不同对象时,同步失效
        synchronized (this) {
            
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(1000);
                    
                    System.out.println(name + "---" + i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
​
}
​

 

package com.ice.test;
​
public class Test {
​
    /**
     * @param args
     */
    public static void main(String[] args) {
    
        Entry entry = new Entry();
        
        //开启子线程1
        MyThread mt1 = new MyThread(entry, "线程1");
        mt1.start();
        
        //开启子线程2
        MyThread mt2 = new MyThread(entry, "线程2");
        mt2.start();
        
​
    }
​
}

 


在前面一节中, 介绍了如何使用 Lock 和 Condition对象。在进一步深人之前,总结一下 有关锁和条件的关键之处:

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

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

•锁可以拥有一个或多个相关的条件对象。 •每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。

换句话说,

public synchronized void method() { 
    method body 
}

 


等价于
    public void methodQ { 
    this.intrinsidock.1ock(); 
    try { 
        method body 
    } finally { 
        this.intrinsicLock.unlock(); 
    }
}

 

 

例如, 可以简单地声明 Bank类的 transfer方法为synchronized, 而不是使用一个显式的锁。

内部对象锁只有一个相关条件。wait 方法添加一个线程到等待集中,notifyAU /notify方 法解除等待线程的阻塞状态。换句话说,调用 wait 或 notityAll 等价于

intrinsicCondition.await();

intrinsicCondition.signalAll();

 

[注]  wait、notifyAll 以及 notify 方法是 Object 类的 final 方法。Condition 方法必须被命 名为 await、signalAll 和 signal 以便它们不会与那些方法发生冲突。

例如,可以用 Java 实现 Bank 类如下:

 class Bank { 
     private double[] accounts; 
     public synchronized void transfer(int from,int to, int amount) throws InterruptedException {
         while (accounts[from] < amount) 
             wait(); // wait on intrinsic object lock's single condition
         accounts[from] -= amount; accounts[to] += amount; notifyAllO;// notify all threads waiting on the condition
     }
     public synchronized double getTotalBalance() { 
    . . . 
     }
}

 

可以看到, 使用 synchronized关键字来编写代码要简洁得多。当然,要理解这一代码,你 必须了解每一个对象有一个内部锁, 并且该锁有一个内部条件。由锁来管理那些试图进入 synchronized 方法的线程,由条件来管理那些调用 wait 的线程。

内部锁和条件存在一些局限。包括:

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

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

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

在代码中应该使用哪一种? Lock 和 Condition 对象还是同步方法?下面是一些建议:

•最好既不使用 Lock/Condition 也不使用 synchronized 关键字。在许多情况下你可以使 用java.util.concurrent 包中的一种机制,它会为你处理所有的加锁。例如, 在 14.6 节, 你会看到如何使用阻塞队列来同步完成一个共同任务的线程。还应当研究一下并行 流,有关内容参见卷n第 1 章。

•如果 synchronized 关键字适合你的程序,那么请尽量使用它,这样可以减少编写的代 码数量,减少出错的几率。程序清单 14-8 给出了用同步方法实现的银行实例。

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

//程序清单 14-8  synch2/Bank
package synch2;
​
/**
 * A bank with a number of bank accounts that uses synchronization primitives.
 * @version 1.30 2004-08-01
 * @author Cay Horstmann
 */
public class Bank
{
   private final double[] accounts;
​
   /**
    * Constructs the bank.
    * @param n the number of accounts
    * @param initialBalance the initial balance for each account
    */
   public Bank(int n, double initialBalance)
   {
      accounts = new double[n];
      for (int i = 0; i < accounts.length; i++)
         accounts[i] = initialBalance;
   }
​
   /**
    * Transfers money from one account to another.
    * @param from the account to transfer from
    * @param to the account to transfer to
    * @param amount the amount to transfer
    */
   public synchronized void transfer(int from, int to, double amount) throws InterruptedException
   {
      while (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();
   }
​
   /**
    * Gets the sum of all account balances.
    * @return the total balance
    */
   public synchronized double getTotalBalance()
   {
      double sum = 0;
​
      for (double a : accounts)
         sum += a;
​
      return sum;
   }
​
   /**
    * Gets the number of accounts in the bank.
    * @return the number of accounts
    */
   public int size()
   {
      return accounts.length;
   }
}
​

 

 

wait与notify

wait()、notify()、notifyAll()

定义在Object类里的方法,可以用来控制线程的状态。

这三个方法最终调用的都是jvm级的native方法。随着jvm运行平台的不同可能有些许差异。

 

wait方法

–如果对象调用了wait方法就会使持有该对象的线程把该对象的控制权交出去,然后处于等待状态。

lwait方法的重载

–wait():一直等待,直到其他线程调用该对象的notify或者notifyAll方法。

–wait(long):等待,直到其他线程调用该对象的notify或者notifyAll方法。或者超时

–wait(long,int):等待,直到其他线程调用该对象的notify或者notifyAll方法。或者超时,参数2是纳秒计时

 

notify方法

如果对象调用了notify方法就会通知某个正在等待这个对象的控制权的线程可以继续运行。

如果对象调用了notifyAll方法就会通知所有等待这个对象控制权的线程继续运行

 

适用场景:多线程应用中一个线程负责图片的显示,一个线程负责图片的下载,需要保证图片下载完成之后再进行显示。

演示Wait和notify的基本用法(一个线程wait,一个线程notify)

 

生产者与消费者

实现生产者与消费者模式

–很多后台服务程序并发控制的基本原理都可以归纳为生产者/消费者模式

–生产者消费者问题是研究多线程程序时绕不开的经典问题之一,它描述是有一块缓冲区作为仓库,生产者可以将产品放入仓库,消费者则可以从仓库中取走产品。

 

wait与notify可以实现生产者和消费者模式

–wait()方法:当缓冲区已满/空时,生产者/消费者线程停止自己的执行,放弃锁,使自己处于等等状态,让其他线程执行。

–notify()方法:当生产者/消费者向缓冲区放入/取出一个产品时,向其他等待的线程发出可执行的通知。

 

同步阻塞

正如刚刚讨论的,每一个 Java 对象有一个锁。线程可以通过调用同步方法获得锁。还有 另一种机制可以获得锁,通过进入一个同步阻塞。当线程进入如下形式的阻塞:

 synchronized (obj) // this is the syntax for a synchronized block 
 { 
     critical section 
 } 

 

于是它获得 Obj 的锁。 有时会发现“ 特殊的” 锁,例如:

public class Bank { 
    private double[] accounts; 
    private Object lock = new Object() ; 
    public void transfer(int from, int to, int amount) { 
        synchronized (lock) // an ad-hoc lock 
        { 
            accounts[from] -= amount; 
            accounts[to] += amount; 
        } 
        System.out.print1n(.. .); 
    } 
}

 

在此,lock 对象被创建仅仅是用来使用每个 Java 对象持有的锁。

 

监视器概念

锁和条件是线程同步的强大工具,但是,严格地讲,它们不是面向对象的。多年来,研 究人员努力寻找一种方法,可以在不需要程序员考虑如何加锁的情况下,就可以保证多线程 的安全性。最成功的解决方案之一是监视器(monitor), 这一概念最早是由 PerBrinchHansen 和 TonyHoare 在 20世纪 70 年代提出的。用 Java 的术语来讲,监视器具有如下特性:

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

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

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

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

 

监视器的早期版本只有单一的条件, 使用一种很优雅的句法。可以简单地调用 await accounts[from] >= balance 而不使用任何显式的条件变量。然而,研究表明盲目地重新测试条 件是低效的。显式的条件变量解决了这一问题。每一个条件变量管理一个独立的线程集。 Java设计者以不是很精确的方式采用了监视器概念, Java 中的每一个对象有一个内部的 锁和内部的条件。如果一个方法用 synchronized 关键字声明,那么,它表现的就像是一个监 视器方法。通过调用 wait/notifyAU/notify 来访问条件变量。 然而, 在下述的 3 个方面 Java 对象不同于监视器, 从而使得线程的安全性下降:

•域不要求必须是 private。

•方法不要求必须是 synchronized。

•内部锁对客户是可用的。

 

Volatile 域

有时,仅仅为了读写一个或两个实例域就使用同步, 显得开销过大了。毕竟,什么地方 能出错呢? 遗憾的是, 使用现代的处理器与编译器, 出错的可能性很大。

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

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

 

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

例如, 假定一个对象有一个布尔标记 done, 它的值被一个线程设置却被另一个线程査 询,如同我们讨论过的那样,你可以使用锁: private boolean done;

public synchronized boolean isDone(){ return done; }

public synchronized void setDone() { done = true; }

 

或许使用内部锁不是个好主意。如果另一个线程已经对该对象加锁,isDone 和 setDone 方法可能阻塞。如果注意到这个方面, 一个线程可以为这一变量使用独立的 Lock。但是,这 也会带来许多麻烦C 在这种情况下,将域声明为 volatile 是合理的:

private volatile boolean done;

public boolean isDone() { return done; }

public void setDone() { done = true; }

 

[警告]  : Volatile 变量不能提供原子性。例如, 方法 public void flipDone() { done = !done; } // not atomic 不能确保翻转域中的值。不能保证读取、翻转和写入不被中断。

final 变置

上一节已经了解到, 除非使用锁或 volatile 修饰符,否则无法从多个线程安全地读取一 个域。

还有一种情况可以安全地访问一个共享域, 即这个域声明为 final 时。考虑以下声明:

final Map<String, Double〉accounts = new HashKap<>();

其他线程会在构造函数完成构造之后才看到这个 accounts 变量。

如果不使用 final,就不能保证其他线程看到的是 accounts 更新后的值,它们可能都只是 看到 null, 而不是新构造的 HashMap。

当然,对这个映射表的操作并不是线程安全的。如果多个线程在读写这个映射表,仍然 需要进行同步。

死锁

在使用wait,notify时,两个或两个以上的线程为了使用某个对象而无限制地等待下去。

死锁出现的最基本原因还是逻辑处理不够严谨,所以一般需要修改程序逻辑才能很好的解决死锁问题

 

 

线程池

线程池的存在价值

–在程序的运行过程中,如果每次需要启动一个子线程都创建一个,自然是最简单的,但是问题在于

•频繁的创建与销毁线程消耗系统资源,也消耗时间

•并行的线程过多也会造成系统资源的匮乏

–那么问题来了!如果我们能将创建的子线程进行复用就好了,如果之前创建的子线程处于空闲状态,就用来执行新的线程任务!(这是核心思想)

 

核心类(ThreadPoolExecutor)

工作机制

–场景描述:公司有一个小组完成上级分发的任务,小组成立时就拥有了核心成员,当上级分发任务时,核心成员优先处理任务,该小组还有一个任务列表,如果核心成员目前都没有空,就会将上级的任务先保存到任务列表中,如果谁空了就谁来执行。但是这个任务列表也不是无限大的(领导不可能一直等着你完成所有任务),因此,如果任务列表中的任务达到了阀值,会请几个外包人员参与工作,但是这个外包人员的数量也是有限定的,不能随便聘用。因此,如果当前每一个核心员工和外聘员工都有任务在做,外聘人员也不能再聘用了,同时,任务列表页满了,那么如果再有任务来,就需要舍弃这任务。

 

public ThreadPoolExecutor(

int corePoolSize,

int maximumPoolSize,

long keepAliveTime,

TimeUnit unit,

BlockingQueue<Runnable> workQueue,

RejectedExecutionHandler handler)

 

参数1:线程池中核心线程数,可以理解为小组中核心员工的数量

参数2:线程池的最大数量,可以理解为小组中员工的最大值,其中包括核心员工和外包员工

参数3:表示当当前线程数大于参数1时,如果线程空闲时间超过该值,将被销毁,可以理解为如果外聘成员空闲多久,就解雇掉。

参数4:参数3的单位,可以为TimeUnit.SECONDS等

参数5:用来装载即将被执行的任务的队列,我们可以理解为任务列表,知道当列表都满了,才会外聘,一般用ArrayBlockingQueue<Runnable>

参数6:如果当前执行线程数达到了参数2,并且,参数5的队列也满了,由参数6来处理新来的任务(拒绝),可以为:ThreadPoolExecutor.AbortPolicy()抛出异常,ThreadPoolExecutor.CallerRunsPolicy() 重新执行当前任务,ThreadPoolExecutor.DiscardOldestPolicy() 抛弃最早的任务,ThreadPoolExecutor.DiscardPolicy() 抛弃当前任务

 

核心函数

–execute(Runnable command):发起任务

–getPoolSize():获取当前存在的线程个数(核心、外聘)

–shutDown():提交的任务会被执行完毕,新的任务不会再被接收了。

–shutdownNow:立刻结束所有任务

 

代码演示使用线程池创建线程

代码演示在核心线程数达到指定值时,会将任务添加到队列

代码演示在队列添加满了之后,会启动新的子线程帮助完成任务

代码演示如果当前线程总数达到最大值(核心线程+外聘线程=最大线程),并且此时任务列表也满了,新任务将被丢弃

posted on 2020-08-20 22:23  ♌南墙  阅读(459)  评论(0)    收藏  举报