JAVA总结(四)----- 线程(一)

注:以下程序和概念均参考自《java编程思想》、《Effective Java》、《java并发编程实战》

目录

一、何为并发

二、线程机制

三、java线程的简单使用

定义任务

①、使用Runnable接口定义任务

②、使用Callable接口定义任务

创建线程

①、继承Thread类创建线程

②、向Thread构造器传入Runnable引用

③、线程和任务的区别

线程池的使用

后台线程

①、通过调用Thread.setDaemon()设置后台线程

②、通过ThreadFactory编写定制的Thread属性(如后台、优先级、名称)

yeild()、sleep()、join()、优先级使用

①、yeild()方法

②、sleep()方法

③、join()方法

④、优先级


 

一、何为并发

  我们为什么要使用并发,必然是想提高程序的“运行速度”,可是使用多个“任务”执行程序这不是增加了处理器的开销(因为要进行上下文切换),所以使用顺序编程开销更小,速度更快。但一旦程序进行阻塞(或者因为I/O,或者因为网络)此时,程序将无法继续进行下去,这样的话开销岂不是更大?可一旦使用并发,程序的一部分任务若进行阻塞,那么也还有其他的部分还能运行,这个程序并没有就此终结。

  java编程语言使用线程实现并发,原因是:线程比进程更轻量级,使用较小的内存空间完成更多的任务。线程之间共享同一地址空间和所有可用数据的能力。


二、线程机制

  java线程调度机制是抢占式,这表示调度机制会周期性中断线程,将上下文切换到另一个线程,从而为每个线程都提供时间片,使得每个线程线程都会分配到数量合理的时间去驱动任务。

  java线程机制将程序划分为多个分离,独立运行的任务。通过使用多线程机制,这些独立的任务每一个都将由相应的执行线程进行驱动。


三、java线程的简单使用

  • 定义任务

①、使用Runnable接口定义任务

      Runnable接口是java常用定义任务的一个接口,实现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!") + ")" +  Thread.currentThread().getName() + ", ";
	}
	
	@Override
	public void run() {
		while(countDown-- > 0) {
			System.out.println(status());
			
			Thread.yield();
		}
	}
}

②、使用Callable接口定义任务

     Runnable是执行工作的独立任务,它没有任何返回值。而Callable接口允许你完成任务的时候能够返回一个值。

     Callable接受泛型参数,实现call()方法。使用ExecutorService.submit()方法调用实现该接口的任务,submit()方法

会产生一个Future对象,它将Callable返回的结果的特定类型进行参数化。然后可通过Future.get()获取。

import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

class TaskWithResult implements Callable {

	private int id;
	
	public TaskWithResult(int id) {
		this.id = id;
	}
	
	@Override
	public String call() throws Exception {
		return "result of TaskWithResult " + id;
	}
}

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

①、继承Thread类创建线程

     编写子类继承自Thread,重写run()方法。实例化它,再调用start()方法,便可以创建一个线程。

​
public class SimpleThread extends Thread {

	private int countDown = 5;
	private static int threadCount = 0;
	
	public SimpleThread() {
		super(Integer.toString(++threadCount));
		start();
	}

	public String toString() {
		return "#" + getName() + "(" + countDown + "), ";
	}
	
	@Override
	public void run() {
		while(true) {
			System.out.println(this);
			if(--countDown == 0)
				return ;
		}
	}

	public static void main(String[] args) {
		for(int i = 0; i < 5; i++)
			new SimpleThread();
	}

}

​

②、向Thread构造器传入Runnable引用

     通过Thread类的构造传递Runnable引用,并实例化该Thread对象也可以创建线程(注:LiftOff类在定义任务的第一个程序)

public class BasicThreads {

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

③、线程和任务的区别

     在我直观印象中,“线程就是任务”。可是事实并不是这样。 任务必须要依附在某个线程上,使得这个线程能够驱动任务,完成任务。这是第一点。线程是Thread类(或者通过线程池创建线程)无论哪一种,我们最终希望能够用线程做能做到的事,而不是控制线程(实际上我们也控制不了线程)。任务是实现Runnable接口的对象,这是我们真正可以控制,编写可用代码的部分。这是第二点。

     在实际创建线程上,常常通过创建Thread对象传递Runnable引用。而不是直接继承自Thread。原因有二、其一,浪费类的继承机会,线程-任务高耦合,这是我们不愿意看到的。

