线程的概述
基本上所有的操作系统都支持同时运行多个应用程序,我们把运行中的每个应用程序称之为进程,同时一个应用程序在运行的过程中又会产生多个子任务程序流,每个子任务程序流就是一个线程,Java作为高级的面向对象语言理所当然支持多线程的开发。
线程的生命周期
Java线程具有五种基本状态,如下图所示:
新建状态(new):一个线程类对象被创建即进入新建状态。
就绪状态(Runnable):线程类对象执行start方法即进入就绪状态。
运行状态(Running):就绪状态的线程被cpu调度执行线程执行体时即进入运行状态。
死亡状态(Dead):线程执行体执行完成或者执行过程中抛出异常时即进入死亡状态。
阻塞状态(Blocked):线程在运行过程中由于各种原因,失去cpu执行权限时即进入阻塞状态或最终进入就绪状态。
阻塞状态由于其造成阻塞原因的不同分为三种类型:
1、同步阻塞:线程在执行过程中由于未获取到同步锁,导致线程阻塞,直到获得同步锁,线程进入就绪状态。
2、等待阻塞:线程在运行状态执行了wait方法导致线程阻塞,直到执行notify或者notifyAll方法时,程序进入就绪状态。
3、其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
线程的创建方式
方式一、继承Thread类,重写run方法。
package com.icypt.thread; public class Test { public static void main(String[] args) { //实例化线程对象 TestThread testThread1 = new TestThread(); TestThread testThread2 = new TestThread(); //使线程进入就绪状态,等待cpu调度 testThread1.start(); testThread2.start(); } } //创建线程类 class TestThread extends Thread { @Override public void run() { //线程执行体 System.out.println("*******执行TestThread方法*******"); for(int i=0; i < 10; i++) { System.out.println("执行线程名字:" + getName() + ",执行结果--" + i); } } }
继承Thread类创建线程类的方式,是比较常用的方式,这种方式唯一的不方便之处,在于无法共享线程类的普通成员变量,也就说线程类的普通成员变量只供当前线程实例操作。
方式二、实现Runnable接口,重写run方法。
实现Runnable接口创建线程类的方式,也是比较常用的方式,相对于继承Thread类的方式,它的优势在于可以方便的共享Runnable接口实现类的普通成员变量。原因是在创建线程类实例对象时,Runnable接口实现类对象只需要创建一次,并把它作为创建线程类实例的目标对象使用。
方式三、实现Callable接口,重写call方法。
package com.icypt.callable; import java.util.concurrent.Callable;import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class Test { public static void main(String[] args) { //创建callable对象 Callable<String> tc = new TestCallable(); //通过callable对象创建线程执行所需target对象 FutureTask<String> ft = new FutureTask<String>(tc); //创建线程对象 Thread thread = new Thread(ft); try { //启动线程 thread.start(); //获得线程执行体返回值 System.out.println(ft.get()); } catch (InterruptedException e) { e.printStackTrace(); }catch (ExecutionException e) { e.printStackTrace(); } } } class TestCallable implements Callable<String> { @Override public String call() throws Exception { System.out.println("*******执行TestThread的run()*******"); String str = ""; for(int i=0; i < 10; i++) { str += "" + i; System.out.println("执行线程名字:" + Thread.currentThread().getName() + ",执行结果--" + i); } return str; } }
实现Callable接口创建线程类,与实现Runnable接口方式类似,不同之处在于:
1、线程执行体位于重写Callable接口的call方法体之中。
2、call方法带有返回值,也就是说通过FutureTask对象包装Callable对象之后,作为创建线程实例的目标对象,线程执行体正常执行完成之后可以通过FutureTask对象获取返回值。
线程的运用
正常情况下,线程体正常执行完成或者在执行的过程中抛出异常则线程进入死亡状态,但是有时我们也需要去手动停止线程的执行,以上栗子在线程类中定义了一个成员变量,来表示是否停止线程的执行,只是为了演示了一个小技巧,其栗子本身并无实际意义。
例子,使用join使线程进入阻塞状态
执行以上栗子,会发现通过join()加入ThreadB 线程运行后,ThreadA 线程将被阻塞,直到ThreadB 线程执行完成后,ThreadA 线程才会继续执行。
线程优先级使用
线程的优先级不是一种确定的优先级,它只是让优先级高的线程有更大的几率执行,使用setPriority来设置线程的优先级,Priority总共有三个值:
后台线程使用
后台线程的主要作用是为其他线程提供服务,也叫"守护线程",之前我们学习过的垃圾回收器,就运用了后台线程的原理。可以通过setDaemon(true)将一前台线程设置为后台线程,如果程序中所有的前台线程都已进入死亡状态则,后台线程自动死亡。
同步锁使用,存钱、取钱实例运用
多线程存钱、取钱实例的核心思想就是要保证经过多次存取之后,存钱数额等于取钱数额加余额,而要保证此等式的成立就需要同步锁来控制在同一时间点只有一个线程操作存钱或者取钱的方法。以上栗子我们在存钱和取钱方法上使用synchronized加了同步锁,当多个取钱线程或存钱线程实例在执行存取方法时首先需要获取同步锁,获取锁的线程先执行,未获取锁的线程被阻塞,等获取锁的线程执行完成后,存钱或取钱方法释放同步锁,同时其他存取钱线程竞锁,依次类推,直到所有存取钱线程的线程体执行结束,存取过程结束。关于同步锁的使用还有另外两种方式:
以上两种同步锁创建方式,各位学友可以将上栗稍作改变,进行测试,其效果是相同的。
生产者、消费者实例与取钱、存钱的实例不同的地方在于,我们需要实现一个零库存的消费模式,也就是生产一件消费一件,库存始终保持为0。要实现这样的操作,不但需要同步锁的支持,还需要使用wait和notify方法来等待和唤醒生产与消费的线程。上栗的工作原理是生产线程在生产之前先判断库存是否大于0,如果大于0则让线程等待进入等待状态同时立即释放同步锁,直到其他获得同步锁的线程调用了notify方法唤醒等待的线程后,则其才能获得同步锁继续向下执行,消费线程也是同理,只是等待的条件变为库存量小于等于0。
java培训班:http://www.baizhiedu.com/java2019