201521123091 《Java程序设计》第11周学习总结

Java 第十一周总结

第十一周的作业。

目录
1.本章学习总结
2.Java Q&A
3.码云上代码提交记录及PTA实验总结
4.课后阅读


1.本章学习总结

1.1 以你喜欢的方式(思维导图或其他)归纳总结多线程相关内容。

  1. Java的并发类库中有显式的互斥机制,即Lock。Lock对象要被显式创建、锁定和释放。
  2. 生产者和消费者模型是线程协作当中比较基础的模型。需要考虑到两个线程之间的通信,还要考虑到在特定情况(例如仓库满或者仓库空)线程应该处于不同的状态。
  3. Condition对象用来管理任务间的通信,这个对象不包含任何有关处理状态的信息。
  4. BlockingQueue是任务交互的高级实现形式,而且相比于wait()和notify()方法要更易于操作。
  5. 某个任务在等待另一个任务,而后者又在等待别的任务,这样一直下去,直到这个链条上的任务又在等待第一个任务释放锁。得到了任务之间相互等待的连续循环,没有哪个线程能运作,就是死锁
  6. 读写锁可以允许同时有多个读取者,只要他们都不试图写入即可。

在课后阅读那边整理了更多更加详细的知识点。


2.Java Q&A

1. 互斥访问与同步访问

1.1 除了使用synchronized修饰方法实现互斥同步访问,还有什么办法实现互斥同步访问(请出现相关代码)?

  使用显式的Lock和Condition对象

class Account {
	private int balance;

	public Account(int balance) {
		super();
		this.balance = balance;
	}
	public int getBalance() {
		return balance;
	}
	public void deposit(int money) {
		lock.lock();
		try {
			balance += money;
			condition.signalAll();
		} finally {
			// TODO: handle finally clause
			lock.unlock();
		}
	}
	public void withdraw(int money) {
		lock.lock();
		try {
			while (balance < money) {
				try {
					condition.await();
				} catch (InterruptedException e) {
					throw new RuntimeException(e);
				}
			}
			balance -= money;
		} finally {
			// TODO: handle finally clause
			lock.unlock();
		}
	}
	private java.util.concurrent.locks.Lock lock = new java.util.concurrent.locks.ReentrantLock();
	private java.util.concurrent.locks.Condition condition = lock.newCondition();
}

  使用同步代码块:

class Account {
	private int balance;

	public Account(int balance) {
		super();
		this.balance = balance;
	}
	public int getBalance() {
		synchronized (this) {
			return balance;
		}
		
	}
	public void deposit(int money) {
		synchronized (this) {
			balance += money;
		}	
	}
	public void withdraw(int money) {
		synchronized (this) {
			balance -= money;
		}	
	}
}

1.2 同步代码块与同步方法有何区别?

有时,你只是希望防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法。通过这种方式分离出来的代码段被称为临界区,它也使用synchronized关键字建立。

  这个就是同步代码块,只要线程获得该对象参数中的锁,就可以进入临界区。

通过使用同步控制块,而不是对整个方法进行同步控制,可以使多个任务访问对象的时间性能得到显著提高。


1.3 实现互斥访问的原理是什么?请使用对象锁概念并结合相应的代码块进行说明。当程序执行synchronized同步代码块或者同步方法时,线程的状态是怎么变化的?

  原理是当资源被一个任务使用时,在其上加锁。现在在访问某项资源的任务必须锁定这种资源,使其他任务无法访问它直至这个资源被解锁。在其被解锁时,就会有另一个任务可以锁定并且使用该资源,以此类推。

class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}

来源:Thread Interference

  假设有线程A调用自增方法,同时线程B调用自减方法,然后c的初始值为0。
  然后操作步骤如下:

  1. 线程A读取c的值,为0
  2. 线程B读取c的值,为0
  3. 线程A在取到的值上加1,结果为1
  4. 线程B在取到的值上减1,结果为-1
  5. 线程A将结果存回c,c现在为1
  6. 线程B将结果存回c,c现在为-1

  可以看到,并没有像我们所期望的那样,c的值为0.线程A的值因为被B的值覆盖而丢失了。有时候也可能是B的值丢失了,或者是没有任何错误,这都是预料不到的。
  所以我们可以使用synchronized来给这个c加上一把锁。那么就会按照这样的步骤进行。

  1. 线程A开始工作,这时c被加锁;读取c的值,为0
  2. 线程A在取到的值上加1,结果为1
  3. 线程A将结果存回c,c现在为1;c上的锁释放
  4. 线程B获得c上的锁,开始工作。读取c的值,为1
  5. 线程B在取到的值上减1,结果为0
  6. 线程B将结果存回c,c现在为0;c上的锁释放

  如果线程没有取得对象锁,并且试图执行synchronized方法或者是代码块,那么就会进入Blocked,在取得对象锁后,会先回到Runnable,然后等待线程调度器将其排入Running。