  • 线程池的使用

线程池是指管理一组同构工作线程的资源池。与工作队列结合紧密,其中工作队列保存了所有的等待执行的任务。工作者线程就从工作队列中获取一个任务,执行任务,然后返回线程池等待下一个任务。通过这种方式,“在线程池执行任务”比“为每个任务都创建一个线程”优势更大。通过重用线程池中线程,而不是重新创建线程,可以节约在线程创建和销毁过程中的巨大开销和避免任务等待而延迟任务的执行,从而提高响应性。

java.util.concurrent提供了这种灵活的线程池。它是Executor框架的一部分(Executor框架基于生产者-消费者模式,提交任务操作相当于生产者,执行任务的线程相当于消费者)这里列举java类库提供的几种线程即它们各自的特点(通过Executors静态工厂方法调用)

4种线程池的特点
newFixedThreadPool() newFixedThreadPool将创建一个固定长度的线程池,每当提交一个任务时就创建一个线程,直到达到线程池的最大数量,这时线程池的规模将不再变化。当有多于线程数量的任务,任务将会在工作队列中等待有工作线程空闲(如果某个线程由于发生了未预期的Exception而结束,那么线程池会补充一个新的线程)
newCachedThreadPool() newCachedThreadPool将创建一个可缓存的线程池,如果线程池的当前规模超过了处理需要时,那么将回收空闲的线程,而当需求增加是,则可以添加新的线程,线程池的规模不存在任何限制。
newSingleThreadExecutor() newSingleThreadExecutor是一个单线程的Executor,它创建单个工作者线程来执行任务,如果这个线程异常结束,会创建另外一个线程来代替。newSingleThreadExecutor能确保任务在队列中的顺序来串行执行
newScheduledThreadPool() newScheduledThreadPool创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务。

线程池的简单使用

    下面程序中创建5个固定大小的newFixedThreadPool()(返回ExecutorService对象),并且执行7个任务。随后main线程调用shutdown()方法,该方法的作用是平缓关闭线程池----不再接受新的任务,同时等待已经提交的任务执行完成。在随后的在接受线程池的方法

public class FixedThreadPool {

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

 

  • 后台线程

后台(Daemon)线程是指在程序运行时在后台提供一种“通用服务”的线程,并且这种线程并不属于程序中不可缺少的一部分。所以,当所有的非后台线程结束时,程序也就终止,同时会杀死所有后台线程。反过来说,只要程序中还有非后台线程运行,程序就不会终止,后台线程也不会死亡。

①、通过调用Thread.setDaemon()设置后台线程

    在该程序中,通过创建10个线程,并在线程启动前全部设置成后台线程。

public class SimpleDaemons implements Runnable {

	@Override
	public void run() {
		try {
			while(true) {
				TimeUnit.MILLISECONDS.sleep(100);
				System.out.println(Thread.currentThread() + " " + this);
			}
		} catch(InterruptedException e) {
			System.out.println("sleep() interrupted");
		}
	}
	
	public static void main(String [] args) throws InterruptedException {
		for(int i = 0; i < 10; i++) {
			Thread daemon = new Thread(new SimpleDaemons());
			daemon.setDaemon(true);
			daemon.start();
		}
		System.out.println("All daemons started");
		TimeUnit.MILLISECONDS.sleep(100);
	}
}

 

②、通过ThreadFactory编写定制的Thread属性(如后台、优先级、名称)

    实现ThreadFactory接口,重写newThread()方法,即可产生定制的Thread。再将其作为参数传递给线程池(Executors.newCachedThreadPool(实现ThreadFactory接口的实例)),就可以产生定制的Thread线程

// 定制线程池的线程样式,这里定制了线程都是后台线程
public class DaemonThreadFactory implements ThreadFactory {

	@Override
	public Thread newThread(Runnable r) {
		Thread t = new Thread(r);
		t.setDaemon(true);
		return t;
	}
}
public class DaemonFromFactory implements Runnable {

	@Override
	public void run() {
		try {
			while(true) {
				TimeUnit.MILLISECONDS.sleep(100);
				System.out.println(Thread.currentThread() + " " + this);
			}
		} catch(InterruptedException e) {
			System.out.println(e);
		}
	}
	
	public static void main(String [] args) throws InterruptedException {
        // 创建具有定制线程的线程池
		ExecutorService exec = Executors.newCachedThreadPool(new DaemonThreadFactory());
		for(int i = 0; i < 10; i++)
            // 线程池执行10个任务 
			exec.execute(new DaemonFromFactory());
		System.out.println("All daemons started");
		TimeUnit.MICROSECONDS.sleep(100000);
	}
}

 ③、通过调用Thread.isDaemon()判断线程是否为后台线程

    Daemon线程被设置成后台线程,从该线程派生的子线程,尽管没有显示设置为后台线程,但也被隐式的设置成后台线程。

class Daemon implements Runnable {
	private Thread[] t = new Thread[10];
	@Override
	public void run() {
		for(int i = 0; i < t.length; i++) {
			t[i] = new Thread(new DaemonSpawn());
			t[i].start();
			System.out.println("DaemonSpanw" + i + " started");
		}
		
		for(int i = 0; i < t.length; i++)
			System.out.println("t[" + i + "].isDaemon() = " + t[i].isDaemon() + ", ");
		while(true)
			Thread.yield();
	}
}

class DaemonSpawn implements Runnable {

