java多线程与线程间通信

时间:2022-02-19 06:07:26
转自(http://blog.csdn.net/jerrying0203/article/details/45563947)
本文学习并总结java多线程与线程间通信的原理和方法,内容涉及java线程的众多常见重要知识点,学习后会对java多线程概念及线程间通信方式有直观清晰的了解和掌握,可以编写并分析简单的多线程程序。

进程与线程

进程:是一个正在执行的程序。
每一个进程执行都有执行顺序,一个执行顺序是一个执行路径,或者叫控制单元;
每一个程序启动时,都会在内存中分配一片空间,进程就用于标识这片空间,并封装一个或若干控制单元。

线程:就是进程中的一个独立的控制单元。
线程控制进程的执行,一个进程至少有一个线程。

Java程序编译时,java编译器启动,对应javac.exe进程启动,编译结束后javac.exe进程退出;java程序运行时,jvm启动,对应java.exe进程启动,java.exe中有一个主线程负责java程序的执行,这个主线程运行的代码就存在于main方法中。其实jvm启动时,不止一个主线程,还有负责垃圾回收机制的线程。

有多条执行路径的程序,称为多线程程序。多线程的好处是可以让程序的多个部分代码产生同时运行的效果,程序的多个功能分支并行执行,优化程序功能结构并提高效率。

自定义创建线程的2种方法

1. 继承Thread类,具体步骤:
    a) 自定义类,继承Thread;
    b) 复写Thread类的run()方法,run()方法中存储线程要运行的代码;
    c)  创建继承Thread的自定义类对象,调用线程的start()方法,start()方法的作用:启动线程,并自动调用run()方法。
2. 实现Runnable接口,具体步骤:
    a) 定义类实现Runnable接口;
    b) 覆盖Runnable接口中的run()方法,run()中存放线程要运行的代码;
    c) 创建Thread类线程对象,并将Runnable接口的子类对象作为实参传递给Thread类构造函数;
    d) 调用Thread类对象的start()方法。
2种方式的区别:
    实现方式时,线程代码存放在实现Runnable接口的子类的run()方法中,可以使用该子类创建多个Thread类,这样多个线程运行时可以共用Runnable子类中的成员变量,实现资源的独立共享。
    继承方式时,线程代码存放在Thread子类的run()方法中,而一个线程不能多次start(),所以达不到Thread子类中资源数据的共享使用。
    自定义线程时,建议使用实现Runnable接口的方式,因为这样还可以避免单继承的局限性。

线程的几个零散知识点

多线程运行结果的随机性:单核CPU环境,多个线程并非真正的同时运行,而是互相抢夺CPU的执行权限和资源,谁抢到谁执行,至于执行多长时间,CPU说了算(所以多线程程序的每次运行结果可能都不一样)(后续可以加以控制)。
多核CPU环境,多个线程可以分布运行到多个CPU上,实现真正的同时运行。
    多核CPU环境上多个线程同时打印输出信息时,可能打印顺序混乱,这是因为多个CPU核抢占DOS输出屏是随机的,有的打印被临时阻塞。
   多核CPU时,程序运行效率就卡在了内在空间上,必须要有足够大的内存存储很多线程,才能让这些线程运行在多个CPU上。
线程状态及状态间切换
java多线程与线程间通信
已start()过的线程不能再次start(), 否则会报异常java.lang.IllegalThreadStateException。
线程的名称
线程对象都有自己默认的名称:Thread-编号,编号从0开始。
设置自定义线程名称,可以在子类构造函数中调用super(name), 也可以直接创建对象后调用setName()方法。
Thread.currentThread(), 返回当前运行的线程对象,也就是this引用指向的对象。

多线程安全问题

当多条语句在操作多个线程共享数据时,一个线程对多条语句执行了一部分,还没执行完,另一个线程参与进来执行,会导致共享数据的错误。
解决方法:对多条操作共享数据语句,只能让一个线程执行完,在执行过程中,其他线程不可以参与执行。
java对多线程安全问题的专业解决方法就是同步synchronized,具体表现形式有同步代码块和同步函数。
同步代码块

copy

synchronized

  • {
  • }

同步函数:将synchronized作为修饰符放在函数定义上,函数返回值类型前面。
同步的原理
     同步代码块对象如同锁,持有锁的线程可以在同步语句中执行;没有持有锁的线程即使获得了CPU执行权,也进不去,无法执行同步代码。
     同步函数使用的锁是this对象;静态同步函数使用的锁是该函数所在类对应的类字节码文件对象,即类名.class,该对象的类型是Class。
     synchronized修饰符不属于方法签名的一部分,当子类覆盖父类方法时,synchronized修饰符不会被继承,因此接口中方法不能被声明为synchronized,同样,构造函数也不能被声明为synchronized。
     线程进入同步代码块或同步函数前先判断锁标志位,若判断结果为真,则进入同步代码块或同步函数后,修改锁标志位为假,线程退出后,再恢复锁标志位为真。

