十一、多线程

多线程与多进程的区别:
本质的区别在于每个进程拥有自己的一整套变量,而线程则共享数据。共享变量使线程之间的通信比进程之间的通信更有效、更容易。此外,在有些操作系统中,与进程相比较,线程更“轻量级”,创建、撤销一个线程比启动新进程的开销要小得多

1、什么是线程

下面是在一个单独的线程中执行一个任务的简单过程:

  1. 将任务代码移到实现了Runnable接口的类的run方法中:
public interface Runnable{
	run();
}

实现一个类

class MyRunnable implements Runnable{
	public void run(){
		task code...
	}
}
  1. 创建一个对象:Runnable r = new MyRunnable();
  2. 由Runnable创建一个Thread对象:Thread t = new Thread(r);
  3. 启动线程:t.start();

Thread类的静态sleep方法将暂停给定的毫秒数。
调用Thread.sleep不会创建一个新线程,sleep是Thread类的静态方法,用于暂停当前线程的活动。

2、中断线程

当线程的run方法执行方法体中最后一条语句后,并经由执行return语句返回时,或者出现了在方法中没有捕获的异常时,线程将终止。
有一种可以强制线程终止的方法。然而,interrupt方法可以用来请求终止线程。
当对一个线程调用interrupt方法时,线程的中断状态将被置位。这是每一个线程都具有的boolean标志。每个线程都应该不时地检查这个标志,以判断线程是否被中断。

要想弄清中断状态是否被置位,首先调用静态的Thread.currentThread方法获得当前线程,然后调用isInterrupted method方法:

while (!Thread.currentThread().isInterrupted() && ...){
	code...
}

但是,如果线程被阻塞,就无法检测中断状态。当一个被阻塞的线程(调用sleep或wait)上调用interrupt方法时,阻塞调用将会被InterruptedException异常中断。

如果在每次工作迭代之后都调用sleep方法(或者其他的可中断方法),isInterrupted检测既没有必要也没有用处。如果在中断状态被置位时调用sleep方法,它不会休眠。相反,它将清除这一状态并抛出InterruptedException。因此,如果你的循环调用sleep,不会检测中断状态。

interrupted和isInterrupted的区别:
interrupted方法是一个静态方法,它检测当前的线程是否被中断。而且,调用interrupted方法会清除该线程的中断状态。另一方面,isInterrupted方法是一个实例方法,可用来检验是否有线程被中断。调用这个方法不会改变中断状态。

3、线程状态

线程可以有如下6中状态:

  • New(新创建)
  • Runnable(可运行)
  • Blocked(被阻塞)
  • Waiting(等待)
  • Timed waiting(计时等待)
  • Terminated(被终止)
3.1 新创建线程

当用new操作符创建一个新线程时,如new Thread(r),该线程还没有开始运行。这意味着它的状态是new。

3.2 可运行线程

一旦调用start方法,线程处于runnable状态。一个可运行的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间。

一旦一个线程开始运行,它不必始终保持运行。事实上,运行中的线程被中断,目的是为了让其他线程获得运行机会。线程调度的细节依赖于操作系统提供的服务。抢占式调度系统给每一个可运行线程一个时间片来执行任务。当时间片用完,操作系统剥夺该线程的运行权。并给另一个线程运行机会。当选择下一个线程时,操作系统考虑线程的优先级。

现在所有的桌面及服务器操作系统都使用抢占式调度。但是,像手机这样的小型设备可能使用协作式调度。在这样的设备中,一个线程只有在调用yield方法、或者被阻塞或等待时,线程才失去控制权。

在具有多个处理器的机器上,每一个处理器运行一个线程,可以有多个线程并行运行。如果线程的数目多余处理器的数目,调度器依然采用时间片机制。

3.3 被阻塞线程和等待线程

当线程处于被阻塞或等待状态时,它暂时不活动。它不运行任何代码且消耗最少的资源。知道线程调度器重新激活它。

  • 当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态。当所有其他县城释放该锁,并且线程调度器允许本线程持有它的时候,该线程变成非阻塞状态。
  • 当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。在调用Object.wait方法或Threa.join方法,或者是等待java.util.concurrent库中的Lock或Condition时,就会出现这种情况。
  • 有几个方法有一个超时参数。调用他们导致线程进入计时等待状态。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有Thread.sleep和Object.wait、Thread.join、Lock.tryLock以及Conditong.await的计时版。
3.4 被终止的线程

线程因如下两个原因之一而被终止:

  • 因为run方法正常退出而自热死亡;
  • 因为一个没有捕获的异常终止了run方法二意外死亡。

线程状态:
线程状态

4、线程属性

4.1 线程优先级

在Java程序设计语言中,每一个线程有一个优先级。默认情况下,一个线程继承它的父线程的优先级。可以用setPriority方法提高或降低任何一个线程的优先级。可以将优先级设置为MIN_PRIORITY (在Thread类中定义为1)与MAX_PRIORITY(定义为0)之间的任何值。NORM_PRIORITY被定义为5。

当线程调度器有机会选择新线程时,它首先选择具有较高优先级的线程。但是,线程优先级是高度依赖于系统的。当虚拟机依赖于宿主机平台的线程实现机制时,Java线程的优先级被映射到宿主机平台的优先级上,优先级个数也许更多,也许更少。这就可能出现所有线程具有相同优先级,造成线程的优先级被忽略的情况。

4.2 守护线程

可以通过调用 t.setDaemon(true); 将线程转换为守护线程。守护线程的唯一用途是为其他线程提供服务。当只剩下守护线程时,虚拟机就推出了,由于如果只剩下守护线程,就没必要继续运行程序了。

