Java多线程学习
一、线程的理解
线程是一种轻量级的进程,它和进程一样拥有独立的执行控制,由操作系统负责调度,区别在于线程没有独立的存储空间,而是和所属进程中的其它线程共享一个存储空间,这使得线程间的通信远较进程简单。即多个线程可以同时执行,就像有多条流水线一样,可以同时进行工作,是并发执行的。
程序是由进程组成的,进程是由线程组成的。其实进程就是一个程序,线程是一个程序正在进行的一部分功能。所谓进程,本身不能执行,它只是一个资源的集合体,拥有地址空间,模块,内存,… 线程是真正的执行单元,一个进程如果没有线程,那么就没有存在的意义,因为不可能执行。
二、Java中实现多线程
在Java中实现多线程有两种方式,分别为继承java.lang.Thread类与实现java.lang.Runnable接口。
方式一:继承Thread类
继承Thread类,然后改写里面的run()方法就可以实现,run()方法里面为需要线程完成的功能代码。调用Thread类中的start()方法执行线程,也就是调用run()方法。(start()方法不能调用一个已经启动的线程)
例子:
public class MyThread1 extends Thread{ //继承类
private int count = 10;
int number;
public MyThread1(int num){ //构造函数
this.number = num;
System.out.println("Create a thread" + number);
}
public void run(){ //重写run()方法
while(true){
System.out.println(count);
if(--count==0){
return;
}
}
}
public static void main(String[] args){
for(int i=1; i<5; i++){
new MyThread1(i).start(); //启动线程
}
}
}
方式二:实现Runnable接口
当我们的类已经继承了其他类时,想要再继承Thread类就不可能了,这时想要实现多线程的功能就只能通过实现Runnable接口。
Runnable 接口只有一个方法 run(),Runnable接口没有提供对Thread的支持,因此需要创建Thread类的实例来与接口实例关联。Thread类中有八个构造方法:
- public Thread( );
- public Thread(Runnable target);
- public Thread(String name);
- public Thread(Runnable target, String name);
- public Thread(ThreadGroup group, Runnable target);
- public Thread(ThreadGroup group, String name);
- public Thread(ThreadGroup group, Runnable target, String name);
- public Thread(ThreadGroup group, Runnable target, String name, long stackSize);
Runnable target:实现了 Runnable 接口的类的实例。
String name:线程的名子。
ThreadGroup group:当前建立的线程所属的线程组
long stackSize:线程栈的大小
有几个个构造方法参数中都存在Runnable实例,可以将Thread类实例和Runnable实例相关联。其实,在实际上,Thread类就是实现了Runnable接口,其中的run()方法正是对Runnable接口中的run()方法的具体实现。因此从这里可以看到两种方法都与Runnable接口有关,第一种是通过Thread类继承实现run()方法,第二种则是直接实现Runnable接口的run()方法,再通过Thread构造函数实现Runnable实例与Thread实现的关联。下面看实际的代码:
public class MyThread1 implements Runnable{
private int count = 10;
int number;
public MyThread1(int num){
this.number = num;
System.out.println("Create a thread " + number);
}
public void run(){
while(true){
System.out.print(count+" ");
if(--count==0){
System.out.println("");
return;
}
}
}
public static void main(String[] args){
for(int i=1; i<5; i++){
new Thread(new MyThread1(i)).start();
}
}
}
在上面的代码中,类MyThread1实现了Runnable接口,代码行:new Thread(new MyThread1(i)).start();,通过new MyThread1创建一个Runnable实例作为Thread类的参数来创建一个线程并执行start()启动线程。
三、run()与start()的区别
从上面的代码可以看到,我们都是实现了run()方法来描述进程需要完成的功能。但是在后面的启动中我们用的却是start()方法,为什么不直接用run()方法呢?实际上,这里可以调用run()方法(new Thread(new MyThread1(i)).run();),程序也是可以运行的,但是区别就在于直接调用run()的话,这个run()就是一个普通的方法了,而不是进程的运行方法。下面来区分这两个方法:
1.start()方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码:通过调用Thread类的start()方法来启动一个线程,这时此线程是处于就绪状态,并没有运行。然后通过此Thread类调用方法run()来完成其运行操作的,这里方法run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程终止,而CPU再运行其它线程。
2.run()方法当作普通方法的方式调用,程序还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码:而如果直接用Run方法,这只是调用一个方法而已,程序中依然只有主线程–这一个线程,其程序执行路径还是只有一条,这样就没有达到写线程的目的。
通过上面的解释基本能知道两者的区别了,实际上start就是一个用来启动线程的,具体之后线程的执行完毕与否它是不关注的。所以上面的代码每次执行的结果都不一定是一样的,因为线程的run方法是输出连续的几个数字,每创建一个线程就会有输出相应文字。若是直接调用run()方法则会顺序的输出一段文字,然后输出连续的数字,再输出文字,是直接按照代码的顺序来的。而实际线程中,可能是输出几段文字后才有数字输出,也就是说线程的创建于线程的执行是不相连的,即使之前的线程还没有执行,也可以继续创建新的线程。
几种不同的实际输出结果:
Create a thread 1 //第一种
Create a thread 2
10 9 8 7 6 5 4 3 2 1
Create a thread 3
Create a thread 4
10 9 8 7 6 5 4 3 2 1
10 10 9 8 7 6 5 4 3 2 1
9 8 7 6 5 4 3 2 1
Create a thread 1 //第二种
Create a thread 2
Create a thread 3
Create a thread 4
10 9 8 7 6 5 4 3 2 1
10 9 8 7 6 5 4 3 2 1
10 9 8 7 6 5 4 3 2 1
10 9 8 7 6 5 4 3 2 1
Create a thread 1 //直接调用run()方法的顺序输出结果
10 9 8 7 6 5 4 3 2 1
Create a thread 2
10 9 8 7 6 5 4 3 2 1
Create a thread 3
10 9 8 7 6 5 4 3 2 1
Create a thread 4
10 9 8 7 6 5 4 3 2 1
参考: http://blog.csdn.net/wangyangkobe/article/details/5839182
四、线程的属性和状态信息
在接受线程的生命周期之前先介绍一下线程属性和状态相关的函数。
getId() //获取线程Id
setName() //设置线程名称
getName() //获取线程名称
getPriority() //获得优先级
setPriority() //设置优先级(1-10,一般为5)
getState() //获取线程状态
isAlive() //线程是否活着
currentThread() //获取当前线程
测试一下:
public class MyThread1 extends Thread{
String name;
public MyThread1(String str){
this.name = str;
System.out.println("create thread:"+name);
}
public void run(){
try{
System.out.println("thread Id:"+ Thread.currentThread().getId());
System.out.println("thread name:"+ Thread.currentThread().getName());
System.out.println("thread priority:"+ Thread.currentThread().getPriority());
System.out.println("thread status:"+ Thread.currentThread().getState());
System.out.println("thread isAlive:"+ Thread.currentThread().isAlive());
}catch(Exception e){
}
}
public static void main(String[] args) throws Exception{
MyThread1 thread1 = new MyThread1("one");
thread1.setName("thread-one");
thread1.setPriority(1);
thread1.start();
thread1.join();
System.out.println("The Thread is finished!");
System.out.println(thread1.getName()+" isAlive:"+ thread1.isAlive());
}
}
//结果
create thread:one
thread Id:8
thread name:thread-one
thread priority:1
thread status:RUNNABLE
thread isAlive:true
The Thread is finished!
thread-one isAlive:false
简单说明:每个线程创建之初都会获得一个Id,这是系统分配的,不能设置。线程的名字可以自习设置,也可以系统默认。线程的优先级是一个整数1-10,默认优先级为5,最大为10,最小为1,一般来说在线程资源竞争过程中,优先级更高的优先获取资源的概率越大。线程的状态有下面几种选项:NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED等。
五、线程的生命周期
线程存在开始(等待),运行,挂起,停止等几种状态。可以通过相应的函数来实现线程状态的控制,前面的start()和run()就是开始状态的控制函数。下面介绍另外几种对应的函数。
1、线程的挂起和唤醒
线程启动开始执行后会运行run()方法直到结束,在run()运行过程中可以使线程中断运行。利用suspend可以挂起线程,然后通过resume唤醒,这种方式可能产生不可预料的错误不推荐。这里介绍另外一种方法,使用sleep(),使线程休眠规定时间然后再继续执行。下面是使用sleep的例子。
public class MyThread1 extends Thread{
int number;
public MyThread1(int num){
this.number = num;
System.out.println("Create a thread " + number);
}
public void run(){
try{
sleep(2000); //睡眠2s
}catch(Exception e){
}
System.out.println("thread "+ number +" sleep 2s");
}
public static void main(String[] args) throws Exception{
Thread thread[] = new Thread[3];
for(int i=1; i<3; i++){
thread[i] = new MyThread1(i);
thread[i].start();
thread[i].join();
}
}
}
//输出结果
Create a thread 1
thread 1 sleep 2s
Create a thread 2
thread 2 sleep 2s
//没有thread[i].join();的情况
Create a thread 1
Create a thread 2
thread 2 sleep 2s
thread 1 sleep 2s
在上面的代码中,通过sleep()使线程睡眠2s再继续执行,第一个是输出结果,输出第一行后会等待2s才输出第二行和第三行,然后再等2s输出第四行。在没有添加join()函数时,先输出前面两行,2s后随机输出下面两行。这里的join()方法就是保证当前线程先执行完,才继续执行下面的内容。
在使用 sleep 时要注意:sleep 只对当前正在执行的线程起作用,不能在一个线程中来休眠另一个线程。如 main 方法中使用 thread[1].sleep( 2000)方法是无法使 thread 线程休眠 2 秒的,而只能使主线程休眠 2 秒。在使用 sleep 方法时必须使用throws 或 try{„„}catch{„„}.因为 run 方法无法使用 throws,所以只能使用 try{„„}catch{„„}。
2、线程的终止
线程的终止有几种方法:1、退出标志结束;2、stop();3interrupt();其中stop()是强制关闭线程,类似于直接拔电源关电脑,容易发生不可预料的结果因此是不推荐的。
public class MyThread1 extends Thread{
int number;
boolean exit = false;
public MyThread1(int num){
this.number = num;
System.out.println("Create a thread " + number);
}
public void run(){
while(!exit){
try{
sleep(1000);
}catch(Exception e){}
System.out.println("running");
}
System.out.println("exit");
}
public static void main(String[] args) throws Exception{
MyThread1 thread = new MyThread1(3);
thread.start();
sleep(3000);
//thread.interrupt(); //interrupt方式
thread.exit = true;
}
}
//输出结果
Create a thread 3
running
running
running
exit
上面是使用退出标志来结束线程的,线程的run方法的执行是在exit=false的条件下的。线程thread开始后,主线程睡眠3s中,然后传入终止信息,thread每1s输出一个信号,因此这里会输出3个信号后得到终止信号从而使线程终止,结果见代码结尾。在代码中有一行:thread.interrupt();这就是interrupt方式的用法,调用这个后,线程就会终止,可以在run()方法中catch(InterruptedException e)。
3.join()方法的使用
在(1、线程的挂起和唤醒)代码中我们已经使用了join(),它的作用就是等待线程执行完,就是使异步执行的线程变成同步执行。也就是说,当调用线程实例的 start 方法后,这个方法会立即返回,如果在调用 start 方法后后需要使用一个由这个线程计算得到的值,就必须使用 join 方法。如果不使用 join 方法,就不能保证当执行到 start 方法后面的某条语句时,这个线程一定会执行完。而使用 join 方法后,直到这个线程退出,程序才会往下执行。换一种理解方式就是:加入线程,即假设当前线程为A,现在需要加入B,让B线程先执行完毕再执行A。
public class MyThread1 extends Thread{
int number;
public MyThread1(int num){
this.number = num;
System.out.println("Create a thread " + number);
}
public void run(){
System.out.println(number);
}
public static void main(String[] args) throws Exception{
Thread thread[] = new Thread[5];
for(int i=1; i<5; i++){
thread[i] = new MyThread1(i);
thread[i].start();
thread[i].join();
}
}
}
//有join()结果
Create a thread 1
1
Create a thread 2
2
Create a thread 3
3
Create a thread 4
4
//无join()的一种结果
Create a thread 1
Create a thread 2
1
Create a thread 3
Create a thread 4
3
2
4
从上面的代码可以看到,有join()的话会顺序输出,没有的话,个线程之间是没有顺序性的,输出的结果顺序是随机的,因此会有多种结果,而join()只有一种结果输出。
六、线程的数据传递
1、向线程传递数据
在线程的执行中,线程在异步的开发模式下数据的传递与函数传递等同步开发模式下是不一样的。由于线程的运行和结束是不可预料的,因此,在传递和返回数据时就无法象函数一样通过函数参数和 return 语句来返回数据。下面介绍几种向线程传递数据的方法。
方法1:通过构造函数传递数据
在定义构造函数的时候,通过构造函数的参数设置,可以将数据传给线程实例的变量,通过变量将数据存储起来。若是数据较为复杂,可以通过类,集合等数据结构来实现。
方法2:通过变量和方法传递数据
在线程执行前,可以通过线程实例对类成员变量进行赋值,或利用定义的方法赋值。
方法3:通过回掉函数传递数据
上面两种方法是最常用的,但都是在main中主动将数据传入,在有些情况下,需要线程动态地获取数据,这就需要回调函数。
下面是方法1和方法2的实例代码,方法3这里不进行详细介绍,具体参考 回调函数这个链接。
public class MyThread1 extends Thread{
String name;
int number;
public void setNum(int num){
this.number = num;
}
public MyThread1(String name){
this.name = name;
System.out.println("Create a thread: " + name);
}
public void run(){
System.out.println("thread number "+number);
}
public static void main(String[] args) throws Exception{
String[] name = {"thread1","thread2"};
MyThread1 thread[] = new MyThread1[2];
for(int i=0; i<2; i++){
thread[i] = new MyThread1(name[i]); //构造函数传递
thread[i].setNum(i); //类方法传递
thread[i].number = i+1; //类变量传递
thread[i].start();
}
}
}
//可能的一种输出结果
Create a thread: thread1
Create a thread: thread2
thread number 2
thread number 1
2、从线程返回数据
从线程返回数据有两种方式,一是通过变量和方法返回数据,二是通过回调函数。这两种方式与上面介绍的原理差不多,可以参考上面的就可以理解了。因此这里不再详细描述。
七、线程的同步
线程的同步主要解决的是资源共享的问题,在单线程中,程序是一路往下执行的,不存在资源共享问题;在多线程中,线程是独立运行的,这是就会出现线程抢占资源的问题。解决问题的思想就是在给定的时间只允许一个线程访问共享资源。这里的资源一般是线程类的类变量。每个线程实例都可以访问类变量,若是不进行同步,每个线程都对变量进行访问修改,那变量的值将无法确定。
线程同步一般是通过synchronized来实现的。一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在 java里边就是拿到某个同步对象的锁(一个对象只有一把锁); 如果这个时候同步对象的锁被其他线程拿走了,他(这个线程)就只能等了(线程阻塞在锁池 等待队列中)。 取到锁后,他就开始执行同步代码(被synchronized修饰的代码);线程执行完同步代码后马上就把锁还给同步对象,其他在锁池中等待的某个线程就可以拿到锁执行同步代码了。这样就保证了同步代码在统一时刻只有一个线程在执行。
锁的原理:Java中每个对象都有且只有一个内置锁,当程序运行到非静态的synchronized同步方法上时,自动获得与正在执行代码类的当前实例(this实例)有关的锁。获得一个对象的锁也称为获取锁、锁定对象、在对象上锁定或在对象上同步。当程序运行到synchronized同步方法或代码块时才该对象锁才起作用。释放锁是指持锁线程退出了synchronized同步方法或代码块。
关于锁和同步,有一下几个要点:
1)、只能同步方法,而不能同步变量和类;
2)、每个对象只有一个锁;当提到同步时,应该清楚在什么上同步?也就是说,在哪个对象上同步?
3)、不必同步类中所有的方法,类可以同时拥有同步和非同步方法。
4)、如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。
5)、如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。
6)、线程睡眠时,它所持的任何锁都不会释放。
7)、线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。
8)、同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块。
9)、在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。
具体请参考《 Java线程:线程的同步与锁》。
1、synchronized关键字同步类一般方法
通过synchronized关键字同步类方法,即可以用synchronized来修饰run(),这样run方法同时只能被一个线程调用,并当前的 run 执行完后,才能被其他的线程调用。先来看一个实例。
public class MyThread1 implements Runnable{
private int count = 3;
int number;
public MyThread1(int num){
this.number = num;
System.out.println("Create a thread " + number);
}
public synchronized void run(){
for(int j=0;j<count; j++){
try{
Thread.sleep(1000);
}catch(Exception e){}
System.out.println("thread name: "+Thread.currentThread().getName()+": "+j+" ");
}
}
public static void main(String[] args){
MyThread1 thread = new MyThread1(1);
Thread threads[] = new Thread[3];
for(int i=0; i<3; i++){
threads[i] = new Thread(thread);
threads[i].start();
}
}
}
//有synchronized一种结果
Create a thread 1
thread name: Thread-0: 0
thread name: Thread-0: 1
thread name: Thread-0: 2
thread name: Thread-1: 0
thread name: Thread-1: 1
thread name: Thread-1: 2
thread name: Thread-2: 0
thread name: Thread-2: 1
thread name: Thread-2: 2
//无synchronized一种结果
Create a thread 1
thread name: Thread-0: 0
thread name: Thread-2: 0
thread name: Thread-1: 0
thread name: Thread-0: 1
thread name: Thread-2: 1
thread name: Thread-1: 1
thread name: Thread-0: 2
thread name: Thread-2: 2
thread name: Thread-1: 2
上面定义了一个对象的多个线程,线程锁为run()方法所在的一个具体对象,也就是代码中的thread,线程threads[i]在同一时刻只能有一个获得线程锁,从而执行run()方法。通过输出结果可以得到,在synchronized同步后,尽管线程0通过sleep()挂起,其它的线程还是不能执行run()函数,因为这时线程0还没有释放线程锁,其他的两个线程不能执行run(),所以输出结果都是每个线程全部输出才轮到另一个线程。而在没有synchronized的情况下,当线程sleep()后,其他的线程就开始执行run()了,因此输出的结果是各个线程随机出现的。
这里需要注意的是要保证多个线程对应的线程锁是唯一的,即对象是一个。因为这里若是创建多个MyThread1对象的话,每个对象都有一个线程锁,那就没法进行顺序输出了。
2、synchronized关键字同步类静态方法
synchronized修饰对于一般的方法则线程锁的对象就是该方法所在的类的一个对象,如上面的例子就是这样的情况,这种情况下,每一个对象都有一个线程锁,可以这么理解:线程锁锁定的是一个线程对象,这个对象可以有运行多个线程,在这多个线程之间执行同步。synchronized还可以修饰静态的方法,因为静态方法利用的是公共的空间,所有对象都可以引用,是属于整个类的类方法,它在内存中的代码段会随类的定义而被分配和装载;而非静态方法是属于具体对象的方法,当这个对象创建时,在对象的内存中会拥有此方法的专用代码段;所以当我们利用synchronized修饰静态方法时,即使是多个对象也是可以进行同步的。看下面的例子:
public class MyThread1 extends Thread{
int name;
public MyThread1(int name){
this.name = name;
System.out.println("Create a thread: " + name);
}
public static synchronized void aa(){
for(int i=0;i<3;i++){
try{
sleep(100);
}catch(Exception e){}
System.out.println("thread name:"+ Thread.currentThread().getName()+":"+ i);
}
}
public void run(){
aa();
}
public static void main(String[] args) throws Exception{
for(int i=0; i<3; i++){
new MyThread1(i).start();
}
}
}
//使用static结果
thread name:Thread-0:0
thread name:Thread-0:1
thread name:Thread-0:2
thread name:Thread-1:0
thread name:Thread-1:1
thread name:Thread-1:2
thread name:Thread-2:0
thread name:Thread-2:1
thread name:Thread-2:2
//不使用static结果
thread name:Thread-1:0
thread name:Thread-2:0
thread name:Thread-0:0
thread name:Thread-1:1
thread name:Thread-2:1
thread name:Thread-0:1
thread name:Thread-1:2
thread name:Thread-0:2
thread name:Thread-2:2
在上面的代码中,利用synchronized修饰了一个static静态方法,创建了3个线程来运行, 结果如上,输出是按顺序的。当我们去掉static时,发现输出的结果是随机的,没有顺序,即各个线程获得的只是各自对象的线程锁,不存在等待其他线程执行完才能获取线程锁的情况。因此,从这里可以看出这里的锁对象是用于整个类对象的锁。
3、synchronized块同步
除了修饰函数之外,还可以利用synchronized修饰块,在块内的数据是同步的,直接看一个例子吧。
public class MyThread1 extends Thread{
int name;
static String sync = "sync";
public MyThread1(int name){
this.name = name;
}
public void run(){
synchronized(sync){
for(int i=0;i<3;i++){
try{
sleep(100);
}catch(Exception e){}
System.out.println("thread name:"+ Thread.currentThread().getName()+":"+ i);
}
}
}
public static void main(String[] args) throws Exception{
for(int i=0; i<3; i++){
new MyThread1(i).start();
}
}
}
这里定义了一个static String对象sync作为块指示域,说明块中的部分是同步的,sync是static变量,因此多个对象也是都共用这一个变量,这一点类似于上面的static函数。这里的线程锁就是sync,它是共享且唯一的。本节部分参考: http://enetor.iteye.com/blog/986623
有关Java多线程的内容暂时到这,更多内容请参考《 Java线程详解》。