有道云笔记 连接:
http://note.youdao.com/noteshare?id=253ac934b878a370051d85a07028eb46
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
以下可忽略:
《Java线程》
对于系统级开发,线程尤为重要。
线程,是一个程序里面,不同的执行路径。
进程:一个静态的概念。eg:一个.exe文件,一个.class文件。
程序的执行过程:OS先把该程序的代码放到内存代码区里面。这时一个进程产生,但还没执行。当该进程中的主线程执行时,该进程称为执行。所以进程相当于线程的“壳”,进程不会执行,只有进程里的线程能够执行。计算机上,实际运行的都是线程。
多线程:线程的并发不是同时执行的,CPU把时间片分给各个线程,切换时间很短,看起来是同时执行的。某个时间点,CPU只能执行一个线程(进程)。
上例,当执行到t.start()的时候,开辟了一个新的线程t,它和主线程开始并行执行(对CPU,实际是交替执行的)。执行结果,打印一会main线程一会t线程,交替显示,没有规律。这就是线程调用和方法调用的区别。若上例中直接调用一个方法执行原来Runner1中run()的内容,那么执行结果将是有顺序的打印。
还要注意,上例,我们重写的是Runner类(实现Runnable接口)的run()方法;而当Thread t = new Thread( r ),把Runner的对象r作为参数来new一个线程t后,启动线程是t.start();实际执行的是r.run(),但在线程中是t.run()。
Runnable还是Thread?
推荐Runnable接口,因为比较灵活,还可再继承别的类和实现别的接口。继承Thread后比较死。
注意,start()方法后线程只是“就绪”(等待调度),而不是直接“运行”。
isAlive(),“活着”是指“就绪”、“运行”、“阻塞”3个状态。
优先级:优先级越高,获得CPU时间片越多。
上例,执行结果为,子线程循环10次(每次1秒);10秒时,主线程将子线程打断(thread.interrupt());子线程捕获InterruptedException,结束;主线程结束。
注意1,Thread.sleep(***)静态方法,在哪个线程中使用,就睡眠哪个线程;即由所处的线程决定。
注意2,上例中,子线程类MyThread的定义中,sleep(1000)方法的异常InterruptedException不能交由run()抛出(throws出去);这是因为run()方法是一个重写父类的方法,只能抛出与父类完全相同的异常。
注意3,“捕获InterruptedException然后return空”不是打断睡眠结束线程的最好方式。eg:打开的资源未必来的及close()。还有个stop()方法来结束线程,但这个方法基本废弃,一般只有在结束锁死的线程时使用。比较温和的方法,下图:
上例,t1.join()方法,使得并行执行的两个线程合并为单一顺序执行的一个线程。结果中,main线程由于t1.join(),必须等待t1执行完后才能继续执行。
上例,观察结果能观察到,i被10整除时必让出cpu一次。
上例,线程同步一个重要例子。
代码分析:首先,Timer类(简单Pojo),有静态变量num和方法add(String name);add(String name)的作用,Timer.add(String name)每被调用一次,静态变量num加1,然后打印“**是第num个被调用的”;
其次,Test类(实现Runnable接口),包含必须的run()方法和main()方法,还有一个属性Timer对象;
再次,main()方法中,用Test类的对象test初始化了两个线程t1和t2,然后t1和t2分别setName();
最后,t1线程启动后,进入run()方法,调用timer.add();在timer.add()中,num由0变1,然后sleep(1),注意此时还没有打印t1的结果;sleep(1)的时候,t2启动,进入run()和add()方法,num再加1变为2;所以,打印的结果是t1和t2都是“第2个”。
此例出错的核心是,t1和t2线程都调用了资源timer(Timer的add()方法),但是add()方法分若干步完成,在没有将这些步骤同步的情况下,会产生问题(sleep(1)加大了错误发生概率)。
此例看起来别扭的原因是,main()方法所在的Test类继承Runnable接口,来初始化main()方法中的两个子线程t1和t2,即“自身使用自身”。
同步的两种方法:
注意,sychronized锁(称为对象互斥锁,但syschronized表示“同步的”,即资源是同步的,使用资源的对象是互斥的),有两种方式。其内部机制是:执行该方法的过程中锁定当前对象。只有当synchronized块的代码执行完,下个使用它的对象才能执行它,否则等待。但这个说法可能有歧义,看后面的笔试题例子。一般synchronized理解为:保证被它包围的代码段是个整体。
死锁机制:对象a需要使用同步资源x和y(先x后y),对象b使用y和x(先y后x)。这样a锁定x等待y,b锁定y等待x。
上例,死锁。
解决死锁,一个办法是把锁的粒度加粗。即尽量不锁定两个对象而是一个对象。
死锁一般在中间件中被解决了,小的项目不容易碰到,系统级的程序会遇到。数据库软件开发经常使用到各种锁。eg:只读锁、只写锁,行级锁、表级锁。
上例,为一常见笔试题。执行过程:从main()方法看,先启动了子线程t,子线程t会调用run()方法中的synchronized方法m1();m1()会把属性b由默认100改为1000,然后睡眠5秒;与此同时,主线程main()睡眠了1秒(为了m1()中的b = 1000执行完),然后调用对象tt的普通方法m2()。
注意,syschronized的“执行该方法的过程中锁定当前对象”,不是指若对象中有一个方法含synchronized,则使用时锁定该synchronized方法(或代码块)就是锁定该对象的所有方法。synchronized很简单,只能保证被它包围的代码段是个整体。上例中,只能保证“b = 1000”、“sleep(50000)”和“system.out.println()”三行代码是个整体。
若将m2()方法改为如下,则结果如下:
上例,先m1()方法的b = 1000,睡眠过程中,m2()方法改为b = 2000,m1()睡了5秒后,打印输出b ,值为2000。表明,synchronized并没有锁住整个对象,只是锁定了它包围的代码段。所以,这样的锁定方法(锁定对象中修改属性的一个方法,但不锁定其他修改属性的方法)是危险的,并没有达到真正锁定属性b的效果。应该,若想在方法中锁定属性的值,则需把所有关于属性操作的方法都锁定。如下:
上例,锁定m2()之后,m2()则必须遵循锁的规则,只有在另一个synchronized方法m1()执行完后,才能执行对b的操作。这就是锁定同一对象中两个方法的效果,一个执行中则另一个必须等待。
即,通过锁定关于属性b更改操作的所有方法,来完成对属性b的锁定。
所以,若想同步一段代码或一个方法,有时是很困难的,需要考虑所在对象中其他方法是否也要同步,还要考虑过多同步带来的开销问题。
上例,描述:Producer(生产者)不停地往SyncStack(筐)中扔Wotou(窝头),Consumer(消费者)不断地从筐中取窝头;而筐最多放6个窝头,当满6时,生产者必须wait(),直到消费者取走馒头,生产者才被notify(),并继续仍馒头;当筐为0时,消费者wait(),同理。
此例核心是SyncStack类:
a) Wotou[] arrwWT= new Wotou[6],用数组表示“筐”。存的时候从0增到6,取的时候从6减到0,实现了栈stack的方式,才符合“筐”的后进先出特点。
b) push()和pop()方法是synchronized的。一是实现了属性index的同步,即index作为共享资源,在多个线程对其加减操作时,必须对其加锁;二是方法内部语句的加锁,arrWT[index] = wt和index++这两句需要时一个整体,否则会发生数组元素被“吃掉”的现象。
c) 在谈到wait()和notify()时,必须注意到:push()和pop()方法公用一个SyncStack对象ss来初始化所有Producer和Consumer的线程。所以当this.notify()执行时,唤醒的只是当前线程(可能是Producer也可能是Consumer)。
d) 右图,是push()方法的开头部分,先判断是否满6个,满了就让SyncStack.push()所在的线程(一定是个Producer线程)wait();然后不管怎样this.notify()一次。注意,这个notify()并不是用来唤醒刚才wait()的那个线程的。因为唤醒后回去判断index还是6,会继续wait()。这个wait()(SyncStack.push()所在的线程),一定是被SyncStack.pop()所在的线程(一定是个Consumer线程)notify()的;因为一个消费者线程pop()了一个窝头后,index才会减1,this.notify()后,一直wait()的线程被唤醒,回去判断发现index不足6了,它才继续执行push()后面的语句。
e) 使用while循环而不是if语句,是因为InterruptException,当wait()被打断捕获异常并处理后,while循环会返回继续循环,而if语句则不会。
f) 此例为方便测试只new了1个生产者和1个消费者,当有多个生产者和消费者时,SyncStack中应是notifAll(),以在消费者取走第6个窝头后,一次唤醒所有因为筐满而等待的生产者。
wait()方法:首先,wait()必须在synchronized用,否则立即报错,即当一个线程锁定中才能wait();其次,wait()与sleep()不同,wait()是Object类的方法,使用时this.wait();其次,wait()后需要被唤醒,否则也会产生死锁;notify()含义是,叫醒一个在当前对象上等待的线程,wait()含义是,将在当前对象上的线程睡眠过去。
什么时候用wait():当某方法(例如发生阻塞事件)需要“暂停”(如判断数组满了),然后等待,直到该对象的某一事件(如判断数组不是满的了)发生并notify()。notifyAll()是指若该对象的多个线程都wait()了,notifyAll()可全都唤醒。