1.4 Java多线程中使用什么关键字实现线程之间的通信,进而实现线程的协同工作?为什么同步访问一般都要放到synchronized方法或者代码块中?

  Object的wait()、notify()可以用来实现线程之间的协作,Java SE5的并发类库中还提供了具有await()和signal()方法的Condition对象。

  同步访问放到synchronized方法或者代码块中是为了防止多个线程访问同一资源所引起的冲突。


2.交替执行

2.1 题目4-6,关键代码截图



2.2 实验总结

要实现交替访问,首先要将方法全部都用synchronized来修饰,确保所有变量都持有锁。其次线程之间的协作可以使用wait()和notify()并配合上boolean变量来完成。任务1执行完后,改变布尔值,并唤醒另一个任务(本例中一共只有两个任务,所以用notify()即可),然后开始执行任务2,任务2执行完后,改变boolean变量,唤醒任务1,这样交替执行下去直至两个任务都不能执行为止(这边就是等到字符串全部输出为止)。


3.互斥访问

3.1 修改TestUnSynchronizedThread.java源代码使其可以同步访问。(关键代码截图,需出现学号)


3.2 进一步使用执行器修改改进相应代码。(关键代码截图,需出现学号)

  为了使用执行器也能够做到join()的效果,所以上网搜到了下面三种方法:
  方法1:使用CountDownLatch,一开始设置成要完成的总任务数,一个任务完成之后,就调用countDown()方法来减小这个数值,main线程调用await()方法,直到数值降为0时,才继续运行。

  方法2:使用invokeAll方法,只有在所有任务都完成,才会返回

  方法3:shutdown()和awaitTermination()方法结合起来一起用:

How to wait for all threads to finish, using ExecutorService?


4.线程间的合作:生产者消费者问题

编程思想上的

4.1 运行MyProducerConsumerTest.java。正常运行结果应该是仓库还剩0个货物。多运行几次,观察结果,并回答:结果正常吗?哪里不正常?为什么?

  不正常,有时候会出现10个货物。因为生产者与消费者的存取速度不一致,所以可能生产者生产的一部分物品有浪费或者是消费者一部分的消费需求有浪费。


4.2 使用synchronized,wait,notify解决该问题(关键代码截图,需出现学号)


4.3 选做:使用Lock与Condition对象解决该问题。



5.查询资料回答:什么是线程安全?(用自己的话与代码总结,写自己看的懂的作业)

Thread safety is a computer programming concept applicable to multi-threaded code. Thread-safe code only manipulates shared data structures in a manner that guarantees safe execution by multiple threads. There are various strategies for making thread-safe data structures.

  只有涉及到多线程的共享资源使用,才会涉及到线程安全的问题。

class Counter {
    private int i = 0;

    public synchronized void inc() {
        i++;
    }
}

  这就是一个线程安全的Java代码,方法使用synchronized关键字,就可以确保一次只能有一个线程在访问共享资源,而不是多个线程同时访问共享资源,这样就可以避免线程的操作对其他线程不可见,还有就是线程执行不确定的问题。
  线程安全通常还要注意避免出现死锁,还有对并发的性能进行优化。
  关于线程安全和非线程安全的问题,非线程安全并非是不安全。如果我们只有一个线程在工作的话,非线程安全是更好的选择,因为线程安全使用synchronized,性能会有一定的降低。就像我们一般用列表的容器,自然而然地就会想到使用ArratList,虽然它并不是线程安全的,但是我们也可以给它加上锁。相比于ArrayList,Vector是线程安全的,但是Vector应该被弃用,如果想要达到线程安全的效果,也有专门为线程安全设计的容器,比如CopyOnWriteArrayList。


6.实验总结

6.1 4-8(CountDownLatch)

采用线程池实现多线程一:没有返回值

CountDownLatch latch = new CountDownLatch(n);
ExecutorService exec = Executors.newFixedThreadPool(poolSize);
for (int i = 0; i < n; i++) {
    exec.execute(new MyTask(latch));
}

latch
英[lætʃ]
n.门闩

  CountDownLatch强制任务等待由其他任务执行的一组操作完成。
首先向CountDownLatch对象设置一个初始的数值,在这个对象上调用await()方法的都将进入阻塞状态,直至这个计数值降为0。其他任务在结束工作的时候,可以调用countDown()方法来减小这个计数值。
  要创建一个固定线程数的线程池,只要调用Executors的工厂方法newFixedThreadPool()就可以了,然后参数指定为需要设定的线程数即可。