	@Override
	public void run() {
		while(true)
			Thread.yield();
	}
	
}

public class Daemons {

	public static void main(String[] args) throws InterruptedException {
		Thread d = new Thread(new Daemon());
		d.setDaemon(true);
		d.start();
		System.out.println("d.isDaemon() = " + d.isDaemon() + ", ");
		// 由Daemons线程产出DaemonSpawn任务的线程
		// 这些线程并没有显示的设置成后台线程,但因为Daemons是后台线程,所以他们也是后台线程
		TimeUnit.MICROSECONDS.sleep(10000);
	}
}

  • yeild()、sleep()、join()、优先级使用

①、yeild()方法

    yeild()方法表示线程让步,指当前线程的任务已经做得差不多,可以让别的线程获得CPU使用权。(但这只是一个“暗示”,实际上没有任何机制可以保证它将会被采纳)

    当调用yeild()方法时,只是在做建议具有“相同优先级”的其他线程可以运行。总之,对于任何重要的控制或者调用整体运用时,最好不要依赖于yeild()

②、sleep()方法

    sleep()方法可以让线程休眠指定的时间(在这段时间内,任务不会被执行)。当休眠时,它不会释放当前线程获取的对象锁。并且线程会处于阻塞状态。对sleep()的调用将会抛出InterruptedException异常。(线程调用抛出的异常,并不会回到main()中,因为异常不能跨线程传播。)

    对于sleep()方法的调用,采用的是JavaSE5引入的TimeUnit类的sleep()版本,而不推荐使用Thread.sleep()方法。就如下面这样

public class SleepingTask extends LiftOff {

	@Override
	public void run() {
		try {
			while(countDown-- > 0) {
				System.out.println(status());
				// 新的线程休眠的方法
				TimeUnit.MILLISECONDS.sleep(100);
			}
		} catch(InterruptedException e) {
			System.err.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();
	}
}

③、join()方法

    一个线程可以在其它线程之上调用join()方法,其效果是等待一段时间直到第二个线程结束才继续执行。

    如果某个线程在另一个线程t调用t.join(),此线程被挂起,直到目标线程t结束才恢复

class Sleeper extends Thread {
	private int duration;
	public Sleeper(String name, int sleepTime) {
		super(name);
		duration = sleepTime;
		start();
	}
	@Override
	public void run() {
		try {
			sleep(duration);
		} catch(InterruptedException e) {
			// 当线程被挂起时,catch将会捕捉到isInterupted中断标志,随后进行语句块中,并且消除这个标志,因此catch语句块中的isInterrupted标志都为false
			System.out.println(getName() + " was interrupted. " + "isInterrupted(): " + isInterrupted());
			return ;
		}
		System.out.println(getName() + " has awakened");
	}
}

class Joiner extends Thread {
	private Sleeper sleeper;
	public Joiner(String name, Sleeper sleeper) {
		super(name);
		this.sleeper = sleeper;
		start();
	}
	@Override
	public void run() {
		try {
			// 在joiner线程调用sleeper线程的join方法(),将会导致,当前(joiner线程)被挂起,而sleepr将会被唤醒(正常的话则继续执行)
			// 然后等待sleeper线程执行结束才会继续执行joiner线程
			sleeper.join();
		} catch(InterruptedException e) {
			System.out.println("Interrupted");
		}
		System.out.println(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();
	}
}

    *结果说明:由于Grumpy线程调用中断方法,因此导致该线程直接被中断结束。而与之相关的Doc线程调用Grumpy线程的join()方法也不会导致自己被挂起。直接完成。

    在Dopey线程中由于调用了Sleepy线程的join()方法,导致自己被挂起,原来睡眠的Sleepy线程被唤醒。继续执行直至死亡,此时Dopey线程才得到运行。

④、优先级

    线程的优先级将线程的重要性传递给调度器。然而,这并不会意味着优先级低就不能执行,仅仅是执行的频率较低。在大多时候,所有线程都应该按照默认的优先级运行。操作线程优先级通常是一种错误的做法。

    设置线程优先级通常通过Thread.setPriority()来修改它,Thread.getPriority()获取线程的优先级。java中总共有10个优先级可以设置。

public class SimplePriorities implements Runnable {

	private int countDown = 5;
	private volatile double d;
	private int priority;
	
	public SimplePriorities() {
		
	}
	
	public SimplePriorities(int priority) {
		this.priority = priority;
	}
	
	public String toString() {
		return Thread.currentThread() + ": " + countDown;
	}
	
	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();
	}

	@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 ;
		}
	}
}

 

你可能感兴趣的:(JAVA总结(四)----- 线程(一))