Java并发基础

并行与并发

并行性是指两个或多个事件在同一时刻发生。而并发性是指连个或多个事件在同一时间间隔内发生。在多道程序环境下,并发性是指在一段时间内宏观上有多个程序在同时运行,但在单处理机环境下(一个处理器),每一时刻却仅能有一道程序执行,故微观上这些程序只能是分时地交替执行。例如,在1秒钟时间内,0-15ms程序A运行;15-30ms程序B运行;30-45ms程序C运行;45-60ms程序D运行,因此可以说,在1秒钟时间间隔内,宏观上有四道程序在同时运行,但微观上,程序A、B、C、D是分时地交替执行的。

 

 

 

进程和线程 

进程:系统进行资源分配调度的一个独立单位

线程:线程是一个实体CPU调度和分派的一个基本单位,基本不占用系统资源

区别

进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。

线程:堆空间是共享的,栈空间是独立的,线程消耗的资源也比进程小,相互之间可以影响的,又称为轻型进程或进程元。

因为一个进程中的多个线程是并发运行的,那么从微观角度上考虑也是有先后顺序的,那么哪个线程执行完全取决于CPU调度器(JVM来调度),程序员是控制不了的。我们可以把多线程并发性看作是多个线程在瞬间抢CPU资源,谁抢到资源谁就运行,这也造就了多线程的随机性。下面我们将看到更生动的例子。

Java程序的进程(Java的一个程序运行在系统中)里至少包含主线程和垃圾回收线程(后台线程):

你可以简单的这样认为,但实际上有四个线程(了解就好):
[1] main——main线程,用户程序入口
[2] Reference Handler——清除Reference的线程
[3] Finalizer——调用对象finalize方法的线程
[4] Signal Dispatcher——分发处理发送给JVM信号的线程

上下文切换

即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务的时候,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

创建线程的两种方式 

继承Thead类:

 

运行结果发现打游戏和播放音乐交替出现,说明已经成功了。

实现Runnable接口:

也能完成效果。

以上就是传统的两种创建线程的方式,事实上还有第三种,我们后边再讲。

继承Thread类还是实现Runnable接口?

吃苹果比赛

想象一个这样的例子:给出一共50个苹果,让三个同学一起来吃,并且给苹果编上号码,让他们吃的时候顺便要说出苹果的编号:

 

 

运行结果可以看到,使用继承方式实现,每一个线程都吃了50个苹果。这样的结果显而易见:是因为显式地创建了三个不同的Person对象,而每个对象在堆空间中有独立的区域来保存定义好的50个苹果。

而使用实现方式则满足要求,这是因为三个线程共享了同一个Apple对象,而对象中的num数量是一定的。

 

对于这两种方式哪种好并没有一个确定的答案,它们都能满足要求。就我个人意见,我更倾向于实现Runnable接口这种方法。因为线程池可以有效的管理实现了Runnable接口的线程,如果线程池满了,新的线程就会排队等候执行,直到线程池空闲出来为止。而如果线程是通过实现Thread子类实现的,这将会复杂一些。

有时我们要同时融合实现Runnable接口和Thread子类两种方式。例如,实现了Thread子类的实例可以执行多个实现了Runnable接口的线程。一个典型的应用就是线程池。

常见的错误:调用run()方法而非start()方法

创建并运行一个线程所犯的常见错误是调用线程的run()方法而非start()方法,如下所示:

1| Thread newThread = new Thread(MyRunnable());
2| newThread.run();  //should be start();

起初你并不会感觉到有什么不妥,因为run()方法的确如你所愿的被调用了。但是,事实上,run()方法并非是由刚创建的新线程所执行的,而是被创建新线程的当前线程所执行了。也就是被执行上面两行代码的线程所执行的。想要让创建的新线程执行run()方法,必须调用新线程的start方法。

吃苹果比赛的问题:线程不安全问题

尽管,Java并不保证线程的顺序执行,具有随机性,但吃苹果比赛的案例运行多次也并没有发现什么太大的问题。这并不是因为程序没有问题,而只是问题出现的不够明显,为了让问题更加明显,我们使用Thread.sleep()方法(经常用来模拟网络延迟)来让线程休息10ms,让其他线程去抢资源。(注意:在程序中并不是使用Thread.sleep(10)之后,程序才出现问题,而是使用之后,问题更明显.)

 

为什么会出现这样的错误呢?

先来分析第一种错误:为什么会吃重复的苹果呢?就拿B和C都吃了编号为47的苹果为例吧:

  • ① A线程拿到了编号为48的苹果,打印输出然后让num减1,睡眠10ms,此时num为47。
  • ② 这时B和C同时都拿到了编号为47的苹果,打印输出,在其中一个线程作出了减一操作的时候,A线程从睡眠中醒过来,拿到了编号为46的苹果,然后输出。在这期间并没有任何操作不允许B和C线程不能拿到同一个编号的苹果,之前没有明显的错误仅仅可能只是因为运行速度太快了。

再来分析第二种错误:照理来说只应该存在1-50编号的苹果,可是0和-1是怎么出现的呢?

  • ① 当num=1的时候,A,B,C三个线程同时进入了try语句进行睡眠。
  • ② C线程先醒过来,输出了编号为1的苹果,然后让num减一,当C线程醒过来的时候发现num为0了。
  • ③ A线程醒过来一看,0都没有了,只有-1了。

归根结底是因为没有任何操作来限制线程来获取相同的资源并对他们进行操作,这就造成了线程安全性问题。

如果我们把打印和减一的操作分成两个步骤,会更加明显:

 

像这样的原子操作,是不允许分步骤进行的,必须保证同步进行,不然可能会引发不可设想的后果。

要解决上述多线程并发访问一个资源的安全性问题,就需要引入线程同步的概念。

 

线程同步

多个执行线程共享一个资源的情景,是最常见的并发编程情景之一。为了解决访问共享资源错误或数据不一致的问题,人们引入了临界区的概念:用以访问共享资源的代码块,这个代码块在同一时间内只允许一个线程执行。

为了帮助编程人员实现这个临界区,Java(以及大多数编程语言)提供了同步机制,当一个线程试图访问一个临界区时,它将使用一种同步机制来查看是不是已经有其他线程进入临界区。如果没有其他线程进入临界区,他就可以进入临界区。如果已经有线程进入了临界区,它就被同步机制挂起,直到进入的线程离开这个临界区。如果在等待进入临界区的线程不止一个,JVM会选择其中的一个,其余的将继续等待。

synchronized关键字

如果一个对象已用synchronized关键字声明,那么只有一个执行线程被允许访问它。使用synchronized的好处显而易见:保证了多线程并发访问时的同步操作,避免线程的安全性问题。但是坏处是:使用synchronized的方法/代码块的性能比不用要低一些。所以好的做法是:尽量减小synchronized的作用域。

我们还是先来解决吃苹果的问题,考虑一下synchronized关键字应该加在哪里呢?

 

发现如果还再把synchronized关键字加在if里面的话,0和-1又会出来了。这其实是因为当ABC同是进入到if语句中,等待临界区释放的时,拿到1编号的线程已经又把num减一操作了,而此时最后一个等待临界区的进程拿到的就会是-1了。

同步锁(Lock)

Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值