Chp13 多线程

概要:                                                                      

    ·多线程与并发的概念

    ·Thread类和Runnable接口

    ·线程的状态

    ·线程同步

    ·synchronized与同步代码块

    ·同步方法

    ·wait与notify

    ·生产者消费者问题



    1.多线程与并发的概念

    ·在一个操作系统中可以运行多个进程,叫做进程的并发。

    ·线程运行时的三大条件:CPU、代码、数据。

    ·线程数据:堆空间共享,栈空间独立。即每个线程之间共享同一个堆空间,而每个线程又拥有各自独立的栈空间。

 

    2.Thread类和Runnable接口

    ·创建一个类继承Thread类,覆盖Thread类的run()方法,在run()中提供线程的代码。

    ·public void run()

    ·如何创建新线程?

    1)用自定义的线程类(继承了Thread类)创建线程对象;2)调用线程的start()方法启动新线程。

    创建线程对象时,系统中没有创建新的线程;而销毁线程时,线程对象可能还存在,要等到垃圾回收时,线程对象才会被销毁。

    Thread t = newMyThread();

 

    ·创建一个类实现Runnable接口,实现该接口内唯一的一个方法run()方法。

    void run();由于是定义在接口中,因此默认为public。

    Runnable target= new MyRunnable();

    Thread t = newMyThread(target);

 

    3.线程的状态

    ·初始状态:创建了线程对象而未调用start()方法,主线程不经历初始状态。主线程结束后,整个程序不一定结束,还可能有其他线程。所以必须等程序中所有线程进入终止状态,程序退出。

    ·可运行状态:在初始状态下调用了start()方法,万事俱备,只欠CPU。

    新启动的线程只能处于可运行状态,因为当t.start()这行代码执行的时候,是有别的线程在调用start()方法。

    ·运行状态:操作系统选中获得CPU,执行代码。CPU时间到转为可运行状态。

    ·终止状态:执行完run()方法的代码,进入终止状态。

    ·阻塞状态:用户输入,等待IO,网络传输,Thread类的sleep、join方法。

    ·锁池状态:等待获取对象的锁标记/其他线程调用了notify或notifyAll方法后。

    ·等待队列状态:调用wait方法后。

 

    ·在线程对象的整个生命周期中,只能调用一次start()方法,否则会出现IllegalStateException异常(public static voidsleep(long millis)throws InterruptedException;已检查异常,必须要处理。)

    ·Thread.sleep(1000);

    由于Thread中run方法没有抛出任何异常,根据方法覆盖,MyThread中也不能抛出任何异常,对于sleep抛出的异常,只能用try-catch的方法处理。

 

    ·join()方法,为线程类增加一个属性t,在住方法中,把t1对象赋值给t,让t属性和t1引用指向同一个对象。

    Threadt1 = new MyThread1();

    Threadt2= new MyThread2();

    t2.t= t1;

    在调用join方法的过程中,调用者被阻塞,阻塞到被调用的线程结束。但不能让两个线程互相join()。

    public finalvoid join(long millis)throws InterruptedException

    t.join(1000);

 

    4.线程同步

    ·多线程并发访问同一个对象,如果破坏了不可分割的操作,则可能产生数据不一致的情况。

    ·多线程访问临界资源,破坏了原子操作,则可能产生同步问题。

 

    5.synchronized与同步代码块

    ·同步机制:

    Java中,每个对象都有一个互斥锁标记,这个锁标记可以用来分配给不同的线程,之所以说是“互斥的”,因为这个锁标记同时只能分配给一个线程。在编写同步代码块的时候,一定要搞清楚,同步代码块锁的是哪个对象。

    Synchronized(obj){

      同步代码块...

    }

    假设线程t1正在代码块1中运行,还有一个线程t2,则t2线程能否进入同步代码块2?能否进入同步代码块3?

    t1正在同步代码块1中运行,意味着obj1对象的锁标记被t1线程获得,t2线程无法获得,因此t2线程无法进入同步代码块2;但t2线程能获得obj2的锁标记,进入同步代码块3。

    synchronized(obj1){

      同步代码块1...

    }

    synchronized(obj1){

      同步代码块2...

    }

    synchronized(obj2){

      同步代码块3...

    }

    ·如果一个线程获得不了某个对象的互斥锁标记,这个线程就会进入一个状态:锁池状态。

    举个例子:定义一个类,用数组来实现一个栈,类中有push方法和pop方法,push方法用来压入一个数据,并让索引加1,pop用来弹出一个元素,先让索引减1,再把弹出元素索引所指向的内存设为空。这个例子中有两个问题:1.在压入和弹出时,是对同一个数组即对同一个对象进行操作,也就是多个线程访问临界资源;2.其中的push和pop操作中,有多个需要完成的步骤,而由于CPU时间片的限制,操作随时可能被打断,从而破坏了原子操作。解决方法之一是,在这个类中增加一个属性lock,用这个属性来表示我们所说的锁标记,来完成对这个类的同步。

    privateObject lock = new Object();

    synchronized(lock){

      push操作的所有步骤

    }

    synchronized(lock){

      pop操作的所有步骤

    }

    现在当t1线程启动后,要执行push方法,由于此时lock对象的锁标记没有分配给其他线程,因此t1得到了lock锁标记,在t1线程运行的时候,如果变成阻塞状态,t2线程开始运行,t2线程要调用pop方法,也就要先得到lock对象的互斥锁标记,但此时lock对象的互斥锁标记分配给了t1,因此t2线程只能进入lock对象的锁池,进入锁池状态。当t1线程执行完,释放lock对象的锁标记之后,t2线程获得锁标记,变为可运行状态,等待执行。

 

    6.同步方法

    除了自定义一个Object类型的lock对象,该类对象本身,也具有互斥锁标记,也可以对当前对象加锁,把上述代码的lock去掉,括号里用this代替。对于这种情况,我们可以用synchronized作为修饰符修饰方法,来表达同样的意思。

    ·同步方法:指的是同步方法中整个方法的实现,需要对当前对象加锁。哪个线程能够拿到对象的锁标记,哪个线程才能调用对象的同步方法。

    当一个线程正在访问某个对象的同步方法时,其他线程不能访问同一个对象的任何同步方法。

 

    7.wait与notify

    在synchronized关键字的作用下,还可能产生新的问题:死锁。

    ·由于两个线程都无法获得所需的锁标记,因此两个线程都无法运行,就是死锁问题。

    synchronized(a){

      synchronized(b){

      }

    }

    synchronized(b){

      synchronized(a){

      }

    }//这里不是递归!不是递归!不是递归!

    现在有两个线程,t1线程获得了a对象的锁标记,t2对象获得了b对象的锁标记,而现在t1线程要进入对b对象加了锁的同步代码块,必须要获得b对象的锁标记,但由于b对象的锁标记分配给了t2线程,t1线程无法获得,因此t1线程进入b对象的锁池,等待b对象的锁标记被释放;同理t2线程要进入对a对象加了锁的同步代码块,最后也只能进入a对象的锁池状态,等待a对象的锁标记被释放。

    ·Java中采用了wait和notify两个方法,来解决死锁机制。

    在Java中,每个对象都有两个方法:wait和notify方法(这两个方法定义在Object类中),对某个对象调用wait()方法,表明让线程暂时释放该对象的锁标记。

    synchronized(a){

      a.wait();

      synchronized(b){

      }

    }

    synchronized(b){

      synchronized(a){

      a.notify();

      }

    }

    要调用一个对象的wait方法,前提是线程已经获得了这个对象的锁标记,否则会产生异常。调用了wait()方法后,就必须要被另一个线程调用a.notify方法把原来线程唤醒,唤醒之后进入a对象的锁池状态,等待另一个线程释放a对象的锁标记。

    由于可能有多个线程先后调用a对象的wait方法,因此在a对象锁池状态中的线程可能有多个,可以调用a.notifyAll()把a对象锁池状态中的所有线程唤醒。

 

    8.生产者消费者问题

    用一个数组来模拟数据结构中的栈,创建两个线程,一个线程每隔一段随机的时间就会往栈中增加一个数据;另一个线程每隔一段随机的时间就会从栈中取出一个数据。为了保证push和pop操作的完整性,应对MyStack对象上锁。当数组满了的时候,入栈线程不能工作;当数组空了的时候,出栈线程不能工作,否则,将会出现数组下标越界异常。

    于是,可以用wait/notify机制,在入栈(/出)栈时,发现数组已满(/空),调用wait()方法去等待。在入栈线程结束入栈工作后,调用notifyAll方法,释放正在等待的出栈线程(因为此时数组不再为空),当出栈线程结束出栈工作后,调用notifyAll方法,释放正在等待的入栈线程(因为此时数组不满)。


    示例代码:

