首页 > 代码库 > 多线程

多线程

Java线程》

一、线程

线程,是一个程序里面,不同的执行路径。

进程:一个静态的概念。eg:一个.exe文件,一个.class文件。  

程序的执行过程:OS先把该程序的代码放到内存代码区里面。这时一个进程产生,但还没执行。当该进程中的主线程执行时,该进程称为执行。所以进程相当于线程的”,进程不会执行,只有进程里的线程能够执行。计算机上,实际运行的都是线程。

多线程:线程的并发不是同时执行的,CPU把时间片分给各个线程, 切换时间很短,看起来是同时执行的。某个时间点,一个CPU只能执行一个线程(进程)。

二、Thread

开启线程两种方式:继承thread类,重写run方法,实现runable接口,实现run方法,然后创建Thread对象。通常采用第二种方法

技术分享

Strart()方法:

当执行到t.start()的时候,开辟了一个新的线程(通知CPU),此时处于就绪状态也有可能处于运行状态,取决于操作系统。一旦运行,它和主线程开始并行执行(实际是交替执行的),会在在调度过程中在运行状态和就绪状态转换。通常使用抢占式调度,但一个线程时间片用完,操作系统考虑优先级选择下个线程。   

Start()与Run()方法的区别:

直接调用一个方法执行原来Runner1run()的内容,此时仍然在一个线程内,必须run执行完才能执行下面的代码。

isAlive()方法:活着”是指“就绪”、“运行”、“阻塞”3个状态,“终止”就死了。

setPriority(),getPriority()方法 : 优先级:优先级越高,获得CPU时间片越多。

技术分享

 

上例,执行结果为,子线程循环10次(每次1秒);10秒时,主线程将子线程打断(thread.interrupt());子线程捕获InterruptedException,结束;主线程结束。

注意1Thread.sleep(***)静态方法,在哪个线程中使用,就睡眠哪个线程;即由所处的线程决定

注意2,上例中,子线程类MyThread的定义中,sleep(1000)方法的异常InterruptedException不能交由run()抛出throws出去);这是因为run()方法是一个重写父类的方法,只能抛出与父类完全相同的异常。

注意3“捕获InterruptedException然后return”不是打断睡眠结束线程的最好方式。eg:打开的资源未必来的及close() 还有个stop()方法来结束线程,但这个方法基本废弃(一棍子打死不给catch异常的机会),一般只有在结束锁死的线程时使用。 比较温和的方法,下图:

 

 

2Join:(另外一个线程合并进来,首先执行子线程,执行完了才能执行主线程)

 

 

上例t1.join()方法,使得并行执行的两个线程合并为单一顺序执行的一个线程。结果中,main线程由于t1.join(),必须等待t1执行完后才能继续执行。

 

3yeild

 

 

上例,观察结果能观察到,i10整除时必让出cpu一次。

 

 

 

线程优先级:

 

 

 

三、线程同步

3.1同步:

几个线程对同一个资源进行访问

 

 

上例,线程同步一个重要例子。

代码分析:首先Timer类(简单Pojo),有静态变量num和方法add(String name)add(String name)的作用,Timer.add(String name)每被调用一次,静态变量num1,然后打印“**是第num个被调用的”;

其次Test类(实现Runnable接口),包含必须的run()方法和main()方法,还有一个属性Timer对象;

再次main()方法中,用Test类的对象test初始化了两个线程t1t2,然后t1t2分别setName()

最后t1线程启动后,进入run()方法,调用timer.add();在timer.add()中,num01,然后sleep(1),注意此时还没有打印t1的结果;sleep(1)的时候t2启动,进入run()add()方法,num再加1变为2;所以,打印的结果是t1t2都是“第2个”。

此例出错的核心是,t1t2线程都调用了资源timerTimeradd()方法),但是add()方法分若干步完成,在没有将这些步骤同步的情况下,会产生问题(sleep(1)加大了错误发生概率)。

此例看起来别扭的原因是,main()方法所在的Test类继承Runnable接口,来初始化main()方法中的两个子线程t1t2,即“自身使用自身”。

3.2synchronized

解决同步出现的问题的两种方法:

 

 

注意,sychronized锁(称为对象互斥锁,但syschronized表示同步”,即资源是同步的,使用资源的对象是互斥的),有两种方式。其内部机制是:执行该方法的过程中锁定当前对象。只有当synchronized块的代码执行完,下个使用它的对象才能执行它,否则等待。但这个说法可能有歧义,看后面的笔试题例子。 一般synchronized理解为:保证被它包围的代码段是个整体

3.3死锁:

 

死锁机制:对象a需要使用同步资源xyxy),对象b使用yx(先yx)。这样a锁定x等待yb锁定y等待x

 

 

 

上例,死锁。

解决死锁,一个办法是把锁的粒度加粗。即尽量不锁定两个对象而是一个对象。把锁的粒度加粗,效率低但是安全。

死锁一般在中间件中被解决了,小的项目不容易碰到,系统级的程序会遇到。数据库软件开发经常使用到各种锁。eg:只读锁、只写锁,行级锁、表级锁。

 

