Java复习8 多线程知识 20131007
前言:
在Java中本身就是支持多线程程序的,而不是像C++那样,对于多线程的程序,需要调用操作系统的API 接口去实现多线程的程序,而Java是支持多线程的,具有并发性。
在程序中使用多线程意味着我们可以同时处理多项任务,在实际的开发中,尤其是大型的项目,多线程程序是十分重要的。
同时多线程的程序会带来一定的问题,比如数据的同步、资源的访问等等。在服务器端,使用多线程处理用户的请求等等的知识。
1.线程的概念
每一个程序至少运行着一个进程,每一个进程至少包含一个线程。进程可以是整个程序或者是部分程序的动态执行。线程是一组指令的集合,或者是进程的特殊段,他可以在程序中独立的运行,也可以把它理解成为代码的上下文。所以线程是轻量级的进程,他负责在单个程序中执行多个任务。线程的调度是使用的操作系统调度和执行。
使用多个线程的好处:
可以将一些耗时的任务放到后台去执行;用户界面更好,当执行一个线程任务的时候,可以查看线程的执行进度;程序的运行速度可以加快(使用的是多核计算机);
Java运行的系统很多方面就是基于线程的,所有类的设计都有考虑过多线程,相比其他的语言,是十分优秀的,Java使得Java程序员可以使用并发编程机制。
2.创建线程
回想一下自己学习Java的经验,Java中创建一个线程有两种方式,一种是继承Thread,重写run函数;另外一种是实现Runable接口
在主线程应用程序中,调用currentThread获得当前线程的引用。使用一个局部变量保存线程的引用。
Thread t = Thread.currentThread();
System.out.println("main thread name:" + t.getName());
t.setName("yang");
System.out.println("main thread name(after changed):" + t.getName());
for(int i = 0; i< 10; i++){
System.out.println(i);
try {
t.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
使用Runable接口创建线程,让自己的类实现Runnable接口然后重写run函数即可。
public class RunThread implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread().getName());
}
}
Main:
Thread t = new Thread(new RunThread());
System.out.println("main thread name:"+Thread.currentThread().getName());
System.out.println("new thread name:" +t.getName());
t.setName("yang");
System.out.println(t.getName());
t.start();
通过Thread类创建线程:
创建Thread的构造函数:
Thread(Runnable target, String name);
Thread(String name);
Thread(ThreadGroup group, Runnable target);
Thread(ThreadGroup group, Runnable target, String name);
Thread();
Thread(Runnable target);
返回一个Thread的引用。
3.创建多线程
可以实现Runnable 或者继承Thread类创建多线程,从面向对象的角度,Thread是一个虚拟处理机的严格封装,因此只有当处理机的模型修改或者扩展是,才能继承。Java是单一继承的机制,继承了Thread类之后,就不可以继承其他的类,所以用户只需实现Runnable接口,当一个run方法体系在继承Thread的类中,可以使用this指针指向实际控制运行的Thread实例.
4.线程的优先级
线程的优先级是用来判断和是执行线程程序的。理论上线程优先级高的比线程优先级低的线程获得高度哦的CPU执行时间。
设置线程的优先级使用setPriority(int level);
final void setPriority(int level);其中的参数1表示最小,10 表示最大。
关键字volatile修饰被不同线程访问和修改的变量;通过setPriority函数设置线程不同的优先级,输出线程的执行结果。
5.控制线程
5.1让当前线程等待join
join函数可以是当前线程停下来等待,直到join方法所调用的线程结束,才会继续执行。让线程进入等待状态,直到该线程结束之后,才会处于Ready状态,然后被操作系统调用该线程继续执行。
public class RunThread extends Thread{
public static int a =0;
public RunThread(String name){
super(name);
}
@Override
public void run(){
for(int i = 0; i< 5; i++){
System.out.println(this.getName() + ":" + (a++));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
Main:
public static void main(String[] args) {
RunThread t = new RunThread("a");
t.start(); //这样不保证会使用输出争取的结果我们使用 t.join(),那么在该线程中需要等待线程t执行结束的时候才会处于就绪状态,被操作系统调度。
System.out.println("main:" + RunThread.a);
}
5.2线程休眠sleep
线程休眠一段时间,也就是放弃一段时间的时间片段,等到时间结束的时候在让线程处于就绪状态,被操作系统调度。这样便让出了CPU的执行时间,然后其他的线程可以更好的竞争到CPU的资源。但是sleep是不会让出其他资源的锁的,只会让出CPU的资源。当时间结束之后,不一定会被操作系统直接调用,而是处于就绪状态。
5.3让当前线程做出让步yield
yield让同等优先级的线程获得CPU资源,只是让当前线程重新处于Ready的状态,这个时候,如果有同等优先级或者是更高的优先级的线程,操作系统管理会调用更高或者同等优先级的线程,如果没有同等优先级的线程或者是更高的线程,则此方法是不起作用的。
6.多个线程之间的同步问题
对于同一个资源,有时候只允许一个线程访问,这就是线程的同步问题。当一个线程正在访问此资源的时候,就不应该让其他的线程访问该资源。
线程之间的同步十分简单访问资源的用户可以使用关键字synchronized标记,这样需要调用这个方法的线程执行完成之后,才会允许其他的线程访问该资源。
使用synchronized可以修饰方法,那么只允许一个线程访问此函数;也可以使用
Synchronized(Object){ code blocks}, 只有获得对象object的锁之后,才会允许进入代码段执行。
7死锁问题
死锁问题在多线程编程的时候,要重点考虑,因为一部小心就会造成死锁问题,这样的问题和数据不同步差不多,死锁是很难被发现的,但是一旦出现死锁问题,整个程序就会一直死耗下去,而且没有做任何工作。我们编写多线程程序的时候要特别注意防止因资源的访问而导致死锁。
我们看一个死锁的例子:
public class RunThread extends Thread{
private String str1;
private String str2;
public RunThread(String str1, String str2, String name){
super(name);
this.str1 = str1;
this.str2 = str2;
}
@Override
public void run(){
synchronized(str1){
System.out.println(this.getName() + " synchronized " + str1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized(str2){
System.out.println(this.getName() + " synchronized " + str2);
System.out.println("end");
//Thread.sleep(1000);
}
}
}
}
Main:
public static void main(String[] args) {
String a = "AAA";
String b = "BBB";
RunThread t1 = new RunThread(a,b,"t1");
RunThread t2 = new RunThread(b,a,"t2");
t1.start();
t2.start();
}
这里我们两个线程就会分别synchronized资源a 和 b,造成两个线程循环等待的状况,这样的话就会死锁,程序永远不会终止,而且没有做任何有意义的事情。
同时这一段代码有主你进一步了解Java内存管理的知识,因为在main函数中的字符串啊a和b这两个字符串首先在编译期间保存在String Pool所有对这样相同内容的访问都是访问的字符串内容在内存中的副本,a,b分别是指向这两个字符串的内存副本的地址。然后将他们当做参数传递给构造函数,也是传递的字符串的地址,所以所有对象中的str1和str2都是同一个内存地址,指向字符串在内存中的地址。
总结一下线程之间死锁的条件:
互斥条件:线程之间的使用的资源至少有一个资源是不可以共享的;
请求与保持条件:当一个进程持有一个资源,去请求其他的资源的时候,但是这个资源被其他的线程持有;
非剥夺条件:分配给线程的资源是不可以从线程中被抢占剥夺的;
循环等待:一个线程等待其他线程,最后一个线程又在等待第一个线程;
8.线程之间的交互
8.1不同线程完成不同的任务,线程执行任务的时候有一定的联系,
需要线程之间进行交互。Java程序中可以使用三个函数进行线程之间的通信交互:
wait(),notify(), notifyAll();
wait():让当前线程进入等待状态,直到其他的线程调用notify()方法;
notify():恢复一个等待状态中的线程;
notifyAll():恢复所有的等待状态中的线程到ready状态。
如果线程执行周期性任务,可以使用while+sleep或者单独使用while+wait(long timeout) .具体使用那一个,需要根据实际情况进行判断。Sleep不会释放资源的锁,但是wait会释放掉资源的锁。在java中每一个对象都会有一个锁,该锁只能够被一个线程占用,当一个线程获得该对象的锁的时候,其他线程就不能够获得改对象的锁,不可以访问。
如果是希望线程只是让出CPU资源,但是不让出占有的对象资源,那么我们可以使用sleep,之后该线程sleep一段时间,进入休眠状态,时间结束之后,该线程进入就绪状态,可以被操作系统调度执行。
但是如果是wait/notify,wait不仅会让当前线程进入休眠状态,还有就是是放掉该线程占有的资源,这样的话,就会允许其他的线程获得该对象的锁。等到另一个线程使用notify or notifyAll的时候,就会将wait唤醒,让之前阻塞的线程唤醒,但是不一定会立即进入ready状态,因为其他的线程也许只是说你可以申请之前释放的资源了,但是另外一个线程在notify之后没有结束synchronized代码段,而是继续执行,这个时候先前的线程是无法获得该对象的资源的,所以自然就无法进入就绪状态,知道另一个线程走出synchronized代码段的时候,之前的线程才可以申请该对象的锁。同时wait和notify必须在synchronized代码段中执行,因为首先需要获得该对象的锁,才能够让wait释放掉资源的锁。
8.2wait/notify机制
如果使用简单的synchronized机制实现互斥,那么会导致线程主动发起轮询,如果N次轮询都没有成功,那么就会浪费CPU的资源,同时线程之间的切换也会耗费大量的资源。这个时候如果使用wait/notify机制就可以避免一些无谓的轮询,减少CPU的没有用的消耗和线程切换的代价。
8.3一个例子线程同步
多线程之间需要协调工作,比如浏览器需要显示一个图片线程displayThread执行显示图片的任务,需要等待下载图片的线程downloadThread下载完成图片之后,才能够显示。如果图片没有下载完成,可以让displayThread线程wait,当downloadThread线程执行完成之后,notify线程displayThread,可以显示图片了。
synchronized(obj){
while(! condition){ obj.wait();}
obj.doSomething();
}
Synchronized(obj){
Dosomething();
condition = true;
obj.notify();
}
当线程A获得obj的锁之后,发现不满足条件,无法继续执行,这个时候调用wait释放掉资源的锁,让当前线程进入block状态;
这个时候线程B也需要获得obj的锁,但是之前被线程A占用,不能够继续执行,一直处于轮询状态,这个时候,线程A释放掉锁,线程B正好可以获得该对象的锁,然后执行一些操作,设置condition为true,之后再notify被obj对象阻塞的线程,可以进入轮询状态,这个时候线程A如果想继续执行,就必须再次获得对象obj的锁。但是也可能线程B没有释放掉obj的锁,直到线程B走出synchronized代码段,线程A才会成功申请到资源obj的锁。继续执行。
总结
1.sleep()方法
在指定时间内让当前正在执行的线程暂停执行,但不会释放“锁标志”。不推荐使用。 sleep()使当前线程进入阻塞状态,在指定时间内不会执行。
2.wait()方法
在其他线程调用对象的notify或notifyAll方法前,导致当前线程等待。线程会释放掉它所占有的“锁标志”,从而使别的线程有机会抢占该锁。
当前线程必须拥有当前对象锁。如果当前线程不是此锁的拥有者,会抛出IllegalMonitorStateException异常。
唤醒当前对象锁的等待线程使用notify或notifyAll方法,也必须拥有相同的对象锁,否则也会抛出IllegalMonitorStateException异常。
wait()和notify()必须在synchronized函数或synchronized block中进行调用。如果在non-synchronized函数或non-synchronized block中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。
3.yield方法
暂停当前正在执行的线程对象。
yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
yield()只能使同优先级或更高优先级的线程有执行的机会。
4.join方法
等待该线程终止。
等待调用join方法的线程结束,再继续执行。如:t.join();//主要用于等待t线程运行结束,若无此句,main则会执行完毕,导致结果不可预测。
sleep和wait的区别有:
1,这两个方法来自不同的类分别是Thread和Object
2,最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
3,wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在
任何地方使用
synchronized(x){
x.notify()
//或者wait()
}
4,sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
追梦的飞飞
于广州中山大学 20131008
HomePage: http://yangtengfei.duapp.com