java多线程与线程间通信


  本文学习并总结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上。
线程状态及状态间切换

已start()过的线程不能再次start(), 否则会报异常java.lang.IllegalThreadStateException。
线程的名称
线程对象都有自己默认的名称:Thread-编号,编号从0开始。
设置自定义线程名称,可以在子类构造函数中调用super(name), 也可以直接创建对象后调用setName()方法。
Thread.currentThread(), 返回当前运行的线程对象,也就是this引用指向的对象。

多线程安全问题

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

synchronized(对象) //括号中对象需手动指定,可以直接在Object对象
{
    需要被同步的代码
}
同步函数:将synchronized作为修饰符放在函数定义上,函数返回值类型前面。
同步的原理
     同步代码块对象如同锁,持有锁的线程可以在同步语句中执行;没有持有锁的线程即使获得了CPU执行权,也进不去,无法执行同步代码。
     同步函数使用的锁是this对象; 静态同步函数使用的锁是该函数所在类对应的类字节码文件对象,即类名.class,该对象的类型是Class。
     synchronized修饰符不属于方法签名的一部分,当子类覆盖父类方法时,synchronized修饰符不会被继承,因此接口中方法不能被声明为synchronized,同样,构造函数也不能被声明为synchronized。
     线程进入同步代码块或同步函数前先判断锁标志位,若判断结果为真,则进入同步代码块或同步函数后,修改锁标志位为假,线程退出后,再恢复锁标志位为真。
/*
简单的售票程序,多个窗口同时卖票
*/
class Ticket implements Runnable{
    private int tick=100;
    Object obj=new Object();
    public void run(){
        while(true){
            synchronized(obj){ /*将操作共享成员数据tick的语句放到同步代码块中,用Object类对象锁住,这样不会出现卖0号票或负数票的情况*/
                if(tick>0){
                    try{Thread.sleep(10);}catch(Exception e){e.printStackTrace();}
                    System.out.println(Thread.currentThread().getName()+"......sale : "+tick--);
                }
            }
        }
    }
}
public class TicketDemo{
    public static void main(String[] args){
        Ticket t=new Ticket();
        Thread t1=new Thread(t);
        Thread t2=new Thread(t);
        Thread t3=new Thread(t);
        Thread t4=new Thread(t);
        t1.start();
        t2.start();
        t3.start(); 
        t4.start();        
    }
}

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

class Zhangsan{        // 定义张三类
        public void say(){
                System.out.println("张三对李四说:“你给我画,我就把书给你。”") ;
        }
        public void get(){
                System.out.println("张三得到画了。") ;
        }
};
class Lisi{        // 定义李四类
        public void say(){
                System.out.println("李四对张三说:“你给我书,我就把画给你”") ;
        }
        public void get(){
                System.out.println("李四得到书了。") ;
        }
};
public class ThreadDeadLock implements Runnable{
        private static Zhangsan zs = new Zhangsan() ;                // 实例化static型对象
        private static Lisi ls = new Lisi() ;                // 实例化static型对象
        private boolean flag = false ;        // 声明标志位,判断那个先说话
        public void run(){        // 覆写run()方法
                if(flag){
                        synchronized(zs){        // 同步张三
                                zs.say() ;
                                try{
                                        Thread.sleep(500) ;
                                }catch(InterruptedException e){
                                        e.printStackTrace() ;
                                }
                                synchronized(ls){
                                        zs.get() ;
                                }
                        }
                }else{
                        synchronized(ls){
                                ls.say() ;
                                try{
                                        Thread.sleep(500) ;
                                }catch(InterruptedException e){
                                        e.printStackTrace() ;
                                }
                                synchronized(zs){
                                        ls.get() ;
                                }
                        }
                }
        }
        public static void main(String args[]){
                ThreadDeadLock t1 = new ThreadDeadLock() ;                // 控制张三
                ThreadDeadLock t2 = new ThreadDeadLock() ;                // 控制李四
                t1.flag = true ;
                t2.flag = false ;
                Thread thA = new Thread(t1) ;
                Thread thB = new Thread(t2) ;
                thA.start() ;
                thB.start() ;
        }
};
运行结果:
张三对李四说:“你给我画,我就把书给你。”
李四对张三说:“你给我书,我就把画给你”  
//双方僵持在这,谁都没法继续运行

线程间通讯

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():线程会释放执行权,但是并不释放锁。

单个消费者生产者例子:

class Resource{  //生产者和消费者都要操作的资源
    private String name;
    private int count=1;
    private boolean flag=false;
    public synchronized void set(String name){
        if(flag)
            try{wait();}catch(Exception e){}
        this.name=name+"---"+count++;
        System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
        flag=true;
        this.notify();
    }
    public synchronized void out(){
        if(!flag)
            try{wait();}catch(Exception e){}
        System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
        flag=false;
        this.notify();
    }
}
class Producer implements Runnable{
    private Resource res;
    Producer(Resource res){
        this.res=res;
    }
    public void run(){
        while(true){
            res.set("商品");
        }
    }
}
class Consumer implements Runnable{
    private Resource res;
    Consumer(Resource res){
        this.res=res;
    }
    public void run(){
        while(true){
            res.out();
        }
    }
}
public class ProducerConsumerDemo{
    public static void main(String[] args){
        Resource r=new Resource();
        Producer pro=new Producer(r);
        Consumer con=new Consumer(r);
        Thread t1=new Thread(pro);
        Thread t2=new Thread(con);
        t1.start();
        t2.start();
    }
}//运行结果正常,生产者生产一个商品,紧接着消费者消费一个商品。

      但是如果有多个生产者和多个消费者,上面的代码是有问题,比如2个生产者,2个消费者,运行结果就可能出现生产的1个商品生产了一次而被消费了2次,或者连续生产2个商品而只有1个被消费,这是因为此时共有4个线程在操作Resource对象r,  而notify()唤醒的是线程池中第1个wait()的线程,所以生产者执行notify()时,唤醒的线程有可能是另1个生产者线程,这个生产者线程从wait()中醒来后不会再判断flag,而是直接向下运行打印出一个新的商品,这样就出现了连续生产2个商品。
