Java核心技术 并发

一个程序同时执行多个任务,每个任务称为一个线程(thread),它是线程控制的简称。可以同时运行一个以上线程的程序称为多线程程序(multithreaded)。
多线程和多进程的区别:本质区别在于每个进程都拥有自己的一套变量,而线程则共享数据。共享变量使线程之间的通信比进程之间的通信更有效、更容易。在有些操作系统中,与进程相比,线程更加轻量级,创建、撤销一个线程比启动新进程的开销要小得多。

1.什么是线程

在这里插入图片描述
当点击Start按钮时,程序将从屏幕左上角弹出一个球,Start按钮的处理程序将调用addBall方法。这个方法循环运行1000次move。每调用一次move,球就会移动一点,当碰到墙壁时,球将调整方法,并重新绘制面板。
调用Thread.sleep不会创建新线程,sleep是Thread类的静态方法,用于暂停当前线程的活动。sleep方法可能会抛出一个InterruptedException方法。

public class Bounce {
    public static void main(String[] args) {
        EventQueue.invokeLater(() -> {
            JFrame frame = new BounceFrame();
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setVisible(true);
        });
    }
}
class BounceFrame extends JFrame {
    private BallComponent comp;
    public static final int STEPS = 1000;
    public static final int DELAY = 3;
    public BounceFrame() {
        setTitle("Bounce");
        comp = new BallComponent();
        add(comp, BorderLayout.CENTER);
        JPanel buttonPanel = new JPanel();
        addButton(buttonPanel, "Start", e -> addBall());
        addButton(buttonPanel, "Close", e -> System.exit(0));
        add(buttonPanel, BorderLayout.SOUTH);
        pack();
    }
    public void addButton(Container c, String title, ActionListener listener) {
        JButton button = new JButton(title);
        c.add(button);
        button.addActionListener(listener);
    }
    private void addBall() {
        try {
            Ball ball = new Ball();
            comp.add(ball);
            for (int i = 1; i <= STEPS; i++) {
                ball.move(comp.getBounds());
                comp.paint(comp.getGraphics());
                Thread.sleep(DELAY);
            }
        } catch (InterruptedException e) {}
    }
}
public class Ball {
    private static final int XSIZE = 15;
    private static final int YSIZE = 15;
    private double x = 0;
    private double y = 0;
    private double dx = 1;
    private double dy = 1;

    public void move(Rectangle2D bounds) {
        x += dx;
        y += dy;
        if (x < bounds.getMinX()) {
            x = bounds.getMinX();
            dx = -dx;
        }
        if (x + XSIZE >= bounds.getMaxX()) {
            x = bounds.getMaxX() - XSIZE;
            dx = -dx;
        }
        if (y < bounds.getMinY()) {
            y = bounds.getMinY();
            dy = - dy;
        }
        if (y + YSIZE >= bounds.getMaxY()) {
            y = bounds.getMaxY() - YSIZE;
            dy = -dy;
        }
    }

    public Ellipse2D getShape() {
        return new Ellipse2D.Double(x, y, XSIZE, YSIZE);
    }
}
public class BallComponent extends JPanel {
    private static final int DEFAULT_WIDTH = 450;
    private static final int DEFAULT_HEIGHT = 350;

    private java.util.List<Ball> balls = new ArrayList<>();

    public void add(Ball b) {
        balls.add(b);
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D) g;
        for (Ball b : balls) {
            g2.fill(b.getShape());
        }
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT);
    }
}

这个程序控制了整个过程。如果在球完成1000次之前点击Close按钮,会发现球仍在弹跳。在球自己结束弹跳之前无法与程序进行交互。
显然这个程序性能相当糟糕。肯定不能让程序用这种方式完成一个非常耗时的工作(网络连接读取数据时,阻塞其他任务是经常发生的,有时确实想要中断读取操作)。

使用线程给其他任务提供机会