6.2 4-9(集合同步问题)

Collections.synchronizedList(new ArrayList());

  synchronizedList会返回当前列表的线程安全的synchronized版本,但是这个线程安全的范围局限于本身这个容器提供的方法。当我们组合使用这些方法,或者使用迭代器的时候,还是需要加上synchronized来保证线程安全。
  就像这样:

List list = Collections.synchronizedList(new ArrayList());
...
synchronized (list) {
  Iterator i = list.iterator(); // Must be in synchronized block
  while (i.hasNext())
      foo(i.next());
}

  如果迭代的时候没有使用synchronized,同样会引发不确定的行为。
Collections API


6.3 较难:4-10(Callable),并回答为什么有Runnable了还需要Callable。

A task that returns a result and may throw an exception. Implementors define a single method with no arguments called call.

The Callable interface is similar to Runnable, in that both are designed for classes whose instances are potentially executed by another thread. A Runnable, however, does not return a result and cannot throw a checked exception.

The Executors class contains utility methods to convert from other common forms to Callable classes.

  JDK文档都已经说的很明白了,Callable有返回值,而且可以抛受检异常。
  而且ExecutorService的invoke***()方法的参数必须是Callable的容器。


7.选做:使用其他方法解决题目4的生产者消费者问题。

7.1 使用BlockingQueue解决生产者消费者问题,关键代码截图


7.2 说明为什么不需要显示的使用wait、notify就可以解决同步问题。这样解决相比较wait、notify有什么优点吗?

  因为用了个比wait和notify更高级的方式来解决线程协作的问题。就是同步队列。同步队列每个时刻都只允许一个任务插入或者移除元素。BlockingQueue提供了这个队列,而且有大量的标准实现。LinkedBlockingQueue是一个无界队列,上面的代码使用的是ArrayBlockingQueue,具有固定的尺寸。如果队列满,就阻塞put()的操作;如果队列空,就阻塞take()的操作。
  优点就是比wait()和notify()简单。


7.3 使用Condition解决生产者、消费者问题。

(见题4.3)


8.编写一段代码,证明你会使用ForkJoinPool

import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveAction;

class QuickSort extends RecursiveAction {

	private final int[] A;
	private final int p;
	private final int r;
	
	public QuickSort(int[] A, int p, int r) {
		// TODO Auto-generated constructor stub
		this.A = A;
		this.p = p;
		this.r = r;
	}
	
	@Override
	protected void compute() {
		// TODO Auto-generated method stub
		if (p < r) {
			int q = partition(A, p, r);
			ArrayList<QuickSort> quickSorts = new ArrayList<>();
			quickSorts.add(new QuickSort(A, p, q - 1));
			quickSorts.add(new QuickSort(A, q + 1, r));
			ForkJoinTask.invokeAll(quickSorts);
		}
		
	}
	
	private int partition(int[] A, int p, int r) {
		int x = A[r];
		int i = p - 1;
		for (int j = p; j < r; j++) {
			if (A[j] <= x) {
				i++;
				swap(A, i, j);
			}
		}
		swap(A, i + 1, r);
		return i + 1;
	}
	
	
	private void swap(int[] A, int i, int j) {
		if (i != j) {
			int tmp = A[i];
			A[i] = A[j];
			A[j] = tmp;
		}
	}
	
}

public class Main {
	public static void main(String[] args) {
		ForkJoinPool forkJoinPool = new ForkJoinPool();
		int[] A = new int[20];
		for (int i = 0; i < 20; i++) {
			A[i] = (int)(Math.random() * 100);
		}
		QuickSort quickSort = new QuickSort(A, 0, A.length - 1);
		forkJoinPool.invoke(quickSort);
		
		System.out.println(Arrays.toString(A));
	}
	
}

3.码云上代码提交记录

题目集:多线程(4-4到4-10)

3.1 码云代码提交记录

  在码云的项目中,依次选择“统计-Commits历史-设置时间段”, 然后搜索并截图
  
  

3.2 截图多线程PTA提交列表


4. 课外阅读

4.1 Questions and Exercises: Concurrency,学习总结。

Questions

1.Can you pass a Thread object to Executor.execute? Would such an invocation make sense?
可以是可以。但是不好。

However it doesn't make sense to use Thread objects this way. If the object is directly instantiated from Thread, its run method doesn't do anything. You can define a subclass of Thread with a useful run method — but such a class would implement features that the executor would not use.