为了避免这种情况,修改代码如下:

class Resource{
    private String name;
    private int count=1;
    private boolean flag=false;
    public synchronized void set(String name){
        while(flag) /*原先是if,现在改成while,这样生产者线程从冻结状态醒来时,还会再判断flag.*/
            try{wait();}catch(Exception e){}
        this.name=name+"---"+count++;
        System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
        flag=true;
        this.notifyAll();/*原先是notity(), 现在改成notifyAll(),这样生产者线程生产完一个商品后可以将等待中的消费者线程唤醒,否则只将上面改成while后,可能出现所有生产者和消费者都在wait()的情况。*/
    }
    public synchronized void out(){
        while(!flag) /*原先是if,现在改成while,这样消费者线程从冻结状态醒来时,还会再判断flag.*/
            try{wait();}catch(Exception e){}
        System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
        flag=false;
        this.notifyAll(); /*原先是notity(), 现在改成notifyAll(),这样消费者线程消费完一个商品后可以将等待中的生产者线程唤醒,否则只将上面改成while后,可能出现所有生产者和消费者都在wait()的情况。*/
    }
}
public class ProducerConsumerDemo{
    public static void main(String[] args){
        Resource r=new Resource();
        Producer pro=new Producer(r);
        Consumer con=new Consumer(r);
        Thread t1=new Thread(pro);
        Thread t2=new Thread(con);
        Thread t3=new Thread(pro);
        Thread t4=new Thread(con);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

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

class Resource{
    private String name;
    private int count=1;
    private boolean flag=false;
    private Lock lock = new ReentrantLock();/*Lock是一个接口,ReentrantLock是该接口的一个直接子类。*/
    private Condition condition_pro=lock.newCondition(); /*创建代表生产者方面的Condition对象*/
    private Condition condition_con=lock.newCondition(); /*使用同一个锁,创建代表消费者方面的Condition对象*/
    
    public void set(String name){
        lock.lock();//锁住此语句与lock.unlock()之间的代码
        try{
            while(flag)
                condition_pro.await(); //生产者线程在conndition_pro对象上等待
            this.name=name+"---"+count++;
            System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
            flag=true;
             condition_con.signalAll();/*signalAll()是唤醒线程池中的所有线程,而指明调用对象是condition_con后是唤醒所有在condition_conn这个对象上等待的所有线程*/
        }
        finally{
            lock.unlock(); //unlock()要放在finally块中。
        }
    }
    public void out(){
        lock.lock(); //锁住此语句与lock.unlock()之间的代码
        try{
            while(!flag)
                condition_con.await(); //消费者线程在conndition_con对象上等待
        System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
        flag=false;
        condition_pro.signqlAll(); /*唤醒所有在condition_pro对象下等待的线程,也就是唤醒所有生产者线程*/
        }
        finally{
            lock.unlock();
        }
    }
}

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

终止线程
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()方法的一个示例。

class StopThread implements Runnable{
	private boolean flag=true;
	public synchronized void run(){
		while(flag){
			try{
				wait(); /*t1或t2线程处于wait()冻结状态时,即使主线程中修改了flag的值,t1和t2都 不能再判断上面while循环的终止条件,会导致2个线程一直在wait()中动不了,所以需要将t1和t2人为的唤醒*/
			}
			catch(InterruptedException e){
				System.out.println(Thread.currentThread().getName()+"...InterruptedException");
				flag=false;//一时接收到了InterruptedException异常,说明线程已被恢复到运行状态,这时再手动设置flag为false,让线程再次判断while循环时不再再次等待*/
			}
			System.out.println(Thread.currentThread().getName()+"...run");
		}
	}
	public void changeFlag(){
		flag=false;
	}
}
public class StopTreadDemo {
	public static void main(String[] args) {
		StopThread st=new StopThread();
		Thread t1=new Thread(st);
		Thread t2=new Thread(st);
		//t1.setDaemon(true);这2句语句执行后,t1,t2不再调用interrupt(),也能让整个程序结束,因为该程序就3个线程,main线程结束后守护线程也会随之终止。
		//t2.setDaemon(true);
		t1.start();
		t2.start();
		int num=0;
		while(true){
			if(num++==60){
				//st.changFlag();
				t1.interrupt(); //将线程t1的冻结状态 清除,让其处于运行
				t2.interrupt(); //将线程t1的冻结状态 清除,让其处于运行
				break;
			}
			System.out.println(Thread.currentThread().getName()+"..."+num);
		}
		System.out.println("over");
	}

}

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()方法
暂时释放执行资格,稍微减缓线程切换的频率,让多个线程得到运行资格的机会均等一些。

  

 


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值