1. 本周学习总结
**1.1 以你喜欢的方式(思维导图或其他)归纳总结多线程相关内容。 **
2. 书面作业
本次PTA作业题集多线程
**1.互斥访问与同步访问 **
完成题集4-4(互斥访问)与4-5(同步访问)
**1.1 除了使用synchronized修饰方法实现互斥同步访问,还有什么办法实现互斥同步访问(请出现相关代码)? **
- 可以使用synchronized代码块
public static void addId() {
synchronized (Counter.class) {//代表Counter类型的对象
id++;
}
}
- 还可以使用Lock与Condition对象方法
public class Bank
{
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();
}
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;
}
private final double[] accounts;
private Lock bankLock;
private Condition sufficientFunds;
}
**1.2 同步代码块与同步方法有何区别? **
区别在于:
- 同步方法直接在方法上使用synchronized修饰,实现加锁;而同步代码块则在方法内部使用synchronized代码块,实现加锁。
- 在所加的锁的作用范围来说,同步方法的范围比较大,而同步代码块的范围会更小一些。
- 对比两个的性能来说,同步代码块的性能更好些。因为一般同步的范围越大,性能就越差,而同步方法的范围比同步代码块的范围要大。
**1.3 实现互斥访问的原理是什么?请使用对象锁概念并结合相应的代码块进行说明。当程序执行synchronized同步代码块或者同步方法时,线程的状态是怎么变化的? **
class MyCounter{
private int i = 0;
public void increment(){
//i++;
synchronized (this) {
i++;
}
}
……
}
- 实现互斥访问的原理是:在程序中,每个对象都有一把锁,只有获得了对象锁,才能执行相应的synchronized代码块或者方法。
例如:在上述的代码段中,可以获得对象“this”上的内部锁。当执行程序时,如果不能获得相对应的对象锁,那么将不能执行上述代码块中的内容,即synchronized同步代码块中的i++语句,就必须等待,等待获得对象锁。从而通过对象锁实现了互斥访问。 - 线程的状态的变化为:如果没有获得对象锁就进入
Look Pool
状态,等待同步锁被释放;同步锁释放后,线程进入Runnable
状态。
**1.4 Java多线程中使用什么关键字实现线程之间的通信,进而实现线程的协同工作?为什么同步访问一般都要放到synchronized方法或者代码块中? **
- Java多线程中使用wait()和notify()/notifyAll()方法来实现线程之间的通信,进而实现线程的协同工作。
- 因为wait()方法和notify()/notifyAll()方法都只能在被声明为synchronized的方法或代码段中调用。
**2.交替执行 **
**实验总结(不管有没有做出来) **
本题是要我们写入一串字符串,然后在构造函数中将字符串以空格为分隔写入另一个字符数组作为任务,并且任务完成后要将任务删除,则相应的需要将任务数减去已经执行的任务数量。因为题目要求两个线程是交替执行的,所以我们需要判断当前的任务是应该由谁来进行执行的。可以使用flag来进行判断,当flag==true时由线程1执行,输出结果并将flag赋值为false和使用notify()唤醒其他线程。当flag==false时,同理。
**3.互斥访问 **
**3.1 修改TestUnSynchronizedThread.java源代码使其可以同步访问。(关键代码截图,需出现学号) **
**3.2 进一步使用执行器改进相应代码(关键代码截图,需出现学号) **
参考资料:Java多线程之Executor、ExecutorService、Executors、Callable、Future与FutureTask
**4.线程间的合作:生产者消费者问题 **
**4.1 运行MyProducerConsumerTest.java。正常运行结果应该是仓库还剩0个货物。多运行几次,观察结果,并回答:结果正常吗?哪里不正常?为什么? **
- 结果不正常。
- 有时候仓库剩余的货物个数不为0个。
- 因为该程序使用的是多线程,而多线程除了互斥问题外,还有同步问题。在本题的程序代码中,使用了synchronized方法,实现了互斥访问,但是并没有对两个相互相交线程之间的运行进度进行协调,导致有可能出现货装进仓库太多了,来不及取的情况。
**4.2 使用synchronized, wait, notify解决该问题(关键代码截图,需出现学号) **
**4.3 选做:使用Lock与Condition对象解决该问题。 **
**5.查询资料回答:什么是线程安全?(用自己的话与代码总结,写自己看的懂的作业) **
- 线程安全就是如果代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码,但是实际上代码每次的运行结果和在单线程时运行的结果是一样的,而且其他的变量的值也和预期的是一样的。
- 例如:两个线程a、b同时执行如下语句,假设id的初值为2
public static void addId() {
id++;
}
- 运行时,a线程读取i的值为2,并将2++得到3,这时候还未来得及将3写回id
- 这时候b读取id的值还为2,并将2++得到3,
- 然后a,b依次把他们的值(3)写回id,那么id最终值为3而不是4
所以,我们需要用到synchronized关键字,在addId方法前加上synchronized声明,将线程不安全转变为线程安全。
public static synchronized void addId() {
id++;
}
**6.选做:实验总结 **
**6.1 4-8(CountDownLatch)实验总结 **
CountDownLatch是一个同步工具类,它的作用主要是使线程等待,等待其他线程完成后再执行。程序中我们建立CountDownLatch对象,且程序中的线程数为poolSize,所以我们要写入调用次数poolSize。使用for循环时,应注意判断条件不为任务数n,应为线程数poolSize,且因为MyTask类的构造函数为public MyTask(CountDownLatch latch),所以我们将子线程加进线程池执行时MyTask的“()”中为CountDownLatch的变量名latch。
**6.2 4-9(集合同步问题)实验总结 **
因为List不是线程安全的,所以我们需要将其转化,获得同步集合。一般来说,我们可以使用Collections工具类提供的synchronizedCollection方法来获得同步集合。即在本题中我们只需要使用synchronizedList方法就可以了。
**6.3 较难:4-10(Callable),并回答为什么有Runnable了还需要Callable?实验总结。 **
- 因为在Runnable接口中的public void run()方法是无返回值的,如果我们希望线程运算后将结果返回,这时候使用Runnable是不能够实现我们的需求的。所以这时候我们就需要使用到Callable了。Callable代表有返回值的任务,能够将我们需要的结果返回。
- 实验总结:CalculateTask类实现了Callable接口,且希望返回n的斐波那契值数值,所以我们需要使用
Callable<Integer>
和Callable接口的call()方法来返回所需的结果。因为输入的n是为个数,所以我们需要在call()方法中编写输出斐波那契值数值的代码。然后在main函数中使用到了线程池,List列表和Future接口,运行任务后,将计算结果写入List列表中,再遍历求和,最后要记得使用exec.shutdown();
来拒绝任务继续输入。
**7.选做:使用其他方法解决题目4的生产者消费者问题。 **
**7.1 使用BlockingQueue解决生产者消费者问题关键代码截图 **
**7.2 说明为什么不需要显示的使用wait、notify就可以解决同步问题。这样解决相比较wait、notify有什么优点吗? **
- 因为BlockingQueue是线程安全的队列类,是阻塞队列。常用的队列主要分为两中:先进先出和后进先出,每个时刻只允许一个任务插入或者删除,保证了线程安全。并且队列会在队空和队满的情况下,自动阻塞,在队满后有空位出现时,线程会被自动唤醒。所以不需要显示的使用wait、notify就可以解决同步问题。
- 相比之下,比wait、notify更加简单,方便。因为当我们使用了BlockingQueue就再也不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程了,这一切BlockingQueue都可以自动完成。
**7.3 使用Condition解决生产者、消费者问题。 **
**8.选做:编写一段代码,证明你会使用ForkJoinPool? **
package keke;
import java.util.Scanner;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;
class Task extends RecursiveTask<Integer>{
private int n;
public Task(int num) {
this.n=num;
}
@Override
protected Integer compute() {
if(n==100){
return n*n;
}
Task newtask=new Task(n+1);
newtask.fork();
return n*n+newtask.join();
}
}
public class Mainboke08 {
public static void main(String[] args) {
Scanner in=new Scanner(System.in);
System.out.println("输入数字num(num<=100):");
int num=in.nextInt();
if(num<100)
System.out.println("计算从"+num+"*"+num+"+...+100*100:");
else if(num>100){
System.out.println("输入的数>100了,请重新输入!");
num=in.nextInt();
}
else
System.out.println("计算"+num+"*"+num+":");
Task task=new Task(num);
ForkJoinPool forkJoinPool = new ForkJoinPool();
Future<Integer> result = forkJoinPool.submit(task);
try {
System.out.println("结果为:"+result.get());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
forkJoinPool.shutdown();
}
}
3. 码云上代码提交记录
题目集:多线程(4-4到4-10)
**3.1. 码云代码提交记录 **
在码云的项目中,依次选择“统计-Commits历史-设置时间段”, 然后搜索并截图