守护线程应该永远不去访问固有资源,如文件、数据库、因为它会在任何时候甚至在一个操作的中间发生中断。

4.3 未捕获异常处理器

线程的run方法不能爬出任何被检测的异常,但是,不被检测的异常会导致线程终止。在这种情况下,线程就死亡了。
但是,不需要任何catch子句来处理可以被传播的异常。相反,就在线程死亡之前,异常被传递到一个用于未捕获异常的处理器。
该处理器必须使用一个实现Thread.UncaughtExceptionHandler接口的类。这个接口只有一个方法:void uncaughtException(Thread t, Throwable e)
可以用setUncaughException方法为任何线程安装一个处理器。也可以用Thread类的静态方法setDefaultUncaughException为所有线程安装一个默认的处理器。替换处理器可以使用日志API发送未捕获异常的报告到日志文件。
如果不安装默认的处理器,默认的处理器为空。但是,如果不为独立的线程安装处理器,此时的处理器就是该线程的ThreadGroup对象。

线程组是一个可以统一管理的线程集合。默认情况下,创建的所有线程属于相同的线程组,但是,也可能会建立其他的组。现在引入了更好的特性用语线程集合的操作,所以建议不要在自己的程序中使用线程组。

ThreadGroup类实现 Thread.UncaughtExceptionHandler 接口。他的uncaughtException方法做如下操作:

  1. 如果该线程组有父线程组,那么父线程组的uncaughtException方法被调用。
  2. 否则,如果Thread.getDefaultException 方法返回一个非空的处理器,则调用该处理器。
  3. 否则,如果Throwable是ThreadDeath的一个实例,什么都不做。
  4. 否则,线程的名字以及Throwable的栈轨迹被输出到System.err上。

5、同步

5.1 竞争条件的一个例子

示例代码:银行转账的例子

package com.java01.day10.unsynch;

/**
 * @author: ju
 * @date: 2020-05-21 10:08
 */
public class Bank {
    //银行账户集合
    private final double[] accounts;

    /**
     * 构造
     * @param n 银行账户总人数
     * @param initialBalance 每个人账户金额
     */
    public Bank(int n , double initialBalance) {
        accounts = new double[n];
        for (int i=0; i<accounts.length; i++){
            accounts[i] = initialBalance;
        }
    }

    /**
     * 转账方法
     * @param from 转出的账户
     * @param to 转入的账户
     * @param amount 转转金额
     */
    public void transfer(int from, int to, double amount){
        //判断转出方金额是否足够
        if (accounts[from] < amount){
            return;
        }
        //输出当前线程
        System.out.print(Thread.currentThread());
        accounts[from] = accounts[from] - amount;
        accounts[to] = accounts[to] + amount;
        System.out.println("---" + amount + "    from " + from + "  to " + to + "---" + "银行所有账户总金额:" + getTotalBalance());
    }

    /**
     * 获取银行所有账户总金额
     */
    public double getTotalBalance(){
        double sum = 0;
        for (double account : accounts){
            sum = sum + account;
        }
        return sum;
    }

    /**
     * 获取银行账户总人数
     */
    public int size(){
        return accounts.length;
    }
}

package com.java01.day10.unsynch;

/**
 * @author: ju
 * @date: 2020-05-21 10:21
 */
public class TransferRunnable implements Runnable {
    private Bank bank;
    private int fromAccount;
    private double maxAmount;
    private int DELAY = 10;

    public TransferRunnable(Bank bank, int fromAccount, double maxAmount) {
        this.bank = bank;
        this.fromAccount = fromAccount;
        this.maxAmount = maxAmount;
    }

