黑马程序员-----多线程


------- android培训java培训、期待与您交流! ----------



1   多线程的概述

1.1  进程

是一个正在执行中的程序,每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个执行单元,一个进程中可以有多个执行单元同时运行。


1.2  线程

就是进程中的一个独立的执行单元,这些执行单元可以看作程序执行的一条条线索,被称为线程,操作系统中的每一个进程都至少存在一个线程,当一个Java程序启动时,就会产生一个进程,该进程会默认创建一个线程,在这个线程中会运行main()方法中的代码。线程控制着进程的执行。


1.3  线程的创建

1.3.1  继承Thread类创建多线程

创建线程的第一种方式:继承Thread类

步骤:

1  定义类继承Thread。

2  复写Thread类中的run方法。

3  调用线程的start方法,

该方法两个作用:启动线程,调用run方法。

 


 

创建线程的第二种方式:实现Runable接口

步骤:

1  定义类实现Runnable接口

2  重写Runnable接口中的run方法

3  通过Thread类建立线程对象

4  将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数

5  调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。


1.3.2  通过匿名内部类

 

1.3.3  两种创建方法的对比

下面通过开启多个线程售票的例子来对比两种方法:

1  通过继承Thread方法

 


运行结果:

发现同一张票三个线程都有售出,说明虽然实现了多线程但是并没有保证资源的共享性。

 

2  通过实现Runnable接口方法

 


运行结果:


发现实现Runable接口的方法可以确保三个线程访问的是同一个x变量,这样实现了资源的共享性。

综上所述,我们可以得出实现Runnable接口相对于继承Thread类来说有如下好处:

1  适合多个相同程序代码的线程去处理同一个资源的情况,可以达到资源的共享性。

2  可以避免由于Java的单继承带来的局限性。

事实上,大部分的应用程序都会选用实现Runnable接口的方式创建多线程。


通过以上案例我们还可以发现,程序运行结果每一次都不同,因为多个线程都在获取cpu执行权,cpu执行到谁,谁就运行,明确一点,在某一个时刻,只能有一个程序在运行。(多核除外)这是多线程的一个特性:随机性

 

1.3.4  获取和设置线程名称

获取线程名称:

1  Thread.currentThread():获取当前线程的对象

2  getName():获取线程名称,线程都有自己默认的名称,Thread-编号,如Thread-1

合起来用:Thread.currentThread().getName();

 

设置线程名称:

1  自定义线程名称:直接在创建线程的时候给父类构造函数赋值。

2  setName("线程名"):设置线程名称

 


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

2.1  线程的状态

线程的整个生命周期可以分为5个状态:新建状态,就绪状态,运行状态,阻塞状态,死亡状态。

1  新建状态

创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,没有表现出任何线程的动态特征。

2  就绪状态

当线程对象调用了start()方法后,该线程就进入了就绪状态,处于就绪状态的线程位于可运行池中,此时它只是具备了运行的条件,能否获得CPU的使用权开始执行还需要等待系统的调度。

3  运行状态

如果处于就绪状态的线程获得了CPU的执行权,开始执行run()方法中的代码,则该线程处于运行状态。当一个线程启动后,它不可能一直处于运行状态,当使用完系统分配的时间后系统就会剥夺它的执行权,让其他线程获得执行的机会。需要注意的是只有处于就绪状态的线程才可能转换到运行状态。

4  阻塞状态

一个正在执行的线程在某些特殊情况下会放弃CPU的执行权,进入阻塞状态,线程进入阻塞状态后就不能进入排队队列,只有引起阻塞的原因被消除后,线程才可以转入就绪状态。

需要注意的是,线程从阻塞状态只能进入就绪状态,不能直接进入运行状态。

5  死亡状态

线程的run()方法执行完毕,或者线程抛出一个未捕获的异常、错误,线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他状态。

 

3  线程的调度

Java虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制称作线程的调度。

 

3.1  线程的优先级

优先级越高的线程获得CPU的执行机会越大,而优先级越低的线程获得CPU执行的机会越小。线程的优先级用1-10之间的整数来表示,数字越大优先级越高。通过Thread类的setPriority()方法进行设置,除了用1-10来表示,还可以调用Thread类提供的三个静态常量:

 

Thread类的静态常量

功能描述

static  int  MAX_PRIORITY

表示最高优先级,相当于10

static  int  NORM_PRIORITY

表示普通优先级,相当于5

static  int  MIN_PRIORITY

表示最低优先级,相当于1

 


运行结果:


 

发现最高优先级的线程比最低优先级的线程要优先被CPU执行。

 

3.2  线程休眠

如果希望人为地控制线程,使正在执行的线程暂停,把CPU的执行权让给别的线程,可以使用静态方法sleep(),该方法可以让当前正在执行的线程暂停一段时间,进入休眠状态。需要注意的是,sleep()方法会抛出一个InterruptedException异常,因此在使用时注意抛出或处理这个异常。


 