3.3synchronized面试题:

 

 

上例,为一常见笔试题。执行过程:从main()方法看,先启动了子线程t,子线程t会调用run()方法中的synchronized方法m1()m1()会把属性b由默认100改为1000,然后睡眠5秒;与此同时,主线程main()睡眠了1秒(为了m1()中的b = 1000执行完),然后调用对象tt的普通方法m2()

syschronized只是锁定了对象的一个方法,其他线程完全可以自由调用对象的其他非同步方法,并且可能会对同步方法中产生影响。

注意syschronized执行该方法的过程中锁定当前对象”,不是指若对象中有一个方法含synchronized,则使用时锁定该synchronized方法(或代码块)就是锁定该对象的所有方法synchronized很简单,只能保证被它包围的代码段是个整体上例中,只能保证b = 1000”、“sleep(50000)”和“system.out.println()”三行代码是个整体。

若将m2()方法改为如下,则结果如下:


上例,先m1()方法的b = 1000,睡眠过程中,m2()方法改为b = 2000m1()睡了5秒后,打印输出b ,值为2000表明synchronized并没有锁住整个对象,只是锁定了它包围的代码段。 所以,这样的锁定方法(锁定对象中修改属性的一个方法,但不锁定其他修改属性的方法)是危险的,并没有达到真正锁定属性b的效果。 应该,若想在方法中锁定属性的值,则需把所有关于属性操作的方法都锁定。如下:

 

 

上例,锁定m2()之后,m2()则必须遵循锁的规则,只有在另一个synchronized方法m1()执行完后,才能执行对b的操作。  

即,通过锁定关于属性b更改操作所有方法,来完成对属性b的锁定

所以,若想同步一段代码或一个方法,有时是很困难的,需要考虑所在对象中其他方法是否也要同步,还要考虑过多同步带来的开销问题。

 

m1m2同时 synchronized:

public class TT implements Runnable {

int b = 100;

 

public synchronized void m1() throws Exception{

b = 1000;                                  3

Thread.sleep(5000);                          6

System.out.println("b = " + b);                  7

}

 

public synchronized void m2() throws Exception {     

Thread.sleep(2500);                          1

b = 2000;                                  2       

}

 

public void run() {

try {

m1();

} catch(Exception e) {

e.printStackTrace();

}

}

 

public static void main(String[] args) throws Exception {

TT tt = new TT();

Thread t = new Thread(tt);

t.start();

tt.m2();      

System.out.println(tt.b);                      4

}

}

 

结果:

1000

b = 1000

 

只有m1 synchronized :

public class TT implements Runnable {

int b = 100;

 

public synchronized void m1() throws Exception{

b = 1000;                                  2

Thread.sleep(5000);                          

System.out.println("b = " + b);                 5

}

 

public void m2() throws Exception {                1

Thread.sleep(2500);                          

b = 2000;                                  3       

}

 

public void run() {

try {

m1();

} catch(Exception e) {

e.printStackTrace();

}

}

 

public static void main(String[] args) throws Exception {

TT tt = new TT();

Thread t = new Thread(tt);

t.start();

tt.m2();      

System.out.println(tt.b);                      4

}

}

结果:

2000

b = 2000

 

生产者与消费者问题:

wait()方法:从object类继承的方法,表示调用这个方法的当前的线程进入wait状态,wait()必须在synchronized用先锁定才有资格用。

 

Notify()方法:从object类继承的方法,唤醒这个对象的其他线程。

 

Waitsleep方法的区别:

wait时别的线程可以访问锁定对象。

Sleep时别的线程不能访问锁定对象。

 

 

 

 

 

 

上例,描述: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] = wtindex++这两句需要时一个整体,否则会发生数组元素“吃掉”的现象。

c 在谈到wait()notify()时,必须注意到push()pop()方法公用一个SyncStack对象ss来初始化所有ProducerConsumer的线程。所以当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才会减1this.notify()后,一直wait()的线程被唤醒,回去判断发现index不足6了,它才继续执行push()后面的语句。

e 使用while循环而不是if语句,是因为InterruptException,当wait()被打断捕获异常并处理后,while循环会返回继续循环,而if语句则不会。

f 此例为方便测试只new1个生产者和1个消费者,当有多个生产者和消费者时,SyncStack中应是notifAll(),以在消费者取走第6个窝头后,一次唤醒所有因为筐满而等待的生产者。

 

wait()方法首先wait()必须在synchronized用,否则立即报错,即当一个线程锁定中才能wait()其次wait()sleep()不同,wait()Object类的方法,使用时this.wait()其次wait()后需要被唤醒,否则也会产生死锁notify()含义是,叫醒一个在当前对象上等待的线程,wait()含义是,将在当前对象上的线程睡眠过去。

什么时候用wait():当某方法(例如发生阻塞事件)需要“暂停”(如判断数组满了),然后等待,直到该对象的某一事件(如判断数组不是满的了)发生并notify() notifyAll()是指若该对象的多个线程都wait()了,notifyAll()可全都唤醒。

 

多线程