JavaSE中线程与并行API框架学习笔记1——线程是什么?

前言:虽然工作了三年,但是几乎没有使用到多线程之类的内容。这其实是工作与学习的矛盾。我们在公司上班,很多时候都只是在处理业务代码,很少接触底层技术。

可是你不可能一辈子都写业务代码,而且跳槽之后新单位很可能有更高的技术要求。除了干巴巴地翻书,我们可以通过两个方式来解决这个问题:一是做业余项目,例如在github上传自己的demo,可以实际使用;二是把自己的学习心得写成博客,跟同行们互相交流。

3.1 线程的初窥门径

我们在之前的文章里提到的程序其实都是单线程程序,也就说启动的程序从main()程序进入点开始到结束只有一个流程。然而,有时候我们设计的程序需要有多个流程,也就是所谓的多线程(Multi-thread)程序。

我们可以通过一个龟兔赛跑的游戏开始学习单线程和多线程的区别。乌龟和兔子赛跑,起点到终点为10米,每经过一秒,乌龟会前进0.1米;兔子可能前进1米或睡觉。如果使用单线程,我们可以这样设计:

 1 /**
 2  * 线程实验用例
 3  */
 4 public class TortoiseHareRace {
 5     public static void main(String[] args) throws InterruptedException {
 6         float totalLength = 10;
 7         float tortoiseLength = 0;
 8         float hareLength = 0;
 9         System.out.println("龟兔赛跑大赛,开始!");
10         while(tortoiseLength < totalLength && hareLength < totalLength) {
11             Thread.sleep(1000);
12             tortoiseLength += 0.1;
13             System.out.println("乌龟跑了 " + tortoiseLength + " 米...");
14             boolean isHareSleep = Math.random()*10 < 9;
15             if(isHareSleep) {
16                 System.out.println("兔子睡着了zzzz");
17             } else {
18                 hareLength ++;
19                 System.out.println("兔子跑了 " + hareLength + " 米...");
20             }
21         }
22     }
23 }

这个程序里面只有一个流程,就是从main()方法开始到结束的流程。要让目前流程暂停指定时间,可以使用java.lang.Thread的静态sleep()方法,单位是毫秒。调用这个方法必须处理java.lang.InterruptedException,在这里直接在main()中声明 throws,由JVM来处理此异常。

每次暂停一秒后,tortoiseLength递增0.1,表示乌龟向前爬了0.1米,兔子则可能有百分之90的可能性睡觉。如果不睡觉,hareLength会递增1,表示兔子向前蹦了1米。只要他们任意一个跑了10米,表示到达终点,分出胜负比赛结束。

由于程序只有一个流程,所以每次只能先让乌龟先跑再让兔子跑,这就很不公平(如果倒过来,也是不公平)。如果程序里再有两个流程,一个是乌龟在跑,一个是兔子在跑,是不是程序逻辑会更加合理和清晰呢?

在Java中,如果想在main()以外独立设计流程,可以实现java.lang.Runnable接口,流程的进入点是在run()方法里面。例如,我们可以这样设计乌龟的流程:

 1 /**
 2  * 乌龟线程
 3  */
 4 public class Tortoise implements Runnable{
 5     private float totalLength;
 6     private float length;
 7
 8     public Tortoise(float totalLength) {
 9         this.totalLength = totalLength;
10     }
11
12     @Override
13     public void run() {
14         try {
15             while(length < totalLength) {
16                 Thread.sleep(1000);
17                 length += 0.1;
18                 System.out.println("乌龟跑了 " + length + " 米...");
19             }
20         } catch (InterruptedException ex) {
21             throw new RuntimeException(ex);
22         }
23     }
24 }

在Tortoise类中,乌龟的流程会从run()开始。在这个流程里,代码只需要专心负责乌龟每秒爬0.1米就可以了,不用夹杂兔子的动作。同样地,我们可以类似地设计兔子的流程:

 1 /**
 2  * 兔子线程
 3  */
 4 public class Hare implements Runnable{
 5     private float totalLength;
 6     private float length;
 7
 8     public Hare(int totalLength) {
 9         this.totalLength = totalLength;
10     }
11
12     @Override
13     public void run () {
14         try {
15             while(length < totalLength) {
16                 Thread.sleep(1000);
17                 boolean isHareSleep = Math.random()*10 < 9;
18                 if(isHareSleep) {
19                     System.out.println("兔子睡着了zzzz");
20                 } else {
21                     length ++;
22                     System.out.println("兔子跑了 " + length + " 米...");
23                 }
24             }
25         } catch (InterruptedException ex) {
26             throw new RuntimeException(ex);
27         }
28     }
29 }