class MyStack{
	private char [] data = new char[5];
	private int index = 0;

	public char pop(){
		index--;
		return data[index];
	}

	public void push(char ch){
		data[index] = ch;
		index++;
	}

	public void print(){
		for (int i = 0; i < index; i++) {
			System.out.println(data[i]+"\t");
		}
		System.out.println();
	}

	public boolean isEmpty(){
		return index == 0;
	}

	public boolean isFull(){
		return index == 5;
	}
}

class Consumer extends Thread{
	private MyStack ms;

	public Consumer(MyStack ms){
		this.ms = ms;
	}

	public void run(){
		//为了保证pop操作的完整性,必须加synchronized
		while (true) {
			synchronized (ms) {
				//如果栈空间为空,则wait()释放ms的锁标记
				while (ms.isEmpty()) {
					try {
						ms.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				char ch = ms.pop();
				System.out.println("Pop " + ch);
				ms.notifyAll();
			}
			//pop之后随机休眠一段时间
			try {
				sleep((int)Math.abs(Math.random()*100));
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

class Producer extends Thread{
	private MyStack ms;

	public Producer(MyStack ms){
		this.ms = ms;
	}

	public void run(){
		//为了保证push操作的完整性,必须加synchronized
		while (true) {
			synchronized (ms) {
				//如果栈空间已满,则wait()释放ms的锁标记
				while (ms.isFull()) {
					try {
						ms.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				ms.push('A');
				System.out.println("push A");
				ms.notifyAll();
			}
			//push之后随机休眠一段时间
			try {
				sleep((int)Math.abs(Math.random()*200));
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

public class TestWaitNotify {
	public static void main(String[] args) {
		MyStack ms = new MyStack();
		Thread t1 = new Producer(ms);
		Thread t2 = new Consumer(ms);
		t1.start();
		t2.start();
	}
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值