Java多线程和并行程序设计
一、基本知识点
1、线程的概念(了解)
程序:代表一个静态的对象,是内涵指令和数据的文件,存储在磁盘或其他存储设备中
进程:代表一个动态地对象,是程序的一个执行过程,存在于系统的内存中,一个进程对应于一个程序
线程:是运行于某个进程中,用于完成某个具体任务的顺序控制流程,有时被称为轻型进程
进程切换和进程间通信开销大,进程间交换数据只能通过:共享内存、管道、消息队列、Socket通信等机制,但是一个进程里的线程切换开销小的多。
当一个进程被创建,自动地创建了一个主线程。一个进程至少有一个主线程。
多线程共享一个CPU:某时刻,只能有一个线程在使用CPU。
现代操作系统都将线程作为最小调度单位,进程作为资源分配的最小单位,分配给进程的资源可以被进程里的线程使用。
线程的作用:一个进程的多个子线程可以并发运行,多线程可以使程序反应更快、交互性更强、执行效率更高。
2、Runnable接口和线程类Thread(必会)
创建线程方法:线程的执行逻辑(后面叫线程任务)必须实现java.lang.Runnable接口的唯一run方法。此外,由于Thread实现了Runnable接口,也可以通过Thread派生线程类。
因此有两种方法可以实现同一个或多个线程的运行:
(1) 定义Thread类的子类并覆盖run方法;
(2) 实现接口Runnable的run方法。(推荐,因为java只支持单继承,但可以实现多个接口)
- 通过实现Runnable结构创建线程
- 实现Runnable接口,需要实现唯一的接口方法run
- void run() 定义了线程执行的功能
- 创建实现Runnable接口的类的对象
- 利用Thread类的构造函数创建线程对象
- public Thread (Runnable target):new Thread对象时需要传入Runnable接口实例
- 通过线程对象的start()方法启动线程
任何线程只能启动一次,多次调用产生。
class PrintChar implements Runnable //实现Runnable接口
{
private char charToPrint; // The character to print
private int times; // The times to repeat
public PrintChar(char c, int t){ charToPrint = c; times = t; }
public void run(){ //实现Runnable中声明的run方法
for (int i=1; i < times; i++) System.out.print(charToPrint);
}
}
public class RunnableDemo
{
public static void main(String[] args){
//以PrintChar对象实例为参数构造Thread对象
Thread printA = new Thread(new PrintChar('a',100));
Thread printB = new Thread(new PrintChar('b',100));
printA.start();//启动线程
printB.start();
}
}
- 通过继承Thread类创建线程
- 定义Thread类的子类-线程任务类
- 通过类创建线程对象
- 通过线程对象thread启动线程(继承了Thread的start方法)
class PrintChar extends Thread //继承Thread类
{
private char charToPrint; //要打印的字符
private int times; //打印的次数
public PrintChar(char c, int t){ charToPrint = c; times = t; }
public void run() {//覆盖run方法,定义线程要完成的功能
for (int i=1; i < times; i++)
System.out.print(charToPrint);
}
}
public class ThreadDemo {
public static void main(String[] args) {
Thread printA = new PrintChar('a',100); //创建二个线程对象
Thread printB = new PrintChar('b',100);
printA.start(); //启动线程
printB.start(); //启动另外一个线程
}
}
- 线程的状态:
- 就绪:等待CPU调度
- 运行:线程获得了CPU的所有权并在上面运行
- 调用yield( )方法主动放弃CPU的所有权,转到就绪态
- 阻塞:等待资源锁
- 消亡:当run( )执行完毕后,线程就消亡。
下面一些情况导致线程从运行状态转到阻塞状态:
1:调用了sleep
2:调用了Object wait( )方法、条件对象的await方法,Thread的join方法以等待其他线程,或者等待资源锁
3:发出了阻塞式IO操作请求,并等待IO操作结果(如等待阻塞式Socket的数据到来)
线程由阻塞状态被唤醒后,回到就绪态。唤醒的原因
1:sleep时间到
2:调用wait(await)的线程被其他线程notify,调用join方法的线程等到了其他线程的完成,线程拿到了资源锁
3:阻塞IO完成
sleep、wait、await、join等进入阻塞状态。sleep达到等待到时间进入就绪态,wait、await需要被唤醒才能进入就绪态,join方法要做的事就是,当有新的线程加入时,主线程会进入等待状态,一直到调用join方法的线程执行结束为止。
yield进入就绪态,也就无需唤醒,CPU一有空,就有可能执行
- 线程优先级(了解)
线程优先级范围从1-10,数字越高越能被优先执行。但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。
每个线程创建时赋予默认的优先级Thread.NORM_PRIORITY.
通过**setPriority(int priority)**为线程指定优先级.
用**getPriority()**方法获取线程的优先级.
Java的线程调度是抢占式的,这表示调度机制会周期性地中断线程,将上下文切换到另外一个线程;从而为每个线程都提供时间片。
多个线程只能是“宏观上并行,微观上串行”。
不要编写依赖于线程优先级的程序。
-
yield和sleep方法
-
使用 yield() 方法为其他线程让出CPU时间
Thread.yield(); 挂起进入ready,给其他进程调度机会
sleep(long mills)方法将线程设置为休眠状态,确保其他线程执行:
public void run() {
try {//循环中使用sleep方法,循环放在try-catch块中
for (int i = 1; i < times; i++) {
System.out.print(charToPrint);
if (i >= 50) Thread.sleep(1);
}
}
// 必检异常:其它线程调当前线程(正在休眠)interupt方法会抛出该异常
catch (InterruptedException ex { }
}
-
-
join方法
join方法的作用:在A线程中调用了B线程(对象)的join()方法时,表示A线程放弃控制权(被阻塞了),只有当B线程执行完毕时,A线程才被唤醒继续执行。
public class JoinDemo {
public static void main(String[] args) throws InterruptedException{
Thread printA = new Thread(new PrintChar('a',100));
Thread printB = new Thread(new PrintChar('b',100));
printA.start(); //在主线程里首先启动printA线程
printA.join(); //主线程被阻塞,等待printA执行完
printB.start(); //主线程被唤醒,启动printB线程
}
}
class PrintChar implements Runnable
{
private char charToPrint; // The character to print
private int times; // The times to repeat
public PrintChar(char c, int t){ charToPrint = c; times = t; }
public void run(){ //实现Runnable中声明的run方法
for (int i=1; i < times; i++)
System.out.print(charToPrint);
}
}
程序在main线程中调用printA线程(对象)的join方法时,main线程放弃cpu控制权(被阻塞),直到线程printA执行完毕,main线程被唤醒执行printB.start();
也就是阻塞调用线程(可以是主线程),直到被调用线程执行完成。
3、线程池
当线程任务执行完毕,即run方法结束后,Thread对象就消亡,然后又为新的线程任务去new新的线程对象…, 当有大量的线程任务时,就不断的new Thread对象,Thread对象消亡,再new Thread对象。(这样就不够高效)
线程池通过有效管理线程、“复用” 线程来提高性能。
从JDK1.5开始使用Executor接口(执行器)来执行线程池中的人物,Executor的子接口ExecutorService管理和控制任务。
java.util.concurrent.Executor中有方法:excute(Runnable object),线程执行器接口,负责执行线程人物。
通过Executor的子类ExecutorService来管理和控制线程任务。
java.util.concurrent.ExecutorService中有方法:shutdown(),关闭执行器;shutdownNow(),立刻关闭执行器,返回一个未完成任务的列表;isShutdown(),isTerminated()
我们无需管Thread的创建,只要把实例化好的线程任务对象交给执行器Executor就可以了。Thread由线程池内部来创建和维护。
创建线程池
java.util.concurrent.Executors:newFixedThreadPool(int numberOfThread),newCachedThreadPool()
Executors还支持其他类型的线程池的创建方法:newScheduledThreadPool、newSingleThreadPool
import java.util.concurrent.*;
public class ExecutorDemo {
public static void main(String[] args) {
// Create a fixed thread pool with maximum three threads
ExecutorService es= Executors.newFixedThreadPool(3);
// Submit runnable tasks to the executor
es.execute(new PrintChar('a', 100));
es.execute(new PrintChar('b', 100));
es.execute(new PrintNum(100));
// Shut down
es.shutdown();//不接受新的线程人物,现有的任务将继续执行直到完成
}
}
线程任务是实现了Runnable接口的类的实例。
线程是Thread类的实例,是任务的运行载体。
4、线程同步(*****)
举例:
public class AccountWithoutSync {
private static class Account{//内部静态类Account
private int balance = 0;
public int getBalance() {
return balance;
}
public void deposit(int amount){
int newBalance = balance + amount; //读取balance
try{ Thread.sleep(5); } //休眠是为了放大数据不一致的可能性
catch(InterruptedException e){ }
balance = newBalance; //写balance
}
}
private static class AddPennyTask implements Runnable{
public void run() { account.deposit(1); }
}
private static Account account = new Account();
public static void main(String[] args){
//创建线程池executor
ExecutorService executor = Executors.newCachedThreadPool();
for(int i = 0; i < 100; i++){
executor.execute(new AddPennyTask());
}
executor.shutdown();//关闭线程池
while(!executor.isTerminated()){ }//等待线程池里的线程全部结束
System.out.println("What is balance?" + account.getBalance());
}
}
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
newCachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程。
多个线程同时访问公共资源,会导致竞争状态(同时去修改公共资源)。为了避免竞争状态,应该防止多个线程同时进入程序的某一特定部分,这样的部分叫临界区。Account类的deposit方法就是临界区。可用synchronized关键字来同步,保证一次只有一个线程可以访问这个方法。当一个方法被synchronized修饰,这个方法就是原子的(一个线程开始执行这个方法,就不可中断)。
synchronized可用于同步方法
使用关键字synchronized 来修饰方法:public synchronized void deposit(double amount)
对于synchronized实例方法,是对调用该方法的对象(this对象)加锁,加锁后这个对象的其他方法也不能访问。
对于synchronized静态方法,是对拥有这个静态方法的类加锁。
被synchronized关键字同步的语句块称为同步块(synchronized Block)
synchronized (expr) { statements; }
表达式expr求值结果必须是一个对象的引用,因此可以通过对任何对象加锁来同步语句块
如果expr指向的对象没有被加锁,则第一个执行到同步块的线程对该对象加锁,线程执行该语句块,然后解锁;
如果expr指向的对象已经加了锁,则执行到同步块的其它线程将被阻塞
expr指向的对象解锁后,所有等待该对象锁的线程都被唤醒
同步语句块允许同步方法中的部分代码,而不必是整个方法,增强了程序的并发能力
private static class Account{
private int balance = 0;
public int getBalance() {
return balance;
}
public synchronized void deposit(int amount){
int newBalance = balance + amount;
try{ Thread.sleep(5); }//由于使用了synchronized关键字,进入后加了锁,sleep方法调用后,并没有释放锁。使得线程仍然可以同步控制,sleep不会让出系统资源。
catch(InterruptedException e){ }
balance = newBalance;
}
}
private static class Account{
private int balance = 0;
public int getBalance() {
return balance;
}
public void deposit(int amount){
synchronized(this){ //对这个账户加锁,和上面的代码等效
int newBalance = balance + amount;
try{ Thread.sleep(5); }
catch(InterruptedException e){ }
balance = newBalance;
}
}
}
下面这种同步是错误的
private static class AddPennyTask implements Runnable{
public void run() {
synchronized(this){//如果将this改成account,就是对的了
account.deposit(1);
}
}
}
告诉我们:要注意加锁的对象,该清楚该给谁加锁,才能实现同步。
线程同步-加锁同步
采用synchronized关键字的同步要隐式地在对象实例或类上加锁,粒度较大影响性能 。
JDK 1.5 可以显式地加锁,能够在更小的粒度上进行线程同步。
一个锁是一个Lock接口的实例。
类ReentrantLock是Lock的一个具体实现:可重入锁。
java.util.concurrent.locks.Lock:lock,得到一个锁;unlock,释放锁;newCondition,返回一个绑定到该Lock实例的Condition实例。
java.util.concurrent.locks.ReentrantLock:ReentrantLock。
可重入性锁描述这样的一个问题:一个线程在持有一个锁的时候,它能否再次(多次)申请该锁。如果一个线程已经获得了锁,它还可以再次获取该锁而不会死锁,那么我们就称该锁为可重入锁。
void methodA(){
lock.lock(); // 获取锁
methodB();
lock.unlock() // 释放锁
}
void methodB(){
lock.lock(); // 再次获取该锁
// 其他业务
lock.unlock();// 释放锁
}
synchronized支持重入
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
public class AccountWithSyncUsingLock {
private static Account account = new Account();
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
// Create and launch 100 threads
for (int i = 0; i < 100; i++) {executor.execute(new AddAPennyTask());}
executor.shutdown();
// Wait until all tasks are finished
while (!executor.isTerminated()) { }
System.out.println("What is balance ? " + account.getBalance());
}
// A thread for adding a penny to the account
public static class AddAPennyTask implements Runnable {
public void run() {account.deposit(1); }
}
public static class Account {// An inner class for account,主要变化在账户类
private static Lock lock = new ReentrantLock(); // 注意这里是静态的,被所有Account实例共享(申请一个可重入锁),类型是Lock
private int balance = 0;
public int getBalance() {return balance;}
public void deposit(int amount) {
lock.lock( ); // Acquire the lock 获取锁
try {
int newBalance = balance + amount;
Thread.sleep(5);
balance = newBalance;
}
catch (InterruptedException ex) { }
finally { lock.unlock(); // Release the lock,在finally中进行锁的释放。}
}//因为finally无论什么情况,都会执行,所以lock.unlock放在这里更加安全。
}
}
synchronized同步场景1
假设一个类有多个用synchronized修饰的同步实例方法,如果多个线程访问这个类的同一个对象,当一个线程获得了该对象锁进入到其中一个同步方法时,这把锁会锁住这个对象所有的同步实例方法。
由于一个同步的实例方法等价于synchronized(this){ }
因此锁的粒度是this对象。只要this对象被上锁,这个对象里所有同步的实例方法、synchronized(this){ }块都被锁住。
再次强调:这个例子说明,二个线程访问同一个对象时, 只要一个线程拿到对象锁,这个对象的所有同步实例方法都被锁。
synchronized同步场景2
假设一个类有多个用synchronized修饰的同步实例方法,如果多个线程访问这个类的不同对象,那么不同对象的synchronized锁不一样,每个对象的锁只能对访问该对象的线程同步
Lock同步场景3
如果采用Lock锁进行同步,一旦Lock锁被一个线程获得,那么被这把锁控制的所有临界区都被上锁,这时所有其他访问这些临界区的线程都被阻塞。(和synchronized同步场景1类似)
Lock同步场景4
如果一个类采用Lock锁对临界区上锁,而且这个Lock锁也是该类的实例成员(见ResourceWithLock的里的lock对象定义),那么这个类的二个实例的Lock锁就是不同的锁,下面的动画演示了这种场景:对象o1的Lock锁和对象o2的Lock锁是不同的锁对象。(和synchronized同步场景2类似)
线程同步总结
- 如果采用synchronized关键字对类 A的实例方法进行同步控制,这时等价于synchronized(this){ }
- 一旦一个线程进入类A的对象o的synchronized实例方法,对象o被加锁,对象o所有的synchronized实例方法都被锁住,从而阻塞了要访问对象o的synchronized实例方法的线程,但是与访问A类其它对象的线程无关
- 如果采用synchronized关键字对类 A的静态方法进行同步控制,这时等价于synchronized(A.class){ }。一旦一个线程进入A的一个静态同步方法,A所有的静态同步方法都被锁(这个锁是类级别的锁),这个锁对所有访问该类静态同步方法的线程有效,不管这些线程是通过类名访问静态同步方法还是通过不同的对象访问静态同步方法。
- 如果通过Lock对象进行同步,首先看Lock对象对哪些临界区上锁,一旦Lock锁被一个线程获得,那么被这把锁控制的所有临界区都被上锁(如场景3);另外要区分Lock对象本身是否是不同的:不同的Lock对象能阻塞的线程是不一样的(如场景4)。
线程同步-线程间协作
线程之间有资源竞争,synchronized和Lock锁这些同步机制解决的是资源竞争问题
线程之间的相互协作:可通过Condition对象的await/signal/signalAll来完成。
- Condition (条件)对象是通过调用Lock实例的newCondition( )方法而创建的对象
- Condition对象可以用于协调线程之间的交互(使用条件实现线程间通信)
- 一旦创建了条件对象condition,就可以通过调用condition.await()使当前线程进入等待状态 , 其它线程通过同一个条件对象调用signal和signalAll()方法来唤醒等待的线程,从而实现线程之间的相互协作
java.util.concurrent.Condition:await,引起当前线程等待,直到收到条件信号signal、signalall;signal,唤醒一个等待线程;signalall,唤醒所有等待线程。
- 使用循环while而不能使用条件if
- 有await,就要有 signal()或者signalAll(),要不然一直等
- 条件对象由Lock对象创建,通过条件对象调用它的方法await/signal/signalAll(),为调用这些方法,必须首先拥有锁(即先调用lock方法)
public class ThreadCooperation {
private static class Account {// An inner class for account
private static Lock lock = new ReentrantLock();//锁
private static Condition newDeposit = lock.newCondition();//由锁创建条件对象
private int balance = 0;
public int getBalance() {return balance;}
public void withdraw(int amount) {
lock.lock(); // Acquire the lock
try {
while (balance < amount) {
System.out.println("\t\t\t\tWait for a deposit");
newDeposit.await();//等待存款,如果没存够,还会再次进入等待
}//wait和sleep不同,wait会释放锁
balance -= amount;
System.out.println("\t\t\t\tWithdraw " + amount +"\t\t\t\t" + getBalance());
}
catch (InterruptedException ex) {ex.printStackTrace();}
finally {lock.unlock(); }
}
public void deposit(int amount){
lock.lock();
try{
balance+=amount;
System.out.println("deposit " + amount + "\t\t\t\t\t\t\t\t" + getBalance( ));
newDeposit.signalAll();//唤醒等待存款的线程任务
}
finally{ lock.unlock(); }
}
}
}
5、信号量
信号量用来限制访问一个共享资源的线程数,是一个有计数器的锁
访问资源之前,线程必须从信号量获取许可
访问完资源之后,该线程必须将许可返回给信号量
semaphore.acquire(),获得许可后可进入临界区,信号量-1,当信号量为0,所有线程必须等待,只要信号量大于0,等待的线程被唤醒。
semaphore.release(),线程退出临界区前调用release,信号量+1,一个车位容量为N的车库,可用一个信号量来管理,信号量计数器为N
为了创建信号量,必须确定许可的数量(计数器最大值),同时可选用公平策略
任务通过调用信号量的acquire()方法来获得许可,信号量中可用许可的总数减1
任务通过调用信号量的release()方法来释放许可,信号量中可用许可的总数加1
import java.util.concurrent.Semaphore;
// An inner class for account
private static class Account {
// Create a semaphore
private static Semaphore semaphore = new Semaphore(1);//许可为1的信号量就相当于互斥锁,和前面的lock效果一样。
private int balance = 0;
public int getBalance() {return balance;}
public void deposit(int amount) {
try {
semaphore.acquire();//获取许可,信号量-1
int newBaance=balance+amount
Thread.sleep(5);
balance=new Balance;
}
finally {
semaphore.release(); //结束,信号量+1
}
}
}
}
避免死锁:
给每一个需要上锁的对象指定一个顺序,确保每个线程都按照这个顺序来获取锁(避免死锁的方式)
public class Account {
private int id; // 用来控制顺序
private String name;
private double balance;
public void transfer(Account from, Account to, double money){
if(from.getId() > to.getId()){
synchronized(from){
synchronized(to){
// transfer
}
}
}else{
synchronized(to){
synchronized(from){
// transfer
}
}
}
}
public int getId() {
return id;
}
}
6、同步集合
Java集合框架 包括:List 、Set 、Map接口及其具体子类,都不是线程安全的。
集合框架中的类不是线程安全的, 可通过为访问集合的代码临界区加锁或者同步等方式来保护集合中的数据
Collections类提供6个静态方法来将集合转成同步版本(即线程安全的版本)
这些同步版本的类都是线程安全的,但是迭代器不是,因此使用迭代器时必须同步:synchronized(要迭代的集合对象){ // 迭代}
二、习题解答
填空题
-
创建线程的方式有_实现java.lang.Runnable接口_和__通过Thread派生线程类__。
-
程序中可能出现一种情况:多个线种互相等待对方持有的锁,而在得到对方的锁之前都不会释放自己的锁,这就是__死锁__。
-
若在线程的执行代码中调用yield方法后,则该线程将___主动放弃CPU的所有权,转到就绪态_。
注意:和sleep、wait、join等进入阻塞状态不同
-
线程程序可以调用_____sleep()____________方法,使线程进入睡眠状态,可以通过调用____setPriority()_____方法设置线程的优先级。
-
获得当前线程id的语句是_______Thread.currentThread().getId();_________。
单选题
- 能够是线程进入死亡状态的是______C_____。
A. 调用Thread类的yield方法
B. 调用Thread类的sleep方法
C. 线程任务的run方法结束
D. 线程死锁
- 给定下列程序:
public class Holder {
private int data = 0;
public int getData () {return data;}
public synchronized void inc (int amount) {
int newValue = data + amount;
try {Thread.sleep(5);
} catch (InterruptedException e) {}
data = newValue;
}
public void dec (int amount) {
int newValue = data - amount;
try {Thread.sleep(1);
} catch (InterruptedException e) {}
data = newValue;
}
}
public static void main (String [] args) {
ExecutorService es = Executors.newCachedThreadPool();
Holder holder = new Holder ();
int incAmount = 10, decAmount = 5, loops = 100;
Runnable incTask = () -> holder.inc(incAmount);
Runnable decTask = () -> holder.dec(decAmount);
for (int i = 0; i < loops; i++) {
es.execute(incTask);
es.execute(decTask);
}
es. shutdown ();
while (! es.isTerminated ()) {}
}
下列说法正确的是_______B___。
A. 当一个线程进入holder对象的inc方法后,holder对象被锁住,因此其他线程不能进入inc方法和dec方法
B. 当一个线程进入holder对象的inc方法后,holder对象被锁住,因此其他线程不能进入inc方法,但可以进入dec方法
C. 当一个线程进入holder对象的dec方法后,holder对象被锁住,因此其他线程不能进入dec方法和inc方法
D. 当一个线程进入holder对象的dec方法后,holder对象被锁住,因此其他线程不能进入dec方法,但可以进入inc方法
只是锁住了所有同步方法,dec没有加锁
- 给定下列程序:
public class Test2_3 {
private static Object lockObject = new Object ();
/**
* 计数器
*/
public static class Counter {
private int count = 0;
public int getCount () {return count;}
public void inc () {
synchronized (lockObject) {
int temp = count + 1;
try {Thread.sleep(5);} catch (InterruptedException e) {}
count = temp;
}
}
public void dec () {
synchronized (lockObject) {
int temp = count - 1;
try {Thread.sleep(5);} catch (InterruptedException e) {}
count = temp;
}
}
}
public static void main (String [] args) {
ExecutorService es = Executors.newCachedThreadPool();
Counter counter1 = new Counter ();
Counter counter2 = new Counter ();
int loops1 = 10, loops2 = 5;
Runnable incTask = () -> counter1.inc ();
Runnable decTask = () -> counter2.dec ();
for (int i = 0; i < loops1; i++) {es. execute(incTask);}
for (int i = 0; i < loops2; i++) {es. execute(decTask);}
es. shutdown ();
while (! es. isTerminated ()) {}
}
}
下面说法正确的是____C_______。
A. incTask的执行线程进入counter1对象的inc方法后,counter1对象被上锁,会阻塞decTask的执行线程进入counter2对象的dec方法
B. incTask的执行线程进入counter1对象的inc方法后,counter1对象被上锁,不会阻塞decTask的执行线程进入counter2对象的dec方法
C. incTask的执行线程进入对象counter1的inc方法后,lockObject对象被上锁,会阻塞decTask执行线程进入counter2对象的方法dec
D. incTask的执行线程进入对象counter1的inc方法后,lockObject对象被上锁,不会阻塞decTask执行线程进入counter2对象的方法dec
- 给定下列程序:
public class Test2_4 {
public static class Resource {
private int value = 0;
public int sum (int amount) {
int newValue = value + amount;
try {Thread.sleep(5);} catch (InterruptedException e) {}
return newValue;
}
public int sub (int amount) {
int newValue = value - amount;
try {Thread.sleep(5);} catch (InterruptedException e) {}
return newValue;
}
}
public static void main (String [] args) {
ExecutorService es = Executors.newCachedThreadPool();
Resource r = new Resource ();
int loops1 = 10, loops2 = 5, amount = 5;
Runnable sumTask = () -> r.sum(amount);
Runnable subTask = () -> r.sub(amount);
for (int i = 0; i < loops1; i++) {es. execute(sumTask);}
for (int i = 0; i < loops2; i++) {es. execute(subTask);}
es. shutdown ();
while (! es. isTerminated ()) {}
}
}
下面说法正确的是_____C______。
A. 由于方法sum和sub都没有采取任何同步措施,所以sumTask和subTask的执行线程都可以同时进入共享资源对象r的sum方法或sub方法,造成对象r的实例成员value的值不一致;
B. 由于方法sum和sub都没有采取任何同步措施,所以sumTask和subTask的执行线程都可以同时进入共享资源对象r的sum方法或sub方法,造成方法内局部变量newValue和形参amount的值不一致;
C. 虽然方法sum和sub都没有采取任何同步措施,但Resource类的sum和sub里的局部变量newValue和形参amount位于每个线程各自的堆栈里互不干扰,同时多个线程进入共享资源对象r的sum方法或sub方法后,对实例数据成员value都只有读操作,因此Resource类是线程安全的
D. 以上说法都不正确
- 给定下列程序:
public class Test2_5 {
public static class Resource {
private static int value = 0;
public static int getValue () {return value;}
public static void inc (int amount) {
synchronized (Resource.Class) {
int newValue = value + amount;
try {Thread.sleep(5);} catch (InterruptedException e) {}
value = newValue;
}
}
public synchronized static void dec (int amount) {
int newValue = value - amount;
try {Thread.sleep(2);} catch (InterruptedException e) {}
value = newValue;
}
}
public static void main (String [] args) {
ExecutorService es = Executors.newCachedThreadPool();
int incAmount = 10, decAmount = 5, loops = 100;
Resource r1 = new Resource ();
Resource r2 = new Resource ();
Runnable incTask = () -> r1.inc(incAmount);
Runnable decTask = () -> r2.dec(decAmount);
for (int i = 0; i < loops; i++) {es. execute(incTask); es. execute(decTask);}
es. shutdown ();
while (! es. isTerminated ()) {}
}
}
下面说法错误的的是_____B______。
A. 同步的静态方法public synchronized static void dec (int amount) {} 等价于public static void dec (int amount) {synchronized (Resource. class) {}}
B. incTask的执行线程访问的对象r1,decTask访问的是对象r2,由于访问的是不同对象,因此incTask的执行线程和decTask的执行线程之间不会同步
C. 虽然incTask的执行线程和decTask的执行线程访问的是Resource类不同对象r1和r2,但由于调用的是Resource类的同步静态方法,因此incTask的执行线程和decTask的执行线程之间是被同步的
D. 一个线程进入Resource类的同步静态方法后,这个类的所有静态同步方法都被上锁,而且上的是对象锁,被锁的对象是Resource.class。但是这个锁的作用范围是Resource类的所有实例,即不管线程通过Resource类的哪个实例调用静态同步方法,都将被阻塞
由于是对类上的锁,尽管是不同对象,但是仍是一把锁。
- 假设一个临界区通过Lock锁进行同步控制,当一个线程拿到一个临界区的Lock锁,进入该临界区后,该临界区被上锁。这时下面的说法正确的是_____D______。
A. 如果在临界区里线程执行Thread.sleep方法,将导致线程进入阻塞状态,同时临界区的锁会被释放;如果在临界区里线程执行Lock锁的条件对象的await方法,将导致线程进入阻塞状态,同时临界区的锁会被释放
B.如果在临界区里线程执行Thread.sleep方法,将导致线程进入阻塞状态,同时临界区的锁不会被释放;如果在临界区里线程执行Lock锁的条件对象的await方法,将导致线程进入阻塞状态,同时临界区的锁不会被释放
C. 如果在临界区里线程执行Thread.sleep方法,将导致线程进入阻塞状态,同时临界区的锁会被释放;如果在临界区里线程执行Lock锁的条件对象的await方法,将导致线程进入阻塞状态,同时临界区的锁不会被释放
D. 如果在临界区里线程执行Thread.sleep方法,将导致线程进入阻塞状态,同时临界区的锁不会被释放;如果在临界区里线程执行Lock锁的条件对象的await方法,将导致线程进入阻塞状态,同时临界区的锁会被释放
sleep不会释放锁,await会释放锁
问答题
1:有三个线程T1,T2,T3,怎么确保它们按指定顺序执行:首先执行T1,T1结束后执行T2,T2结束后执行T3,T3结束后主线程才结束。请给出示意代码。
T1.start();//启动t1线程
T1.join();//阻塞主线程,执行完t1再返回
T2.start();//启动t2线程
T2.join();//阻塞主线程,执行完t2再返回
T3.start();//启动t3线程
T3.join();//阻塞主线程,执行完t3再返回
join会阻塞所在的线程的调用线程,这里是主线程
编程题
- 下面程序是一个泛型容器Container<T>的定义,该容器是对ArrayList的一个封装,实现了四个公有的方法add、remove、size、get。
class Container<T> {
private List<T> elements = new ArrayList<>();
/**
* 添加元素
*
* @param e 要添加的元素
*/
public void add(T e) {
elements.add(e);
}
/**
* 删除指定下标的元素
*
* @param index 指定元素下标
* @return 被删除的元素
*/
public T remove(int index) {
return elements.remove(index);
}
/**
* 获取容器里元素的个数
*
* @return 元素个数
*/
public int size() {
return elements.size();
}
/**
* 获取指定下标的元素
*
* @param index 指定下标
* @return 指定下标的元素
*/
public T get(int index) {
return elements.get(index);
}
}
public class ContainerWithLock<T> {
private final ReentrantLock lock=new ReentrantLock();
private List<T> elements = new ArrayList<>();
/**
* 添加元素
*
* @param e 要添加的元素
*/
public void add(T e) {
lock.lock();
try {
elements.add(e);
}
finally {
lock.unlock();
}
}
/**
* 删除指定下标的元素
*
* @param index 指定元素下标
* @return 被删除的元素
*/
public T remove(int index) {
lock.lock();
try {
return elements.remove(index);
}
finally {
lock.unlock();
}
}
/**
* 获取容器里元素的个数
*
* @return 元素个数
*/
public int size() {
lock.lock();
try {
return elements.size();
}
finally {
lock.unlock();
}
}
/**
* 获取指定下标的元素
*
* @param index 指定下标
* @return 指定下标的元素
*/
public T get(int index) {
lock.lock();
try {
return elements.get(index);
}
finally {
lock.unlock();
}
}
}
package homework.ch30.P1;
import java.util.ArrayList;
import java.util.List;
public class SynchronizedContainer<T> {
private List<T> elements = new ArrayList<>();
/**
* 添加元素
*
* @param e 要添加的元素
*/
public synchronized void add(T e) {
elements.add(e);
}
/**
* 删除指定下标的元素
*
* @param index 指定元素下标
* @return 被删除的元素
*/
public synchronized T remove(int index) {
return elements.remove(index);
}
/**
* 获取容器里元素的个数
*
* @return 元素个数
*/
public synchronized int size() {
return elements.size();
}
/**
* 获取指定下标的元素
*
* @param index 指定下标
* @return 指定下标的元素
*/
public synchronized T get(int index) {
return elements.get(index);
}
}
- 实现一个线程安全的同步队列SyncQueue<T>,模拟多线程环境下的生产者消费者机制,SyncQueue<T>的定义如下:
/**
* 一个线程安全同步队列,模拟多线程环境下的生产者消费者机制
* 一个生产者线程通过produce方法向队列里产生元素
* 一个消费者线程通过consume方法从队列里消费元素
* @param <T> 元素类型
*/
public class SyncQueue1<T> {
/**
* 保存队列元素
*/
private ArrayList<T> list = new ArrayList<>();
//TODO 这里加入需要的数据成员
private ReentrantLock lock=new ReentrantLock();
private Condition newProduce=lock.newCondition();
private Condition newConsume=lock.newCondition();
/**
* 生产数据
*
* @param elements 生产出的元素列表,需要将该列表元素放入队列
* @throws InterruptedException
*/
public void produce(List<T> elements) throws InterruptedException{
//TODO 这里需要实现代码
lock.lock();
try{
while(!this.list.isEmpty()){//队列不是空,等待
newProduce.await();
}
System.out.println("Produce elements:"+elements.toString());
this.list.addAll(elements);
newConsume.signalAll();
}
finally {
lock.unlock();
}
}
/**
* 消费数据
*
* @return 从队列中取出的数据
* @throws InterruptedException
*/
public List<T> consume() {
//TODO 这里需要实现代码
lock.lock();
try {
while (this.list.isEmpty())
newConsume.await();
System.out.println("Consume elements:" + this.list.toString());
List<T> tmpList = (List<T>) this.list.clone();
this.list.clear();
newProduce.signalAll();
return tmpList;
} catch (InterruptedException e) {
e.printStackTrace();
return null;
} finally {
lock.unlock();
}
}
}
这里try、catch里都有return,或者finally里有return、或者finally后面有return都可以。
- 我们知道JDK提供了线程池的支持,线程池可以通过重复利用已创建的线程降低线程创建和销毁造成的消耗。但是Thread一旦启动,执行完线程任务后,就不可再次启动。
public class ReusableThread extends Thread{
private Runnable runTask = null; //保存接受的线程任务
//TODO 加入需要的数据成员
private ReentrantLock lock = new ReentrantLock();
private Condition newTask = lock.newCondition();
private Condition newSubmit = lock.newCondition();
//只定义不带参数的构造函数
public ReusableThread(){ }
/**
* 覆盖Thread类的run方法
*/
@Override
public void run() {
//这里必须是永远不结束的循环
lock.lock();
try{
while(true){
while(this.runTask==null)//提醒:==null不可省,java和C++不一样
newTask.await();
this.runTask.run();
this.runTask=null;
newSubmit.signalAll();
}
}
catch (InterruptedException e){
e.printStackTrace();
}
finally {
lock.unlock();
}
}
/**
* 提交新的任务
* @param task 要提交的任务
*/
public void submit(Runnable task) {
lock.lock();
try {
while(this.runTask!=null)
newSubmit.await();
this.runTask = task;
newTask.signalAll();
}
catch (InterruptedException e){
e.printStackTrace();
}
finally {
lock.unlock();
}
}
}
一个锁可以有多个条件对象,一个条件对象只有被是signal唤醒才能再次被执行,并且不能获得锁。