在Java中,从main()方法开始的流程会由主线程(Main Thread)来跑。那么刚才设计的兔子线程和乌龟线程,应该让谁来执行呢?我们可以通过创建Thread对象的方法来执行Runnable对象定义的run()方法,例如:

 1 /**
 2  * 龟兔赛跑主线程
 3  */
 4 public class TortoiseHare2 {
 5     public static void main(String[] args) {
 6         Tortoise tortoise = new Tortoise(10);
 7         Hare hare = new Hare(10);
 8         Thread tortoiseThread = new Thread(tortoise);
 9         Thread hareThread = new Thread(hare);
10         tortoiseThread.start();
11         hareThread.start();
12     }
13 }

要记住,在创建Thread对象之前,必须确保实例化的参数实现了Runnable接口,并且要在之后执行start()方法,以下是其中一个执行结果:

我们都在学校里学习过线程的概念,但这都只是逻辑上的认识,并不代表我们已经完全掌握其实际内容。通过做实验的方式,我们写了代码,引入了耳熟能详的儿童故事,加深认识的同时也增强了记忆。

3.2继承父类 or 实现接口

从抽象观点与开发者的角度来看,JVM是台虚拟计算机,只安装了一颗被称为主线程的CPU,可执行main()定义的执行流程。如果想要为JVM加装CPU,就是创建Thread实例,要启动额外CPU就是调用Thread实例的start()方法。额外CPU执行流程的进入点,可以定义在Runnable接口的run()方法里面。

当然了,实际上JVM启动之后并不只有一个主线程,还有垃圾收集、内存管理等线程,不过这是底层机制。我们暂时不用理会。

除了将流程定义在Runnable的run()方法之外,还有另外一个使用多线程的方法,就是继承Thread类,重新定义run()方法。例如我们可以这样的改写乌龟流程:

 1 /**
 2  * 乌龟的线程
 3  */
 4 public class Tortoise extends Thread{ //继承Thread类
 5     private float totalLength;
 6     private float length;
 7
 8     public Tortoise(float totalLength) {
 9         this.totalLength = totalLength;
10     }
11
12     @Override
13     public void run() {
14         try {
15             while(length < totalLength) {
16                 Thread.sleep(1000);
17                 length += 0.1;
18                 System.out.println("乌龟跑了 " + length + " 米...");
19             }
20         } catch (InterruptedException ex) {
21             throw new RuntimeException(ex);
22         }
23     }
24 }

由于大部分代码都一样,所以就不再把整个demo重新贴出来一遍了。但还是建议各位朋友,尤其是初学者们,千万要自己动手改一改,看看两种写法到底有什么不同。

在Java中,任何线程可执行的流程都要定义在Runnable的run()方法。事实上,Thread类本身也实现了Runnable接口,从JDK源码当中我们可以看到:

 1     /**
 2      * If this thread was constructed using a separate
 3      * <code>Runnable</code> run object, then that
 4      * <code>Runnable</code> object‘s <code>run</code> method is called;
 5      * otherwise, this method does nothing and returns.
 6      * <p>
 7      * Subclasses of <code>Thread</code> should override this method.
 8      *
 9      * @see     #start()
10      * @see     #stop()
11      * @see     #Thread(ThreadGroup, Runnable, String)
12      */
13     @Override
14     public void run() {
15         if (target != null) {
16             target.run();
17         }
18     }

了解完两种多线程的实现方法之后,我们会有一个疑问:到底是实现Runnable()好呢,还是直接继承Thread类更好?这就要根据具体情况具体分析了。

实现接口的好处是灵活,这个类还可以继承其他类。如果你想要直接利用Thread中定义的某些特定方法,那就可以考虑直接继承Thread类。要想做到灵活应用,这就要求开发者足够了解这些接口和类。没有别的捷径,就是多看API说明文档和多阅读JDK源代码。

3.3 线程的生命周期

有一个古老的谜语,说是有一种动物,早上是四条腿,中午就成了两条腿,到了傍晚却是三条腿。我们大家都知道,谜底就是人。

从小孩到长大成人,再到衰老死亡,这就是我们人类的生命周期。线程也是一样,从创建到开始,再到最后的结束。理解人类的生命周期,有助于我们更好地认识自己,规划自己的人生。熟悉线程的生命周期,则会对我们使用线程有莫大的帮助。