    @Override
    public void run() {
        while (true){
            //随机获取转入账户
            int toAccount = (int) (bank.size() * Math.random());
            //获取随机转入金额
            double amount = maxAmount * Math.random();
            //转账操作
            bank.transfer(fromAccount, toAccount, amount);
            try {
                Thread.sleep((int) (DELAY * Math.random()));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

package com.java01.day10.unsynch;

/**
 * @author: ju
 * @date: 2020-05-21 10:09
 */
public class UnSynchBankTest {
    //定义银行总账户100
    private static final int NACCOUNTS = 100;
    //每个账户有1000元
    private static final double INITIAL_BALANCE = 1000;

    public static void main(String[] args){
        Bank bank = new Bank(NACCOUNTS, INITIAL_BALANCE);
        for (int i = 0; i< NACCOUNTS; i++){
            TransferRunnable r = new TransferRunnable(bank, i, INITIAL_BALANCE);
            Thread t = new Thread(r);
            t.start();
        }

    }
}

运行结果:
在这里插入图片描述
可以看到,因为共100个账户,每个账户1000元,所以银行总额应该保持在100000,但是我们发现程序运行都后面银行金额还剩99376,发生了错误。

5.2 竞争条件详解

假定两个线程同时执行指令accounts[to] = accounts[to] + amount;
问题在于这不是原子操作。该指定可能被处理如下:

  1. 将accounts[to] 加载到寄存区
  2. 增加amount。
  3. 将结果写回accounts[to]。

现在,假定第一个线程执行步骤1和2,然后它被剥夺了运行权。假定第2个线程被唤醒并修改了accounts数组中的同一项。然后,第一个线程被唤醒并完成其第3步。这样,这一动作擦去了第二个线程所做的更新。于是,总金额不再正确。
在这里插入图片描述

5.3 锁对象

有两种机制防止代码块受并发访问的干扰。JAVA语言提供一个synchronized关键字达到这一目的,并且Java SE 5.0 引入了ReentrantLock类。synchronized关键字自动提供一个锁以及相关的“条件”,对于大多数需要显式锁的情况,这是很便利的。

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

myLock.lock();
try{
	...
}finally{
	myLock.unlock();
}

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

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

如果使用锁,就不能使用带资源的try语句。首先,解锁方法名不是close。不过,即使将它重命名,带资源的try语句也无法正常工作。它的首部希望声明一个新变量。但是如果使用一个锁,你可能想使用多个线程共享的那个变量(而不是新变量)

使用锁来保护Bank类的transfer方法:

package com.java01.day10.unsynch;

/**
 * @author: ju
 * @date: 2020-05-21 10:08
 */
public class Bank {
	//声明一个锁对象
    private Lock bankLock = new ReentrantLock();
    ...
    
    public void transfer(int from, int to, double amount){
		bankLock.lock();
        //输出当前线程
        try{
        	System.out.print(Thread.currentThread());
        	accounts[from] = accounts[from] - amount;
        	accounts[to] = accounts[to] + amount;
        	System.out.println("---" + amount + "    from " + from + "  to " + to + "---" + "银行所有账户总金额:" + getTotalBalance());
        }finally{
        	bankLock.unlock();
        }
    }
}

注意每一个Bank对象有自己的ReetrantLock对象。如果两个线程试图访问同一个Bank对象,那么锁以串行方式提供服务。但是,如果两个线程访问不同的Bank对象,每一个线程得到不同的锁对象,两个线程不会发生阻塞。

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

通常,可能想要保护需若干个操作来更新或检查共享对象的代码块。要确保这些操作完成后,另一个线程才能使用相同对象。

5.4 条件对象

一个锁对象可以有一个或多个相关的条件对象。你可以用newCondition方法获得一个条件对象。例如,设置一个条件对象来表达“余额充足”条件:

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

如果transfer方法发现余额不足,它调用sufficientFunds.await();
当前线程现在被阻塞了,并放弃了锁。我们希望这样可以使得另一个线程可以进行增加账户余额的操作。

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

当另一个线程转账时,它应该调用sufficientFunds.signalAll();
这一调用重新激活因为这一条件而等待的所有线程。当这些线程从等待集当中移出时,他们再次成为可运行,调度器将再次激活他们。同时,他们将试图重新进入该对象。一旦锁成为可用的,它们中的某个将从await调用返回,获得该锁并从被阻塞的地方继续执行。

通常对await的调用应该在如下形式的循环体中:

while(!ok){
	condition.await();
}

至关重要的是最终需要某个其他线程调用signalAll方法。当一个线程调用await时,它没有办法重新激活自身。它寄希望于其他线程。如果没有其他线程来重新激活等待的线程,它就永远不再运行了。这将导致死锁现象。

应该何时调用signalAll呢?在对象的状态有利于等待线程的方向改变时调用signalAll。

    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] = accounts[from] - amount;
            accounts[to] = accounts[to] + amount;
            System.out.println("---" + amount + "    from " + from + "  to " + to + "---" + "银行所有账户总金额:" + getTotalBalance());
        } finally {
            bankLock.unlock();
        }
    }

注意调用signalAll不会立即激活一个等待线程。它仅仅解除等待线程的阻塞,以便这些线程可以在当前线程退出同步方法之后,通过竞争实现对对象的访问。

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

当一个线程拥有某个条件的锁时,它仅仅可以在该条件上调用await、signalAll或signal方法。

package com.java01.day10.synch;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author: ju
 * @date: 2020-05-21 10:08
 */
public class Bank {
    private final double[] accounts;
    private Lock bankLock;
    private Condition sufficientFunds;

    /**
     * 构造
     * @param n 银行账户总人数
     * @param initialBalance 每个人账户金额
     */
    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();
    }

    /**
     * 转账方法
     * @param from 转出的账户
     * @param to 转入的账户
     * @param amount 转转金额
     */
    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] = accounts[from] - amount;
            accounts[to] = accounts[to] + amount;
            System.out.println("---" + amount + "    from " + from + "  to " + to + "---" + "银行所有账户总金额:" + getTotalBalance());
        } finally {
            bankLock.unlock();
        }
    }

    /**
     * 获取银行所有账户总金额
     */
    public double getTotalBalance(){
        bankLock.lock();
        try {
            double sum = 0;
            for (double account : accounts){
                sum = sum + account;
            }
            return sum;
        } finally {
            bankLock.unlock();
        }
    }

    /**
     * 获取银行账户总人数
     */
    public int size(){
        return accounts.length;
    }
}

5.5 synchronized关键字

有关锁和条件的关键之处:

  • 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
  • 锁可以管理试图进入被保护代码段的线程
  • 锁可以拥有一个或多个相关的条件对象。
  • 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。

Lock和Condition接口为程序设计人员提供了高度的锁定控制。然而,大多数情况下,并不需要那样的控制,并且可以使用一种嵌入到Java语言内部的机制。从1.0版开始,Java中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。
换句话说:

public synchronized void method(){
	...
}

等价于

public void method(){
	this.intrinsicLock.lock();
	try{
		...
	}finally{
		this.intrinsicLock.unlock();
	}
}

可以简单地声明Bank类的transfer方法为synchronized,而不是使用一个显式的锁。
内部对象锁只有一个相关条件。wait方法添加一个线程到等待集中,notifyAll/notify方法解除等待线程的阻塞状态。换句话说,调用wait或notifyAll等价于:

intrinsicCondition.await();
intrinsicCondition.signalAll();

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

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

  • 不能中断一个正在试图获得锁的线程。
  • 试图获得锁时不能设定超时。
  • 每个锁仅有单一的条件,可能是不够的。

