java多线程
进程和线程都是由操作系统所体会的程序运行的基本单元,系统利用该基本单元实现系统对应用的并发性。进程和线程的区别在于:
线程的划分尺度小于进程,使得多线程程序的并发性高。
另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程
序提供多个线程执行控制。
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源
分配。这就是进程和线程的重要区别。
——以上是废话;
个人理解:windows系统打开一个应用程序,可能打开一个或多个进程,在任务管理器中可以看到,线程是指进程中的一个执行流程,一个进程中可以运行多个线程,线程总是属于某个进程,进程中的多个线程共享进程的内存。
JAVA多线程实现方式主要有两种:继承Thread类与实现Runnable接口,java只可以继承一个类,并且继承Thread不容易实现资源共享,所以用实现Runnable接口较多;
先看个小例子:
public class TestThread extends Thread{
private String name;
public TestThread(String name){
this.name = name;
}
public void run(){
for (int i = 0; i < 5; i++) {
System.out.println(name+i);
}
}
public static void main(String[] args) {
Thread t1 = new TestThread("线程1---");
Thread t2 = new TestThread("线程2---");
t1.run();
t2.run();
}
}
运行打印出:
线程1---0
线程1---1
线程1---2
线程1---3
线程1---4
线程2---0
线程2---1
线程2---2
线程2---3
线程2---4
可见并没有实现多线程,应该用start方法,打印结果可能是:
线程1---0
线程2---0
线程1---1
线程2---1
线程1---2
线程2---2
线程1---3
线程2---3
线程1---4
线程2---4
每次运行结果可能都不一样,这就是多线程的概念,多个线程共用一个资源,谁抢到就是谁的。
那么如何实现不同线程要等之前获取资源的线程结束之后再执行呢,这就引入了线程同步的概念,关键字是synchronized,意思是锁住对象,上例子:
public class TestThread extends Thread{
public void run(){
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}
public static void main(String[] args) {
TestThread obj = new TestThread();
Thread t1 = new Thread(obj,"线程1--");
Thread t2 = new Thread(obj,"线程2---");
t1.start();
t2.start();
}
}
这里两线程用到了同一个对象实例,而这个对象被synchronized锁住了,所以线程2等待线程1执行完后执行,打印结果为:
线程1--0
线程1--1
线程1--2
线程1--3
线程1--4
线程2---0
线程2---1
线程2---2
线程2---3
线程2---4
然而继承Thread类实现资源共享,经典例子便是车站购票窗口问题,上例子:
//使用Thread实现线程不能实现资源共享 ,打印结果重复
class MyThread extends Thread
{
private int ticket=10;
private String name;
public MyThread(String name ){
this.name=name;
}
public void run(){
for(int i=0;i<10;i++){
if(ticket>0){
System.out.println("线程"+name+"卖票"+(ticket--));
}
}
}
}
public class ThreadTicket
{
public static void main(String args[]){
MyThread A = new MyThread("A");
MyThread B = new MyThread("B");
A.start();
B.start();
}
}
//使用Runnable实现线程可以实现资源共享 ,打印结果不重复
class MyThread implements Runnable
{
private int ticket=10;
private String name;
public MyThread(String name){
this.name=name;
}
public void run(){
for(int i=1;i<=10;i++){
if(ticket>0){
System.out.println("线程"+name+"卖票"+(ticket--));
}
}
}
}
public class ThreadTicket
{
public static void main(String args[]){
MyThread A = new MyThread("A"); //实例化线程要执行的任务
Thread Ta = new Thread(A); //实例两个线程对象,实际传递的是一个任务
Thread Tb = new Thread(A); //因为两个线程执行的是一个任务,所以资源是共享的
Ta.start();
Tb.start();
}
}
PS:继承Thread类可以直接调用start方法,实现Runnable接口不能直接调用start方法,只能构造方法Thread(Runnable r)调用;
线程的阻塞
为了解决对共享存储区的访问冲突,Java 引入了同步机制,现在让我们来考察多个线程对共享资源的访问,显然同步机制已经不够了,因为在任意时刻所要求的资源不一定已经准备好了被访问,反过来,同一时刻准备好了的资源也可能不止一个。为了解决这种情况下的访问控制问题,Java 引入了对阻塞机制的支持。
阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪),学过操作系统的同学对它一定已经很熟悉了。Java 提供了大量方法来支持阻塞,下面让我们逐一分析。
1. sleep() 方法:sleep() 允许 指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU 时间,指定的时间一过,线程重新进入可执行状态。
典型地,sleep() 被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间后重新测试,直到条件满足为止。
2. suspend() 和 resume() 方法:两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。
3. yield() 方法:yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。
4. wait() 和 notify() 方法:两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种允许 指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用。
初看起来它们与 suspend() 和 resume() 方法对没有什么分别,但是事实上它们是截然不同的。区别的核心在于,前面叙述的所有方法,阻塞时都不会释放占用的锁(如果占用了的话),而这一对方法则相反。上述的核心区别导致了一系列的细节上的区别。
首先,前面叙述的所有方法都隶属于 Thread 类,但是这一对却直接隶属于 Object 类,也就是说,所有对象都拥有这一对方法。初看起来这十分不可思议,但是实际上却是很自然的,因为这一对方法阻塞时要释放占用的锁,而锁是任何对象都具有的,调用任意对象的 wait() 方法导致线程阻塞,并且该对象上的锁被释放。而调用 任意对象的notify()方法则导致因调用该对象的 wait() 方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。
其次,前面叙述的所有方法都可在任何位置调用,但是这一对方法却必须在 synchronized 方法或块中调用,理由也很简单,只有在synchronized 方法或块中当前线程才占有锁,才有锁可以释放。同样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁可以释放。因此,这一对方法调用必须放置在这样的 synchronized 方法或块中,该方法或块的上锁对象就是调用这一对方法的对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现IllegalMonitorStateException 异常。
wait() 和 notify() 方法的上述特性决定了它们经常和synchronized 方法或块一起使用,将它们和操作系统的进程间通信机制作一个比较就会发现它们的相似性:synchronized方法或块提供了类似于操作系统原语的功能,它们的执行不会受到多线程机制的干扰,而这一对方法则相当于 block 和wakeup 原语(这一对方法均声明为 synchronized)。它们的结合使得我们可以实现操作系统上一系列精妙的进程间通信的算法(如信号量算法),并用于解决各种复杂的线程间通信问题。
关于 wait() 和 notify() 方法最后再说明两点:
第一:调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。
第二:除了 notify(),还有一个方法 notifyAll() 也可起到类似作用,唯一的区别在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。
谈到阻塞,就不能不谈一谈死锁,略一分析就能发现,suspend() 方法和不指定超时期限的 wait() 方法的调用都可能产生死锁。遗憾的是,Java 并不在语言级别上支持死锁的避免,我们在编程中必须小心地避免死锁。
以上我们对 Java 中实现线程阻塞的各种方法作了一番分析,我们重点分析了 wait() 和 notify() 方法,因为它们的功能最强大,使用也最灵活,但是这也导致了它们的效率较低,较容易出错。实际使用中我们应该灵活使用各种方法,以便更好地达到我们的目的。