可以将移动球的代码放置在一个独立的线程中,运行这段代码可以提高球的响应能力。
AWT的事件分派线程将一直并行运行,以处理用户界面的事件。由于每个线程都有机会得以运行,所以在球弹跳过程中,当用户点击close按钮时,时间调度线程将有机会关注这个事件,已处理关闭动作。
一个单独线程中执行一个任务的简单过程:
1.将任务代码移到实现了Runnable接口的类的run方法中。这个接口只有一个方法:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

由于Runnable是一个函数式接口,可以用lambda表达式建立一个实例:
Runnable r = () -> { task code };
2.由Runnable创建一个Thread对象:
Thread t = new Thread®;
3.启动线程:
t.start();

无论何时点击Start按钮,球会移入一个新的线程。
在这里插入图片描述

public class BounceThread {
    public static void main(String[] args) {
        EventQueue.invokeLater(() -> {
            JFrame frame = new BounceFrameThread();
            frame.setTitle("BounceThread");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setVisible(true);
        });
    }
}
class BounceFrameThread extends JFrame {
    private BallComponent comp;
    public static final int STEPS = 1000;
    public static final int DELAY = 5;

    public BounceFrameThread() {
        comp = new BallComponent();
        add(comp, BorderLayout.CENTER);
        JPanel buttonPanel = new JPanel();
        addButton(buttonPanel, "Start", e -> addBall());
        addButton(buttonPanel, "Close", e -> System.exit(0));
        add(buttonPanel, BorderLayout.SOUTH);
        pack();
    }

    public void addButton(Container c, String title, ActionListener listener) {
        JButton button = new JButton(title);
        c.add(button);
        button.addActionListener(listener);
    }

    private void addBall() {
        Ball ball = new Ball();
        comp.add(ball);

        Runnable r = () -> {
            try {
                for (int i = 1; i <= STEPS; i++) {
                    ball.move(comp.getBounds());
                    comp.repaint();
                    Thread.sleep(DELAY);
                }
            } catch (InterruptedException e) {}
        };
        Thread t = new Thread(r);
        t.start();
    }
}

也可以通过构建Thread类的子类定义一个线程:

class MyThread extends Thread {
	public void run(){
		...
	}
}

构造一个子类对象,并调用start方法,不过这种方法不再推荐。应该将要并行运行的任务与运行机制解耦合。如果有很多任务,为每个任务创建一个独立的线程所付出的代价太大。可以使用线程池解决问题。
不要调用Thread类或Runnable类的run方法。直接调用run方法,只会执行同一个线程中的任务,而不会启动新线程。应该调用Thread.start方法,这个方法将创建一个执行run方法的新线程。

2.中断线程

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

// 检测中断状态是否被置位
while (!Thread.currentThread().isInterrupted()){
	//do more work
}

如果线程被阻塞,就无法检测中断状态。当一个被阻塞的线程(sleep或wait)上调用interrupt方法时,阻塞调用将会被InterruptedException异常中断。
中断一个线程只是为了引起它的注意。被中断的线程可以决定如何响应中断。某些线程是如此重要,以至于应该处理完异常后,继续执行,而不理会中断。普遍情况是,线程将简单地将中断作为一个终止的请求:

Runnable r = () -> {
	try {
		...
		while (!Thread.currentThread().isInterrupted() /**&& more work to do*/) {
			//do more work
		}
	} catch (InterruptedException e) {
		// 线程被sleep或wait中断
	} finally {
		//cleanup,if required
	}
	// 退出run方法终止线程
};

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

Runnable r = () -> {
	try {
		...
		while (/**more work to do*/) {
			//do more work
			Thread.sleep(delay);
		}
	} catch (InterruptedException e) {
		// 线程被sleep中断
	} finally {
		//cleanup,if required
	}
	// 退出run方法终止线程
};

静态方法interrupted检测当前线程是否被中断,而且会清除该线程的中断状态。
isInterrupted是一个实例方法,可用来检测是否有线程被中断,不会改变中断状态。
在很多代码中会发现InterruptedException异常被抑制在很低的层次上:

void mySubTask() {
	...
	try {sleep(delay);}
	catch (InterruptedException e) {} //不要忽略
}

如果不认为在catch子句中做这一处理有什么好处的话,有两种合理的选择:
1.在catch子句中调用Thread.currentThread().interrupted()来设置中断状态,调用者可以对其进行检测:

void mySubTask() {
	...
	try {sleep(delay);}
	catch (InterruptedException e) {
		Thread.currentThread().interrupted();
	}
}

2.更好的选择,抛出InterruptedException异常,调用者(最终的run方法)可以捕获这一异常:

void mySubTask() throws InterruptedException {
	...
	sleep(delay);
	...
}

3.线程状态

线程有6种状态,可调用getState方法确定一个线程的当前状态:
New(新创建)
Runnable(可运行)
Blocaked(被阻塞)
Waiting(等待)
Timed waiting(计时等待)
Terminated(被终止)

新创建线程

当用new操作符创建一个新线程时,如new Thread®,该线程还没有开始运行。意味着它的状态是new。
当一个线程处于新创建状态时,程序还没有开始运行线程中的代码。在线程运行之前还有一些基础工作要做。

可运行线程

一旦调用start方法,线程处于runnable状态。一个可运行的程序可能正在运行也可能没有运行,这取决于操作系统给线程提供的运行时间(一个正在运行中的线程仍处于可允许状态)。
运行中的线程可能被中断,为了让其他线程获得运行机会。线程调度的细节依赖于操作系统提供的服务。
抢占式调度系统给每一个可运行线程一个时间片来执行任务。当时间片用完,操作系统剥夺该线程的运行权,并给另一个线程机会。当先一个线程时,操作系统考虑线程的优先级。
所有的桌面以及服务器操作系统都是用抢占式调度。但是,像手机这样的消息设备可能使用协作式调度,一个线程只有在调用yield方法、或者被阻塞或等待时,线程才失去控制权。

被阻塞线程和等待线程

当线程处于被阻塞或等待状态时,它暂时不活动,不允许任何代码且消耗最少的资源。直到线程调度器重新激活它。细节取决于它是怎样达到非活动状态的:
1.当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态。当所有其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程将变成非阻塞状态。
2.当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。在调用Object.wait或Thread.join方法,或者是等待java.util.concurrent库中的Lock或Condition时,就会出现这种情况。实际上,被阻塞状态和等待状态有很大不同
3.有几个方法有一个超时参数(Thread.sleep、Object.wait、Thread.join、Lock.tryLock、Condition.await的计时版)。调用它们导致线程进入计时等待状态。这一状态将一直保持到超时期满或接收到适当的通知。
在这里插入图片描述

被终止的线程

两个原因:
1.因为run方法正常退出而自然死亡
2.因为一个没有捕获的异常终止了run方法而意外死亡
特别是,可以调用线程的stop方法杀死一个线程。该方法抛出ThreadDeath错误对象,由此杀死线程。但是,stop方法已过时,不要在调用它。

4.线程属性

线程优先级

Java中,每一个线程有一个优先级。默认情况下,一个线程继承它的父线程的优先级。可以用setPriority方法提高或降低任何一个线程的优先级。可以将优先级设置为在MIN_PRIORITY(1)与MAX_PRIORITY(10)之间的任何值。NORM_PRIORITY被定义为5
当虚拟机依赖于宿主主机平台的线程实现机制时,Java线程的优先级被映射到宿主机平台的优先级上,优先级个数也许更多,也许更少。
Windows有7个优先级,一些Java优先级将映射到相同的操作系统优先级。Oracle为Linux提供的Java虚拟机中,线程优先级被忽略——所有线程具有相同的优先级。
如果有几个高优先级的线程没有进入非活动状态,低优先级的线程可能永远不能运行(完全饿死)。

守护线程

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

未捕获异常处理器