在代码中应该使用哪一种?

  • 最好既不使用Lock/Condition也不使用synchronized关键字。在多数情况下可以使用java.util.concurrent包中的一种机制,来处理加锁。
  • 如果synchronized关键字适合你的程序,那么尽量使用它。
  • 如果特别需要Lock/Condition结构提供的独有特性时,才使用Lock/Condition。
package com.java01.day10.synch2;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author: ju
 * @date: 2020-05-21 10:08
 */
public class Bank {
    private final double[] accounts;

    /**
     * 构造
     *
     * @param n              银行账户总人数
     * @param initialBalance 每个人账户金额
     */
    public Bank(int n, double initialBalance) {
        accounts = new double[n];
        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = initialBalance;
        }
    }

    /**
     * 转账方法
     *
     * @param from   转出的账户
     * @param to     转入的账户
     * @param amount 转转金额
     */
    public synchronized void transfer(int from, int to, double amount) throws InterruptedException {
        while (accounts[from] < amount) {
            wait();
        }
        System.out.print(Thread.currentThread());
        accounts[from] = accounts[from] - amount;
        accounts[to] = accounts[to] + amount;
        System.out.println("---" + amount + "    from " + from + "  to " + to + "---" + "银行所有账户总金额:" + getTotalBalance());
    }

    /**
     * 获取银行所有账户总金额
     */
    public synchronized double getTotalBalance() {
        double sum = 0;
        for (double account : accounts) {
            sum = sum + account;
        }
        return sum;

    }

    /**
     * 获取银行账户总人数
     */
    public int size() {
        return accounts.length;
    }
}

5.6 同步阻塞

每一个Java对象有一个锁。线程可以通过调用同步方法获得锁。还有另一种机制可以获得锁,通过进入一个同步阻塞。

synchronized(obj){
	...
}

于是它获得obj的锁。

有时会发现“特殊的”锁,例如:

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

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

有时程序员使用一个对象的锁来实现额外的原子操作,实际上成为客户端锁定。客户端锁定是非常脆弱的,通常不推荐使用。

5.7 监视器概念

可以使用监视器,在不需要考虑如何加锁的情况下,保证多线程的安全性。
监视器具有如下特性:

  • 监视器是只包含私有域的类。
  • 每个监视器类的对象有一个相关的锁。
  • 使用该锁对所有的方法进行加锁。换句话说,如果客户端调用obj.method(),那么obj对象的锁是在方法调用开始时自动获得,并且当方法返回时自动释放该锁。
  • 该锁可以有任意多个相关条件。

如果一个方法用synchronized关键字声明,那么,它表现的就像是一个监视器方法。通过调用wait/notifyAll/notify来访问条件变量。

在下述的3个方面Java对象不同于监视器,从而使得线程的安全性下降:

  • 域不要求必须是private
  • 方法不要求必须是synchronized
  • 内部锁对客户是可用的
5.8 Volatile域
  • 多处理器的及所及能够暂时在寄存器或本地内存缓冲区中保存内存中的值。结果是,运行在不同处理器上的线程可能再同一个内存位置取到不同的值。
  • 编译器可以改变指令执行的顺序以使吞吐量最大化。这种顺序上的变化不会改变代码的语义,但是编译器假定内存的值仅仅在代码中有显式的修改指令时才会改变。然而,内存的值可以被另一个线程改变

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

private volatile boolean done;
public boolean isDone(){
	return done;
}
public void setDone(){
	done = true;
}

volatile变量不能提供原子性,例如:

public void flipDone(){
	done = !done;
}

不能确保翻转域中的值。

5.9 final变量

还有一种情况可以安全地访问一个共享域,即这个域声明为final时。final Map<String, Double> accounts = new HashMap<>();其他线程会在构造函数完成构造之后才看到这个accounts变量。
如果不使用final,就不能保证其他线程看到的是accounts更新后的值,他们可能都只是看到null,而不是新构造的HashMap。
当然,对这个映射表的操作并不是线程安全的。如果多个线程再读写这个映射表,仍然性需要进行同步。

5.10 原子性

假设对共享变量除了赋值之外并不完成其他操作,那么可以将这些共享变量声明为volatile。
java.util.concurrent.atomic包中有很多类使用了很搞笑的机器级指令(而不是使用锁)来保证其他操作的原子性。例如,AtomicInteger类提供了方法incrementAndGet和decrementAndGet,他们分别以源自方式讲一个整数自增或自减。可以安全的使用AtomicInteger作为共享计数器而无需同步。

另外这个包中还包括AtomicBoolean、AtomicLong和AtomicReference以及Boolean值、整数、long值和引用的原子数组。应用程序员不应该使用这些类,它们仅供那些开发并发工具的系统程序员使用。

5.11 死锁

锁和条件不能解决多线程中的所有问题。例如下面这种情况:

账户1: 200¥
账户2: 300¥
线程1:从账户1转移300¥到账户2
线程2:从账户2转移400¥到账户1

线程1和线程2都被阻塞了。因为账户1以及账户2中的余额都不足以进行转账,两个线程都无法执行下去。
有可能会以内每一个线程要等待更多的钱款存入而导致所有线程都被阻塞。这样的状态称为死锁。

5.12 线程局部变量

有时可能要避免共享变量,使用ThreadLocal辅助类为各个线程提供各自的实例。例如:

public static final SimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");

如果两个线程都执行以下操作:

String dateStamp = dateFormat.format(new Date());

结果可能会混乱,因为dateFormat使用的内部数据结构可能会被并发的访问所破坏。

要为每个线程构造一个实例,可以使用以下代码:

public static final ThreadLocal<SimpleDateFormat> dateFormat = new ThreadLocal<SimpleDateFormat>(){
	protected SimpleDateFormat initialValue(){
		return new SimpleDateFormat("yyyy-MM-dd");
	}
};

要访问具体的格式化方法,可以调用:

String dateStamp = dateFormat.get().format(new Date());

在一个给定线程中首次调用get时,会调用initialValue方法。在此之后,get方法会返回属于当前线程的那个实例。

5.13 锁测试与超时

线程再调用lock方法来获得另一个线程所持有的的锁的时候,很可能发生阻塞。应该更加谨慎的申请锁。tryLock方法视图申请一个锁,在成功获得锁后返回true,否则,立即返回false,而且线程可以立即离开去做其他事情。

if(myLock.tryLock()){
	try{
		...
	}finally{
		myLock.unlock();
	}
}else{
	... 
}

可以调用tryLock时,使用超时参数:if(myLock.tryLock(100, TimeUnit,MILLISECONDS)) {...}

lock方法不能被中断。如果一个线程在等待获得一个锁时被中断,中断线程在获得锁之前一直处于阻塞状态。如果出现死锁,那么,lock方法就无法终止。
然而,如果调用带有超时参数的tryLock,那么如果线程在等待期间被中断,将抛出InterruptedException异常。允许程序打破死锁。
也可以调用lockInterruptibly方法。它就相当于一个超时设为无限的tryLock方法。

在等待一个条件时,也可以提供一个超时:myCondition.await(100, TimeUnit.MILLISECONDS))
如果一个线程被另一个线程通过调用signalAll或signal激活,或者超时时限已达到,或者新线程被中断,那么await方法将返回。
如果等待的线程被中断,await方法将抛出一个InterruptedException异常。

5.14 读、写锁

java.util.concurrent.locks包定义了两个锁类:ReentrantLock 和 ReentrantReadWriteLock 。如果很多线程工一个数据结构读取数据而很少线程修改其中数据的话,后者十分有用。在这种情况下,允许对读者线程共享访问是合适的。当然,写者线程依然必须是互斥访问的。

使用读写锁的必要步骤:
1.构造一个ReentrantReadWriteLock对象:
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
2.抽取读锁和写锁:
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();
3.对所有的获取方法加读锁:

public double getTotleBalance(){
	readLock.lock();
	try{
		...
	}finally{
		readLock.unlock();
	}
}

4.对所有的修改方法加写锁:

public void transfer(){
	writeLock.lock();
	try{
		...
	}finally{
		writeLock.unlock();
	}
}
5.15 为什么弃用stop和suspend方法

初始的Java版本定义了一个stop方法来终止一个线程,用suspend方法来阻塞一个线程直至另一个线程调用resume。stop和suspend方法都有一些共同点:都试图控制一个给定线程的行为。

这两个方法已被弃用。stop方法终止所有未结束的方法,包括run方法。当线程被终止,立即释放被它锁住的所有对象的锁。这会导致对象处于不一致的状态。是不安全的。而使用suspend挂起一个持有一个锁的线程,那么,该锁在回复之前是不可用的。如果调用suspend方法的线程试图获得同一个锁,那么程序死锁;被挂起的线程等着被恢复,而其他挂起的线程等待获得锁。

如果想要安全地挂起线程,引入一个变量suspendRequested 并在run方法的某个安全的地方测试它,安全的地方是指该线程没有封锁其他线程需要的对象的地方。当该线程发现suspendRequested变量已经设置,将会保持等待状态知道它再次获得为止。

private volatile boolean suspendRequested = false;
private Lock suspendLock = new ReentrantLock();
private Condition suspendCondition = suspendLock.newCondition();

public void run(){
	while(...){
		...
		if(suspendRequested){
			suspendLock.lock();
			try{
				while(suspendRequested){
					suspendCondition.await();
				}
			}finally{
			suspendLock.unlock();
			}
		}
	}
}
public void requestSuspend(){
	suspendRequested = true;
}
public void requestResume(){
	suspendRequested = false;
	suspendLock.lock();
	try{
		suspendCondition.signalAll();
	}finally{
		suspendLock.unlock();
	}
}

6、阻塞队列

当试图向队列添加元素而队列已满,或是想从队列移出元素而队列为空的时候,阻塞队列导致线程阻塞。工作者线程可以周期性地将中间结果存储在阻塞队列中。其他的工作者线程移出中间结果并进一步加以修改。队列会自动地平衡负载。如果第一个线程集运行的比第二个慢,第二个线程再等待结果时会阻塞。如果第一个线程集运行的快,它将等待第二个队列集赶上来。

阻塞队列方法:

方法正常动作特殊情况下的动作
add添加一个元素如果队列满,则抛出异常
element返回队列的头元素如果队列空,抛出异常
offer添加一个元素并返回true如果队列满,返回false
peek返回队列的头元素如果队列空,则返回null
poll移出并返回队列的头元素如果队列空,则返回null
put添加一个元素如果队列满,则阻塞
remove移出并返回头元素如果队列空,则抛出异常
take移出并返回头元素如果队列空,则阻塞

还可以调用上面对应方法的超时方法,例如:
boolean success = q.offer(x, 100, TimeUnit.MILLISECONDS); 尝试在100毫秒的时间内在队列的尾部插入一个元素,如果成功返回true,否则,返回false。

