Java基础<十>--------多线程


Java基础<十>--------多线程

1.进程、线程、多进程的概念

进程:正在进行中的程序(直译)。

线程:进程中一个负责程序执行的控制单元(执行路径)。

注:1)、一个进程中可以有多个执行路径,称之为多线程。

2)、一个进程中至少要有一个线程。

3)、开启多个线程是为了同时运行多部分代码,每一个线程都有自己运行的内容,这个内容可以称为线程要执行的任务。

多线程的好处:解决了多部分代码同时运行的问题。

多线程的弊端:线程太多,会导致效率的降低。

其实,多个应用程序同时执行都是CPU在做着快速的切换完成的。这个切换是随机的。CPU的切换是需要花费时间的,从而导致了效率的降低。

VM启动时启动了多条线程,至少有两个线程可以分析的出来:

1).执行main函数的线程,该线程的任务代码都定义在main函数中。

2).负责垃圾回收的线程。

示例:


运行结果:


2. 创建线程方式一:继承Thread类

1).定义一个类继承Thread类。

2).覆盖Thread类中的run方法。

3).直接创建Thread的子类对象创建线程。

4).调用start方法开启线程并调用线程的任务run方法执行。

单线程程序示例:


运行结果:


可以看到在单线程程序中,只有上一句代码执行完,下一句代码才有执行的机会。

创建线程的目的就是为了开启一条执行路径,去运行指定的代码和其他代码实现同时运行,而运行的指定代码就是这个执行路径的任务。

jvm创建的主线程的任务都定义在了主函数中。而自定义的线程,它的任务在哪儿呢?

Thread类用于描述线程,线程是需要任务的。所以Thread类也有对任务的描述。这个任务就是通过Thread类中的run方法来体现。也就是说,run方法就是封装自定义线程运行任务的函数,run方法中定义的就是线程要运行的任务代码。

开启线程是为了运行指定代码,所以只有继承Thread类,并复写run方法,将运行的代码定义在run方法中即可。

多线程程序示例:


运行结果:


注:1)、可以通过Thread的getName方法获取线程的名称,名称格式:Thread-编号(从0开始)。

2)、Thread在创建的时候,该Thread就已经命名了。

3.  创建线程方式二:实现Runnable接口

1).定义类实现Runnable接口。

2).覆盖接口中的run方法,将线程的任务代码封装到run方法中。

3).通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。为什么?因为线程的任务都封装在Runnable接口子类对象的run方法中。所以要在线程对象创建时就必须明确要运行的任务。

4).调用线程对象的start方法开启线程。

实现Runnable接口的好处:

1).将线程的任务从线程的子类中分离出来,进行了单独的封装,按照面向对象的思想将任务封装成对象。

2).避免了Java单继承的局限性。所以,创建线程的第二种方式较为常用。

示例:


运行结果:


4. 线程安全问题

线程安全问题产生的原因

需求:模拟4个线程同时卖100张票。

运行结果:


原因分析:

出现上图安全问题的原因在于Thread-0通过了if判断后,在执行到“num--”语句之前,num此时仍等于1。CPU切换到Thread-1、Thread-2、Thread-3之后,这些线程依然可以通过if判断,从而执行“num--”的操作,因而出现了0、-1、-2的情况。


5.线程安全问题产生的原因:

1).多个线程在操作共享的数据。

2).操作共享数据的线程代码有多条。

当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生。

6.线程安全问题的解决方案

  思路:

就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程不可以参与运算。必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。

在java中,用同步代码块就可以解决这个问题。

同步代码块的格式:

synchronized(对象){

需要被同步的代码;

}

同步的好处:解决了线程的安全问题。

同步的弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。

同步的前提:必须有多个线程并使用同一个锁。

修改后代码:


运行结果:



 原因分析:

上图显示安全问题已被解决,原因在于Object对象相当于是一把锁,只有抢到锁的线程,才能进入同步代码块向下执行。因此,当num=1时,CPU切换到某个线程后,如上图的Thread-3线程,其他线程将无法通过同步代码块继而进行if判断语句,只有等到Thread-3线程执行完“num--”操作(此后num的值为0),并且跳出同步代码块后,才能抢到锁。其他线程即使抢到锁,然而,此时num值已为0,也就无法通过if语句判断,从而无法再执行“num--”的操作了,也就不会出现0、-1、-2等情况了。


 利用同步代码块解决安全问题案例:

需求:储户,两个,每个都到银行存钱,每次存100,共存三次。

代码实现:


运行结果:


原因分析:

由如下代码中可以看到,同步代码块中的语句,存在可能有多个线程同时操作共享数据(sum)的情况,通过同步代码块即可解决存在的安全问题。


安全问题的另一种解决方案:同步代码块

格式:在函数上加上synchronized修饰符即可。

注:同步函数和同步代码块的区别:

1).同步函数的锁是固定的this。

2).同步代码块的锁是任意的对象。

建议使用同步代码块。

由于同步函数的锁是固定的this,同步代码块的锁是任意的对象,那么如果同步函数和同步代码块都使用this作为锁,就可以实现同步。

示例:



运行结果:



静态的同步函数使用的锁是该函数所属字节码文件对象,可以用getClass方法获取,也可以用当前类名.class表示。

示例(例子同上,这是部分修改而已):


7. 多线程下的单例模式

1).  饿汉式:


注:饿汉式不存在安全问题,因为不存在多个线程共同操作数据的情况。

2).  懒汉式:


注:懒汉式存在安全问题,可以使用同步函数解决。但若直接使用同步函数,则效率较低,因为每次都需要判断。


但若采取如下方式,即可提升效率。


原因在于任何一个线程在执行到第一个if判断语句时,如果Single对象已经创建,则直接获取即可,而不用判断是否能够获取锁,相对于上面使用同步函数的方法就提升了效率。如果当前线程发现Single对象尚未创建,则再判断是否能够获取锁。

1).如果能够获取锁,那么就通过第二个if判断语句判断是否需要创建Single对象。因为可能当此线程获取到锁之前,已经有一个线程创建完Single对象,并且放弃了锁。此时它便没有必要再去创建,可以直接跳出同步代码块,放弃锁,获取Single对象即可。如果有必要,则再创建。

2).如果不能获取到锁,则等待,直至能够获取到锁为止,再按步骤一执行。

8.死锁示例

死锁常见情景之一:同步的嵌套。

示例:


运行结果:

 

原因分析:

由上图可以看到程序已经被锁死,无法向下执行。由下图代码可以看到,run方法中的同步代码块需要获取obj对象锁,才能执行代码块中的show方法。

而执行show方法则必须获取this对象锁,然后才能执行其中的同步代码块。

当线程t1获取到obj对象锁执行同步代码块,线程t2获取到this对象锁执行show方法。同步代码块中的show方法因无法获取到this对象锁无法执行,show方法中的同步代码块因无法获取到obj对象锁无法执行,就会产生死锁。



 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值