运行结果:

 


可以看出在主线程休眠的200ms内,线程一执行完毕,然后才到主线程。

需要注意的是sleep()方法是静态方法,只能控制当前正在运行的线程休眠,不能控制其他线程休眠,当休眠时间结束后,线程就会返回到就绪状态,而不是立即开始运行。

 

3.3  线程让步

线程让步可以用yield()方法来实现,该方法和sleep()方法有点相似,都可以让正在运行的线程暂停,区别在于yield()方法不会阻塞该线程,只是将线程转换成就绪状态,让CPU再重新调度一次。当某个线程调用yield()方法之后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会。

 注意:使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。


运行结果:

 


从运行结果可以看出,当线程B输出3以后,会做出让步,线程A执行,同样,线程A输出3以后,也会做出让步,线程B执行。


3.4  线程插队

当在某个线程中调用其它线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后才会继续运行。

 


运行结果:

 


可以看出,当主线程运行到x=3时,线程A开始插队,并且线程A全部运行完之后主线程才开始运行。注意join()方法需要抛出或处理InterruptedException异常。

 

3.5  守护线程

如果所有的线程中只有守护线程,那么守护线程才会停止运行。

 

运行结果:


 

可以很清楚的看到当主线程运行完毕后,只剩下了守护线程,所以守护线程也会停止运行。

需要注意的是,要将某个线程设置为守护线程,必须在该线程启动之前,也就是说setDaemon()方法必须在start()方法之前调用,否则会引发异常。

 

4  线程的同步

4.1  线程的安全问题

根据售票案例,极有可能碰到“意外的情况”,如一张票被打印多次,或者出现票号为0甚至是负数的情况,这是什么原因引起的呢?下面我们通过加入sleep()方法来重现这个错误。

 

 

运行结果:

 

 

发现出现了0号票甚至是-1号票,这是因为,线程在进入x>0的循环体内有可能不执行打印,但其他线程又进入,所以会导致本来要进行的判断失效,一下多打印了好几次。

 

4.2  同步代码块

要想解决上述问题,必须保证用于处理共享资源的代码在任何时刻只能有一个线程访问。

为了实现这种限制,Java提供了同步机制,即将处理共享资源的代码放置在一个代码块中,使用synchronized关键字来修饰,语法格式:

Synchronized(lock)

{

操作共享资源代码块。

}

lock是一个锁对象,当执行到同步代码块时,首先会检查锁对象的标志位,默认情况下标志位为1,此时线程会执行同步代码块,同时将锁对象的标志位置为0。当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,新线程会阻塞,等待当前线程执行完后锁对象的标志位被置为1,新线程才能进入。

 

 

同步的前提:

1  必须要有两个或者两个以上的线程。

2  必须是多个线程使用同一个锁。

3  必须把同步代码块放在判断的外面

同步失败的例子:


好处:解决了多线程的安全问题

弊端:多个线程需要判断锁,较为消耗资源


4.3  同步函数

只需把synchronized作为修饰符加到函数上,则此函数具有同步功能,同步函数的锁是this

静态同步函数的锁是class对象,类名.class

 

 


4.4  多线程的死锁

如果两个线程都在等待对方的锁,这样就会造成程序的停滞,这种现象称为死锁。

 

 

5  线程的通信

多个线程之间需要协同完成工作就需要线程之间进行通信。

模拟一个场景,假设有两个线程同时去操作同一个存储空间,其中一个线程负责存入数据,另一个线程负责取出数据,通过一个案例来实现上述情况。

 

运行结果:

 


这样便实现线程间交替执行了

需要注意的是,wait(),notify(),notifyAll(),因为要对持有监视器(锁)的线程操作,所以要使用在同步中,因为只有同步才具有锁。

不可以对不同锁中的线程进行唤醒。也就是说,等待和唤醒必须是同一个锁。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
黑马程序员多线程练习题主要包括两个问题。第一个问题是如何控制四个线程在打印log之前能够同时开始等待1秒钟。一种解决思路是在线程的run方法中调用parseLog方法,并使用Thread.sleep方法让线程等待1秒钟。另一种解决思路是使用线程池,将线程数量固定为4个,并将每个调用parseLog方法的语句封装为一个Runnable对象,然后提交到线程池中。这样可以实现一秒钟打印4行日志,4秒钟打印16条日志的需求。 第二个问题是如何修改代码,使得几个线程调用TestDo.doSome(key, value)方法时,如果传递进去的key相等(equals比较为true),则这几个线程应互斥排队输出结果。一种解决方法是使用synchronized关键字来实现线程的互斥排队输出。通过给TestDo.doSome方法添加synchronized关键字,可以确保同一时间只有一个线程能够执行该方法,从而实现线程的互斥输出。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [黑马程序员——多线程10:多线程相关练习](https://blog.csdn.net/axr1985lazy/article/details/48186039)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值