阻塞队列描述
LinkedBlockingQueue容量没有上边界,但是,也可以选择指定最大容量
LinkedBlockingDeque双端的LinkedBlockingQueue
ArrayBlockingQueue在构造时需要指定容量,并且有一个可选的参数来指定是否需要公平性。若设置了公平参数,则那么等待了最长时间的线程会优先得到处理
PriorityBlockingQueue带优先级的队列,不是先进先出队列,元素按照它们的优先级顺序被移出,该队列没有容量上限
DelayQueue包含实现了Delayed接口的对象
TransferQueue允许生产者线程等待,直到消费者准备就绪可以接收一个元素。LinkedTransferQueue类实现了这个接口

示例代码

package com.java01.day10.blockingqueue;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
 * @description: 获取目录文件下的文件中所有包含某关键字的行
 * @author: ju
 * @date: 2020-05-21 17:54
 */
public class BlockingQueueTest {
    public static void main(String[] args){
        Scanner in = new Scanner(System.in);
        System.out.print("请输入目录路径:");
        String directory = in.nextLine();
        System.out.println();
        System.out.print("请输入查找的关键字:");
        String keyword = in.nextLine();
        System.out.println();

        //文件队列大小
        final int FILE_QUEUE_SIZE = 10;
        //查询线程数
        final int SEARCH_THREADS = 100;

        BlockingQueue<File> queue = new ArrayBlockingQueue<File>(FILE_QUEUE_SIZE);
        FileEnumerationTask enumerationTask = new FileEnumerationTask(queue, new File(directory));
        //一条线程遍历出目录中所有文件
        new Thread(enumerationTask).start();
        //100条线程共同查询文件中关键字
        for (int i = 1; i<= SEARCH_THREADS ; i++){
            new Thread(new SearchTask(queue, keyword)).start();
        }
    }
}

/**
 * 遍历目录,将文件put进队列中
 */
class FileEnumerationTask implements Runnable{
    public static File DUMMY = new File("");
    private BlockingQueue<File> queue;
    private File startingDirectory;

    public FileEnumerationTask(BlockingQueue<File> queue, File startingDirectory) {
        this.queue = queue;
        this.startingDirectory = startingDirectory;
    }

