首选了解一下什么是进程和线程的概念?

进程是运行中的应用程序,每个进程都有自己独立的一块内存空间,一个进程可以启动多个线程,而线程是指进程中的一个执行流程,也可以理解成一段代码。比如java.exe进程中可以运行很多线程,线程永远属于某个进程,进程中的多个线程共享进程的内存。


在Java中为了创建一个新的线程,必须指明线程所要执行的代码。通过Java提供的类java.lang.Thread来方便多线程编程,这个类提供了大量的方法来方便我们控制自己的各个线程。那么如何通过Java线程来执行的代码呢?


Thread类中提供了一个最重要的run()方法,它被Thread类的start()方法调用,提供给我们线程要执行的代码。为了执行我们自己的代码只需要重写该方法即可。


方式一:继承Thread类,重写run()方法

创建Thread类的子类继承该对象重写run()方法,具体代码实例如下:

public class MyThread extends Thread {
	int count = 1, number;

	public MyThread(int num) {
		number = num;
		System.out.println("创建线程 " + number);
	}

	public void run() {
		while (true) {
			System.out.println("线程 " + number + ":计数 " + count);
			if (++count == 6)
				return;
		}
	}

	public static void main(String args[]) {
		for (int i = 0; i < 5; i++)
			new MyThread(i + 1).start();
	}
}

上述方法简单明了符合大家的写作习惯,但是有一个很大的缺点,那就是如果我们的类已经是其他类的子类,就无法再继承Thread类,此时如果不想创建一个新的类,应该怎么办呢?


我们只能将我们的方法作为参数传递给Thread类的实例,类似回调函数,但是Java是没有指针的,我们只能传递一个包含该方法的实例。Java使用接口java.lang.Runnable解决这个问题。


Runnable接口只有一个run()方法,声明类实现Runnable接口并提供这一方法,将线程代码写入这个方法完成任务,但是Runnable接口并没有任何对线程的支持,因此必须创建Thread类的实例,通过Thread类的构造函数完成。


方式二:实现Runnable接口,代码实例如下:

public class MyThread implements Runnable {
	int count = 1, number;

	public MyThread(int num) {
		number = num;
		System.out.println("创建线程 " + number);
	}

	public void run() {
		while (true) {
			System.out.println("线程 " + number + ":计数 " + count);
			if (++count == 6)
				return;
		}
	}

	public static void main(String args[]) {
		for (int i = 0; i < 5; i++)
			new Thread(new MyThread(i + 1)).start();
	}
}

使用Runnable接口来实现多线程使得我们能够在一个类中包容所有的代码,有利于封装。缺点在于只能使用一套代码,若想创建多个线程并使各个线程执行不同的代码,则仍必须额外创建类,如果这样的话,在大多数情况下也许还不如直接用多个类分别继承Thread来得方便。综上所述比较两种实现多线程的方法各有千秋,大家可以灵活运用。


下面和大家分析一下多线程在使用中的一些常见问题。

一:线程可以划分为七种状态(有些人认为只有五种,将锁池和等待池看成阻塞状态的特殊情况,这种认知也是正确的但是锁池和等待池单独分离有利于对程序的理解)。

1.初始状态,线程创建,线程对象调用start()方法

2.可运行状态,也就是等待CPU资源,等待运行的状态

3.运行状态,获得了CPU资源,正在运行状态

4.阻塞状态,也就是让出CPU资源,进入一种等待状态,而且不是可运行状态,有三种情况会进入阻塞状态。

1)如等待输入(输入设备进行处理,而CUP不处理),则放入阻塞,直到输入完毕,阻塞结束后会进入可运行状态。

2)线程休眠,线程对象调用sleep()方法,阻塞结束后会进入可运行状态。

3)线程对象 2 调用线程对象 1 的join()方法,那么线程对象 2 进入阻塞状态,直到线程对象 1 中止。

5.中止状态,也就是执行结束

6.锁池状态

7.等待队列


二:线程优先级

线程的优先级代表该线程的重要程度,当有多个线程同时处于可执行状态并等待获得CPU时间时,线程调度系统根据各个线程的优先级来决定给谁分配CPU时间,优先级高的线程有更大的机会获得CPU时间,优先级低的线程获取CPU时间几率比较小。调用Thread类的getPriority()和setPriority()方法来存取线程的优先级,线程的优先级界于1(MIN_PRIORITY)和10(MAX_PRIORITY)之间,缺省是5(NORM_PRIORITY)


三:线程同步

由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突严重的问题。Java语言提供了专门的机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问。由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized方法和synchronized块。


1. 通过在方法声明中加入synchronized关键字来声明synchronized方法。如:public synchronized void accessVal(int newVal);synchronized方法控制对类成员变量的访问:每个类实例对应一把锁,每个synchronized方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为synchronized的成员函数中至多只有一个处于可执行状态,从而有效避免了类成员变量的访问冲突。在Java中不单单只有类实例,每一个类也是对应一把锁,这样我们也可将类的静态成员函数声明为synchronized,以控制其对类的静态成员变量的访问。