线程的run方法不能抛出任何受检异常,但是非受检异常会导致线程终止。在这种情况下,线程就会死亡。
但是不需要任何catch子句来处理可以被传播的异常,相反就在线程死亡之前,异常被传递到一个用于未捕获异常的处理器。该处理器必须属于一个实现Thread.UncaughtExceptionHandler接口的类。这个接口只有一个方法:void uncaughtException(Thread t, Throwable e)。
可以用setUncaughtExceptionHandler方法为任何线程安装一个处理器。也可以用Thread类的静态方法setDefaultUncaughtExceptionHandler为所有线程安装一个默认处理器。替换处理器可以使用日志API发送未捕获异常的报告到日志文件。
如果不安装默认处理器,默认的处理器为空。但是如果不为独立的线程安装处理器,此时的处理器就是该线程的ThreadGroup对象。
ThreadGroup线程组是一个可以统一管理的线程集合。默认情况下,所有线程都属于相同的线程组(也可能会建立其他的组)。现在引入了更好的特性用于线程集合操作,所有建议不要使用线程组。
ThreadGroup类实现Thread.UncaughtExceptionHandler接口,它的uncaughtException方法:
1.如果该线程组有父线程组,那么父线程组的uncaughtException方法被调用
2.否则,如果Thread.setDefaultUncaughtExceptionHandler方法返回一个非空处理器,则调用该处理器
3.否则,如果Throwable是ThreadDeath的一个实例,什么都不做
4.否则,线程的名字以及Throwable的栈轨迹输出到System.err上

5.同步

竞争条件的一个例子

为了避免多线程引起的对共享数据的讹误(竞争条件(race condition)),必须知道如何同步存取。
模拟一个有若干账户的银行,随机地生成在这些账户之间转移钱款的交易。每个账户有一个线程。每笔交易中,会从线程所服务的账户随机转移一定的数目的钱款到另一个账户:

public class Bank {
    private final double[] accounts;

    public Bank(int n, double initialBalance) {
        accounts = new double[n];
        Arrays.fill(accounts, initialBalance);
    }

    public void transfer(int from, int to, double amount) {
        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());
    }

    public double getTotalBalance() {
        double sum = 0;

        for (double a: accounts) {
            sum += a;
        }
        return sum;
    }

    public int size() {
        return accounts.length;
    }
}
public class UnsynchBankTest {
    public static final int NACCOUNTS = 100;
    public static final double INITIAL_BALANCE = 1000;
    public static final double MAX_AMOUNT = 1000;
    public static final int DELAY = 10;

    public static void main(String[] args) {
        Bank bank = new Bank(NACCOUNTS, INITIAL_BALANCE);
        for (int i = 0; i < NACCOUNTS; i++) {
            int fromAccount = i;
            Runnable r = () -> {
              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 (InterruptedException e){}
            };
            Thread t = new Thread(r);
            t.start();
        }
    }
}
//Thread[Thread-11,5,main]     272.18 from 11 to 69 Total Balance:  100000.00
//Thread[Thread-88,5,main]     828.72 from 88 to 92 Total Balance:  100000.00
//Thread[Thread-3,5,main]     966.22 from 3 to 47 Total Balance:  100000.00
//Thread[Thread-35,5,main]Thread[Thread-68,5,main]     238.30 from 68 to 63 Total Balance:   99176.76
//Thread[Thread-23,5,main]     279.66 from 23 to 21 Total Balance:   99176.76
//Thread[Thread-69,5,main]     282.43 from 69 to 82 Total Balance:   99176.76
//Thread[Thread-70,5,main]     297.22 from 70 to 12 Total Balance:   99176.76
//Thread[Thread-50,5,main]     152.47 from 50 to 39 Total Balance:   99176.76

这个模拟程序运行时,不清楚在某个时刻某一个账户中钱有多少,但是知道所有账户总金额会保持不变。
从结果看,出现了错误,余额总量有轻微的变化。

竞争条件详解