    @Override
    public void run() {
        try {
            enumerate(startingDirectory);
            queue.put(DUMMY);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 将目录下的所有文件都添加到队列中
     * @param directory 目录
     * @throws InterruptedException 异常
     */
    public void enumerate(File directory) throws InterruptedException {
        File[] files = directory.listFiles();
        for (File file : files){
            if (file.isDirectory()){
                enumerate(file);
            }else {
                queue.put(file);
            }
        }
    }

}

/**
 * 查找文件中的关键字所在行
 */
class SearchTask implements Runnable{
    private BlockingQueue<File> queue;
    private String keyword;

    public SearchTask(BlockingQueue<File> queue, String keyword) {
        this.queue = queue;
        this.keyword = keyword;
    }

    @Override
    public void run() {
        boolean done = false;
        while(!done){
            try {
                File file = queue.take();
                //如果查到最后一个文件,DUMMY文件,则结束
                if (file == FileEnumerationTask.DUMMY){
                    queue.put(file);
                    done = true;
                }else {
                    search(file);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 查询文件中关键字所在行
     * @param file 文件
     * @throws FileNotFoundException 异常
     */
    public void search(File file) throws FileNotFoundException {
        try(Scanner in = new Scanner(file)){
            int lineNumber = 0;
            while(in.hasNext()){
                lineNumber++;
                String line = in.nextLine();
                //如果这一行包含关键在则打印输出
                if (line.contains(keyword)){
                    System.out.print(file.getPath() + "---" + lineNumber + "---" + line);
                }
            }
        }
    }
}

7、线程安全的集合

7.1 高效的映射表、集合和队列

ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和ConcurrentLinkedQueue 这些集合使用复杂的算法,通过允许并发访问数据结构的不同不问来使竞争极小化。与大多数集合不同,size方法不必在常量时间内操作。确定这样的集合当前的大小通常需要遍历。

集合返回弱一致性的迭代器。这意味着迭代器不一定能反映出它们被构造之后所有的修改,但是,它们不会将同一个值返回两次,也不会抛出ConcurrentModificationException异常。

ConcurrentHashMap 和 ConcurrentSkipListMap 类有相应的方法用于原子性的关联插入以及关联删除:
cache.putIfAbsent(key, value); putIfAbsent自动添加新的关联,前提是原来没有这一关联;
cache.remove(key, value);remove (removeIfPersent) 将原子性地删除键值对;
cache.replace(key, oldValue, newValue);原子性地用新值替换旧值;

7.2 写数组的拷贝

CopyOnWriteArrayList 和CopyOnWriteArraySet 是线程安全的集合,其中所有的修改线程对底层的数组进行复制。当构建一个迭代器的时候,它包含一个对当前数组的引用。如果数组后来被修改了,迭代器仍然引用就数组,但是,集合的数组已经被替换了。因此,旧的迭代器拥有一致的(可能过时的)视图,访问它无需任何同步开销。

7.3 较早的线程安全集合

Vector和HashTable提供了线程安全的动态数组和散列表的实现,与之对应的是ArrayList和HashMap,是线程不安全的,但是可以通过同步包装器变成线程安全的:List<E> synchArrayList = Collections.synchronizedList(new ArrayList<E>());

8、Callable 与 Future

Runnable 封装了一个异步运行的任务,可以把它想象成为你一个没有参数和返回值的异步方法。Callable 与 Runnable类似,但是有返回值。Callable接口是一个参数化的类型,只有一个方法call。

public interface Callable<V>{
	V call() throws Exception;
}

类型参数是返回值的类型。例如,Callable< Integer> 表示一个最终返回Integer对象的异步计算。

Future保存异步计算的结果。可以启动一个计算,将Future对象交给某个线程,然后忘掉它。Future对象的所有者在结果计算好之后就可以获得它。

public interface Future<V> {
	//get方法调用被阻塞,直到计算完成;如果运行该计算的线程被中断,抛出异常
	V get() throws...;
	//如果在计算完成之前,get调用超时,抛出超时异常;如果运行该计算的线程被中断,抛出异常
	V get(long timeout, TimeUnit unit) throws ...;
	//取消该计算,如果还没开始,则取消且不再开始;如果已经在运行中,如果参数为true,那么被中断。
	void cancel(boolean mayInterrupt);
	boolean isCancelled();
	//如果计算还在进行,返回false;如果完成了,返回true
	boolean isDone();
}

FutureTask包装器是一种非常便利的机制,可将Callable转换为Future和Runnbale,它同时实现两者的接口,例如:

Callable<Integer> myComputation = ...;
FutureTask<Integer> task = new FutureTask<Integer>(myComputation);
Thread t = new Thread(task); //task == Runnable
t.start();
...
Integer result = task.get(); //task == Future

示例代码:

package com.java01.day10.future;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

/**
 * @description: 获取目录文件下的所有包含某关键字的文件数
 * @author: ju
 * @date: 2020-05-22 11:06
 */
public class FutureTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Scanner in = new Scanner(System.in);
        System.out.print("请输入目录路径:");
        String directory = in.nextLine();
        System.out.println();
        System.out.print("请输入查找的关键字:");
        String keyword = in.nextLine();
        System.out.println();

        MatchCounter counter = new MatchCounter(new File(directory), keyword);
        FutureTask<Integer> task = new FutureTask<>(counter);
        Thread t = new Thread(task);
        t.start();
        System.out.println("包含关键字的文件数有:" + task.get());
    }
}

class MatchCounter implements Callable<Integer>{
    private File directory;
    private String keyword;
    private int count;

    public MatchCounter(File directory, String keyword) {
        this.directory = directory;
        this.keyword = keyword;
    }

    @Override
    public Integer call() throws Exception {
        count = 0;
        File[] files = directory.listFiles();
        List<Future<Integer>> results = new ArrayList<>();
        for (File file : files){
            if (file.isDirectory()){
                MatchCounter counter = new MatchCounter(file, keyword);
                FutureTask<Integer> task = new FutureTask<>(counter);
                results.add(task);
                Thread t = new Thread(task);
                t.start();
            }else {
                if (search(file)){
                    count ++;
                }
            }
        }
        for (Future<Integer> result : results){
            count = count + result.get();
        }
        return count;
    }

    public boolean search(File file){
        try(Scanner in = new Scanner(file)){
            boolean found = false;
            while(!found && in.hasNext()){
                String line = in.nextLine();
                if (line.contains(keyword)){
                    found = true;
                }
            }
            return found;
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return false;
        }
    }
}

9、执行器

构建一个新的线程是有一定代价的,因为涉及与操作系统的交互。如果程序中创建了大量的生命期很短的线程,应该使用线程池。一个线程池中包含许多准备运行的空闲线程。将Runnable对象交给线程池,就会有一个线程调用run方法。当run方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。

另一个使用线程池的理由是减少并发线程的数目。创建大量线程会大大降低性能甚至使虚拟机崩溃。如果有一个会创建许多线程的算法,应该使用一个线程数“固定的”线程池以限制并发线程的总数。

执行器类由许多静态工厂方法用来构造线程池:

方法描述
newCachedThreadPool必要时创建新线程,空闲线程会被保留60秒;对于每个任务,如果有空闲线程可用,立即让它执行任务,如果没有可用的空闲线程,则创建一个新线程
newFixedThreadPool该池包含固定数量的线程,空闲线程会一直被保留;如果提交的任务数多于空闲的线程数,那么把得不到服务的任务放置到队列中,当其他任务完成后再运行它们
newSingleThreadExecutor只有一个线程的“池”,该线程顺序执行每一个提交的任务
newScheduledThreadPool用于预定执行而构建的固定线程池 ,替代 java.util.Timer
newSingleThreadScheduledExecutor用来预定执行而构建的单线程“池”
9.1 线程池

可以使用submit方法将一个Runnable对象或Callable对象提交给ExecutorService,会得到一个Future对象,可以用来查询该任务的状态。
当用完一个线程池的时候,调用shutdown启动该池的关闭序列。被关闭的执行器不再接受新的任务。当所有任务都完成后,线程池中的线程死亡。另一种方法是调用shutdownNow,取消尚未开始的所有任务并试图中断正在运行的线程。

总结在使用连接池时应该做的事:
1.调用Executors类中静态的方法 newCachedThreadPool 或 newFixedThreadPool
2.调用submit提交Runnable对象或Callable对象
3.如果想要取消一个任务,或如果提交Callable对象,那就要保存好返回的Future对象
4.当不再提交任何任务时,调用shutdown

示例代码:

package com.java01.day10.threadpool;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.*;

/**
 * @author: ju
 * @date: 2020-05-22 13:31
 */
public class ThreadPoolTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Scanner in = new Scanner(System.in);
        System.out.print("请输入目录路径:");
        String directory = in.nextLine();
        System.out.println();
        System.out.print("请输入查找的关键字:");
        String keyword = in.nextLine();
        System.out.println();

        ExecutorService pool = Executors.newCachedThreadPool();
        MatchCounter counter = new MatchCounter(new File(directory), keyword, pool);
        Future<Integer> result = pool.submit(counter);
        System.out.println("包含关键字的文件数有:" + result.get());
        pool.shutdown();
        int largestPoolSize = ((ThreadPoolExecutor) pool).getLargestPoolSize();
        System.out.println("largestPoolSize: " + largestPoolSize);
    }
}


class MatchCounter implements Callable<Integer> {
    private File directory;
    private String keyword;
    private ExecutorService pool;
    private int count;

    public MatchCounter(File directory, String keyword, ExecutorService pool) {
        this.directory = directory;
        this.keyword = keyword;
        this.pool = pool;
    }

    @Override
    public Integer call() throws Exception {
        count = 0;
        File[] files = directory.listFiles();
        List<Future<Integer>> results = new ArrayList<>();
        for (File file : files){
            if (file.isDirectory()){
                MatchCounter counter = new MatchCounter(file, keyword, pool);
                Future<Integer> result = pool.submit(counter);
                results.add(result);
            }else {
                if (search(file)){
                    count ++;
                }
            }
        }
        for (Future<Integer> result : results){
            count = count + result.get();
        }
        return count;
    }

    public boolean search(File file){
        try(Scanner in = new Scanner(file)){
            boolean found = false;
            while(!found && in.hasNext()){
                String line = in.nextLine();
                if (line.contains(keyword)){
                    found = true;
                }
            }
            return found;
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return false;
        }
    }
}
9.2 控制任务组

执行器服务除了可以作为线程池使用,还可以控制一组任务。例如:可以在执行器中使用shutdownNow方法取消所有的任务。

invokeAny 方法提交所有对象到一个Callable 对象的集合中,并返回某个已经完成的任务的结果。无法知道返回的究竟是哪个任务的结果,对于搜索问题,如果你愿意接受任何一种解决方法的话,就可以使用这个方法。

invokeAll 方法提交所有对象到一个Callable对象的集合中,并返回一个Future对象的列表,代表所有任务的解决方案。当计算结果可获得时,可以像下面这样对结果进行处理:

List<Callable<T>> tasks = ... ;
List<Future<T>> results = executor.invokeAll(tasks);
for(Future<T> result : results){
	processFurther(result.get());
}

这个方法的缺点是如果第一个任务恰巧花去了很多时间,则可能不得不进行等待。将结果按可获得的顺序保存起来更有实际意义。可以用ExecutorCompletionService来进行排序。

ExecutorCompletionService service = new ExecutorCompletionService(executor);
for (Callabe<T> task : tasks){
	service.submit(task);
}
for (int i = 0; i < tasks.size; i++){
	processFurther(service.take().get());
}
9.3 Fork-Join框架

有一些应用可能对每个处理器内核分别使用一个线程,来完成计算密集型任务,如图像或视频处理。针对这种应用, 引入了fork-join框架,将一个处理任务,分解为子任务来并行运行。
要采用这种框架需要提供一个扩展RecursiveAction的类(如果不生成任何结果)或者RecursiveTask< T>类(如果计算会生成一个类型为T的结果),再覆盖compute方法来生成并调用子任务,然后合并其结果。

invokeAll方法接受很多任务并阻塞,知道所有这些任务都已经完成。
join方法将生成结果。

示例:统计一个数组中有多少个元素满足某个特定属性(将这个数组一分为二,分别对这两部分进行统计,再将结果相加)

package com.java01.day10.forkjoin;

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

/**
 * @author: ju
 * @date: 2020-05-22 14:37
 */
public class ForkJoinTest {
    public static void main(String[] args){
        final int SIZE = 1000;
        double[] numbers = new double[SIZE];
        for (int i = 0; i<SIZE ; i++){
            numbers[i] = Math.random();
        }
        Counter counter = new Counter(numbers, 0, numbers.length, new Filter() {
            @Override
            public boolean accept(double t) {
                return t > 0.5;
            }
        });
        ForkJoinPool pool = new ForkJoinPool();
        pool.invoke(counter);
        System.out.println(counter.join());
    }
}

interface Filter{
    boolean accept(double t);
}

class Counter extends RecursiveTask<Integer>{
    public static final int THRESHOLD = 1000;
    private double[] values;
    private int from;
    private int to;
    private Filter filter;

    public Counter(double[] values, int from, int to, Filter filter) {
        this.values = values;
        this.from = from;
        this.to = to;
        this.filter = filter;
    }

    @Override
    protected Integer compute() {
        if (to - from < THRESHOLD){
            int count = 0;
            for (int i = from; i< to; i ++){
                if (filter.accept(values[i])){
                    count++;
                }
            }
            return count;
        }else {
            int mid = (to - from) / 2;
            Counter first = new Counter(values, from, mid, filter);
            Counter second = new Counter(values, mid, to, filter);
            invokeAll(first, second);
            return first.join() + second.join();
        }
    }
}

10、同步器

同步器具有为线程之间共用集结点模式提供的“预置功能”。如果有一个相互合作的线程集满足这些行为模式之一,,那么应该直接重用合适的类库而不要试图提供手工的锁与条件的集合。

同步器:

它能做什么何时使用
CyclicBarrier允许线程集等待直至其中预定数目的线程到达一个公共障栅,然后可以选择执行一个处理障栅的动作当大量的线程需要在它们的结果可用之前完成时
CountDownLatch允许线程集等待直至计数器减为0当一个或多个线程需要等待直至指定数目的事件发生
Exchanger允许两个线程再要交换的对象转备好时交换对象当两个线程工作在同一数据结构的两个实例上的时候,一个向实例添加数据,而另一个从实例清除数据
Semaphore允许线程集等待直到被允许继续运行限制访问资源的线程总数,如果许可数是1,常常use线程直到另一个线程给出许可为止
SynchronousQueue允许一个线程把对象交给另一个线程在没有显式同步的情况下,当两个线程准备好将一个对象从一个线程传递到另一个时
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值