Thinking in Java——第二十一章-( 一)并发&Java中的多线程
2016-09-01 14:43
399 查看
实现并发的方式有很多种,我们主要来学习Java中的多线程~
Java的线程机制:时间片轮转+优先级Java多线程的调度策略
定义任务
线程可以驱动任务,因此你需要一种描述任务的方式,这可以由Runnable接口来提供。要想定义任务,只需实现Runnable接口并编写run()方法,使得该任务可以执行你的命令。例如,下面的LiftOff任务将显示发射之前的倒计时:
Thread类
Thread 构造器只需要一个Runnable对象。调用Thread对象的start方法为该线程执行必要的初始化操作,然后调用Runnable中的run()方法,以便在这个新线程中启动该任务,尽管start()方法看起来产生了一个对长期运行方法的调用,但是从输出中可以看出,start()迅速返回了。你可以很容易的添加更多的线程去驱动更多的任务。下面,你可以看到所有任务彼此之间是如何呼应的
如果把LiftOff中的run()方法中的Thread.yield()删掉的话输出是这样的
看来分给每个线程的时间片还是挺长的。但肯定不是先执行完一个线程再执行另一个。不信你把线程数改成50试一试~
使用Executor
这里的CachedThreadPool可以是
从任务中产生返回值
必须使用ExecutorService.submit()方法来调用Callable对象。同时submit()方法会产生Future对象,你可以用isDone()方法来查询Future是否已经完成。当任务完成时,它具有一个结果,你可用get方法来获取结果。你也可以不用isDone
方法进行检查就直接调用get方法,这种情况下,get将阻塞,直至结果准备就绪。
休眠
影响任务行为的一种简单方法是调用sleep()方法,这将使任务中止执行给定的时间。
优先级
让步
就是
后台线程
所谓后台线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程不属于程序中不可或缺的部分。因此,当所有非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。比如,正在执行的main()就是一个非后台线程
1、必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。
2、main()线程被设定为短暂睡眠,所以可以观察到所有后台线程启动后的结果。不这样的话,你就只能看到一些线程创建时的结果。
编程的变体
自管理线程
加入一个线程
一个线程可以在其他线程上调用Join()方法,其效果是等待一段时间知道第二个进程结束才继续执行。如果某个线程在另一个线程t上调用t.join(),此线程将被挂起,知道目标线程t结束才恢复。
也可以在调用join()时带上一个超时参数,这样如果目标线程在这段时间还没有结束的话,join()方法总能返回。
对join()方法的调用可以被中断,做法是在调用线程上调用interrupt()方法,这时用到try/catch语句。
下面的例子演示了所有的这些操作。
线程组
线程组持有一个线程集合
捕获异常
由于线程的本质特性,使得你不能捕获从线程中逃逸的异常,你可以自己用try/catch试一试。捕获异常的方法是
共享受限资源
多线程嘛,肯定会有资源方面的问题。直接上代码了
一个线程执行了第一个自加之后,另一个线程也执行next()方法,问题就产生了~,其次,即使只有一个自加操作也一样不安全,如果两个线程同时执行next()方法就会有问题了~
解决共享资源竞争
Java以提供关键字synchronized的形式,为防止资源冲突提供了大量内置支持。当任务要执行被synchronized关键字保护的代码片段时,它将检查锁是否可用,然后获取锁,执行代码,释放锁。(详细的内容自行google~)
使用显式的Lock对象
Java SE5的java.util.concurrent类库还包含在java.util.concurrent.locks中的显式的互斥机制。Lock对象必须被显式的创建、锁定和释放。因此它和上一种方式比,代码缺乏优雅性。但是,对于解决某些类型的问题来说,它更加灵活,见代码
大体上,当你使用synchronized关键字时,需要些的代码更少,并且用户出现错误的可能性也会降低,因此通常只有在解决特殊问题时,才使用显式的lock对象。例如,用synchronized关键字不能尝试着获取锁并且最终获取锁失败,或者尝试着获取锁一段时间,然后放弃它,要实现这些,你必须使用concurrent类库~
临界区
有时,你只是希望阻止多个线程同时访问方法内部的部分代码而不是访问整个方法。通过这种方式分离出来的代码被称为临界区,它也使用synchronized关键字建立。这里,synchronized被用来指定某个对象,此对象的锁被用来对花括号内的代码进行同步控制
Java的线程机制:时间片轮转+优先级Java多线程的调度策略
定义任务
线程可以驱动任务,因此你需要一种描述任务的方式,这可以由Runnable接口来提供。要想定义任务,只需实现Runnable接口并编写run()方法,使得该任务可以执行你的命令。例如,下面的LiftOff任务将显示发射之前的倒计时:
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!") + "), "; } //当从Runnable导出一个类时,它必须具有run()方法,但是这个方法并无特殊之处——它不会产生任何内在的线程能力。要实现线程行为,你必须显示地将一个任务附着到线程上。 public void run(){ while (countDown-- > 0){ System.out.println(status()); Thread.yield();//它的作用是告诉CPU,我已经执行完声明周期中最重要的部分了,此刻正是切换给其他任务的大好时机。当有多个线程时可以发现它的用处 } } }
//场景类 public class MainThread { public static void main(String[] args) { LiftOff liftOff = new LiftOff(); liftOff.run(); } } /*Output #0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(LiftOff!), */
Thread类
public class BasicThreads { public static void main(String[] args) { Thread t = new Thread(new LiftOff()); t.start(); System.out.println("Waiting for LiftOff"); } } /*Output Waiting for LiftOff #0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(LiftOff!), */
Thread 构造器只需要一个Runnable对象。调用Thread对象的start方法为该线程执行必要的初始化操作,然后调用Runnable中的run()方法,以便在这个新线程中启动该任务,尽管start()方法看起来产生了一个对长期运行方法的调用,但是从输出中可以看出,start()迅速返回了。你可以很容易的添加更多的线程去驱动更多的任务。下面,你可以看到所有任务彼此之间是如何呼应的
public class MoreBasicThreads { public static void main(String[] args) { for(int i = 0; i < 5; ++i){ new Thread(new LiftOff()).start(); } System.out.println("Waiting for LiftOff"); } } /*Output Waiting for LiftOff #0(9), #1(9), #2(9), #3(9), #4(9), #0(8), #1(8), #2(8), #3(8), #4(8), #0(7), #1(7), #2(7), #3(7), #4(7), #0(6), #1(6), #2(6), #3(6), #4(6), #0(5), #1(5), #2(5), #3(5), #4(5), #0(4), #1(4), #2(4), #3(4), #4(4), #0(3), #1(3), #2(3), #3(3), #4(3), #0(2), #1(2), #2(2), #3(2), #4(2), #0(1), #1(1), #2(1), #3(1), #4(1), #0(LiftOff!), #1(LiftOff!), #2(LiftOff!), #3(LiftOff!), #4(LiftOff!), */
如果把LiftOff中的run()方法中的Thread.yield()删掉的话输出是这样的
/* Waiting for LiftOff #0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(LiftOff!), #1(9), #1(8), #1(7), #1(6), #1(5), #1(4), #1(3), #1(2), #1(1), #1(LiftOff!), #2(9), #2(8), #2(7), #2(6), #2(5), #2(4), #2(3), #2(2), #2(1), #2(LiftOff!), #3(9), #3(8), #3(7), #3(6), #3(5), #3(4), #3(3), #3(2), #3(1), #3(LiftOff!), #4(9), #4(8), #4(7), #4(6), #4(5), #4(4), #4(3), #4(2), #4(1), #4(LiftOff!), */
看来分给每个线程的时间片还是挺长的。但肯定不是先执行完一个线程再执行另一个。不信你把线程数改成50试一试~
使用Executor
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();//不再接收新任务 } }
这里的CachedThreadPool可以是
exec = Executors.newFixedThreadPool(5)【限制线程数量】等,也可以是
exec = Executors.newSingleThreadExecutor()【挨个执行线程,一个完了再另一个】等
从任务中产生返回值
public class TaskWithResult implements Callable<String> { 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<Future<String>> results = new ArrayList<>(); for(int i = 0; i < 10; ++i){ results.add((Future<String>) exec.submit(new TaskWithResult(i))); } for(Future<String> fs: results){ try{ System.out.println(fs.get()); }catch (InterruptedException e){ e.printStackTrace(); }catch (ExecutionException e){ e.printStackTrace(); }finally { exec.shutdown(); } } } }
必须使用ExecutorService.submit()方法来调用Callable对象。同时submit()方法会产生Future对象,你可以用isDone()方法来查询Future是否已经完成。当任务完成时,它具有一个结果,你可用get方法来获取结果。你也可以不用isDone
方法进行检查就直接调用get方法,这种情况下,get将阻塞,直至结果准备就绪。
休眠
影响任务行为的一种简单方法是调用sleep()方法,这将使任务中止执行给定的时间。
public class SleepingTask extends LiftOff { @Override public void run() { while(countDown -- > 0){ System.out.println(status()); try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); for(int i = 0; i < 5; ++i){ executorService.execute(new SleepingTask()); } executorService.shutdown(); } } //我的运行结果的顺序是杂乱无章的,这是因为顺序行为依赖于底层的线程机制,这种机制在不同的操作系统上都是不同的,因此,你不能依赖它,如果你必须控制任务执行的顺序,那么最好的押宝就是使用同步控制(稍后描述),或者在某些情况下,压根不适用线程,但是要编写自己的协作例程,这些例程将会按照指定的顺序在互相之间传递控制权。
优先级
package com.sdkd.hms; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * Created by hms on 2016/9/2. */ public class SimplePriorities implements Runnable{ private int ThreadId; private int countDown = 5; private volatile double d; private int priority; public SimplePriorities(int priority, int ThreadId){ this.ThreadId = ThreadId; this.priority = priority; } public String toString(){ return "ThreadId" + " " + ThreadId + ": " + countDown; } @Override public void run() { Thread.currentThread().setPriority(this.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 executorService = Executors.newCachedThreadPool(); for(int i = 0; i < 5; ++i){ executorService.execute(new SimplePriorities(Thread.MIN_PRIORITY,i)); } executorService.execute(new SimplePriorities(Thread.MAX_PRIORITY, 6)); } } /* ThreadId 6: 5 ThreadId 3: 5 ThreadId 3: 4 ThreadId 3: 3 ThreadId 3: 2 ThreadId 3: 1 ThreadId 0: 5 ThreadId 0: 4 ThreadId 0: 3 ThreadId 0: 2 ThreadId 0: 1 ThreadId 1: 5 ThreadId 1: 4 ThreadId 1: 3 ThreadId 1: 2 ThreadId 1: 1 ThreadId 2: 5 ThreadId 6: 4 ThreadId 6: 3 ThreadId 6: 2 ThreadId 6: 1 ThreadId 2: 4 ThreadId 2: 3 ThreadId 4: 5 ThreadId 4: 4 ThreadId 4: 3 ThreadId 4: 2 ThreadId 4: 1 ThreadId 2: 2 ThreadId 2: 1 */
让步
就是
Thread.yield()方法,但是这个方法只是来暗示CPU,没有决定作用。所以大体上,对于任何重要的控制或在调整应用时,都不能依赖于
yield()。
后台线程
所谓后台线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程不属于程序中不可或缺的部分。因此,当所有非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。比如,正在执行的main()就是一个非后台线程
public class SimpleDasemons implements Runnable { @Override public void run() { while(true){ try { TimeUnit.MILLISECONDS.sleep(100); System.out.println(Thread.currentThread() + " " + this); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { for(int i = 0; i < 10; ++i){ Thread daemon = new Thread(new SimpleDasemons()); daemon.setDaemon(true); daemon.start(); } System.out.println("All daemons started"); try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } }
1、必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。
2、main()线程被设定为短暂睡眠,所以可以观察到所有后台线程启动后的结果。不这样的话,你就只能看到一些线程创建时的结果。
编程的变体
public class SimpleThread extends Thread{ public void run(){ //你想执行的任务 } } psvm(String[] args){ new SimpleThread(); }
自管理线程
public class SelfManaged implemetns Runnable{ public void run(){} private Thread t = new Thread(this); public SelfManaged(){ t.start(); } }
加入一个线程
一个线程可以在其他线程上调用Join()方法,其效果是等待一段时间知道第二个进程结束才继续执行。如果某个线程在另一个线程t上调用t.join(),此线程将被挂起,知道目标线程t结束才恢复。
也可以在调用join()时带上一个超时参数,这样如果目标线程在这段时间还没有结束的话,join()方法总能返回。
对join()方法的调用可以被中断,做法是在调用线程上调用interrupt()方法,这时用到try/catch语句。
下面的例子演示了所有的这些操作。
public 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) { System.out.println(getName() + " was interrupted. " + "isInterrupted(): " + isInterrupted()); } System.out.println(getName() + " has awakened"); } }
public 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) { 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); Sleeper grumpy = new Sleeper("Grumpy", 1500); Joiner dopey = new Joiner("Dopey", sleepy); Joiner doc = new Joiner("Doc", grumpy); grumpy.interrupt(); } } /*Output Grumpy was interrupted. isInterrupted(): false Grumpy has awakened Doc join completed Sleepy has awakened Dopey join completed */
线程组
线程组持有一个线程集合
捕获异常
由于线程的本质特性,使得你不能捕获从线程中逃逸的异常,你可以自己用try/catch试一试。捕获异常的方法是
Thread t = new Thread(); t.setUncaughtException(new MyUncaughtionExceptionHander());
共享受限资源
多线程嘛,肯定会有资源方面的问题。直接上代码了
public abstract class IntGenerator { private volatile boolean canceled = false; public abstract int next(); public void cancel(){ canceled = true; } public boolean isCanceled(){ return canceled; } }
public class EvecChecker implements Runnable{ private IntGenerator generator; private final int id; public EvecChecker(IntGenerator g, int ident){ generator = g; id = ident; } @Override public void run() { while(!generator.isCanceled()) { int val = generator.next(); System.out.println("ThreadId" + id + " " + "currentEvenValue" + " " + val); if (val % 2 != 0) { System.out.println("ThreadId " + id + " " + val + " not even!"); generator.cancel(); } Thread.yield(); } } public static void test(IntGenerator gp, int count){ System.out.println("Press Gontrol-C to exit"); for(int i = 0; i < count; ++i){ new Thread(new EvecChecker(gp, i)).start(); } } public static void test(IntGenerator gp){ test(gp, 10); } }
public class EvenGenerator extends IntGenerator { private int currentEvenValue = 0; @Override public int next() { ++currentEvenValue;//Danger point here! ++currentEvenValue; return currentEvenValue; } public static void main(String[] args){ EvecChecker.test(new EvenGenerator()); } } /*Output Press Gontrol-C to exit ThreadId 8 12991 not even! ThreadId 3 12995 not even! ThreadId 2 12997 not even! ThreadId 1 12989 not even! ThreadId 7 12999 not even! ThreadId 5 12993 not even! */
一个线程执行了第一个自加之后,另一个线程也执行next()方法,问题就产生了~,其次,即使只有一个自加操作也一样不安全,如果两个线程同时执行next()方法就会有问题了~
解决共享资源竞争
Java以提供关键字synchronized的形式,为防止资源冲突提供了大量内置支持。当任务要执行被synchronized关键字保护的代码片段时,它将检查锁是否可用,然后获取锁,执行代码,释放锁。(详细的内容自行google~)
public class SynchronizedEvenGenerator extends IntGenerator { private int currentEvenValue = 0; @Override public synchronized int next() { ++currentEvenValue; Thread.yield(); ++currentEvenValue; return currentEvenValue; } public static void main(String[] args){ EvecChecker.test(new SynchronizedEvenGenerator()); } }
使用显式的Lock对象
Java SE5的java.util.concurrent类库还包含在java.util.concurrent.locks中的显式的互斥机制。Lock对象必须被显式的创建、锁定和释放。因此它和上一种方式比,代码缺乏优雅性。但是,对于解决某些类型的问题来说,它更加灵活,见代码
public class MutexEvenGenerator extends IntGenerator { private int currentValue = 0; private Lock lock = new ReentrantLock(); @Override public int next() { lock.lock(); try{ ++currentValue; ++currentValue; return currentValue; }finally { lock.unlock(); } } public static void main(String[] args) { EvecChecker.test(new MutexEvenGenerator()); } }
大体上,当你使用synchronized关键字时,需要些的代码更少,并且用户出现错误的可能性也会降低,因此通常只有在解决特殊问题时,才使用显式的lock对象。例如,用synchronized关键字不能尝试着获取锁并且最终获取锁失败,或者尝试着获取锁一段时间,然后放弃它,要实现这些,你必须使用concurrent类库~
临界区
有时,你只是希望阻止多个线程同时访问方法内部的部分代码而不是访问整个方法。通过这种方式分离出来的代码被称为临界区,它也使用synchronized关键字建立。这里,synchronized被用来指定某个对象,此对象的锁被用来对花括号内的代码进行同步控制
synchronized(syncObject){ }
相关文章推荐
- 多线程并发库高级应用 之 java5中的线程并发库--线程池、Callable&Future
- Windows并发&异步编程(1)JAVA&多线程
- 多线程并发库高级应用 之 java5中的线程并发库--线程池、Callable&Future
- java多线程并发库高级应用 之 java5中的线程并发库--线程池、Callable&Future
- Java多线程编程--(8)学习Java5.0 并发编程包--线程池、Callable & Future 简介
- Thinking in Java学习笔记 第二十一章:并发
- Thinking in Java:第二十一章-并发
- 多线程并发库高级应用 之 java5中的线程并发库--线程池、Callable&Future
- Java 并发 (多线程) 讲解<一>
- Java多线程编程--(7)学习Java5.0 并发编程包--Lock & Condition
- Java 多线程与并发编程专题
- JAVA NIO TCP SOCKET 聊天群发(并发多线程写消息篇)
- 探索并发编程(五)------Java多线程开发技巧
- 实现 Java 多线程并发控制框架
- java处理多线程并发
- [Java 多线程] 并发集合类
- IBM---Java 多线程与并发编程专题
- Java 多线程与并发编程专题
- Java 多线程与并发编程专题(转)
- jdk1.4 构建 java多线程,并发设计框架 使用列子(五)