Java学习笔记-多线程编程

首先回顾一下进程(Process)和线程(Thread)的区别:

进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1–n个线程。(进程是资源分配的最小单位)

线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)

线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。

一、基本概念

Java支持多线程,也就是一个程序里面可以有多个并发运行的线程,在多线程模型中,多个线程共存于同一块内存中,且共享资源。CPU的时间被划分为多个片段,每个线程都分配有限的时间片来处理任务。至于你为什么觉得它们在并行运行,那是因为你的能力让你看到的是这样的,切换速度贼快所以你感觉不到。

多线程的特点:

多个线程在运行时,系统在各个线程间进行切换;
由于多个线程在同一块内存里面,线程之间通信非常容易;
Java将线程当做对象来管理。(通过类Thread或者接口Runable)
根据优先级确定线程间切换的顺序。优先级是1~10的整数;
优先级是线程之间的相对关系,只有一个线程的时候无关紧要;
由于位于同一块内存,共享资源,所以可能产生冲突,可以采用synchronized关键字协调资源,实现线程同步。

二、用线程类实现多线程

我们之前写的程序都是单线程的,也就是main函数那一个线程(实际上也是多线程,因为还有一个垃圾回收线程,是系统的);

要想实现多线程,就必须通过实例化一个Thread线程类的对象或者实现Runable接口;

例如:继承Thread类,然后实例化线程类

package Thread;

class PrimeThread extends Thread{//继承Thread类,成为线程类
	long minPrime;
	PrimeThread(long minPrime){
		this.minPrime=minPrime;
	}
	public void run(){
		//将实际执行代码写到run()方法中(重写Thread中的run()方法)
	}
}


public class test {
	public static void main(String[] args){
		PrimeThread p=new PrimeThread(143);//实例化一个线程类对象
		p.start();//通过start()方法启动线程
	}
}

实现Runable接口

package Thread;

class PrimeRun implements Runnable{//实现Runable接口
	long minPrime;
	PrimeRun(long minPrime){
		this.minPrime=minPrime;
	}
	public void run(){
		//实现run()方法
	}
}


public class test {
	public static void main(String[] args){
		PrimeThread p=new PrimeThread(143);//实例化一个线程类对象
		new Thread(p).start();//这里实际有两个部分。首先先将p作为参数传给Thread类的构造函数,然后调用线程类的start()函数启动线程
	}
}

start()方法会自动调用run()方法,启动线程。启动线程只能用start()而不能直接用run();
Thread类的run()方法具有public属性,所以继承Thread类的子类在重写(覆盖)该方法的时候必须也带上public属性,否则就是方法的重载了。可以在方法上一行加上“@Override”来判断是不是在重写父类的方法;
Runable接口中的run()方法也有public属性,所以和上面的一样,实现该接口类的run()方法也要带上public;
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
实现Runnable接口比继承Thread类所具有的优势:
1)适合多个相同的程序代码的线程去处理同一个资源;
2)可以避免java中的单继承的限制;
3)增加程序的健壮性,代码可以被多个线程共享,代码和数据独立;
4)线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类。
在java中所有的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。

三、线程的基本操作

1.启动——start()

Thread t=new Thread();
t.start();

2.睡眠——sleep()

try{
	Thread.sleep(1000);//括号里面表示毫秒,1000毫秒=1秒
}
catch(InterruptedException e){
	e.printStackTrace();
}

3.停止

系统提供的stop()方法不建议使用了。所以我们一般采用run()方法中加入while循环的形式,采用布尔型标记控制循环的停止与运行,基本形式如下:

public class Interrupted extends Thread{
	private boolean isContinue=false;
	public void run(){
		while(true){
			//...
			if(!isContinume){
				break;
			}
		}
	}
	public void setContinue(boolean par){
		this.isContinue=par;
	}
}

四、资源的协调与同步

1.线程调度模型

线程分配处理器使用权的过程,主要有两种:协同式线程调度(Cooperative Thread-Scheduling)和抢占式线程调度(Preemptive Threads-Scheduling)。
Java使用的是抢占式线程调度,通过设置线程优先级“建议”系统给有些线程多分配时间,不同操作系统对于优先级的反应并不一定相同(比如Windows操作系统优先级7种,Java有10种,那么有些优先级映射到系统原生线程后就是一样的)
在抢占模式下,操作系统负责分配CPU时间给各个进程,一旦当前的进程使用完分配给自己的CPU时间,操作系统将决定下一个占用CPU时间的是哪一个线程。因此操作系统将定期的中断当前正在执行的线程,将CPU分配给在等待队列的下一个线程。所以任何一个线程都不能独占CPU。每个线程占用CPU的时间取决于进程和操作系统。进程分配给每个线程的时间很短,以至于我们感觉所有的线程是同时执行的。实际上,系统运行每个进程的时间很短(可能就几毫秒),然后调度其它的线程。它同时维持着所有的线程和循环,分配很少量的CPU时间给线程。线程的的切换和调度是如此之快,以至于感觉是所有的线程是同步执行的。
在非抢占的调度模式下,每个线程可以需要CPU多少时间就占用CPU多少时间。在这种调度方式下,可能一个执行时间很长的线程使得其它所有需要CPU的线程”饿死”。在处理机空闲,即该进程没有使用CPU时,系统可以允许其他的进程暂时使用CPU。占用CPU的线程拥有对CPU的控制权,只有它自己主动释放CPU时,其他的线程才可以使用CPU。

2.优先级高的线程让出CPU的情况

调用yield()方法退出;
不再是可运行的(处于消亡或者阻塞状态);
被其他优先级更高的线程替代(具有更高的优先级的线程可能已经休眠了指定的时间段,或者它的I/O操作已经结束,或者调用了resume()或者notify()方法);
这时候,调度程序会选择一个在可运行线程中优先级最高的线程运行,如果多个线程具有相同的优先级,那么它们会轮流调度。