线程的生命周期相当复杂,我们将会从最简单的开始讲起。

3.3.1 Daemon线程

Daemon,本义是守护神的意思,在希腊神话里是半人半神的精灵。在计算机领域,daemon已经是一个专业术语,我们可以理解成守护进程或守护线程。

大人物出现活动,如果他不离场,安保人员是不可能会撤离的。Java中的守护进程也一样,如果主线程中启动了额外线程,默认会等待被启动的所有线程都执行完run()方法之后才中止JVM。如果一个Thread被标识为Daemon线程,那么在所有的非Daemon线程都结束时,它也会被JVM自动终止。

从main()方法开始的就是一个非Daemon线程。我们可以使用setDaemon()方法来设定一个线程是否属于Daemon线程。下面是一个非常简单的demo,实验的时候你可以试着取消setDaemon()那行代码的注释,看看会有什么不同,为什么:

 1 /**
 2  * Daemon实验用例
 3  */
 4 public class DaemonDemo {
 5     public static void main(String[] args) {
 6         Thread thread = new Thread() {
 7             public void run() {
 8                 while(true) {
 9                     System.out.println("run...");
10                 }
11             }
12         };
13         //thread.setDaemon(true); //把线程设定为Daemon线程
14         thread.start();
15     }
16 }

如果没有使用setDaemon()方法设定为true,上面这个程序就会不会输出“run...”,永远不会自动终止。可是如果一旦将其设置为Daemon线程,在其启动的一瞬间就会被终止,甚至连一句输出都没有。

有趣的是,默认所有从Daemon线程产生的线程也是Daemon线程。其实这也不难理解,因为基本上来说由一个后台服务线程衍生出来的线程,也应该是为了在后台服务而产生的,所以在产生它的线程停止时,也应该一并跟着停止。

3.3.2 线程状态图

在看过最简单的线程生命周期之后,我们再来看复杂一点的。调用Thread对象的start()方法之后,该线程的基本状态基本可以分为三种:可执行(Runnable)、别阻断(Blocked)、执行中(Running)。

三种状态之间的转移一两句话说不清楚,我们可以借助状态图来帮助我们理解:

我们可以一边对照着上面这幅图,一边看接下来的讲述。实例化Thread并执行start()方法之后,线程就会进入Runnable状态。这个时候线程并没有真正开始执行run()方法,必须等待排班器(Scheduler)排入CPU执行,线程才会跑run()方法,这就进入了Running状态。

线程有优先权重,可以使用Thread的setPriority()方法来设定优先权。设定的范围是1(Thread.MIN_PRIORITY)到 10(Thread.MAX_+PRIORITY),默认是5(Thread.NORM_PRIORITY)。数字越大优先权越高,调度器越有限排入CPU,也就是越优先进入Running状态。如果优先权相同,则轮流执行(Round-robin)。

有几种情况会让线程进入Blocked状态,例如前面调用过的Thread.sleep()方法,又例如等待输入输出等等。我们之所以要运用多线程,就是要让当前线程进入Blocked状态的时候让另一线程排入CPU执行,即进入Running状态,避免CPU无谓的空闲。这就是我们常用的,用来改进性能的方式之一。

下面我们可以用一个下载网页的demo来进行测试,看看不使用多线程时需要花费多长时间:

 1 import java.net.URL;
 2 import java.io.*;
 3 import java.util.Date;
 4
 5 /**
 6  * 单线程实验用例
 7  */
 8 public class Download {
 9     public static void main(String[] args) throws Exception {
10         URL[] urls = {
11                 new URL("http://www.cnblogs.com/levenyes/p/7117559.html"),
12                 new URL("http://www.cnblogs.com/levenyes/p/7120267.html"),
13                 new URL("http://www.cnblogs.com/levenyes/p/7145214.html"),
14                 new URL("http://www.cnblogs.com/levenyes/p/7163843.html")
15         };
16
17         String[] fileNames = {
18                 "file1.html",
19                 "file2.html",
20                 "file3.html",
21                 "file4.html"
22         };
23         Date begin = new Date();
24         for(int i = 0 ; i < urls.length; i++) {
25             dump(urls[i].openStream(), new FileOutputStream(fileNames[i]));
26         }
27         Date end = new Date();
28         System.out.println(end.getTime() - begin.getTime());
29     }
30
31     private static void dump(InputStream src, OutputStream dest) throws IOException {
32         try (InputStream input = src; OutputStream output = dest) {
33             byte[] data = new byte[1024];
34             int length = -1;
35             while((length = input.read(data)) != -1) {
36                 output.write(data, 0, length);
37             }
38         }
39     }
40 }

