首先,我们讨论一下进程,进程是操作系统级别下,单独执行的一个任务。Win32 Unix都是多任务操作系统。多任务,并发执行是一个宏观概念,实际微观串行。CPU同一时间刻只能执行一个任务。OS负责进程调度,使用CPU,也就是获得时间片。在一个进程中,再可以分为多个程序顺序执行流,每个执行流就是一个线程。分配CPU时间片的依然是CPU,多线程时,程序会变慢,每个线程分配到的时间片少了。进程与线程的区别,进程是数据独占的(独立数据空间),线程是数据共享的(这也是线程之间通讯容易的原因,不需要传递数据)。Java是语言级支持多线程的,体现在有现成的封装类(java.lang.Thread)完成了必要的并发细节的工作(与操作系统打交道,分配PID等)。两种方式来得到一个线程对象。
一个线程对象--〉代表着一个线程--〉一个顺序执行流(run方法)这个程序有两个线程,一个是main主线程,它调用了t1.start(),这是t1线程只是就绪状态,还没有真正启动线程,main主线程结束了!!t1运行。两个线程都退出了,进程完结。一个进程退出,要等待进程中所有线程都退出,再退出虚拟机。方式2,实现java.lang.Runable接口,这是这个类的对象是一个目标对象,而不能理解为是一个线程对象。
不要调用run()方法,它只是执行一下普通的方法,并不会启动单独的线程。上面只是线程状态图。在某一个时间内,处于运行状态的线程,执行代码,注意可能多个线程多次执行代码。CPU会不断从可运行状态线程调入运行,不会让CPU空闲。Thread.sleep(1000);当前线程睡眠1秒钟,休眠后->进入阻塞->休眠结束->回到可运行状态。在run(),有异常抛出,必须try{}catch(Exception e){},不能throws Exception,因为run()方法覆盖不能抛例外。能进入运行状态,只能由操作系统来调度。一旦sleep-〉阻塞->交出程序执行权。等待用户输入,输入输出设备占用CPU,处于阻塞的线程没有机会运行,输入完毕,重新进入可运行状态。第三种进入阻塞状态的可能。t1.join()调用后,运行状态线程放出执行权,作为t1的后续线程,等待t1结束。也就是说至少得等t1线程run完毕,才可能进入运行状态来执行,可不是说t1执行完,一定马上就是调用t1.join()的线程马上进入可运行行状态。只有操作系统有权利决定谁进入运行状态。join的实质就是让两个线程和二为一,串行。t1.join();执行这条语句现场是被保护起来的。t1结束,调用线程有机会运行时,会从上次的位置继续运行。
线程优先级,setPriotity(1--100),数越大,优先级越高。开发中不提倡自省设置优先级,操作系统可能忽略优先级,不具有跨平台性(两方面,可运行,执行效果一致),因为这种方式很粗略。static void yield(),运行状态的线程(当前线程),调用yield方法,马上交出执行权。回到可运行状态。
=============================
Thread对象有个run方法,当start()时,Thread进行系统级调用,系统分配一个线程空间,此时对象可以获得CPU时间片,一个顺序执行流程可以独立运行,线程结束,对象还在,只是系统回收线程。=============================两个线程同时的资源,称为临界资源,会有冲突。堆栈数据结构,有一个char[]和一个index(表示实际长度,也表示下一个要插入元素的位置)。一个push操作,有两个核心操作(加元素,修改index)。都执行和都没执行,没有问题。但假设一个线程做了一个步,就交出执行权,别的线程,执行同样的代码,会造成数据不一致。数据完整性也是一个要在开发中注意的地方。------------------------------------------所以为了保证数据安全,要给数据加锁。一个Java对象,不仅有属性和方法,还有别的东西。任何一个对象,都有一个monitor,互斥锁标记,可以交给一个线程。只有拿到这个对象互斥锁标记的线程,才能访问这个对象。synchronized可以修饰方法和代码块。synchronized(obj){ obj.setValue(123);}不是每个线程都能进入这段代码块,只有拿到锁标记的线程才能进入执行完,释放锁标记,给下一个线程。记住,锁标记是对对象来说的,锁的是对象。当synchronized标识方法时,那么就是锁当前对象。
注意此代码中,Stack这个临界资源类中的push方法中,有一个Thread.sleep,它让当前进程阻塞,也就是让拥有Stack对象s锁标记的线程阻塞,但这时它并不释放锁标记。所以Synchronized使用是有代价的,牺牲效率换数据安全,要控制synchronized代码块,主要是数据写,修改做同步限制,读就不用了。还有一点要注意:synchronized不能继承, 父类的方法是synchronized,那么其子类重载方法中就不会继承“同步”。一个线程可以拥有很多对象锁标记,但一个对象的锁标记只能给一个线程。等待锁标记的线程,进入该对象的锁池。每个对象都有一个空间,锁池,里面都是等待拿到该对象的锁标记的线程。当然还是操作系统来决定谁来获得锁标记,在上一个锁标记释放掉后。死锁,线程A拿到resourceA标记,去请求resourceB;线程B拿到resourceB标记,去请求resourceA;线程间通讯机制->协调机制一个对象不仅有锁和锁池,另外还有一个空间[等待队列]。synchronized(路南){ 想要获得路北资源的线程,调用路南.wait();将自己的所有锁标记都释放。以便其他线程满足条件运行程序后,自己也就可以正常通过了。}调用obj.wait(),表示某一个线程释放所有锁标记并进入obj这个对象的等待队列。等待队列也是阻塞状态。一个线程调用obj对象的notify(),会通知等待队列中的一个线程可以出来,notifyAll()是通知所有线程。
上面为经典的生产者消费者问题,生产者使用SyncStack的push方法,消费者使用pop方法。push方法: while (index==data.length) { try{ this.wait();//<-----------释放所有锁标记,阻塞现场保留 } catch (InterruptedException e){} }如果货架满了,生产者即使拥有锁标记,也不能再生产商品了,必须wait()。等待消费者消费物品,否则永远不会从SyncStack对象的等待队列中出来。等待通知,何时通知呢?public synchronized char pop() { while (index==0) { try{ this.wait(); } catch (InterruptedException e){} } index--; char c=data[index]; data[index]=' '; System.out.println("Char "+c+" Poped from Stack"); for(int k=0;k<data.length;k++) System.out.print(data[k]); System.out.println(); this.notifyAll(); //<-------------所有等待队列中的生产者都出了队列,因为没有锁标记,只能进入锁池。
return c; }
为什么判断是一个while循环,而不是一个if呢?注意:我们假设一种情形:1) 有十个生产者线程,货架已经满了,10生产者依次获得锁标记,依次都调用this.wait(),都进入同一个SyncStack对象的等待队列,10个进程阻塞住,2) 有一个消费者线程获得该SyncStack对象锁标记,一个消费者消费一个,执行完消费,调用this.notifyAll()释放锁标记。3) 刚才消费者调用SyncStack对象的notifyAll()后,10个线程都出来了,准备生产商品,全部进入锁池。这十个线程的代码现场,还在wait()这个函数调用后面,也就是一旦或者锁标记,要继续从这里执行。this.wait();//从这一句的后面继续执行。4) 但如果有一个生产者push的话,货架已经就满了,但这时还有9个在锁池中,依次获得锁标记,但由于是while需要再次判断是否货架满不满,才能继续前行进行生产。如果是if,就会直接push,数组越界。===================================释放锁标记只有两种途径,代码执行完,wait()让线程结束,就是想办法让run方法结束。注意下面的bStop,标志位,可以在线程进入wait状态时,对某一线程调用interrupt(),线程抛出InterruptedException,然后根据标志位,方法返回。
Exc:AB1CD2.... 数字与字母依次打印。用线程完成。