欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

《Java编程思想》第21章 并发(1)

程序员文章站 2022-05-07 08:41:09
...


看过之后觉得这一章显得杂乱。但是正如书中说的,学习并发编程就像进入一个全新的领域,本章的目的就是要让读者对并发的基础知识打下坚实的基础。

1 并发的多面性

用并发解决的问题大体上可以分为“速度”和“设计可管理型”两种。、

阻塞:如果程序中某个任务因为该程序控制范围之外的某些条件(通常是I/O)而导致不能继续执行,那么我们就说这个任务或线程阻塞了。

2 基本的线程机制

并发编程使我们可以将程序划分为多个分离的、独立运行的任务。通过使用多线程机制,这些独立任务(也被称为子任务)中的每一个都将执行线程来驱动。一个线程就是在进程中的一个单一的顺序控制流,因此,单个进城可以拥有多个 并发执行的任务。

  1. Runnable接口。要想定义任务,只需实现Runnable接口并编写run()方法。
public class LiftOff implements Runnable{
	protected int countDown = 10;
	private static int taskCount = 0;
	private final int id = taskCount++;
	public LiftOff(){}
	public LiftOff(int countDown){
		this.countDown = countDown;
	}
	public String status() {
		return "#"+id+"("+ (countDown > 0 ? countDown : "LiftOff!") + "), ";
	}

	@Override
	public void run() {
		while(countDown-- > 0){
			System.out.println(status());
			Thread.yield();
		}
		
	}

}
  1. Thread类。将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread构造器。调用Thread对象的start()方法为该线程执行必须的初始化操作,然后调用Runnable的run()方法,以便在这个新线程中启动该任务。
public class BasicThreads {

	public static void main(String[] args) {
		Thread t = new Thread(new LiftOff());
		t.start();
		System.out.println("Waiting for LiftOff");

	}
}
  1. 执行器(Executor)。java.util.concurrent包中的Executor将为你管理Thread对象,从而简化了并发编程。Executor允许你管理异步任务的执行,而无须显式地管理线程的生命周期。ExecutorService(具有服务生命周期的Executor,例如关闭)知道如何构建恰当的上下文来执行Runnable对象。ExecutorService对象是使用静态的Executer方法创建的,这个方法可以确定其Executor类型:
    ????CachedThreadPool。在程序中执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,因此它时合理的Executor的首选。
    ???? FixedThreadPool。使用了有限的线程集来执行所提交的任务。
    ????SingleThreadExecutor。如果向SingleThreadExecutor提交了多个任务,那么这些任务将排队,每个任务都会在下一个任务开始之前运行结束,所有的任务将使用相同的线程。
public class CachedThreadPool {

	public static void main(String[] args) {
		ExecutorService exec = Executors.newCachedThreadPool();
		for(int i = 0;i < 5;i++)
			exec.execute(new LiftOff());
		exec.shutdown();

	}
}
  1. Callable接口。Runnable是执行工作的独立任务,但是它不返回任何值。如果需要返回值可以实现Callable接口,Callable是一种具有类型参数的泛型,它的类型参数表示的是从方法call()中返回的值,并且必须使用ExecutorServiec.submit()方法调用它。submit()方法会产生Future对象。可以用isDone()方法来查询Future是否已经完成。当任务完成时,它具有一个结果,可以调用get()方法来获取该结果。
class TaskWithResult implements Callable<String>{
	private int id;
	public TaskWithResult(int id){
		this.id = id;
	}
	public String call(){
		return "result of TaskWithResult " + id;
	}
}

public class CallableDemo {