假定两个线程同时执行指令account[to] += amount,该语句不是原子性操作,该指令可能被处理如下:
1.将accounts[to]加载到寄存器
2.增加amount
3.将结果写回到accounts[0]
假定第一个线程执行了步骤1,2,然后被剥夺了运行权。假定第2个线程被唤醒并修改了accounts数组中的同一项。然后第1个线程被唤醒并完成第三步。这一动作擦去了第二个线程所做的更新,于是总金额不再正确。
在这里插入图片描述
类中的每一个语句在虚拟机中被翻译成字节码,运行命令javap -c -v Bank,对Bank.class文件进行反编译,accounts[to] += amount被转换为下面字节码:

aload_0
getfield      #2                  // Field accounts:[D
iload_2
dup2
daload
dload_3
dadd
dastore

增值命令是有几条指令组成的,执行它们的线程可以在任何一条指令点上被中断。
这里通过将打印语句和更新余额的语句交织在一起执行,增加了发生这种情况的机会。如果删除打印语句,讹误的风险会降低一点,因为每个线程在再次睡眠之前所做的工作很少,调度器在计算过程中剥夺线程的运行权可能性很小。但是讹误风险并没有消失。

锁对象

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

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] -= 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();
        }
    }
	...
}

用ReentrantLock保护代码块的基本结构,确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,它们被阻塞,直到第一个线程释放锁对象。
解锁操作放在finally子句至关重要,如果在临界区代码抛出异常,锁必须被释放,否则其他线程将永远阻塞。不要使用带资源的try语句,首先解锁方法名字不是close。即使将它重命名,带资源的try语句也无法正常工作。它的收不希望声明一个新变量,但是如果使用一个锁,可能想使用多个线程共享这个变量(而不是新变量)。

每个Bank对象有自己的ReentrantLock对象。如果两个线程试图访问同一个Bank对象,那么锁以串行方式提供服务。但是如果两个线程访问不同的Bank对象,每个线程得到不同的锁对象,两个线程都不会阻塞。
锁是可重入的,因为线程可以重复地获得已持有的锁。锁保持一个持有计数来跟踪对lock方法的嵌套调用。线程在每一次调用lock都要调用unlock释放锁。由于这一特性,被一个锁保护的代码,可以调用另一个使用相同锁的方法。

条件对象

进入临界区,却发现某一条件满足之后它才能执行。要使用Java库中条件对象(条件变量(conditional variable))来管理那些已经获得了一个锁但却不能做有用工作的线程。

bankLock.lock();
try{
	while (accounts[from] < amount){
		// wait
		...
	}
} finally {
	bankLock.unlock();
}

当账户没有足够的余额,等到直到另一个线程向账户中注入资金。但是,这一线程刚刚获得对bankLocak的排它性访问,因此别的线程没有进行存款操作的机会,这里需要使用条件对象。
一个锁对象可以有一个或多个相关的条件对象。可以用newCondition方法获得一个条件对象。习惯上给每一条件对象命名为可以反应它所表达的条件的名字:

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

如果transfer方法发现余额不足,调用sufficientFunds.await(),当前线程被阻塞了,并放弃了锁(希望可以使得另一个线程可以进行增加账户余额操作)。
等待获得所得线程和调用await方法的线程存在本质不同,一旦一个线程调用await方法,它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直至另一个线程调用统一条件上的signalAll方法时为止。
当另一个线程转账时,它应该调用sufficientFunds.signalAll(),这一调用重新激活因为这一条件而等到的所有线程。当这些线程从等待集当中移出时,它们再次成为可运行的,调度器将再次激活它们。同时,它们将试图重新进入该对象。一旦锁成为可用的,它们中的某个将从await调用返回,获得该锁并从被阻塞的地方继续执行。
此时,线程应该再次测试该条件。由于无法确保该条件被满足——signalAll方法仅仅是通知正在等待的线程:此时有可能已经满足条件,值得再次去检测该条件。

通常,对await的调用应该在下面循环体中:

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