每一次for循环时,会先开启网络链接、进行HTTP请求,然后再进行文档写入等等。在等待网络链接、HTTP协议时很耗时,这就意味着进入Blocked的时间相当长。因为是单线程,所以必须等第一个网页下载完了之后才能下载第二个,如此类推。

因为受到网络状态不稳定的影响,有的时候可能会比较快,有的时候会比较慢。经过多次测试,大概的耗时为400毫秒。

如果我们可以在下载第一个网页遇到等待时就开始下载其他网页,这样会不会让速度加快许多呢?例如下面这个demo:

 1 import java.net.URL;
 2 import java.io.*;
 3 import java.util.Date;
 4
 5 /**
 6  * 多线程实验用例
 7  */
 8 public class Download {
 9     public static void main(String[] args) throws Exception {
10         final URL[] urls = {
11                 new URL("http://www.cnblogs.com/levenyes/p/7117559.html"),
12                 new URL("http://www.cnblogs.com/levenyes/p/7120267.html"),
13                 new URL("http://www.cnblogs.com/levenyes/p/7145214.html"),
14                 new URL("http://www.cnblogs.com/levenyes/p/7163843.html")
15         };
16
17         final String[] fileNames = {
18                 "file1.html",
19                 "file2.html",
20                 "file3.html",
21                 "file4.html"
22         };
23         Date begin = new Date();
24         for(int i = 0 ; i < urls.length; i++) {
25             final int index = i;
26             new Thread() {
27                 @Override
28                 public void run() {
29                     try {
30                         dump(urls[index].openStream(), new FileOutputStream(fileNames[index]));
31                         Date end = new Date();
32                         System.out.println(end.getTime() - begin.getTime());
33                     } catch (IOException ex) {
34                         throw new RuntimeException(ex);
35                     }
36                 }
37             }.start();
38
39         }
40
41     }
42
43     private static void dump(InputStream src, OutputStream dest) throws IOException {
44         try (InputStream input = src; OutputStream output = dest) {
45             byte[] data = new byte[1024];
46             int length = -1;
47             while((length = input.read(data)) != -1) {
48                 output.write(data, 0, length);
49             }
50         }
51     }
52 }

经过多次测试,我们会发现耗费的时间要比单线程快得多,多线程下载4个网页的时间跟单线程下载1个网页的时间近似。

3.3.3 安插线程

程序员常常会有这样的经验:正在热火朝天地赶一个需求,突然被告知之前负责的项目出了严重问题要紧急处理,这时候只好放下开发工作去修bug。

如果一个线程正在运行,有另一个线程要插入进来优先运行,要怎么做呢?我们可以使用join()方法,会让插入的线程执行完毕之后再继续原来的线程。我们可以通过一个简单的实验来感受一下join()的用法:

 1 /**
 2  * Join实验用例
 3  */
 4 public class JoinDemo {
 5     public static void main(String[] args) {
 6         System.out.println("Main thread 开始...");
 7         Thread threadB = new Thread() {
 8             @Override
 9             public void run() {
10                 try {
11                     System.out.println("Thread B 开始...");
12                     for (int i = 0; i < 5; i++) {
13                         Thread.sleep(1000);
14                         System.out.println("Thread B 执行...");
15                     }
16                     System.out.println("Thread B 将结束...");
17                 } catch (InterruptedException e) {
18                     e.printStackTrace();
19                 }
20             }
21         };
22
23         threadB.start();
24
25         try {
26             threadB.join();//B线程插入到Main线程里
27         } catch(InterruptedException e) {
28             e.printStackTrace();
29         }
30         System.out.println("Main thread 将结束...");
31     }
32 }

因为要等到B线程运行完毕才能执行主线程之后的动作,所以我们会发现输出结果如下:

如果把join()相关部分的代码注释掉,运行结果会有什么不同呢?我们可以先在脑海里想象一下,然后再做实验:

有的时候插入的线程可能会耗时很久,甚至会有死循环,我们不可能无限制地等下去。针对这个需求,我们可以使用带参数的join()方法,例如join(1000),这就表示插入的线程最多运行1秒钟。如果加入的线程耗时超过1秒钟就不理它了,当前线程可以继续执行原来的动作流程。

3.3.4 停止线程

线程完成run()方法后,就会进入Dead状态。进入Dead状态(或已经调用过start()方法)的线程不可以再次调用start()方法,否则会抛出IllegalThreadStateException的异常。

