java多线程基础

    本文主要讲java多线程的一些入门知识,包括线程的创建、调度、优先级、等待、休眠、中断及守护线程等...,在讲之前让我们先建立一个“数据字典”以简单了解多线程中出现的一些名词。

 

    进程:进程是系统进行资源分配和调度的基本单位,是操作系统结构的基础。可以简单的理解为运行中的应用程序。

    多进程:允许计算机“同时”运行多个进程(应用程序)以提高CPU的利用率

    线程:线程是进程中的一个实体,是程序使用CPU的基本单位,也就是说线程是依赖进程而存在的。    

    多线程:在一个进程中同时有多个线程并发的执行。多线程可以提高应用程序的使用率。

    并行:指两个或两个以上事件(或线程)在同一时刻发生,是真正意义上的不同事件或线程在同一时刻,在不同CPU(多核)上同时执行。

    并发:并发的实质是一个物理CPU(也可以多个物理CPU)在若干道程序(或线程)之间多路复用,并发是对有限物理资源强制行使多用户共享以提高效率。


    在我们没接触多线程之前,我们所学到的都是有关顺序编程的知识。即程序中的所有事物在任意时刻都只能执行一个步骤。   

 

    不过java虚拟机(JVM)的启动却是多线程的。在我们每次运行java程序的时候就会有相应的java命令去启动JVMJVM一启动也就相当于系统启动了一个进程,紧接着该进程就创建了一个主线程用于调用main方法,还创建了一个垃圾回收线程用于回收程序运行中可能出现的垃圾。也就是说JVM的启动至少运行着两个线程。那么我们如何实现自己的线程呢?

 

    Java给我们提供了两种方式实现多线程,一种是继承Thread类,然后该子类应重写Thread类的run方法。例如,用于输出1 -- 10的线程可以写成:


package com.gk.thread.demo;

public class MyThread extends Thread{
	
	@Override
	public void run() {
		
		for (int x=1; x<=10; x++) {
			System.out.println(x);
		}
	}

}


    然后就可以创建并启动一个线程:


MyThread mt = new MyThread();	
mt.start();


    另一种创建线程的方式是实现Runnable接口:


package com.gk.thread.demo;


public class MyRunnable implements Runnable {

	@Override
	public void run() {

		for (int x=1; x<=10; x++) {
			System.out.println(x);
		}
		
	}

}

    再在创建Thread时将Runnable作为参数来传递并启动:


Runnable runnable = new MyRunnable();
Thread th = new Thread(runnable);
th.start();



    以上两种创建线程的方式都要实现run方法,那么run方法有什么作用呢?原来run方法是用来封装那些要被线程执行的代码的,因为我们的Thread类中可能不止有一个方法,run方法就是用来标记哪些代码将来要被线程执行。不过直接调用run方法也是不起多线程作用的。正如上面示例看到的那样,要启动线程,调用的是Thread类的start方法,然后JVM就会帮我们去调用该线程的run方法。

 

    不过要注意的是同一个线程只能调用一次start方法,调用多次就会抛出IllegalThreadStateException。例如下面那样写就会出现问题:

Runnable runnable = new MyRunnable();
		
Thread th = new Thread(runnable);
th.start();
th.start();



    了解了两种创建线程的方式后我们再来思考这样一个问题:既然继承Thread类已经可以实现多线程了,为什么还会有实现Runnable接口方式呢?

 

    其实,实现Runnable接口相对于继承Thread类来说,有如下好处:

      1、可以避免由于java单继承特性带来的局限。我们经常碰到这样一种情况,即当我们要将已经继承了某一个类的子类放入多线程中,由于一个类不能同时有多个父类,所以不能用继承Thread的方式,那么这个类就只能采用实现Runnable接口的方式了。

      2、适合多个相同程序代码的线程去处理同一资源的情况,把线程同程序代码、数据有效分离,更好的体现了面向对象的设计思想。