3.资源冲突

多个线程同时运行会提高程序的执行效率,但是由于它们在同一个内存区域,共享一组资源,在运行的时候可能会引起死锁(Deadlock)。
举一个资源冲突的例子:

class UserThread{
    void Play(int n) {
        System.out.println("运行线程 NO:"+n);

        try{
            Thread.sleep(3);	// 采用睡眠模拟程序的运行
        }catch(InterruptedException e) {
            System.out.println("线程异常,NO:"+n);
        }
        System.out.println("结束线程 NO:"+n);
    }
}

class UserMultThread implements Runnable{//实现Runable接口成为线程类
    UserThread UserObj;
    int num;

    UserMultThread(UserThread o,int n) {
        UserObj=o;
        num=n;
    }

    public void run( ) {
        UserObj.Play(num);
    }
}

public class multTheadTwo { 
    public static void main(String args[ ]) {
        UserThread Obj=new UserThread( );  		// 定义对象Obj
        Thread t1=new Thread(new UserMultThread(Obj,1));// 采用Obj产生三个线程对象
        Thread t2=new Thread(new UserMultThread(Obj,2));
        Thread t3=new Thread(new UserMultThread(Obj,3));

        t1.start( );//分别启动三个线程
        t2.start( );
        t3.start( );
    }
}

输出结果为:

输出结果为:
运行线程 NO:2
运行线程 NO:1
运行线程 NO:3
结束线程 NO:2
结束线程 NO:1
结束线程 NO:3

这只是一种可能的结果,因为系统对于三个线程的处理情况是不确定的,但是可以看出来一个线程的play()方法还没有执行完,就被另一个线程抢了过去,说明这其中三个线程就对play()这个产生了竞争。

4.同步方法

上述问题可以通过关键字synchronized关键字实现线程的同步来解决。
当一个对象使用了synchronized关键字后,这个对象就被锁定或者说进入了监视器(monitor),这样可以保证每时每刻都只有一个线程进入监视器来访问被锁定的对象,其它线程就被挂起直到之前的线程退出监视器。
Java中每一个对象都与监视器相连,但是如果不使用同步关键字,监视器就不会真的被分配。
实现同步有两种方法:
1.锁定冲突的对象

synchronized (ObjRef){
    Block   //需要同步执行的语句体
}

例如将上面的例子中的run()方法里面的UserObj锁定

public void run(){
       synchronized (UserObj){
               UserObj.Play(num);
      } 
}

这样就实现了同步,执行一次结果如下:

运行线程 NO:2
结束线程 NO:2
运行线程 NO:1
结束线程 NO:1
运行线程 NO:3
结束线程 NO:3

2.锁定冲突的方法

synchronized 方法的定义

例如将上面的play()方法锁定

class UserThread{
    synchronized void Play(int n) {
        System.out.println("运行线程 NO:"+n);

        try{
            Thread.sleep(3);	// 采用睡眠模拟程序的运行
        }catch(InterruptedException e) {
            System.out.println("线程异常,NO:"+n);
        }
        System.out.println("结束线程 NO:"+n);
    }
}

当一个线程在执行play()的时候,另一个线程如果也要执行这方法,就会被阻塞;

所以输出一次结果如下:

运行线程 NO:2
结束线程 NO:2
运行线程 NO:3
结束线程 NO:3
运行线程 NO:1

需要注意:
对run()方法无法加锁,编译可通过但是无法避免冲突;
无法对构造函数加锁,会出现语法错误;

五、线程间通信

Java实现多线程通信是通过系统方法实现的,主要是wait()、notify()和notifyAll()方法。

wait()方法

wait()方法使得当前线程必须要等待,等到另外一个线程调用notify()或者notifyAll()方法。
线程调用wait()方法,释放它对锁的拥有权,然后等待另外的线程来通知它(通知的方式是notify()或者notifyAll()方法),这样它才能重新获得锁的拥有权和恢复执行。
注意:
要确保调用wait()方法的时候拥有锁,即wait()方法的调用必须放在synchronized方法或synchronized块中;
另一个会导致线程暂停的方法:Thread.sleep(),它会导致线程睡眠指定的毫秒数,但线程在睡眠的过程中不会释放掉对象的锁。

notify()方法

notify()方法通知等待监视器的线程,该对象的状态已经改变。会唤醒一个等待的线程。
注意:
如果多个线程在等待,它们中的一个将会选择被唤醒。这种选择是随意的,和具体实现有关;
被唤醒的线程是不能被执行的,需要等到当前线程放弃这个对象的锁;
被唤醒的线程将和其他线程以通常的方式进行竞争,来获得对象的锁。也就是说,被唤醒的线程并没有什么优先权,也没有什么劣势,对象的下一个线程还是需要通过一般性的竞争,也就是看优先级;
notify()方法应该是被拥有对象的锁的线程所调用。也就是和wait()方法一样,必须放在synchronized方法或synchronized块中。

notifyAll()方法
notifyAll()方法会唤醒从同一个监视器中用wait()方法退出的所有线程,使它们按照优先级顺序重新排队

线程调度比较复杂,编写程序应该遵守以下规则:

如果有多个线程访问同一个对象,就要将执行修改操作的方法声明为synchronized;
如果线程必须等待某个对象的状态,则应该在同步方法内调用wait()方法;
每当一个方法改变了某个对象的状态,就应该调用notify()方法。这样,等待线程就有机会检查环境是否发生了变化。

在这里插入图片描述
1、新建状态(New):新创建了一个线程对象。

2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。

4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait()会释放持有的锁)
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁,上面我已经说过了)
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值