概要:
·多线程与并发的概念
·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();
}
}