对于并发,书中花了很长的篇幅来讲,而我需要重点学习的也是并发,所以这里分多个笔记来记录。
1.并发的多面性
用并发的用途大体分为:性能 和 设计可管理性 两种。
实现并发最直接的方法:在操作系统级别使用进程。
多任务操作系统可以通过周期性地将CPU从一个进程切换到另一个进程,来实现同时运行多个进程(程序)。
编写多线程的基本困难是协调不同线程驱动的任务之间对这些资源的使用,以使这些资源不会同时被多个任务访问。
2.基本的线程机制
2.1定义任务
写线程需要实现Runnable接口来编写run()方法。
public class ThreadTest implements Runnable {
@Override
public void run() {
System.out.println("沙丁鱼flat线程执行");
Thread.yield();
}
}
public class ThreadMain {
public static void main(String[] args){
ThreadTest threadTest=new ThreadTest();
threadTest.run();
}
}
这里面的Thread.yield()相当于让步给其它线程来执行,书里写的是声明了“已完成生命周期最重要的部分,切换给其它任务执行”。
2.2 Thread类
上面是通过对象执行线程,这里还有一种方法就是将Runnable转化为工作任务来驱动对象:
Thread test=new Thread(new ThreadTest()); 将Runnable对象给构造器,并提供给Thread使用,thread构造器需要一个Runnable对象
public class ThreadTest implements Runnable {
@Override
public void run() {
System.out.println("沙丁鱼flat线程执行");
Thread.yield();
}
}
public class ThreadMain {
public static void main(String[] args){
Thread test=new Thread(new ThreadTest());
test.start();
System.out.println("Main这个主线程仍在运作");
System.out.println("由于上面test是另外的线程,不是主要线程,但test线程已开启,异步进行");
}
}
也就是说,这里有两个线程,一个是main线程,一个是自己写的test线程。他们执行时间是分开的。
多线程测试:
public class ThreadTest1 implements Runnable {
public int ThreadTestNumb=0;
public void UseThreads(){
System.out.println("沙丁鱼的线程集合中,线程编号:"+ThreadTestNumb+"执行了");
}
public ThreadTest1(int threadTestNumb) {
ThreadTestNumb = threadTestNumb;
}
@Override
public void run() {
System.out.println("沙丁鱼flat线程执行");
UseThreads();
Thread.yield();
}
}
public class ThreadMain {
public static void main(String[] args){
for(int i=0;i<10;i++){
new Thread(new ThreadTest1(i)).start();
}
}
}
这里我设置了10个线程来执行,这里是为了体现一个线程的性质,就是非确定性,这里线程运行时间不同结果也不同。
下面是两次运行的结果:
2.3 使用Executor
2.3.1 Executor是什么?
JDK1.5中提供了Executor
接口,处于java.util.concurrent
包下,而Executor翻译过来是执行器。
也就是说Executor是一个执行器,用来管理Thread对象。
Executor的运行原理是在客户端和任务执行之间提供一个间接层,由间接层这个中介对象来执行任务。
Executor的优点是它允许你管理异步任务的执行,而无须显式地管理线程的生命周期。
2.3.2 实际使用Executor
public class ExecutorMain {
public static void main(String[] args){
ExecutorService exec= Executors.newCachedThreadPool();
for(int i=0;i<10;i++){
exec.execute(new ThreadTest1(i));
}
exec.shutdown();
}
}
这里是CachedThreadPool(Cached线程池)来控制。英文翻译顾名思义:缓存线程池,所以这个线程池的做法是在执行过程中创建与所需数量相同的线程,然后在回收旧线程时会停止创建新线程。
exec.shutdown()是要求关闭这个Executor执行器,防止新任务,而shutdown之前的任务将继续执行。然后执行完就正式关闭Executor了。
当然除了创建这个线程池外还可以使用其他线程池(不过CachedThreadPool是推荐首选的线程池)。
额外的线程池:FixedThreadPool(Fixed线程池)
public class ExecutorMain {
public static void main(String[] args){
//ExecutorService exec= Executors.newCachedThreadPool();
ExecutorService exec= Executors.newFixedThreadPool(5);
for(int i=0;i<10;i++){
exec.execute(new ThreadTest1(i));
}
exec.shutdown();
}
}
这个线程池缺点是,在一开始,它会先执行代价高额的线程分配(用来限制线程数量,比如这里我限制了5个,那么线程池里运作时最多同时5个,其他的线程等待)。
除此之外还有SingleThreadExecutor线程池,这个线程池,相当于线程数量为1的FixedThreadPool。这个线程池的作用在于顺序化线程,线程根据提交的顺序来执行。
public class ExecutorMain {
public static void main(String[] args){
//ExecutorService exec= Executors.newCachedThreadPool();
//ExecutorService exec= Executors.newFixedThreadPool(5);
ExecutorService exec= Executors.newSingleThreadExecutor();
for(int i=0;i<10;i++){
exec.execute(new ThreadTest1(i));
}
exec.shutdown();
}
}
2.4 从任务中产生返回值
这里Runnable接口实现的是执行工作的独立任务,但它不会返回任何值。,所以如果希望任务完成时能有返回值的话,那么实现Callable接口代替Runnable接口。
public class CallableTest implements Callable<String> {
public int ThreadTestNumb=0;
public CallableTest(int threadTestNumb) {
ThreadTestNumb = threadTestNumb;
}
@Override
public String call() throws Exception {
String result="沙丁鱼的线程集合中,线程编号:"+ThreadTestNumb+"执行了";
return result;
}
}
public class CallableMain {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService exec= Executors.newCachedThreadPool();
for(int i=0;i<10;i++){
Future<String> result= exec.submit(new CallableTest(i));
//if(result.isDone()) {
System.out.println(result.get());
//}
}
exec.shutdown();
}
}
Callable接口是用call()方法,不是run()方法,
然后Executor接收Callable对象的值时,用的是ExecutorService.submit().然后返回的是Future<?>类型
这里我省略了result.isDone(),这个表示的是检测Future<?>对象是否已经生成完毕,由于我这边设计得不好,main线程过快导致,基本上检测到的是未生成完毕的,所以直接跳过这个,执行Future<?>对象的.get()方法,获取String
注意:这边的get()最好写在try-catch里面,因为这个默认需要有抛出异常的代码
比如我代码里的:throws ExecutionException, InterruptedException
2.5 休眠
这里写的是线程的休眠,也就是sleep()方法的使用。
public class ThreadTest implements Runnable {
@Override
public void run() {
System.out.println("沙丁鱼flat线程执行");
//Thread.yield();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这里使用sleep()代替yield()了。同时sleep的100代表该线程休眠100毫秒,100毫秒后再唤醒。
2.6 优先级
这一节讲得是如何修改设置线程的优先级。首先还是上代码演示。
public class ThreadPriorityTest implements Runnable {
public int ThreadTestNumb=0;
public void UseThreads(){
System.out.println("沙丁鱼的线程集合中,线程编号:"+ThreadTestNumb+"执行了");
}
public ThreadPriorityTest(int threadTestNumb) {
ThreadTestNumb = threadTestNumb;
}
@Override
public void run() {
Thread.currentThread().setPriority(ThreadTestNumb);
UseThreads();
System.out.println("线程优先级:"+Thread.currentThread().getPriority());
Thread.yield();
}
}
public class ThreadPriorityMain {
public static void main(String[] args){
ExecutorService exec= Executors.newSingleThreadExecutor();
for(int i=0;i<10;i++){
exec.execute(new ThreadPriorityTest(i));
}
exec.shutdown();
}
}
这里报错了,为什么,因为JDK优先级不是无限的,只有10个优先,由1-10,所以优先级没有0,
这里还得说明一点,JDK虽然有10个优先,但由于操作系统和JDK映射,不同,Window只有7个优先级且不固定,所有实际上的优先级最后只有三个级别:MAX_PRIORITY,NORM_PRIORITY,MIN_PRIORITY.
具体使用是:
2.7 让步
这节很短,就是用thread.yield()方法。就是让步给其它线程来使用CPU。
2.8 后台线程
2.8.1 什么是后台线程?
后台(daemon)线程:指程序运行时在后台提供一种用用服务的线程,且该线程并不属于程序中必要的一部分。
相对于普通的非后台线程(前台线程来说),这两者的区别就是:
前台线程:应用程序必须运行完所有的前台线程才可以退出。(比如main线程)
后台线程:应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。(比如垃圾回收线程)
2.8.2 设置线程为后台线程
public class DaemonMain {
public static void main(String[] args){
//ExecutorService exec= Executors.newCachedThreadPool();
for(int i=0;i<10;i++){
Thread daemon=new Thread(new ThreadTest1(i));
daemon.setDaemon(true);
daemon.start();
//exec.execute(daemon);
}
//exec.shutdown();
}
}
设置线程为后台线程,需要使用.setDaemon(true)。把线程确定使用为后台线程
但这里必须注意一点:必须在线程启动前调用setDaemon方法,才能把线程设置为后台线程
上面是简单的设置线程为后台线程
下面是如何把这个放入Executor中去
public class DaemonThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable MyRunnable) {
Thread t=new Thread(MyRunnable);
t.setDaemon(true);
System.out.println("线程是否已改为后台"+t.isDaemon());
return t;
}
}
public class DaemonMain {
public static void main(String[] args){
ExecutorService exec= Executors.newCachedThreadPool(new DaemonThreadFactory());
for(int i=0;i<10;i++){
exec.execute(new DaemonThreadFactory().newThread(new ThreadTest1(i)));
}
exec.shutdown();
}
}
这里主要就是建立一个线程工厂,然后在线程工厂里面设置线程为后台线程,然后再返回对应的Runnable对象给Execute使用。
2.9 编码的变体
线程方面还有多种变体可以进行编码
2.9.1 直接使用Thread
public class SimpleThread extends Thread {
public SimpleThread(){
start();
}
public void run(){
System.out.println("我爱吃沙丁鱼flat");
yield();
}
public static void main(String[] args){
for (int i=0;i<5;i++) {
new SimpleThread();
}
}
}
这里注意一点,这里虽然没有重写注解,但这里的函数还是run(),那么在线程start()后,它还是会执行run函数,如果函数名不是run那么它就不会自动执行。
2.9.2 Runnable对象中直接写main
public class SimpleRunnable implements Runnable {
private Thread t=new Thread(this);
public SimpleRunnable(){t.start();}
@Override
public void run() {
System.out.println("沙丁鱼flat又出现了!");
}
public static void main(String[] args){
for (int i=0;i<5;i++) {
new SimpleRunnable();
}
}
}
除此之外,还可以使用内部类来写。这里就不写了。
2.10 术语
注意:Thread不是任务,Thread类只是驱动赋予它的任务,而任务由Runnable接口来书写。
2.11 加入一个线程
这里是书中的说明:
加入一个线程的意思是在一个线程上加入另外一个线程,其运作效果是等待一段时间直到第二个线程结束。
1.如果某个线程在另一个线程t上调用t.join(),此线程将被挂起,直到目标线程t结束才恢复。
2.也可以在调用join()时带上一个超时参数(单位可以是毫秒,或者毫秒和纳秒),这样如果目标线程在这段时间到期时还没有结束的话,join方法总能返回。
3.对join方法的调用可以被中断,做法是在调用线程上调用interrupt方法,这时需要用到try-catch语句。
用举个例子来说明,线程A,B:B加到A的线程的中间中去。那么A线程执行到中间时会先执行B线程。
这里有三种情况:
1.正常情况:等B线程执行完,再继续执行A线程的剩余部分。
2.超时情况:如果B线程一直没执行完,超时的话,那么A不会继续等待B了,直接继续执行A剩下的部分。
3.打断情况:调用interrupt方法,如果打断了B线程,那么A继续执行剩下的部分。
2.11.1 正常情况
public class ThreadJoinATest extends Thread {
private Thread otherThread;
public ThreadJoinATest(Thread ThreadJoinB){
otherThread=ThreadJoinB;
}
public void run(){
System.out.println("运行A线程前部分");
try {
otherThread.join();
System.out.println("运行A线程后部分");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ThreadJoinBTest extends Thread {
private int Bsleeptime;
public ThreadJoinBTest(int time){
Bsleeptime=time;
start();
}
public void run(){
try {
sleep(Bsleeptime);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("运行B线程:沙丁鱼flat大成功!");
}
}
public class ThreadJoinMain extends Thread {
public static void main(String[] args){
new ThreadJoinATest(new ThreadJoinBTest(10000)).start();
}
}
先出现A前部分,然后等待了10000毫秒(明显的等待后),再出现下面B线程的输出和A的后部分。
2.11.2 修改代码,尝试超时情况:
public class ThreadJoinATest extends Thread {
private Thread otherThread;
public ThreadJoinATest(Thread ThreadJoinB){
otherThread=ThreadJoinB;
}
public void run(){
System.out.println("运行A线程前部分");
try {
otherThread.join(100);
System.out.println("运行A线程后部分");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
B线程需要10000毫秒,而这边join设置只有100毫秒,那么它就会A不等待,继续执行A
2.11.3 打断情况,修改正常情况的代码:
public class ThreadJoinBTest extends Thread {
private int Bsleeptime;
public ThreadJoinBTest(int time){
Bsleeptime=time;
start();
}
public void run(){
try {
interrupt();
sleep(Bsleeptime);
System.out.println("运行B线程:沙丁鱼flat大成功!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
之后其实还有几个知识点,一个是java图形界面,一个是线程组、一个是捕获异常。
图形界面方面:java优势在于后端,所以图形界面PASS
线程组方面:这个当初是为了JDK1.2线程管理而设置,而现在出了Executor后,已经被抛弃了
捕获异常:这个可以说说,不过还是一样,Executor有了后,就简单很多了,像上面我写的例子里已经自动抛出很多异常了。所以这节我也不多说了。