首先前半句好理解,如果是直接继承自Thread类的话,那得到的就是空的run()方法,没什么用。后面半句的意思是可以继承Thread类,写一个有用的run()方法,但是可能会有一些执行器用不了的特性。可能是Thread的子类又包含了其他的方法,但是execute只能够用上run()方法。

1.Exercise: Compile and run BadThreads.java:
The application should print out "Mares do eat oats." Is it guaranteed to always do this? If not, why not? Would it help to change the parameters of the two invocations of Sleep? How would you guarantee that all changes to message will be visible to the main thread?
一开始看到这个问题的时候还楞了一下,以为不会总是输出"Mares do eat oats.",后来试了十几次,一直都是,再去看看答案,原来真的是一直输出,被问法坑住了。

In Java specifically, a happens-before relationship is a guarantee that memory written to by statement A is visible to statement B, that is, that statement A completes its write before statement B starts its read.

The happened-before relation is formally defined as the least strict partial order on events such that.
这边讲到了一个happens-before的关系,这是一个偏序关系。如果A happens-before B,就说明A操作时候在工作内存上所产生的影响对于线程B来说都是可见的。但是happens-before指的是可见性上的先后关系,并不是时间上的。针对这个问题有三种解法:

  1. 在主线程当中保留CorrectorThread实例的引用。然后在要读取message之前调用join()方法,因为线程线程当中的操作都happen-before对此线程的终止操作。
  2. 涉及到message的方法都加上synchronized关键字,而且只通过这些方法来操作message。
  3. 使用volatile关键字。以为对一个volatile变量的写操作happen—before后面对该变量的读操作。

4.2 Java多线程之Executor、ExecutorService、Executors、Callable、Future与FutureTask

前面几个概念我在4.4都好好地整理过了,FutureTask4.4没有,这边整理一下这个。

  1. FutureTask实现了RunnableFuture接口,看名字就知道,既有Runnable的特性,可以跑,也可以得到Callable的返回值。即可以当作一个有返回值的Runnable任务来用。
  2. ExecutorService也可以接受ExecutorService为入参。

4.3 线程池,这一篇或许就够了

  1. 创建和销毁线程需要一定的系统开销,线程池可以缓存线程,使用闲置线程来执行新任务避免不必要的系统开销。
  2. 运用线程池可以有效控制最大并发数。
  3. 可以对线程进行简单的管理,比如延时执行或者循环执行之类的。
  4. CachedThreadPool是可缓存的线程池,对于线程数没有限制,可以充分运用闲置的线程,减小开销。
  5. FixedThreadPool可以决定线程池的最大容量(即同时执行的线程数),剩下的线程就将等待。

4.4 Java 8 Concurrency Tutorial: Threads and Executors

  1. 可以用lamda表达式来实现一个Runnable接口,比如Runnable task = (args) -> {do sth};这样的。
  2. 并发执行程序的顺序存在不确定性(non-deterministic),因此在大的应用上面编写复杂的并发程序是一件很复杂的事情。
  3. 并发的API引入了ExecutorService来进行线程的运行和管理,我们不需要手动的建立新的线程。Executors类提供了创建不同类型ExecutorService的工厂方法。newSingleThreadExecutor是一个容量仅为1的线程池。
  4. ExecutorService提供两种停止线程的方法,一种是shutdown()方法,等待现在正在跑的任务结束,另一种是shutdownNow(),会立即终止所有正在运行的任务。
  5. Callable和Runnable的最大区别,就是前者有返回值,后者没有。
  6. Callable和Runnable一样可以被提交给执行器。但是submit()方法不会等任务完成,执行器会返回一种特殊的类型就是Future,可以在之后的时间中得到真正的结果。调用Future的get()方法会阻塞并且知道Callable执行完毕。
  7. 如果Callable运行时间超出我们的期望,我们可以让程序抛出TimeoutException来提前结束。
  8. Executors支持批量提交Callable,接受一个Callable的容器,并返回Future的列表,这就是invokeAll()方法。invokeAny()方法与
    invokeAll()不同的是,它只会返回一组Callable中最快结束运算的结果,然后就结束。
  9. ScheduledExecutorService支持定时及周期性任务执行。调度一个任务或得到特殊的返回值类型ScheduledFuture,在Future的基础上又加了getDelay()方法去获得剩余的延迟时间。有两种方法可以使任务周期性执行:scheduleAtFixedRate()和scheduleWithFixedDelay()。前者是不会考虑任务的具体执行时间的,只要周期一到,就新开一个任务,这样线程池可能很快就会被占满;后者是等待的周期时间是介于前一任务完成和后一任务开始的间隔。

看的不过瘾的请点下面
回到顶部


原文地址:https://www.cnblogs.com/ljl36/p/6813556.html