copy

/*

  • */
    class Runnable{
  • tick=;
  • Object obj= Object();
  • run(){
  • (){
  • (obj){
  • (tick>){
  • {Thread.sleep();}(Exception e){e.printStackTrace();}
  • System.out.println(Thread.currentThread().getName()+

    public TicketDemo{

  • main(String[] args){
  • Ticket t= Ticket();
  • Thread(t);
  • Thread t2= Thread(t);
  • Thread(t);
  • Thread t4= Thread(t);
  • }

同步的前提
1. 必须要有2个或者2个以上的线程
2. 必须是多个线程使用同一个锁,多个线程可以同时操作同一个锁下的代码。
同步的弊端
1. 线程每次进入同步代码块或同步函数都要判断锁,浪费资源,影响效率
2. 可能出现死锁现象,多发生在一个同步代码块或同步函数中嵌套另一个同步函数或同步代码块,且2个同步上使用不同的锁。即同步中嵌套同步而锁不同就容易引发死锁。
下面是一个很直观的死锁的例子,跟毕老师讲得MyLock的例子原理一样,只是形式上有差别:

copy

class

  • say(){
  • ) ;
  • }
  • get(){
  • System.out.println() ;
  • class
  • say(){
  • ) ;
  • }
  • get(){
  • System.out.println() ;
  • public ThreadDeadLock  Runnable{
  • Zhangsan zs =  Zhangsan() ;
  • Lisi ls =  Lisi() ;
  • flag =  ;
  • run(){
  • (flag){
  • (zs){
  • zs.say() ;
  • {
  • Thread.sleep() ;
  • (InterruptedException e){
  • e.printStackTrace() ;
  • (ls){
  • {
  • (ls){
  • ls.say() ;
  • {
  • Thread.sleep() ;
  • (InterruptedException e){
  • e.printStackTrace() ;
  • (zs){
  • main(String args[]){
  • ThreadDeadLock() ;
  • ThreadDeadLock t2 =  ThreadDeadLock() ;
  • ;
  • t2.flag =  ;
  • Thread(t1) ;
  • Thread thB =  Thread(t2) ;
  • //双方僵持在这,谁都没法继续运行

线程间通讯

Object类方法wait(),notify(),notifyAll()
      线程执行wait()后,就放弃了运行资格,处于冻结状态;线程运行时,内存中会建立一个线程池,冻结状态的线程都存在于线程池中,notify()执行时唤醒的也是线程池中的线程,线程池中有多个线程时唤醒第一个被冻结的线程。
      notifyall(), 唤醒线程池中所有线程。
      wait(), notify(),notifyall()都用在同步里面,因为这3个函数是对持有锁的线程进行操作,而只有同步才有锁,所以要使用在同步中。
      wait(),notify(),notifyall(),  在使用时必须标识它们所操作的线程持有的锁,因为等待和唤醒必须是同一锁下的线程;而锁可以是任意对象,所以这3个方法都是Object类中的方法。

wait和sleep区别:从执行权和锁上来分析这2个方法
wait():可以指定时间也可以不指定时间,不指定时间时,只能由对应的notify()或notifyAll()来唤醒。
sleep():必须指定时间,时间到自动从冻结状态转入运行状态或临时阻塞状态。
wait():线程会释放执行权,并释放锁。
sleep():线程会释放执行权,但是并不释放锁。

单个消费者生产者例子:

copy

class

  • String name;
  • count=;
  • flag=;
  • set(String name){
  • (flag)
  • {wait();}(Exception e){}
  • .name=name++count++;
  • +.name);
  • flag=;
  • .notify();
  • }
  • out(){
  • (!flag)
  • {wait();}(Exception e){}
  • System.out.println(Thread.currentThread().getName()++.name);
  • ;
  • .notify();
  • class Runnable{
  • Resource res;
  • .res=res;
  • run(){
  • (){
  • res.set();
  • class Runnable{
  • Resource res;
  • Consumer(Resource res){
  • .res=res;
  • }
  • run(){
  • (){
  • public ProducerConsumerDemo{
  • main(String[] args){
  • Resource();
  • Producer pro= Producer(r);
  • Consumer(r);
  • Thread t1= Thread(pro);
  • Thread(con);
  • t1.start();
  • //运行结果正常,生产者生产一个商品,紧接着消费者消费一个商品。

但是如果有多个生产者和多个消费者,上面的代码是有问题,比如2个生产者,2个消费者,运行结果就可能出现生产的1个商品生产了一次而被消费了2次,或者连续生产2个商品而只有1个被消费,这是因为此时共有4个线程在操作Resource对象r,

而notify()唤醒的是线程池中第1个wait()的线程,所以生产者执行notify()时,唤醒的线程有可能是另1个生产者线程,这个生产者线程从wait()中醒来后不会再判断flag,而是直接向下运行打印出一个新的商品,这样就出现了连续生产2个商品。
为了避免这种情况,修改代码如下:

copy

class
 String name;

  • count=;
  • flag=;
  • set(String name){
  • (flag)
  • {wait();}(Exception e){}
  • .name=name++count++;
  • +.name);
  • flag=;
  • .notifyAll();
  • }
  • out(){
  • (!flag)
  • {wait();}(Exception e){}
  • System.out.println(Thread.currentThread().getName()++.name);
  • ;
  • .notifyAll();
  • public ProducerConsumerDemo{
  • main(String[] args){
  • Resource();
  • Producer pro= Producer(r);
  • Consumer(r);
  • Thread t1= Thread(pro);
  • Thread(con);
  • Thread t3= Thread(pro);
  • Thread(con);
  • t1.start();
  • }

jdk1.5中,提供了多线程的升级解决方案:将同步synchronized替换为显式的Lock操作,将Object类中的wait(),
notify(),notifyAll()替换成了Condition对象,该对象可以通过Lock锁对象获取;
一个Lock对象上可以绑定多个Condition对象,这样实现了本方线程只唤醒对方线程,而jdk1.5之前,一个同步只能有一个锁,不同的同步只能用锁来区分,且锁嵌套时容易死锁。

copy

class
 String name;

  • count=;
  • flag=;
  • Lock lock =  ReentrantLock();
  • Condition condition_pro=lock.newCondition();
  • Condition condition_con=lock.newCondition();
  • set(String name){
  • lock.lock();
  • {
  • (flag)
  • .name=name++count++;
  • +.name);
  • flag=;
  • }
  • {
  • lock.unlock();
  • out(){
  • lock.lock();
  • {
  • (!flag)
  • System.out.println(Thread.currentThread().getName()++.name);
  • ;
  • condition_pro.signqlAll();
  • {
  • }

线程通信的其他几个常用方法:

终止线程
jdk1.5起,stop()方法(非静态)已过时,不能再使用(否则会报错),终止线程的唯一方法是run()方法结束。
开启多线程运行时,运行代码通过是循环结构,只要控制住循环,就可以让run()方法结束。
中断线程
interrupt()方法,如果线程在调用Object类的
wait()、wait(long) 或wait(long,int) 方法,或者该类的
join()、join(long)、join(long,int)、sleep(long) 或sleep(long,int)
方法过程中受阻,则其中断状态将被清除,它还将收到一个 InterruptedException。  
线程的中断状态即冻结状态,interrupt()是将处于冻结状态的线程强制地恢复到运行状态。  
守护线程
setDaemon(), 将线程设置为守护线程,当正在运行的所有线程都是守护线程时,jvm自动退出。意思差不多是:前台线程(如main线程)结束后,后台线程(如t1,t2)也自动结束。
setDaemon()方法必须在启动线程前调用。下面是interrupt()和setDeamon()方法的一个示例。

copy

class Runnable{

  • flag=;
  • run(){
  • (flag){
  • {
  • wait();
  • (InterruptedException e){
  • );
  • flag=;
  • );
  • changeFlag(){
  • flag=;
  • public StopTreadDemo {
  • main(String[] args) {
  • StopThread();
  • Thread t1= Thread(st);
  • Thread(st);
  • t1.start();
  • num=;
  • (){
  • (num++==){
  • t1.interrupt();
  • ;
  • +num);
  • );
  • }

join()方法
当A线程执行到了B线程的join()方法时,A就放弃运行资格,处于冻结等待状态,等B线程执行完,A才恢复运行资格;如果B线程执行过程中挂掉,那需要用interrupt()方法来清理A线程的冻结状态;join()可以用来临时加入线程执行。
toString()方法
返回线程名称、优先级和线程组字符串。
默认情况下,哪个线程启动了线程t1,
t1就属于哪个线程组,也可创建新的ThreadGroup对象;所有方法,包括main(),线程优先级默认是5;Thread.MAX_PRORITY为10,Thread.MIN_PROTITY为1,NOR_PRORITY为5.
yield()方法
暂时释放执行资格,稍微减缓线程切换的频率,让多个线程得到运行资格的机会均等一些。