	public static void main(String[] args) {
		ExecutorService exec = Executors.newCachedThreadPool();
		ArrayList<Future<String>> results = new ArrayList<Future<String>>();
		for(int i=0;i<10;i++)
			results.add(exec.submit(new TaskWithResult(i)));
		for(Future<String> fs : results){
			try{
				System.out.println(fs.get());
			}catch(InterruptedException e){
				System.out.println(e);
				return;
			}catch(ExecutionException e){
				System.out.println(e);
			}finally{
				exec.shutdown(); 
			}
		}

	}
}
  1. sleep()休眠。影响任务行为的一种简单方法是调用sleep(),这将使任务中止执行给定的时间。对sleep()的调用可以抛出InterruptedException异常,并且可以在run()中被捕获。(TimeUnit.MILLISECONDS.sleep(500)????
public class SleepingTask extends LiftOff{
	public void run(){
		try{
			while(countDown-- > 0){
				System.out.println(status());
				TimeUnit.MILLISECONDS.sleep(100);
			}
		}catch(InterruptedException e){
			System.out.println("Interrupted");
		}
	}

	public static void main(String[] args) {
		ExecutorService exec = Executors.newCachedThreadPool();
		for(int i=0;i<5;i++)
			exec.execute(new SleepingTask());
		exec.shutdown();
	}

}

  1. 优先级。线程的优先级将该线程的重要性传递给了调度器。优先级较低的线程仅仅是执行的频率较低。
    ????‍♀️可以用getPriority()来读取现有的线程的优先级
    ????‍♀️并且在任何时刻都可以通过setPriority()来修改它。
public class SimplePriorities implements Runnable{
	
	private int countDown = 5;
	private volatile double d;
	private int priority;
	public SimplePriorities(int priority){
		this.priority = priority;
	}
	public String toString(){
	//可以在一个任务的内部,通过调用Thread.currentThread()来获得对驱该任务的Thread对象的引用。
		return Thread.currentThread() + ": "+countDown;
	}
	
	@Override
	public void run() {
		Thread.currentThread().setPriority(priority); //设置优先级
		while(true){
			for(int i=1;i<100000;i++){
				d += (Math.PI + Math.E)/(double)i;
				//if(i % 1000 == 0)
					//Thread.yield();
			}
			System.out.println(this);
			if(--countDown == 0)
				return;
		}
	}


	public static void main(String[] args) {
		ExecutorService exec = Executors.newCachedThreadPool();
		for(int i=0;i<5;i++){
			exec.execute(new SimplePriorities(Thread.MIN_PRIORITY)); //优先级的级别
		}
		exec.execute(new SimplePriorities(Thread.MAX_PRIORITY));
		exec.shutdown();
	}

}

  1. yield()让步。
  2. 后台(daemon)线程。所谓后台线程是指,在程序运行的时候在后台提供一种通用服务的线程,并且这种线程不属于程序中不可或缺的部分。因此,当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。比如,执行main()的就是一个非后台线程。
    ????‍♀️必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。
    ????‍♀️可以通过调用isDaemon()方法来确定线程是否是一个后台线程。
  3. 加入一个线程。一个线程可以在其他现呈上调用join()方法,其效果是等待一段时间直到第二个线程结束才继续执行。对join()的调用可以被中断,做法是在调用线程上调用interrrupt()方法,这时需要用到try-catch子句。
class Sleeper extends Thread{
	private int duration;
	public Sleeper(String name, int sleepTime){
		super(name);
		duration = sleepTime;
		start();
	}
	public void run(){
		try{
			sleep(duration);
		}catch(InterruptedException e){
			print(getName()+" was interrupted. "+"isInterrrupted(): "+isInterrupted());
			return;
		}
		print(getName()+" has awakened");
	}
}

class Joiner extends Thread {
	private Sleeper sleeper;
	public Joiner(String name, Sleeper sleeper){
		super(name);
		this.sleeper = sleeper;
		start();
	}
	public void run(){
		try{
			sleeper.join();//加入
		}catch(InterruptedException e){
			print(getName()+" was interrupted. "+"isInterrrupted(): "+isInterrupted());
		}
		print(getName() + " join completed");
	}
}

public class Joining {
	public static void main(String[] args) {
		Sleeper
			sleepy = new Sleeper("Sleepy",1500),
			grumpy = new Sleeper("Grumpy",1500);
		Joiner
			dopey = new Joiner("Dopey",sleepy),
			doc = new Joiner("doc",grumpy);
		grumpy.interrupt(); //中断

	}

}

3. 共享受限资源

  1. 解决资源共享竞争
    ????synchronized
    ????Biran的同步规则:如果你正在写一个变量,它可能接下来将被另一个程序读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且,读写线程都必须用相同的监视器锁同步。
    ????使用显示的Lock对象。java.util.concurrent类库还包含有定义在java.util.concurrent.locks中的显式的互斥机制。Lock对象必须被显式地创建、锁定和释放。相对于内建的synchronized锁来说,还赋予了你更细粒度的控制力。
public class MutexEvenGenerator extends IntGenerator{
	private int currentEvenValue = 0;
	private Lock lock = new ReentrantLock(); //创建
	
	public int next(){
		lock.lock(); //锁定
		try{
		++currentEvenValue;
		Thread.yield();
		++currentEvenValue;
		return currentEvenValue;
		}finally{
			lock.unlock();//释放
		}
	}


	public static void main(String[] args) {
		EvenChecker.test(new MutexEvenGenerator());

	}

}

  1. 原子性与易变性。volatile关键字。
  2. 原子类。AtomicIntefer、AtomicLong、AtomicReference等特殊的原子性变量类,它们提供了下面形式的原子性条件更新操作:boolean compareAndSet(expectedValue, updateValue);
  3. 临界区。有时,只是希望防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法。通过这种方式分离出来的代码被称为临界区,它也使用synchronized关键字建立。这里,synchronized被用来制定某个对象,此对象的锁被用来对花括号内的代码进行同步控制。这也被称为同步控制块;在进入此段代码前,必须得到syncObjec对象的锁。如果其他线程已经得到这个锁,那么就得等到锁被释放以后,才能进入临界区。
synchronized(syncObject) {
	//This code can be accessed by only one task at a time
}