Thread类上定义有stop方法,不过已经被标识为Deprecated。Deprecated,顾名思义,就是“不赞成”、“反对”的意思。在JavaSE中被表示为Derecated的API,表示过去定义过,后来因为可能会引发某些问题,同时又为了兼容性,这些API没有直接删去,但也不建议现在的程序再使用它们。

 1     /* @deprecated This method is inherently unsafe.  Stopping a thread with
 2      *       Thread.stop causes it to unlock all of the monitors that it
 3      *       has locked (as a natural consequence of the unchecked
 4      *       <code>ThreadDeath</code> exception propagating up the stack).  If
 5      *       any of the objects previously protected by these monitors were in
 6      *       an inconsistent state, the damaged objects become visible to
 7      *       other threads, potentially resulting in arbitrary behavior.  Many
 8      *       uses of <code>stop</code> should be replaced by code that simply
 9      *       modifies some variable to indicate that the target thread should
10      *       stop running.  The target thread should check this variable
11      *       regularly, and return from its run method in an orderly fashion
12      *       if the variable indicates that it is to stop running.  If the
13      *       target thread waits for long periods (on a condition variable,
14      *       for example), the <code>interrupt</code> method should be used to
15      *       interrupt the wait.
16      *       For more information, see
17      *       <a href="{@docRoot}/../technotes/guides/concurrency/threadPrimitiveDeprecation.html">Why
18      *       are Thread.stop, Thread.suspend and Thread.resume Deprecated?</a>.
19      */
20     @Deprecated
21     public final void stop() 

直接调用Thread的stop()方法,将不理会原来所设定的释放、取得锁定流程,会直接释放所有已锁定对象,这有可能使对象陷入无法预期的状态。如果要停止线程,最好自行操作,让线程跑完所有的流程,合理地进入Dead状态。

除了stop()方法外,Thread的rerume()、suspend()、destroy()等方法也不建议再使用。更合理的做法是正确地设计你的线程,不要采用这种“走捷径”的粗暴方法。

3.4 线程小结

多线程其实并不是真正意义上的并发,只是一种更高效率地利用CPU的手段。就像我们可以在烧水的时候拿茶叶,在煮茶的时候拿快递,但是不可能在开煤气的同时还能去开茶叶罐,也不可能在把茶叶放进茶壶的同时去开门。在程序开发当中,多线程是提高效率的常用手段之一。

实现多线程有两种方式,一是直接继承Thread类,二是实现Runnable接口。根据不同的需求和情况,我们可以选择更合适的方法。要想做到因时制宜,必须要做到对各方面都了解的程度。

线程什么时候创建,什么时候启动,什么时候运行,什么时候挂起,什么时候终止··· ···这些统称为线程的生命周期。掌握线程的生命周期,就等于掌握了线程,让其更符合程序员的设计运作。

相关文章推荐:

JavaSE中Collection集合框架学习笔记(1)——具有索引的List

JavaSE中Collection集合框架学习笔记(2)——拒绝重复内容的Set和支持队列操作的Queue

JavaSE中Collection集合框架学习笔记(3)——遍历对象的Iterator和收集对象后的排序

JavaSE中Map框架学习笔记

如果你喜欢我的文章,可以扫描关注我的个人公众号“李文业的思考笔记”。

不定期地会推送我的原创思考文章。

时间: 2024-10-14 14:19:41

JavaSE中线程与并行API框架学习笔记1——线程是什么?的相关文章

JavaSE中线程与并行API框架学习笔记——线程为什么会不安全?

前言:休整一个多月之后,终于开始投简历了.这段时间休息了一阵子,又病了几天,真正用来复习准备的时间其实并不多.说实话,心里不是非常有底气. 这可能是学生时代遗留的思维惯性--总想着做好万全准备才去做事.当然,在学校里考试之前当然要把所有内容学一遍和复习一遍.但是,到了社会里做事,很多时候都是边做边学.应聘如此,工作如此,很多的挑战都是如此.没办法,硬着头皮上吧. 3.5 线程的分组管理 在实际的开发过程当中,可能会有多个线程同时存在,这对批量处理有了需求.这就有点像用迅雷下载电视剧,假设你在同时

JavaSE中Collection集合框架学习笔记(2)——拒绝重复内容的Set和支持队列操作的Queue

