Java的多线程

来源:互联网 发布:网络出错1003什么意思 编辑:程序博客网 时间:2024/06/18 07:29

  今天我们来讲一讲多线程的问题。Java中:通过创建Thread或者其子类的对象代表一个线程

  1. 在讲多线程之前呢,我们先来讲讲三个重要的相似概念

  ①程序: 是一段静态的代码,为完成特定任务,用某种语言写的一组指令的集合。

  ②进程:动态的过程,有其产生、进行和消亡的过程。是指程序的一次执行过程,或者是一个正在运行的程序

  ③线程:是对进程进一步的细化,是一个程序内部的一条执行路径。

  当一个程序可以同时运行多个线程时,我们就称这个程序时多线程的。

  我们知道一个进程可以包含多个子线程,但是每个程序中必定只有一个隐含的主线程,即main()方法。

  多线程的使用:当需要同时执行任务时,当需要在后台运行程序时,当程序需要实现一些须等待的任务时。

  2.继承的方式实现多线程

  我们的思路是,通过创建继承Thread类的类对象,代表一个线程,然后调用每个对象的start()方法来做两件事情,一是启动这个线程类对象代表的线程,二是执行线程类对象当中重写的run()方法,注意:业务的所有主体内容都是写在这个run()方法之内的,所以我们将写在run()方法中的内容称为线程体。

  继承方式创建并启动一个子线程的步骤:

  ①创建一个类继承java.lang.Thread类

  ②在这个类中重写run()方法,所有的主体业务的代码写在这里边。

  ③在主线程中创建一个这个线程类的对象,然后调用对象的start()方法启动线程并执行重写的run()方法。

  我们需要知道,一个线程对象只能同时start()一次,否则会包线程非法状态异常。若想实现多个线程同时调用,就需要创建多个线程类的对象,然后调用各自的start()方法来启动这个对象代表的线程或执行run()方法。启动一个线程必须也只能用start()方法,而不是run()方法。

  3.实现的方式创建多线程

  我们知道java.lang.Thread也是继承了Runnable接口的,Runnable接口中只有一个简单的run()方法,而没有start()线程启动方法,所以实现接口的类的对象不能代表一个线程。我们可以通过Thread类的构造器传入这个对象再创建一个新的Thread类的对象,这个对象才是代表一个新的线程,可以使用start()方法启动线程和调用run()方法。

  实现方法创建并启动一个子线程:

  ①创建一个类实现Runnable接口。

  ② 重写run()方法,所有的主体代码都在这里边。

  ③使用Thread类的构造器包含进一个以上类的对象,创建一个Thread类的对象(这个对象就代表一个新的线程)

  ④调用这个线程对象的start()方法启动线程并且执行run()方法。

  对比实现和继承两种方式创建子线程,我们知道,实现的方式更好一些:一是因为避免了线程类的单继承性;二是因为实现的方式自动将类中的属性当成了共享属性(因为只创建了一个线程类的对象),而继承还需要在线程类中将属性设置为static才可以共享。

  4.多线程的优点和生命周期

  ① 多线程的优点

  提高用户体验,是图形化的界面更有意义(相对于黑屏界面)。

  提高了CPU的利用率,因为CPU得不断的执行,但是在CPU切换线程执行时会消耗很多时间。(这也是为什么同时复制多个文件比按次序一个一个复制文件用时久的原因

  改善程序的结构,将复杂又长的进程分成多个线程独立运行,便于理解和修改。

  ②线程分类

  分为两类,守护线程(JVM的垃圾回收机制)和用户线程(程序员创建的线程)。

  二者基本相同,唯一不同的是判断JVM何时离开:当程序中都是守护线程时,JVM停止退出。

  守护线程是来服务用户线程的,可以将一个用户线程变为守护线程:在调用线程对象的start()方法之前,调用线程对象的setDaemon()方法即刻。

  ③ 线程的生命周期及线程状态的转换

  Java中用Thread.state表示当前线程及线程对象所处的状态。这个state是一个enum枚举的变量。内部有六个枚举对new,runnable,blocked,waiting,timed_waiting,terminal。表示除运行时的四种不同的线程状态。

  new :表示这个线程刚刚创建,两种方式。

  runnable:就绪状态,可由start()方法将new状态启动到这个状态,表示线程可执行,等待CPU时间片的调用。

  运行状态:表示当前线程正在被CPU调用,main方法主线程正在执行其中的代码。

  blocked:阻塞状态,让出CPU资源并暂时终止其中的线程操作和功能。

  terminated:死亡状态,表示当前线程终止且死亡。

  线程状态的转换:

  new->runnable:start()。

  runnable->运行状态:获取CPU资源并执行。

  运行状态->死亡:正常执行完线程的run()方法,或执行run()方法时出现异常,或调用了线程对象的stop()方法(过时)

  运行状态->runnable:失去CPU的执行权,或调用线程对象的yield()方法。

  运行状态->blocked:调用线程对象的sleep()方法,wait()方法,等待同步锁,join()(别的线程强行获取CPU资源),suspend()(过时)

  blocked->runnable:sleep()时间到,获取同步锁,notify()或notifyAll()方法,resume()(过时)

  5. 线程的同步机制

  当我们使用多线程操作一份共享数据时,若一个线程的run方法中的代码还未执行完毕就挂起,另外一个线程此时进入操作共享数据的run方法,那么就会导致共享数据的安全性问题。怎么消除这个安全性问题呢?我们必须确保当一个线程在操作共享数据时,若这个线程不终止其他的线程就无法进入。Java当中使用线程的同步机制来解决这个共享数据安全性的问题。

  要实现Java的同步机制有两种方式:一是同步代码块,synchronized(obj){}。其中的obj作为一个监视器或者说同步锁可以是任意类型的对象,但是要求这个对象被所有的线程共有,那个线程获取了这把锁,就可以执行。一般实现的方式创建的线程对象可以用this代替这把锁,继承的方式创建的线程对象就不可以用this代替这把锁。

  第二种实现同步机制的方式是同步方法的方式。将操作共享数据的内容的方法用synchronized声明,可以确保党一个线程进入此方法时,其他线程不会进入。同步方法的同步锁默认是this,所以实现方式创建的多线程是可以用同步方法来避免共享数据安全性问题的,继承方式创建多线程的要避免共享数据的安全性问题,最好用同步代码块的方式,当然也可以用同步方法的方式,但是需要修改默认的同步锁this。若同步方法用了static修饰,则默认同步锁不再是this,因为static不可以this。此时默认同步锁对象为类名.class。(在懒汉式单例模式变成线程安全时用到了static不可用this)

  一个线程什么时候才会释放同步锁呢?

  当前线程顺利执行完毕,当前线程遇到异常未处理,调用了当前线程的wait()方法,当前线程的代码中有return,break等终止方法执行的关键字。(需要特别注意的是:调用线程的wait()方法这个线程会释放同步锁,并停止运行,等待notify()唤醒进入可执行状态;而调用线程的sleep()、yield()方法不会释放同步锁,只是暂时让出CPU停止一段时间,时间过了再执行

  6. 死锁

  同步机制当中可能会造成线程的死锁。

  死锁就是当两个线程都想操作被对方形成锁住的资源时,就形成了死锁。

  举个例子:线程对象t1和线程对象t2,共有类属性o1和o2,其中t1想先锁住o1再锁住o2,t2想先锁住o2再锁住o1,但是当t1锁住o1想去锁o2时,此时o2已经被t2锁住了,同时t2也想锁住被t1锁住的o1,t1和t2都锁住自己的对象不放,同时都在等待对方先释放已经锁住的对象,就形成了死锁。

  解决死锁的办法:一是尽量加大锁定的对象的粒度,即尽量锁大的对象。二是尽量避免同时锁两个或者两个以上的对象。

  7.线程的通信

  线程的通信涉及到三个方法:wait(),notify(),notifyAll()。这三个方法必须只能用在同步内容及synchronized中,否则会报错

  wait():停止当前线程,释放锁,等待被唤醒,唤醒之后进入队列等待CPU调度执行。

  notify():唤醒处于等待状态中的优先级最高的线程,进入runnable状态。

  notifyAll():唤醒所有处于等待状态的线程,进入runnable状态。

  需要注意的是,这三个方法都是定义在顶级类Object当中的。与wait()方法不同的是,sleep()方法定义在Thread类中,它只会让线程释放CPU资源,但是不会释放同步锁,等待睡眠时间过了,会接着执行,二wait()被唤醒了只会重新进入队列等待CPU调用。调用sleep()方法的方式是:Thread.currentThread.sleep()。


原创粉丝点击