名字

    当有多个线程同时在执行时,为了区别它们,可以给它们“起名字”。可以通过构造器Thread(Runnable target, String name)传递,也可以通过setName()方法设置。getName()方法可以获取名字,当实现Runnable接口时应该这样获取Thread.currentThread().getName()。如果没有设置名字默认名字是Thread-0、Thread-1、Thread-2...按照线程的创建顺序以此类推。

 

    修改MyRunnablerun方法的输出语句,输出的时候输出相应线程的名字。


@Override
public void run() {

	for (int x=1; x<=10; x++) {
			
			System.out.println(Thread.currentThread().getName() + " : " + x);
			
	}
		
}


    运行代码


package com.gk.thread.demo;

public class Test{
	
	public static void main(String[] args) throws InterruptedException {
		
		startThread();
	}
	
	public static void startThread() {
		
		Runnable runnable = new MyRunnable();	// 多态的方式创建Runnable对象			
		// 创建Thread对象,runnable接口作为参数传递
		Thread th = new Thread(runnable);
		Thread th2 = new Thread(runnable);
		
		// 启动线程
		th.start();
		th2.start();
		
	}

}



    上面程序没有给线程设置名字,所以输出默认名字Thread-0、Thread-1。可以看到启动的两个线程在相互“抢资源”,所以不是顺序输出的。从中也可以得出线程执行的特点是随机不确定的。

 

    下面通过给线程设置名字,看看输出有什么不同。


public static void setThreadName() {

	Runnable runnable = new MyRunnable();
		
	// 创建Thread对象,runnable接口作为参数传递
	Thread th = new Thread(runnable,"zhangSan");	// 通过构造器给线程设置名字
	Thread th2 = new Thread(runnable);
		
	th2.setName("liSi"); 	// 通过setName方法给线程设置名字
		
	// 启动线程
	th.start();
	th2.start();
}


调度、优先级

    接下来说说java中线程调度的问题,线程调度是指按照特定机制为多个线程分配CPU的使用权。一般来说线程的调度有如下两种方式:

    1、协作式调度:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片,也就是使用权。

    2、抢占式调度:优先让优先级高的线程使用CPU。如果线程的优先级相同,那么会随机选择一个执行,也就是说优先级高的线程获得的CPU使用权会相对多一些。

 

    在java多线程环境中,为了保证所有线程的执行能按照一定规则执行,JVM实现了一个线程调度器,它定义了线程调度策略,对于CPU运算的分配都进行了规定,按照这些特定的机制为多个线程分配CPU的使用权。

 

    说了这么多,那么java到底使用了哪种调度策略呢?其实java使用的是抢占式调度模型。在JVM规范中规定每个线程都有优先级,且优先级越高优先执行,但优先级高并不代表能独自拥有CPU执行的时间片,只能说优先级越高执行的几率越大。

 

       java线程优先级在1--10范围内,默认是5,可以通过Thread.MAX_PRIORITY、Thread.MIN_PRIORITY、Thread.NORM_PRIORITY查看。我们也可以通过setPriority方法来设置线程的优先级,如果设置的优先级不在1--10之内就会抛出IllegalArgumentException异常。

 

    线程的优先级将该线程的重要性传递给了调度器。尽管CPU处理现有线程集的顺序是不确定的,但是调度器将倾向于让优先级高的线程先执行。然而,这并不是意味着优先级较低的线程得不到执行(也就是说优先权不会导致死锁)。优先级较低的线程仅仅是执行的频率较低。

 

    在绝大多数的时间里,所有线程都应该以默认的优先级运行。试图通过操纵线程优先级来改变线程的执行顺序是一种错误的做法。


