----- http://www.itheima.comjava培训、android培训期待与您交流!-----
在我们玩电脑时,我们可以同时打开多个应用程序,并可以一边听歌,一边玩游戏,还可以浏览网站,打文件夹。在我们看来,电脑上的这么多应用程序是同时运行 的,其实,不然。CPU在某一时刻只能运行一个应用程序,这些程序看起来在同时运行,其实他们在做着飞速的转换,只是时间及其短,短的我们不能感觉到他们 的停止。而双核的电脑运行快,性能好。而这是怎么做到的呢?首先我们了解一些概念:
进程是正在执行的程序(QQ、stormplayer等)。每个进程执行都有一个执行顺序,该执行顺序是一个执行路径,也可以叫控制单元。
线程是进程中的一个独立的控制单元。线程在控制着进程的执行。
一个进程中至少有一个线程。 在我们启动JVM时,不止有一个线程,还有垃圾回收机制(GC)这个线程(在对内存中)。
1、创建线程
创建线程的方式有两种:继承Tread类、实现Runnable接口
A、继承Tread类
步骤:(1)定义一个类并继承Thread类
(2)复写Thread类中的run方法
(3)调用线程的start方法
class MyThread extends Thread { public void run() { for (int x=0;x<30 ; x++) { System.out.println("demo run------"+x); } } } class ThreadDemo { public static void main(String[] args) { //新建对象(线程) ThreadDemo td = new ThreadDemo(); //调用start方法,开启线程,并运行run方法, 运行结果:demo run---和hello world---交替打印 td.start(); //若调用run方法: //只是对象调用方法并没有运行实现线程间的交替 运行结果:demo run--- 打印完 hello world---再打印 //td.run(); for (int x =0;x<1000;x++){ System.out.println("hello world---"+x); } } }
这两个线程在切换着执行,谁先获得CPU的执行权谁先执行。 线程的一个特性:随机性
为什么要覆盖run方法?
run方法用于存储线程要运行的自定义代码。
start方法的作用:启动线程,调用run方法。
线程的运行状态:
线程被创建-------运行(start方法)--------冻结(放弃执行资格)(sleep和wait、notify方法)
在运行和冻结状态时,也可能存在阻塞(具有运行资格但没有执行权)和消亡状态(stop或run结束)
获取线程的对象以及名称:Thread.currentThread() 获取当前线程对象
getName():获取线程名称
B、实现Runnable接口:
步骤: (1)定义一个类并实现Runnable接口
(2)覆盖Runnable接口中的run方法
(3)通过Thread类建立线程对象
(4)将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数
(5)调用Thread类的start方法开启线程,并调用Runnable接口的run方法
实现方式和继承方式的区别:
实现方式的好处:避免了单继承的局限性
继承Thread类:线程代码存放在Thread 子类的run方法中
实现Runnable接口:线程代码存放在接口的子类的run方法中
例子:卖票程序(多线程) 多个窗口卖票
class Ticket implements Runnable { private int tick = 50; public void run() { while(true) if (tick>0) { System.out.println(Thread.currentThread().getName()+"---sale:---"+tick--); } } } class TicketDemo { public static void main(String[] args) { Ticket t = new Ticket();//创建对象 Thread t1 = new Thread(t);//创建一个线程(对象) Thread t2 = new Thread(t);//t:run方法所属对象 Thread t3 = new Thread(t); Thread t4 = new Thread(t); t1.start(); t2.start(); t3.start(); t4.start(); /* Ticket t1 = new Ticket("第一个窗口");并没有创建线程 Ticket t2 = new Ticket("第二个窗口"); Ticket t3 = new Ticket("第三个窗口"); Ticket t4 = new Ticket("第四个窗口"); t1.start(); 线程已经开启,再开启无意义 t2.start(); t3.start(); t4.start(); */ } }
2、多线程的安全问题
在上面的售票的例子中,我们知道,程序一共创建了4个线程:Thread-0、Thread-1、Thread-2、Thread-3。线程具有随机的特点,假设现在票数为1,而线程0抢到了CPU的执行权,此时,判断票数不为0,但线程0还未判断完,执行权就让线程1抢去了,这时线程0就处于等待状态不再向下执行。而线程1还未判断完线程2就抢到了执行权,依次类推,线程0、1、2、3都处于等待状态。当然,这时线程0又抢到了执行权,继续向下执行,tick = 1;同样线程1抢到了执行权,判断后继续执行,tick = 0;根据实际情况,票数不可能为0,所以这时,程序就出现了问题,票已经卖完了,但是线程还有可能继续,票数出现了负数。
由于票数是共享数据,是这四个卖票窗口同时售。
所以,这时,我们就要考虑线程的安全问题。
出现安全问题的原因:
多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还未执行完,另一个线程就参与进来执行,导致共享数据错误。
如何解决呢?
因为线程在对语句执行时没有执行完就被其他线程抢去了执行权,所以,我们可以控制一个线程在确保将多条语句执行完后再让其他线程获得执行权。
在java中,解决多线程的安全问题则使用关键字synchronized
同步代码块:
synchronized(对象){需要被同步的代码}
3、线程同步
所谓线程同步,我所理解的就是,一个线程在执行完后才将执行权释放出去,留给其他的线程执行。
在卖票例子中,如果给判断语句加上同步(并不是run方法中代码都加同步,下面会讲到加同步的前提),那么,当其中一个线程获得执行权后,进行判断,在判断前,会先将锁(这里的锁是对象)的状态改为锁上(这里的状态一般用0 和 1标识),那么这个程序的其他线程就进不来,直到该线程执行完,才将执行权释放(在释放之前,会把锁的状态改变)。
(1)同步的前提:
必须是两个或两个以上的线程
必须保证多个线程使用同一个锁
且同步中只能有一个线程在运行
(2) 同步的好处:解决了多线程的安全问题
同步的弊端:多个线程需要判断锁,较为耗费资源。(防盗门例子)
(3)同步函数
同步的方式有两种:同步代码块和同步函数(让函数具有同步性)。
同步代码块是将要进行同步的代码(当数据被共享时)封装在synchronized关键字中。
而在封装功能时,也可能会有共享数据,当然也可以用同步代码块,但,同步代码块的代码有些繁琐,还要缩进,所以,只要在函数上用synchronized关键字就可以了(要保证满足同步的两个前提),比较简洁,又能保证多线程的安全性。
如何定义同步函数?要明确多线程运行代码、明确共享数据(一般是成员)、明确多线程运行代码中那些代码操作共享数据。
同步函数的锁:this
(4)静态同步函数的锁
静态同步方法使用的锁是: 该方法所在类的字节码文件对象。类名.class
静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象。静态在类加载的时候就已经进内存,即优先于主函数存在了。
(5)单例设计模式(参见博客 黑马程序员---单例设计模式)
(6)死锁
死锁就是同步中嵌套同步。比如,我们在同一个类中定义一个同步函数(将需要同步的代码封装成函数)和一个同步代码块,同步函数使用的锁是this,同步代码块使用的锁是任意对象(这里不考虑其他情况),如果在同步代码块中调用同步函数,那么,这时同步代码块中就嵌套了同步函数,且他们使用的锁不是同一个,这时就会发生死锁。
4、线程间通信
在上面的卖票小程序中,多个线程只是对同一个动作进行操作,即多个线程同时进行卖票的动作。而线程间通信,是指多个线程操作同一个资源,但操作动作不同。
模拟一个生产车间,当生产一个物品,假如就消费一个,那么,这时,生产者和消费者就在同时操作这个物品,但一个是生产一个是消费。他们(多个线程)操作的是同一个资源,但操作的动作不同,这就是线程间的通信。在多个线程操作同一个资源时,就会出现安全问题。如,生产一个物品而被消费两次。
等待唤醒机制:
wait();当线程处于等待状态,即该线程放弃了执行资格
notify();唤醒等待的线程。
当线程处于等待状态时,该线程在线程池中,通常唤醒的是线程池中第一个等待的线程。
JDK1.5新特性:
Lock替换了synchronized
Condition封装了await()、signal(),替换了wait()、notify()、notifyAll()
一个lock中可以有多个condition对象,实现了本方只唤醒本方的操作。
5、线程的其他操作
(1)停止线程
要想停止线程,只要run方法结束就可以。
方式:
a.控制住循环
b.interrupt(): 唤醒已处于冻结状态的线程并使用改变标记的方法停止线程。
(2)守护线程(后台线程、用户线程)
setDaemon(boolean);在启动线程前调用。如果设置为true,那么该线程结束,程序也就结束。
(3)join
将线程加入到正在运行的程序中,其他线程(主线程)处于冻结状态,只有该线程将执行权释放,其他线程(主线程)才能获得执行权。
(4)优先级
线程执行的顺序。优先级高则获得执行权的概率就会大。
setPriority(); 设置优先级
MAX_PRIORITY 10 最大优先级
MIN_PRIORITY 1 最小优先级
NORM_PRIORITY 5 中间级
如果只设置为数字,阅读性很差,所以,一般用常量表示。
(5)yield
Thread.yield(); 表示临时释放执行权。
6、实际开发中的线程
当多个程序中需要被同时执行时,这时就用到了多线程的编写(在实际开发中很常用)。通常使用匿名内部类。
public static void main(String[] args) { //直接编写 for (int x = 0;x<30 ;x++ ) { System.out.println(Thread.currentThread().getName()+x+"----for----"); } //通过继承Thread类,复写其run方法,调用start方法, new Thread(){ public void run() { for (int x = 0;x<30 ;x++ ) { System.out.println(Thread.currentThread().getName()+"---thread----"); } } }.start(); //实现Runnable接口,复写run方法,建立Thread类对象,并将子类对象作为参数传递给Thread类的构造函数 //调用thread类的start方法启动线程 Runnable r = new Runnable() { public void run() { for (int x = 0;x<30 ;x++ ) { System.out.println(Thread.currentThread().getName()+"---runnable----"); } } }; new Thread(r).start(); }