synchronized方法的缺陷:若将一个大的方法声明为synchronized将会大大影响效率,典型地,若将线程类的run()方法声明为synchronized,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何synchronized方法的调用都永远不会成功。当然我们可以通过将访问类成员变量的代码放到专门的方法中将其声明为synchronized,并在主方法中调用来解决这一问题,但是Java为我们提供了更好的解决办法,那就是synchronized块。


2. 通过synchronized关键字来声明synchronized块,语法如下:

synchronized(syncObject) {
 //允许访问控制的代码
}

synchronized块是一个代码块,其中的代码必须获得对象syncObject(类实例或类)的锁方能执行。由于可以针对任意代码块且可任意指定上锁的对象,因此灵活性比较高。


四:线程阻塞

为了解决对共享存储区的访问冲突,Java引入了同步机制。阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪),Java提供了大量方法来支持阻塞,下面逐一进行分析:

1. sleep()方法:sleep()允许指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU时间,指定的时间一过,线程重新进入可执行状态。


2. suspend()和resume()方法:两个方法配合使用,suspend()使得线程进入阻塞状态不会自动恢复,必须其对应的resume()被调用,才能使得线程重新进入可执行状态。


3. yield()方法:yield()使得线程放弃当前分得的CPU时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得CPU时间。调用yield()的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。


4. wait()和 notify()方法:两个方法配合使用,wait()使得线程进入阻塞状态,它有两种形式,一种允许指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的notify()被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的notify()被调用。注意的是1 2 3 步骤阻塞时都不会释放占用的锁而这一对方法则会释放占用的锁。


关于 wait()和 notify()方法最后再说明两点:

1)调用notify()方法导致解除阻塞的线程是因为调用该对象的wait()方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。


2)除了notify(),还有一个方法notifyAll()也可起到类似作用,唯一的区别在于,调用notifyAll()方法将把因为调用该对象的wait()方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。


说到阻塞就不能不说一说死锁的概念,略一分析就能发现suspend()方法和不指定超时期限的wait()方法的调用都可能产生死锁。遗憾的是Java并不在语言级别上支持死锁的避免,因此我们在编程中必须小心地避免死锁。


五:守护线程

守护线程是一类特殊的线程,它和普通线程的区别在于它并不是应用程序的核心部分,当一个应用程序的所有非守护线程终止运行时,即使仍然有守护线程在运行,应用程序也将终止,相反只要有一个非守护线程在运行应用程序就不会终止。守护线程一般被用于在后台为其它线程提供服务。可以通过调用isDaemon()方法来判断一个线程是否是守护线程,可以调用setDaemon()方法来将一个线程设为守护线程。


六:线程组

线程组是Java特有的一个概念,在Java中线程组是ThreadGroup类的对象,每个线程都隶属于唯一一个线程组,这个线程组在线程创建时指定并在线程的整个生命期内都不能更改。你可以通过调用包含 ThreadGroup 类型参数的 Thread 类构造函数来指定线程属的线程组,若没有指定则线程缺省的隶属于名为system的系统线程组。


在Java中除了预建的系统线程组外,所有线程组都必须显式创建并且每个线程组又隶属于另一个线程组。你可以在创建线程组时指定其所隶属的线程组,若没有指定则缺省的隶属于系统线程组。这样所有线程组组成了一棵以系统线程组为根的树。

Java线程组实例代码,具体如下:

public class MyThreadGroup {
	
	public static void main(String[] args) {
		ThreadGroup group = Thread.currentThread().getThreadGroup();
		System.out.println("-主线程组" + group.getName());
		System.out.println("--主线程组是否是后台线程组" + group.isDaemon());
		new MyThread("---主线程组线程").start();
		ThreadGroup tg = new ThreadGroup("----新线程组");
		tg.setDaemon(true);
		System.out.println("-----tg线程组是否是后台线程组" + tg.isDaemon());
		new MyThread(tg, "------tg组的线程一").start();
		new MyThread(tg, "-------tg组的线程二").start();
	}
}

class MyThread extends Thread {
	
	public MyThread(String name) {
		super(name);
	}

	public MyThread(ThreadGroup group, String name) {
		super(group, name);
	}

	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(getName() + "线程的i变量值为" + i);
		}
	}
}

Java允许我们对一个线程组中的所有线程同时进行操作,比如我们可以通过调用线程组的相应方法来设置其中所有线程的优先级,也可以启动或阻塞其中的所有线程。


Java的线程组机制的另一个重要作用是线程安全。线程组机制允许我们通过分组来区分有不同安全特性的线程,对不同组的线程进行不同的处理,还可以通过线程组的分层结构来支持不对等安全措施的采用。Java 的 ThreadGroup 类提供了大量的方法来方便我们对线程组树中的每一个线程组以及线程组中的每一个线程进行操作。

评论

  1. #1

    啦啦 (2017/06/07 16:07:04)回复
    今晚打老虎..............................

分享:

支付宝

微信