前言:俗话说“金三银四铜五”,不知道我要在这段时间找工作会不会很艰难.不管了,工作三年之后就当给自己放个暑假. 面试当中Collection(集合)是基础重点.我在网上看了几篇讲Collection的文章,大多都是以罗列记忆点的形式书写的,没有谈论实现细节和逻辑原理.作为个人笔记无可厚非,但是并不利于他人学习.希望能通过这种比较“费劲”的讲解,帮助我自己.也帮助读者们更好地学习Java.掌握Java. 无论你跟我一样需要应聘,还是说在校学生学习Java基础,都对入门和进一步启发学习有所帮助.(关

JavaSE中Collection集合框架学习笔记(3)——遍历对象的Iterator和收集对象后的排序

前言:暑期应该开始了,因为小区对面的小学这两天早上都没有像以往那样一到七八点钟就人声喧闹.车水马龙. 前两篇文章介绍了Collection框架的主要接口和常用类,例如List.Set.Queue,和ArrayList.HashSet.LinkedList等等.根据核心框架图,相信我们都已经对Collection这个JavaSE中最常用API之一有一个较为全面的认识. 这个学习过程,还可以推及到其他常用开源框架和公司项目的学习和熟悉上面.借助开发工具或说明文档,先是对项目整体有一个宏观的认识,再根

Java集合框架学习笔记之集合与Collection API

一.CollectionAPI 集合是一系列对象的聚集(Collection).集合在程序设计中是一种重要的数据接口.Java中提供了有关集合的类库称为CollectionAPI. 集合实际上是用一个对象代表一组对象,在集合中的每个对象称为一个元素.在集合中的各个元素的具体类型可以不同,但一般说来,它们都是由相同的类派生出来的(而这一点并不难做到,因为Java中的所有类都是Object的子类).在从集合中检索出各个元素是,常常要根据其具体类型不同而进行相应的强制类型转换. Collection

Yii框架学习笔记(二)将html前端模板整合到框架中

选择Yii 2.0版本框架的7个理由 http://blog.chedushi.com/archives/8988 刚接触Yii谈一下对Yii框架的看法和感受 http://bbs.csdn.net/topics/390807796 更多内容 百度:yii 前端 http://my.oschina.net/u/1472492/blog/221085 摘要 Yii框架学习笔记(二)将html前端模板整合到框架中 原文地址:http://www.ldsun.com/1309.html 上一节成功将Y

Linux System Programming 学习笔记(七) 线程

1. Threading is the creation and management of multiple units of execution within a single process 二进制文件是驻留在存储介质上,已被编译成操作系统可以使用,准备执行但没有正运行的休眠程序 进程是操作系统对 正在执行中的二进制文件的抽象:已加载的二进制.虚拟内存.内核资源 线程是进程内的执行单元 processes are running binaries, threads are the smal

MEAN框架学习笔记

MEAN框架学习笔记 MEAN开发框架的资料非常少.基本的资料还是来自于learn.mean.io站点上的介绍. 于是抱着一种零基础学习的心态,在了解的过程中,通过翻译加上理解将MEAN框架一点点消化而且吸收,一步一步来.慢慢地记录我学习MEAN的点点滴滴. 1.MEAN是可以管理用户的 通过MEAN的mean-cli来管理用户.命令是: $ mean user <email> $ mean user <email> --addRole <role>; $ mean u

操作系统学习笔记----进程/线程模型----Coursera课程笔记

操作系统学习笔记----进程/线程模型----Coursera课程笔记 进程/线程模型 0. 概述 0.1 进程模型 多道程序设计 进程的概念.进程控制块 进程状态及转换.进程队列 进程控制----进程创建.撤销.阻塞.唤醒.... 0.2 线程模型 为什么引入线程 线程的组成 线程机制的实现 用户级线程.核心级线程.混合方式 1. 进程的基本概念 1.1 多道程序设计 允许多个程序同时进入内存运行,目的是为了提高CPU系统效率 1.2 并发环境与并发程序 并发环境: 一段时间间隔内,单处理器上

windows下scrapy框架学习笔记—&#39;scrapy&#39; 不是内部或外部命令

最近几天在深入的学习scrapy框架,但是装完各种需要的基础包之后却发现scrapy命令在别的路径下都用不了,我一开始是把python安装在F:\Python路径下的,安装了scrapy后它默认都会安装在这个路径下,scrapy在路径F:\Python\Scripts路径下,我的scrapy命令只能在此路径下用,因此创建什么工程也都只能在此文件下. 想了一下它的工作原理:它在F:\Python\Scripts路径下,就会在Scripts文件下存在一个scrapy批处理文件,那么在DOS下想要命令