public static void priority() {
		
	// 输出优先级
	System.out.println("最大优先级 : " + Thread.MAX_PRIORITY);
	System.out.println("最小优先级 : " + Thread.MIN_PRIORITY);
	System.out.println("默认优先级 : " + Thread.NORM_PRIORITY);
	System.out.println("\n================================\n");
	//
		
	Runnable runnable = new MyRunnable();
		
	// 创建Thread对象,runnable接口作为参数传递
	Thread th = new Thread(runnable,"zhangSan");	// 通过构造器给线程设置名字
	Thread th2 = new Thread(runnable);
		
	th2.setName("liSi"); 	// 通过setName方法给线程设置名字
		
	th.setPriority(1);		// 设置th线程的优先级为1
	th2.setPriority(10);	// 设置th2线程的优先级为10
		
	System.out.println("th(zhangSan)的优先级为 : " + th.getPriority());
	System.out.println("th2(liSi)的优先级为 : " + th2.getPriority());
		
	System.out.println("\n================================\n");
	// 启动线程
	th.start();
	th2.start();
}



    可以看到设置了th2优先级为10后,th2的执行几率比th大了,但这种情况不是绝对的,应该多运行几次才能比较出效果。



加入

    虽然设置优先级可以提高执行几率,不过有时候想让某一线程执行完之后才允许其它线程执行。这时可以使用join()方法,它的作用是优先执行该线程后其它线程才能执行。


public static void join() throws InterruptedException {
		
	Runnable r = new MyRunnable();
		
	Thread th = new Thread(r, "leader");
	Thread th2 = new Thread(r, "zhangSan");
	Thread th3 = new Thread(mr, "liSi");
		
	th.start();
	th.join();		// 注意使用顺序
		
	th2.start();
	th3.start();
				
}

    可以看到leader线程执行完之后zhangSan线程和liSi线程才开始抢资源。不过该方法的使用要注意顺序,一定要在该线程的start()方法之后且在其他线程的start()方法之前使用,不然将不起作用。



礼让

    我们都知道,现代社会的竞争很激烈,不过从小父母、老师就教我们待人要友善,要懂得谦让。同理,线程之间也可以实现这种“礼让”。yield()方法就是暂停当前正在执行的线程对象,并执行其他线程。


package com.gk.thread.demo;

public class YieldRunnable implements Runnable{

	@Override
	public void run() {

		for (int x=1; x<=10; x++) {
			
			System.out.println(Thread.currentThread().getName() + " : " + x);
			Thread.yield();		// yield
		}
	}
	
}


    运行代码


public static void yield() {

	Runnable r = new  YieldRunnable();
		
	new Thread(r, "zhangSan").start();		// 匿名对象
	new Thread(r, "liSi").start();
		
}



    运行结果基本上都是zhangSan一次,liSi一次、zhangSan一次,liSi一次...而极少出现同一线程同时运行多次的结果,从而让线程的执行在一定程度上看起来更加的和谐。不过这只是一种暗示,并没有任何机制保证这种和谐的绝对性。当调用yield()时,也是在建议具有相同优先级的其他线程可以运行。



守护线程

    中国象棋很多人都玩过吧,就算没玩过也都应该听说过。里面有一个规则是这样的,当将或者帅“挂”了的时候游戏就结束了而不管其他角色还存不存在。在线程中有一种被称为守护线程(也叫后台线程)的就跟这个类似。被标记为守护线程的线程就相当于中国象棋的其他角色。


package com.gk.thread.demo;

public class DaemonRunnable implements Runnable{

	@Override
	public void run() {
		
		for(int x=1; ; x++) {	// 注意这是个死循环
			System.out.println(Thread.currentThread().getName() + " : " + x);
		}
	}
}


    运行代码


public static void daemon() {

	Runnable r = new DaemonRunnable();
		
	Thread th = new Thread(r, "zhangSan");
	Thread th2 = new Thread(r, "liSi");

	th.setDaemon(true);		// 设置th为守护线程
	th2.setDaemon(true);	// 设置th2为守护线程
		
	th.start();
	th2.start();
		
        // main线程
	for (int x=1; x<=5; x++) {
		System.err.println(Thread.currentThread().getName() + " : " + x);
	}
}


    在DaemonRunnable类的run方法中写了一个死循环,然后在daemon方法中让th线程和th2线程去执行这个死循环,在daemon方法中还有一个main线程用于输出1 -- 5,这三个线程同时在执行。由于th线程和th2线程被设置为守护线程,所以当main线程执行完毕退出后thth2这两个守护线程也就消失而不会无限循环下去。

 

    也许有人会有疑惑,刚才不是说main线程结束后守护线程thth2就会消失停止执行,那为什么还会再输出zhangSan : 1及后面一些呢?其实是这样的,之所以看到还有输出是因为当main线程结束的那一瞬间thth2刚好正在执行。可以形象的想象成在那一瞬间thth2正在挣扎一会就挂了。^_^   ^_^  ^_^



