参考资料:郝斌老师的Java学习视频
一,进程( process )与程序( program )
1,定义:
程序:只是一组指令的有序集合,它本身没有任何运行的含义,它只是一个静态的实体
进程:是程序在某个数据集上的执行。
进程是一个动态的实体,它有自己的生命周期。它因创建而产生,因调度而运行,因等待资源或事件而被处于等待状态,因完成任务而被撤消。
2,进程的由来:
通俗点说,为了不破坏“程序”这个词原有的含义:“是一个在时间上严格有序的指令集合,是静态的保存在存储介质上。”而又能刻画多个程序共同运行时呈现出的新特征,所以引入了进程这一概念。
如:记事本是一个程序,打开的记事本是运行的程序,但在电脑中我们同时打开多个记事本,此时程序与运行的程序无法对应,所以就需要引入进程的概念。即一个记事本程序打开了多个记事本进程。
二,线程( thread )
1,定义:
线程:一个程序里的不同执行路径。
2,解释
以前所编写的程序,每个程序都有一个入口,一个出口以及一个顺序执行的序列,在程序执行过程中的任何指定时刻,都只有一个单独的执行点。
事实上,在单个程序内部是可以在同一时刻进行多种运算的,即所谓的多线程。
另外:ctrl + c 可以停止死循环(cmd 中的)
3,例子
class A extends Thread { public void run() { while(true) { System.out.println("AAA"); } } } public class M { public static void main(String[] args) { A aa = new A(); // aa.start(); // 会开辟一个新的线程,并自动调用run方法,如果类中没有run方法(注意方法名字只能run),则不会调用 aa 的任何方法 aa.start(); // aa.run(); // 单线程执行,直接调用 run() 是不会创建一个新的线程 while(true) { System.out.println("BBB"); } } }
4,多线程的优势:
多线程编程简单,效率高(能直接共享数据和资源,多进程不能)
适合于开发服务程序(如服务,聊天服务等)
5,创建一个线程的第一种方法
① 创建一个继承Thread的类(假定类名为A),并重写Thread中的run方法
② 构造一个A类对象,假定对象名为aa
③ 调用aa的start方法(start方法是从Thread继承过来的)
注意:
调用start() 方法,会开辟一个新的线程,并自动调用run方法,如果类中没有run方法(注意方法名字只能run),则不会调用 aa 的任何方法
一个Thread对象能且只能代表一个线程。
一个Thread对象不能调用两次start()方法,否则会抛出java.lang.IllegalThreadStateException异常
6,线程与CPU
执行完aa. start();后并不表示aa所对应的线程就一定会立即得到了执行,
aa.start();执行完后 只是表示aa线程具有了可以立即被CPU执行的资格,
但由于想抢占CPU执行的线程很多,CPU并不一定会立即去执行aa所对应的线程。
所以 AAA 和 BBB 不一定哪个先输出。
CPU:优先级,执行时间,等待时间,紧急程度
7,多线程的切换
8,线程的生命周期
① 创建状态
当用new操作符创建一个新的线程对象时,该线程处于创建状态。
处于创建状态的线程只是一个空的线程对象,系统不为它分配资源。
要通过调用start方法启动该线程。
② 可运行状态
执行线程的start()方法将为线程分配必须的系统资源,安排其运行,并调用线程体中的run()方法,这样就使得该线程处于可运行( Runnable )状态。
这一状态并不是运行中状态(Running ),因为线程还要在cpu排队。
返回可运行状态的原因:
处于睡眠状态的线程在指定的时间过去后;
如果线程在等待某一条件,另一个对象必须通过notify()或notifyAll()方法通知等待进程条件的改变;
如果线程是因为输入/输出阻塞,输入/输出完成
③ 不可运行状态
当发生下列事件时,处于运行状态的线程会转入到不可运行状态:
调用了sleep()方法;
线程调用wait方法所等待的特定条件的满足;
线程输入/输出阻塞。
④ 消亡状态
当线程的run方法执行结束后,该线程自然消亡。
9,创建一个新线程的第二种方法
① 定义一个实现了Runnable接口的类,假定为A
② 创建A类对象aa,代码如下
A aa = new A();
③ 利用aa构造一个Thread对象tt,
Thread tt = new Thread(aa);
④ 调用t中的start方法
tt.start();
其中:Thread的构造方法为:public Thread(Runnable target)
class A implements Runnable { public void run() { while(true) { System.out.println("AAA"); } } } public class M { public static void main(String[] args) { A aa = new A(); Thread t = new Thread(aa); //Thread的构造方法 public Thread(Runnable target) t.start(); while(true) { System.out.println("BBB"); } } }
10,Thread 的常用方法
View Code
public final void setName(String name)
设置当前线程的名字
public static Thread currentThread()
返回对当前正在执行的线程对象的引用.
public final String getName()
返回当前线程的名字
class A extends Thread { public void run() { System.out.printf("%s在执行! ",Thread.currentThread().getName()); } } public class M { public static void main(String[] args) { A aa1 = new A(); aa1.setName("张三"); aa1.start(); A aa2 = new A(); aa2.setName("李四"); aa2.start(); A aa3 = new A(); aa3.setName("王五"); aa3.start(); System.out.printf("%s在执行! ",Thread.currentThread().getName()); } }
三,线程控制
1,线程优先级
View Code
View Code
3,线程的让步
View Code
Java提供 一个线程调度器来监控程序中启动后进入就绪状态的所有线程。线程调度器按照线程的优先级决定应调度哪个线程来执行。线程的优先级用数字表示范围从1到10。主线程的缺省优先级是5,子线程的优先级默认与其父线程相同。通常高优先级的线程将先于优先级低的线程执行,但并不总是这样,因此实际开发中并不单纯依赖优先级来决定线程运行次序。
Thread.NORM_PRIORITY = 5
Thread.MAX_PRIORITY = 10
Thread.MIN_PRIORITY = 1
可以使用下述线方法获得或设置线程对象的优先级:
int getPriority();
void setPriority(int newPriority);
class T1 implements Runnable { public void run() { for(int i=0; i<100; i++) { System.out.println("T1: " + i); } } } class T2 implements Runnable { public void run() { for(int i=0; i<100; i++) { System.out.println("------T2: " + i); } } } public class M { public static void main(String[] args) { Thread t1 = new Thread(new T1()); Thread t2 = new Thread(new T2()); t1.setPriority(Thread.NORM_PRIORITY + 5); t1.start(); t2.start(); } }
2,线程的休眠.
线程休眠:暂停执行当前运行中的线程,使之进入阻塞状态,待经过指定的“延迟时间"后再醒来并转入到就绪状态。
Thread类提供的相关方法:
public static void sleep(long millis) throws InterruptedException
public static void sleep(long millis, int nanos) throws InterruptedException
由于是静态方法,可以由Thread直接调用。
class A implements Runnable { public void run() // throws InterruptedException { while(true) { System.out.println(Thread.currentThread().getName()); int j = 100; while(j-- != 0) System.out.println("hhhhhhhhhhhhhhhhh"); try { Thread.sleep(4000); // 如果没有其他线程,则控制台只能干等 } catch (Exception e){} /* Thread.sleep(4000); 重写方法抛出的异常范围不能大于被重写方法抛出的异常范围 如果没有 try—catch 的话,直接在 run 方法抛出异常,throws InterruptedException 因为 run 是重写方法,实现的接口 Runnable run() 方法,而接口的 run() 方法 是不抛出异常的 所以会报错的 综上,sleep 会抛出异常,而 run() 方法无法抛出异常,所以必须用 try—catch 语句捕捉异常 */ } } } public class M { public static void main(String[] args) { A aa = new A(); Thread tt = new Thread(aa); tt.start(); while(true) { System.out.println("哈哈"); } } }
注意:重写方法抛出的异常范围不能大于被重写方法抛出的异常范围。
如果直接在 run 方法抛出异常,throws InterruptedException。因为 run 是重写方法,实现的是Runnable接口的run() 方法,而Runnable的 run() 方法 是不抛出异常的,所以会报错的。Thread 中的 run() 方法也是一样。
简单来说,就是 sleep 会抛出异常,而 run() 方法无法抛出异常,所以必须用 try—catch 语句捕捉异常。
3,线程的让步
让运行中的线程主动放弃当前获得的CPU处理机会,但不是使该线程阻塞,而是使之转入就绪状态。所以如果没有优先级更高或相等的线程,让步就相当什么也没做,
public static void yield(
class MyThread implements Runnable { public void run() { for(int i = 1 ; i <= 100 ; i++) { System.out.println(Thread.currentThread().getName()+": " + i); if(0 == i % 10) { Thread.yield(); } } } } public class M { public static void main(String[] args) { MyThread mt = new MyThread(); Thread t1 = new Thread(mt); Thread t2 = new Thread(mt); t1.setName("线程A"); t2.setName("线程B"); t1.start(); t2.start(); } }
4,线程的串行化
在多线程程序中,如果在一个线程运行的过程中要用到另一个线程的运行结果,则可进行线程的串型化处理。
public final void join() throws InterruptedException
暂停当前正在执行 t.join();的线程,直到t所对应的线程运行终止之后,暂停的线程才会获得继续执行的机会。
class MyRunner implements Runnable { public void run() { for(int i = 0 ; i < 50 ; i++) { System.out.println("子线程: " + i); } } } public class M { public static void main(String args[]) { MyRunner r = new MyRunner(); Thread t = new Thread(r); t.start(); // 开启子线程 try { t.join(); // 暂停当前正在执行 t.join();的线程(main 函数) // 直到t所对应的线程运行终止之后,当前线程(mian 函数)才会获得继续执行的机会 } catch(InterruptedException e) { e.printStackTrace(); } for(int i = 0 ; i < 50 ; i++) { System.out.println("主线程:" + i); } } }
四,线程的同步
1,需要线程同步的原因——买票问题
if ( 票数 > 0 ) {
买一张票
票数--
}
此时如果有三个网站在买票,当你在 A 网站买了一张票,但票数还没有减一的情况下,线程跳到了 B 网站,这样就导致了混乱,且到了最后一张票,还可能发生多人都买了,这样就导致最终票数被买到负数,而不是 0。
所以,就需要将整个 if 当成整体,让 cpu 无法在 任意一个点切换,必须整体执行完才能切换,即线程的同步。
// 一张票卖了两次 class A implements Runnable { private static int tickets = 100; public void run() { while (true) { if (tickets > 0) { System.out.printf("%s线程 正在卖出第% d 张票 ", Thread.currentThread().getName(), tickets); --tickets; } else { break; } } } } public class M { public static void main(String[] args) { A aa1 = new A(); Thread t1 = new Thread(aa1); t1.start(); // 这样会导致一张票卖了两次 A aa2 = new A(); Thread t2 = new Thread(aa2); t2.start(); } }
2,synchronized 关键字
synchronized 可以修饰:一个方法或者一个方法内部的某个代码块
格式:
synchronized(类对象名 aa)
{
同步代码块
}
synchronized(类对象名aa)的含义是:
判断aa是否已经被其他线程霸占,如果发现已经被其他线程霸占,则当前线程陷入等待中。
如果发现aa没有被其他线程霸占,则当前线程霸占住aa对象,并执行内部的同步代码块,在当前线程执行synchronized同步代码块时,其他线程将无法再执行该代码(因为当前线程已经霸占了aa对象)。
若当前线程执行完其同步代码块,会自动释放对AA对象的霸占。
此时其他线程会相互竞争对AA的霸占,最终CPU会选择其中的某个线程执行。
最终导致的结果是一个线程正在操作某资源的时候,将不允许其它线程操作该资源,即一次只允许一个线程处理该资源。
注:
霸占的专业术语叫锁定,霸占住的那个对象专业术语叫做监听器
synchronized修饰一个方法时,实际霸占的是该方法的this指针所指向的对象,即正在调用该方法的对象。
// 卖票 class A implements Runnable { private static int tickets = 100; // tickets 要用 static 不然各买各的,就有200张票 private static String str = new String("切不了我了吧"); // str 要用 static 修饰,保证两个线程锁定的是同一个资源 public void run() { while (true) { synchronized (str) { if (tickets > 0) { System.out.printf("%s 线程 正在卖出第 %d 张票 ", Thread.currentThread().getName(), tickets); --tickets; } else break; } } } } public class M { public static void main(String[] args) { A aa = new A(); Thread t1 = new Thread(aa); t1.start(); Thread t2 = new Thread(aa); t2.start(); } }
3,生产与消费问题
通常,一些同时运行的线程需要共享数据。在这种时候,每个线程就必须要考虑与其他一起共享数据的线程的状态与行为(同步),否则的话就不能保证共享数据的一致性,从而也就不能保证程序的正确性。
与买票问题类似,在生产与消费问题,要用到栈。在用数组模拟栈时,元素的出入栈与栈顶指针的移动必须同步。
错误的设计:无法保证生产先于消费运行
// 不合理的消费和生产设计——无法保证生产先于消费运行 class SynStack { private int top = 0 ; private char data[] = new char[110]; public void push(char ch) // 生产,入栈 { data[top] = ch; top++; } public char pop() // 消费,出栈 { top--; return data[top]; } } class Producer implements Runnable // 生产者 { private SynStack ss = null; public Producer(SynStack ss) { this.ss = ss; } public void run() { ss.push('a'); } } class Consumer implements Runnable // 消费者 { private SynStack ss = null; public Consumer(SynStack ss) { this.ss = ss; } public void run() { System.out.printf("%c ",ss.pop()); } } public class M { public static void main(String[] args) { SynStack ss = new SynStack(); Producer p = new Producer(ss); Consumer c = new Consumer(ss); Thread t1 = new Thread(p); t1.start(); Thread t2 = new Thread(c); t2.start(); } }
aa.wait()
将执行aa.wait()的当前线程转入阻塞状态,让出CPU的控制权,并释放对aa的锁定
aa.notify()
假设执行aa. notify()的当前线程为T1。如果当前时刻有其他线程因为执行了aa.wait()而陷入阻塞状态,则叫醒其中一个,但具体是叫醒哪一个,这是由系统调度器控制,程序员无法控制。
执行aa.notify() 方法时如果一个线程都没有叫醒的话,也是可以的。
所谓叫醒某个线程就是令该线程从因为wait而陷入阻塞的状态转入就绪状态
aa.notifyAll()
叫醒所有因为执行了aa.wait()而陷入阻塞状态的线程
正确的设计:
// 生产消费 正确的设计 class SynStack { private int top = 0 ; // 栈顶指针 private char st[] = new char[20]; public synchronized void push(char ch) // 生产 { /** * 如果空间满了,无法再生产的话,就将生产的方法wait()掉。 * 显然,虽然下面有 notify() 但当前线程已经 wait() 掉了,就无法执行后面的语句,所以自己是唤醒不了自己的 * 只能通过该对象的另外一个方法:消费,使用notify()来唤醒。 * 这正对应无法生产下去的情况下,只能先消费一部分,再来生产。 */ /** * 如果只有一个生产和消费者: * 这里因为只有生产和消费相互影响,所以可以用 if * 因为如果 wait 之后被唤醒,一定是消费过了, * 所以就可以继续生产了 * 但如果是有三个线程相互影响,则要用 while * 即唤醒之后,要再度循环,判断是因为什么而被唤醒的 * * 但是如果有多个消费者和生产者: * 也是要用while。 * 因为生产者_1可能被生产者_2唤醒,而此时还没有消费, * 所以此时还是没有空间方产品 * 一生产就数组越界了 * * 所以最好还是用 while 保险 */ while(top == st.length) { try{ this.wait(); } catch(Exception e){ } } this.notify(); // 如果生产没有唤醒的话,消费完了,就会陷入一直的等待生产了 st[top] = ch; top++; System.out.printf("%s正在生产第 %d 个产品,该产品是: %c ",Thread.currentThread().getName(), top, ch); } public synchronized char pop() // 消费 { /** * 如果没有产品了,无法再消费的话,就将消费的方法wait()掉。 * 显然,虽然下面有 notify() 但当前线程已经 wait() 掉了,就无法执行后面的语句,所以自己是唤醒不了自己的 * 只能通过该对象的另外一个方法:生产,使用notify()来唤醒。 * 这正对应在没有产品的情况下,只能等生产出来产品,才能消费。 */ /** * 如果只有一个生产和消费者: * 这里因为只有生产和消费相互影响,所以可以用 if * 因为如果 wait 之后被唤醒,一定是产生过了, * 所以就可以继续消费了 * 但如果是有三个线程相互影响,则要用 while * 即唤醒之后,要再度循环,判断是因为什么而被唤醒的 * * 但是如果有多个消费者和生产者: * 也是要用while。 * 因为消费者_1可能被消费者_2唤醒,而此时还没有生产, * 所以此时还是没有产品 * 一消费就数组越界了 * * 所以最好还是用 while 保险 */ while(top == 0) { try{ this.wait(); } catch(Exception e){ } } this.notify(); // 如果消费没有唤醒的话,生产结束了,就会陷入一直的等待消费了 top--; System.out.printf("%s正在消费第 %d 个产品,该产品是: %c ", Thread.currentThread().getName(), top+1, st[top]); return st[top]; } } class Producer implements Runnable // 生产者 { private SynStack ss = null; public Producer(SynStack ss) { this.ss = ss; } public void run() { for(int i = 0 ; i < 50; i++) // 生产 50 个字母 { char ch = (char) ('a' + i%26); ss.push(ch); } } } class Consumer implements Runnable // 消费者 { private SynStack ss = null; public Consumer(SynStack ss) { this.ss = ss; } public void run() { for(int i = 0 ; i < 50; i++) // 消费 50 个字母 { ss.pop(); } } } public class M { public static void main(String[] args) { // 必须保证执行生产方法和消费方法的对象,是同一个对象 SynStack ss = new SynStack(); Producer p = new Producer(ss); Consumer c = new Consumer(ss); Thread p1 = new Thread(p); // 生产者1 p1.setName("生产者_1"); Thread p2 = new Thread(p); // 生产者2 p2.setName("生产者_2"); Thread c1 = new Thread(c); // 消费者1 c1.setName("消费者_1"); Thread c2 = new Thread(c); // 消费者2 c2.setName("消费者_2"); p1.start(); p2.start(); c1.start(); c2.start(); } }
========== ======== ======= ====== ====== ===== ==== === == =