当一个线程调用await时,它没有办法重新激活自身。它寄希望于其他线程。如果没有其他线程来重新激活等待的线程,它就永远不再运行了。这将导致死锁(deadlock)现象。
如果所有其他线程被阻塞,最后一个活动线程在解除其他线程的阻塞状态之前就调用await方法,那么它也被阻塞。没有任何程序可以解除其他程序的阻塞,那么该程序就挂起了。
经验上讲,在对象的状态有利于等待线程的方法改变时调用signalAll。例如,当一个账户余额发生变化时,等待线程会有机会检查余额。

还有一个方法signal,则是随机解除等待集中某个线程的阻塞状态。这比解除所有线程的阻塞更加有效,但也存在危险。如果随机选择的线程发现自己仍不能运行,那么它再次被阻塞。如果没有其他线程再次调用signal,那么系统就死锁了。
当一个线程拥有某个条件锁时,他仅仅可以在该条件时调用await、signalAll或signal方法。

public class Bank {
    private final double[] accounts;
    private Lock bankLock;
    private Condition sufficientFunds;

    public Bank(int n, double initialBalance) {
        accounts = new double[n];
        Arrays.fill(accounts, initialBalance);
        bankLock = new ReentrantLock();
        sufficientFunds = bankLock.newCondition();
    }

    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();
        }
    }

    public double getTotalBalance() {
        bankLock.lock();
        try {
            double sum = 0;

            for (double a: accounts) {
                sum += a;
            }
            return sum;
        } finally {
            bankLock.unlock();
        }

    }

    public int size() {
        return accounts.length;
    }
}

synchronized关键字

有关锁和条件的关键之处:
1.锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码
2.锁可以管理试图进入被保护代码段的线程
3.锁可以拥有一个或多个相关的条件对象
4.每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程
Lock和Condition接口为程序设计人员提供了高度的锁定控制。然而大多数情况下,并不需要,并且可以使用一种嵌入到Java内部的机制。从1.0版本开始,Java中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。

public synchronized void method(){
	// method body
}
// 等价于
public void method() {
	this.intrinsicLock.lock();
	try {
		// method body
	} finally {this.intrinsicLock.unlock();}
}

可以简单地声明Bank类的transfer方法为synchronized,而不是使用一个显式的锁。
内部对象锁只有一个相关条件。wait方法添加一个线程到等待集中,notifyAll/notify方法解除等待线程的阻塞状态(等价于await和signalAll,wait和notifyAll是Object类的final方法,Condition方法必须被重新命名,以便不会发生冲突)。

用Java实现Bank类:

public class Bank {
    private final double[] accounts;
    public Bank(int n, double initialBalance) {
        accounts = new double[n];
        Arrays.fill(accounts, initialBalance);
    }
    
    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();
    }

    public synchronized double getTotalBalance() {
        double sum = 0;

        for (double a: accounts) {
            sum += a;
        }
        return sum;
    }
    
    public int size() {
        return accounts.length;
    }
}

将静态方法声明为synchronized也是合法的。如果调用这个方法,该方法获得相关的类对象的内部锁。例如,Bank有一个静态同步方法,那么当该方法被调用时,Bank.class对象的锁被锁住。因此,没有其他线程可以调用同一个类的这个或任何其他的同步静态方法。

内部锁和条件存在一些局限:
1.不能中断一个正在试图获得锁的线程
2.试图获得锁时不能设定超时
3.每个锁仅有一个单一条件

那么Lock和Condition对象与同步方法,应该使用那一种:
1.最好都不使用。在许多情况下,可以使用java.util.concurrent包中的一种机制,它会处理所有的加锁。在后面会介绍使用阻塞队列来完成一个共同任务的线程,还要研究并行流。
2.如果synchronized适合程序,那么尽量使用它,这样可以减少编写代码的数量,减少出错的机率。
3.如果特别需要使用Lock和Condition结构提供的独有特性时,才使用Lock和Condition。

发布了233 篇原创文章 · 获赞 22 · 访问量 13万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章