休眠

    有时候我们学习累了,是不是应该休息一下呢。同样,线程也可以“休息”,sleep(long millis)方法可以在指定的毫秒数内让当前正在执行的线程休眠(即阻塞)。为了能看出效果,我们再写一个能够输出时间的类SleepRunnable,并设置线程休眠一秒钟。


package com.gk.thread.demo;

import java.text.SimpleDateFormat;
import java.util.Date;

public class SleepRunnable implements Runnable {

	@Override
	public void run() {

		for (int x = 1; x <= 10; x++) {

			System.out.println(
					"时间 : " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + 
					"  ------>  " + 
					Thread.currentThread().getName() + " : " + x);

			try {
				Thread.sleep(1 * 1000); // 设置休眠1秒
			} catch (InterruptedException e) {
				throw new RuntimeException(e);
			}
		}
	}

}


    运行程序


public static void sleep() {
		
	Runnable r = new SleepRunnable();
		
	new Thread(r, "zhangSan").start();		// 匿名对象
	new Thread(r, "liSi").start();
		
}


    可以看到,当设置了sleep(1 * 1000)之后每个线程执行完一次后总是暂停一秒再执行。

 

    这里再说一下异常的处理原则,在SleepRunnable类的run方法中调用Thread类的sleep()方法时会有InterruptedException异常,该异常在run方法内只能try...catch...而不能throws。我在java异常那些事中说过,当父类方法没有异常抛出时,子类重写父类该方法时只能try...catch...而不能throws。所以在父类Runnablerun方法中没有抛出此异常的情况下,子类SleepRunnable重写run方法内有异常只能try...catch...



中断

    线程虽然可以“睡觉”(sleep),但是睡觉是一件不靠谱的事情,就像我们小学语文学过的课文《一分钟》,元元因为多睡了一分钟而迟到了二十分钟。所以说时间观念很重要。在多线程中如果某个线程“睡太久”了,我们是不是有什么办法来阻止呢?stop()和interrupted()方法就是来对其进行“惩罚”的。不过由于stop()方法具有固有的不安全性所以现在不怎么使用了。下面用interrupted()模拟元元由于迟到时间超过了一分钟,所以被老师惩罚不准进教室。


package com.gk.thread.demo;

import java.text.SimpleDateFormat;
import java.util.Date;

public class InterruptRunnable implements Runnable{

	@Override
	public void run() {

		System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
		
		try {
			Thread.sleep(1 * 1000 * 60 * 20);	// 模拟元元“迟到”了20分钟

		} catch (InterruptedException e) {
			//throw new RuntimeException(e);
//System.out.println("catch : " + Thread.currentThread().isInterrupted());		// false
			System.out.println("元元,你迟到的时间超过一分钟,已经不能进教室了...");
		}
		
		System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
	}
}

    运行程序


public static void interrupt() throws InterruptedException {

	Runnable r = new InterruptRunnable();
		
	Thread th = new Thread(r, "元元");
	th.start();
		
	// 如果元元迟到的时间超过1分钟就不准他进教室
	Thread.sleep(1 * 1000 * 60);
	th.interrupt();
		
}


    当另一个线程在该线程上调用interrupt()方法的时候,将给该线程设定一个标志,表明该线程已经被中断,并抛出了InterruptedException异常。然而异常被捕获时将清理这个标志,所以在catch子句中,如果使用isinterrupted()判断该异常是否中断时将返回false。

 

    至此,我们已经学习了java多线程的一些基本知识,虽然在具体的编程中这些并不怎么用,但是却是我们以后理解那些高级应用的基础,所以说基础很重要。在以后的时间中我会写一些关于同步、死锁和生产者消费者模式及其他一些JDK1.5java.util.concurrent包类的应用。





  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值