零基础学习之Java多线程
概述
要深入学习多线程,首先我们需要找到什么是线程,根据百度百科的定义:线程(thread)是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。。我们了解到线程是进程的实际运行单位,那问题来了什么是进程呢?根据百度百科定义:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,进程是程序的实体。这个就比较好理解了,进程不就是我们运行的程序吗?那这样来看,线程就是某个程序的一次实际的运行(一个程序可以打开多次,一个进程可以有多个线程组成)。
那这里的Java多线程指的就是Java开发中,程序启动多个线程实现并发执行,提高CPU的利用率。
并发:指两个或多个事件可以在同一个时间段内发生。也就是说在同一个时刻只能执行一条指令,但多个指令可以被快速轮换执行,使得在宏观上具有多个进程同时执行的效果
线程的创建
创建线程是使用多线程的第一步。首先先介绍了Java创建线程的方式,创建了线程才会有多个线程的情况。Java创建多线程有四种方式:
- 继承Thread类
- 实现Runnable接口
- 实现实现Callable(本文未涉及)
- 使用线程池方式(本文未涉及)
本篇文章主要介绍通过继承Thread类和实现Runnable接口这两种方式来创建线程。 具体如下:
继承Thread类
在Java中,使用Thread类来表示线程,所有的线程都是Thread类的对象或者其子类的实例。(Thread类Java.lang包下的类)
通过继承方式实现的线程,会有Java单继承的缺点
创建线程的步骤
- 定义一个类,让其继承Thread类
- 在该类中重写Thread类的run()方法、
- 创建该类的实例,即创建了线程对象
其中:run()方法的方法体就是线程的执行逻辑,也称为线程执行体。
线程的使用步骤
- 创建线程
- 通过线程对象调用start()方法。
注:线程不用手动结束,执行完线程其自动结束
代码示例
/*
继承Thread创建线程
*/publicclassDemo1{publicstaticvoidmain(String[] args){//创建自定义线程对象
MyThread myThread =newMyThread();//调用start方法,开启线程
myThread.start();}}//继承Thread方法,自定义线程classMyThreadextendsThread{/*
* 重写run方法,完成该线程执行的逻辑
*/@Overridepublicvoidrun(){
System.out.println("继承Thread方式创建线程");}}
实现Runnable接口
上面说到通过继承Thread类的方式实现线程有Java单继承的缺点,针对单继承这样一个问题,Java类是通过实现接口来解决的。那么创建线程是不是也可以通过接口呢?答案是肯定的。在Java中,可以通过实现Runnable接口的方式来创建线程。
注:在Thread类的源码中可以看出,其也是实现了Runnable接口
创建线程的步骤
- 定义一个类,让其实现Runnable接口
- 在该类中重写Runnable接口中的run()方法
- 创建该类的对象(线程的任务对象)
- 通过Thread类的有参构造方法创建线程对象(真正的线程对象)
线程的使用步骤
- 创建线程
- 通过线程对象调用start()方法。
代码示例
/*
实现Runnable类创建线程
*/publicclassDemo2{publicstaticvoidmain(String[] args){//创建自定义类对象 线程任务对象
MyThread1 runnable =newMyThread1();//创建线程对象
Thread thread =newThread(runnable,"xiaocheng");//调用start方法,开启线程
thread.start();}}//实现Runable类,创建线程classMyThread1implementsRunnable{/*
* 重写run方法,完成该线程执行的逻辑
*/@Overridepublicvoidrun(){
System.out.println("实现Runable类,创建线程");}}
使用匿名内部类创建线程
既然创建多线程是通过继承Thread类,然后创建类的实例的方式实现的。那如果是本身这个线程只用一次,在其他地方没有调用的情况,是不是可以通过匿名内部类来创建线程呢? Why not?
下面用一个例子来说明使用匿名内部类如何来创建线程:
代码示例
/*
匿名内部类创建线程
*/publicclassDemo3{publicstaticvoidmain(String[] args){//匿名内部类,一个参数newThread(newRunnable(){//依然需要重写run方法,实现线程的执行逻辑@Overridepublicvoidrun(){
System.out.println("匿名内部类创建线程1");}}){}.start();//匿名内部类,两个参数newThread(newRunnable(){//依然需要重写run方法,实现线程的执行逻辑@Overridepublicvoidrun(){//调用currentThread().getName()方法,获取当前线程的名字
System.out.println(Thread.currentThread().getName());}},"匿名内部类创建线程2 ").start();}}
匿名内部类不熟的话,可以点击上面的链接看下我写过的博客
Thread类常用的方法
从上面的例子我们可以看出来,线程对象实际都是Thread实例。那这样来说,如果我们使用线程的话,除了我们自定以类中的方法外,Thread类中的方法也是必须要知道的。那下面就介绍下Thread类中常用的方法:
- public void run():实现多线程的执行逻辑(必不可少的方法)
- public void start() :线程开始执行的方法 (线程启动方法)
- public static void sleep(long millis) :暂停当前线程的执行(参数为毫秒数)
- public String getName() :获取当前线程名字
- public static Thread currentThread() :返回对当前正在执行的线程对象的引用(可以和其他的一起用)
- public final boolean isAlive():判断线程的状态,是否为活动状态(非终止)
- public final int getPriority() :获取线程的优先级
- public final void setPriority(int newPriority) :改变线程的优先级
- public final void stop():使线程停止执行
- public final void setDaemon(boolean on):指定线程设置为守护线程。(必须在线程启动之前设置,否则会报IllegalThreadStateException异常)
- public final boolean isDaemon():判断当前线程是否为守护线程
线程生命周期
前面说到创建线程和启动线程,还有常用方法里说到的线程的停止,这些都是线程在整个执行过程中可能出现的状态。总的来说,线程在其生命周期内(执行过程中)一共有五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。
线程生命周期中的状态转换图如下:
其中,各个状态说明如下:
新建(New)
通过上述方法创建线程,不调用任何方法即为新建一个线程,此时该线程只有JVM为其分配的内存,并初始化了实例变量的值,没有执行线程任何操作。
就绪(Runnable)
线程的就绪状态是在其调用start()方法之后,此时线程启动,即变成了就绪状态,JVM此时会为其创建方法调用栈和程序计数器。(注意这个时候只是启动线程,没有线程的执行行为)。
注意:程序只能对新建状态的线程调用start(),并且只能调用一次,如果对非新建状态的线程,如已启动的线程或已死亡的线程调用start()都会报错IllegalThreadStateException异常。
运行(Running)
处于就绪状态的线程获得CPU资源,那该线程就会调用run()方法,开启线程的执行逻辑,此时线程处于运行状态。(所谓多线程,指的是系统中有多个CPU资源,多个线程获得CPU资源后处于运行状态)。
阻塞(Blocked)
系统中的线程由于实际情况并不总是能够顺利执行完毕,但如果稍微有情况不能执行就杀死线程会造成很大的浪费,这时候就会出现线程阻塞的概念,阻塞的线程还可以通过一定的条件转化为就绪状态,继续等待下次的运行。
线程遇到如下情况,会进入阻塞状态:
- 线程调用了sleep()方法
- 线程试图获取一个同步监视器,但该同步监视器正被其他线程持有;
- 线程执行过程中,同步监视器调用了wait(),让它等待某个通知(notify / notifyAll);
- 线程执行过程中,同步监视器调用了wait(time)
- 线程执行过程中,遇到了其他线程对象的加塞(join);
- 线程被调用suspend方法被挂起(已过时,因为容易发生死锁);
阻塞状态的线程,可以通过下面的形式重新转换到就绪状态,等待下次的运行:
- 线程的sleep()时间到;
- 线程成功获得了同步监视器;
- 线程等到了通知(notify / notifyAll);
- 线程wait的时间到了
- 加塞的线程结束了;
- 被挂起的线程又被调用了resume恢复方法(已过时,因为容易发生死锁);
死亡(Dead)
线程总归要结束的,不管是被动的还是主动的(执行完了),
- run()方法执行完成;
- 线程执行过程中抛出了一个未捕获的异常(Exception)或错误(Error)
- 直接调用该线程的stop()来结束该线程(已过时,因为容易发生死锁)
线程安全
这里说的线程安全指的是多线程才会有的安全性问题。这是因为当系统同时运行多个线程的时候,会出现多个线程同时访问同一个资源的情况,如果仅仅是涉及到读操作的话,不会出现问题。但是涉及到写(即修改)就会出安全问题。
举个例子解释一下安全问题:
比如在我们在线上购票的过程中,是很多人可以同时购买的(多线程),我们首先要看看有没有票(涉及到读操作),然后如果有票的话,就可以买票了(涉及到写操作)。理论上,每次有人购票的时候,系统就会让票的数量减少一个,但是如果多人同时购票的话(多个线程访问同一个资源),会出现多人同时购票,而系统只让票的数量减一,这些人就会是重票。
那出现上述的肯定是不行的,下面介绍三种方法来解决线程安全问题
- 同步代码块
- 同步方法
- 锁机制
下面就对这三种方法进行分别介绍
同步代码块
Java中同步代码块是通过添加 synchronized 关键字实现的,将synchronized 关键字用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问(同时只能由一个线程访问该该区块中的资源)。
语法格式
synchronized(同步锁){//需要互斥的代码}
这里的同步锁只是一个概念,指的是为你想实现互斥访问资源的对象上标记了一个锁。
想对谁互斥访问,同步锁里面就写谁,锁对象可以是任意类型
代码示例
/*
解决线程安全:同步代码块
*/publicclassDemo7{publicstaticvoidmain(String[] args){
Tacket1 tacket1 =newTacket1();//通过匿名内部类创建线程newThread(newRunnable(){//重写run方法实现线程执行逻辑@Overridepublicvoidrun(){//售票员1卖票(线程1)for(int i =0; i <40; i++){
tacket1.sellTacket();}}},"售票员1").start();newThread(newRunnable(){//重写run方法实现线程执行逻辑@Overridepublicvoidrun(){for(int i =0; i <40; i++){//售票员2卖票(线程2)
tacket1.sellTacket();}}},"售票员2").start();}}classTacket1{privateint number =30;//票有30张publicvoidsellTacket(){//票数大于0才能卖synchronized(this){//this作为锁,是因为对于这几个线程,Ticket的this是同一个if(number >0){try{
Thread.sleep(10);//等10毫秒,效果好点}catch(InterruptedException e){
e.printStackTrace();}//票数-1
System.out.println(Thread.currentThread().getName()+"卖票,剩余票:"+number--);}}}}
同步方法
在Java中,同步方法使用的也是synchronized 关键字,不同于同步代码块的是,同步方法直接将synchronized 关键字修饰方法。这使得当线程执行该方法时候,其他线程就不能执行该方法,也实现了互斥访问资源(方法里有访问资源的操作)。
语法格式
publicsynchronizedvoidmethodname(){//可能会产生线程安全问题的代码}
同步方法的锁对象:
- 静态方法:当前类的Class对象
- 非静态方法:this
代码示例
/*
解决线程安全:同步方法
*/publicclassDemo8{publicstaticvoidmain(String[] args){
Tacket2 tacket2 =newTacket2();//通过匿名内部类创建线程newThread(newRunnable(){//重写run方法实现线程执行逻辑@Overridepublicvoidrun(){for(int i =0; i <40; i++){//售票员1卖票(线程2)
tacket2.sellTacket();}}},"售票员1").start();newThread(newRunnable(){@Overridepublicvoidrun(){for(int i =0; i <40; i++){//售票员2卖票(线程2)
tacket2.sellTacket();}}},"售票员2").start();}}classTacket2{privateint number =30;//票有30张publicsynchronizedvoidsellTacket(){//同步方法if(number >0){//票数大于0才能卖try{
Thread.sleep(10);//等10毫秒,效果好点}catch(InterruptedException e){
e.printStackTrace();}//票数-1
System.out.println(Thread.currentThread().getName()+"卖票,剩余票:"+number--);}}}
锁机制
任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,这种锁定保证了线程安全。
释放锁的操作
可以锁定,但又不能一直锁定,所以什么时候释放锁也是一个关键点。下面介绍释放锁的操作:
- 当前线程的同步方法、同步代码块执行结束。
- 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致当前线程异常结束。
- 当前线程在同步代码块、同步方法中执行了锁对象的wait()方法,当前线程被挂起,并释放锁。
不会释放锁的操作
有的方法执行了,当前线程会暂停,但此时该线程并不会释放锁,仍然保持锁定状态。具体方法如下:
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该该线程挂起,
死锁
就像不是所有的线程都能完成一样,有些锁可能就不能释放了,变成了死锁。当不同的线程分别锁住对方需要的同步监视器对象不释放,都在等待对方先放弃时就形成了线程的死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
版权归原作者 Faith_xzc 所有, 如有侵权,请联系我们删除。