最具影响力的数字化技术在线社区

168大数据

 找回密码
 立即注册

QQ登录

只需一步,快速开始

1 2 3 4 5
打印 上一主题 下一主题
开启左侧

Java 基础教程 (15) 多线程

[复制链接]
跳转到指定楼层
楼主
发表于 2018-1-23 15:56:11 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

马上注册,结交更多数据大咖,获取更多知识干货,轻松玩转大数据

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
1、 线程和进程#
计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。
file
假定工厂的电力有限,一次只能供给一个 车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务。
file
进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。

以下内容回帖刷新可见#
file

一个车间里,可以有很多工人。他们协同完成一个任务。
file

线程就好比车间里的工人。一个进程可以包括多个线程。
file

进程:
是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
线程:
是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
file

进程与线程的区别
1) 一个程序至少有一个进程,一个进程至少有一个线程。
2) 线程的划分尺度小于进程,使得多线程程序的并发性高。
3) 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
4) 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
5) 线程的调度和管理由进程本身负责。
6)进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

什么是多线程?
多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。

什么时候考虑使用多线程
1)程序需要同时执行两个或多个任务
2)程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等
3)需要一些后台运行的程序时

多线程优点
充分发挥多核处理器优势,将不同线程任务分配给不同的处理器核心,真正进入“并行运算”状态
将耗时的任务分配到其他线程执行,由主线程负责统一更新界面会使应用程序更加流畅,用户体验更好

多线程弊端
创建线程会消耗内存空间和CPU时间
线程切换占用系统资源,线程太多会降低系统的运行性能,不是线程越多越快
增加程序复杂度

2、 线程的创建和启动#
创建线程的三种方式
通过继承Thread 类
通过实现Runnable接口
使用Callable 和 Futrue 创建线程

2.1 Thread 类#
通过继承Thread类来创建并启动线程的步骤如下:
1)定义Thread类的子类,并重写Thread类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。因此把run()方法称为线程执行体。
2)创建Thread子类的实例,及创建了线程对象。
3)调用线程对象的start()方法来启动该线程。
Thread类常用方法
Thread.currentThread():是Thread类的静态方法,该方法总是返回当前正在执行的线程对象。
String getName():该方法是Thread类的实例方法,是返回调用该方法的线程名字。
示例:
file

运行结果:
file
注意:使用Thread类的方法创建线程类时,多个线程之间无法共享线程类的实例变量。

2.2 Runnable 接口#
实现Runnable接口来创建并启动多线程的步骤如下:
1)定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法同样是线程执行体。
2)创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
3)调用线程对象的start()方法来启动该线程。
示例:
file

匿名类写法:
file

运行结果:
file
注意:采用Runnable接口的方式创建的多个线程可以共享线程类的实例属性。

2.3 使用Callable 和 Future 创建线程#
前两种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。
自从Java 1.5开始,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。

2.3.1 Callable#
Callable位于java.util.concurrent包下,是个接口,它提供了一个call() 方法可以作为线程执行体。
call() 方法可以有返回值。
call() 方法可以声明抛出异常。
file
可以看到,这是一个泛型接口,call()函数返回的类型就是传递进来的V类型

2.3.2 Future#
Future就是对于具体的 Runnable 或者 Callable 任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过 get 方法获取执行结果,该方法会阻塞直到任务返回结果。

在Future 接口中声明了5个方法,下面依次解释每个方法的作用:
boolean cancel(boolean mayInterruptIfRunning)
试图取消对此任务的执行。如果取消任务成功则返回true,如果取消任务失败则返回false。
参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true;若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。
V get()
方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;
V get(long timeout, TimeUnit unit)
用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。
boolean isCancelled()
如果在任务正常完成前将其取消,则返回 true。
boolean isDone()
如果任务已完成,则返回 true。

Future 接口的实现类是 FutureTask,FutrueTask 实现类提供了两个构造方法:
FutureTask(Callable callable)
创建一个 FutureTask,一旦运行就执行给定的 Callable。
FutureTask(Runnable runnable, V result)
创建一个 FutureTask,一旦运行就执行给定的 Runnable,并安排成功完成时 get 返回给定的结果 。

创建并启动有返回值的线程的步骤:
1)创建Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,且该 call() 方法有返回值 。
2)创建Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象, 该 FutrueTask 对象封装了该 Callable 对象的 call() 方法的返回值。
3)使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
4)调用FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
代码示例:
file

代码示例:
file

2.4 创建线程方式对比#
采用实现Runnable接口、Callable 接口方式
线程类只实现了Runnable接口或 Callable 接口,还可以继承其他类。
多线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源情况。
编程稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。
采用继承Thread类方式
线程类已经继承了Thread类,不能再继承其他父类。
编程简单,如果需要访问当前线程,直接使用this即可获得当前线程。

实现Runnable接口和实现Callable接口的区别:
Runnable是自从java1.1就有了,而Callable是1.5之后才加上去的
Callable规定的方法是call(),Runnable 规定的方法是run()
Callable的任务执行后可返回值,而Runnable的任务是不能返回值(是void)
call方法可以抛出异常,run方法不可以
运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
推荐采用实现 Runnable 接口、Callable 接口的方式创建多线程。

3、线程的生命周期#
当线程被创建并启动以后,它既不是一启动就进入执行状态,也不是一直处于执行状态,在线程的生命周期中,它要经过新建、就绪、运行、阻塞、死亡5种状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独立运行,所以CPU需要在多条线程间切换,于是线程状态也会多次在运行、阻塞之间切换。
下图是线程状态转换图
file

新建:
当程序使用new关键字创建了一个线程后,该线程就处于新建状态。

就绪:
当线程对象调用了start()方法后,该线程处于就绪状态。处于就绪状态的线程并没有开始运行,只是表示该线程可以运行了。至于该线程合适开始运行,取决于JVM里线程调度器的调度。

运行:
如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。如果计算机只有一个CPU,那么在任何时刻只有一个线程处于运行状态。
正在运行的线程失去CPU执行权或调用yield()方法可以让运行状态的线程转入就绪状态。

阻塞:
暂停一个线程的执行以等待某个条件发生(如某资源就绪)。
当前正在执行的线程阻塞后,其他线程就可以获得执行机会。被阻塞的线程会合适的时候重新进入就绪状态,而不是运行状态。
发生如下情况,线程将会进入阻塞状态
1)线程执行了Thread.sleep(int n)方法,线程放弃CPU,睡眠n毫秒,然后恢复运行。
2)线程要执行一段同步代码,由于无法获得相关的同步锁,只好进入阻塞状态,等到获得了同步锁,才能恢复运行。
3)线程执行了一个对象的wait()方法,进入阻塞状态,只有等到其他线程执行了该对象的notify()或notifyAll()方法,才可能将其唤醒。
4)线程执行I/O操作或进行远程通信时,会因为等待相关的资源而进入阻塞状态。例如,当线程执行System.in.read()方法时,如果用户没有向控制台输入数据,则该线程会一直等读到了用户的输入数据才从read()方法返回。进行远程通信时,请求与服务器建立连接时,即当线程执行Socket的带参数的构造方法,或执行Socket的connect()方法时,会进入阻塞状态,直到连接成功,此线程才从Socket的构造方法或connect()方法返回。
5)程序调用了线程的suspend()方法将该线程挂起。但这种方法容易导致死锁,尽量避免使用该方法。
发生如下情况,可以解除阻塞
1)调用Thread.sleep(int n)方法的线程过了指定时间。
2)线程调用的阻塞式IO方法已经返回。
3)线程要执行一段同步代码,获得了同步锁。
4)等到其他线程执行了该对象的notify()或notifyAll()方法。
5)处于挂起状态的线程被调用了resume()恢复方法。

死亡:
线程完成了它的全部工作或线程被提前强制性地中止。
线程会以如下3种方式结束,结束后处于死亡状态。
1)run()方法执行完成。
2)线程抛出一个未捕获的Exception 或 Error。
3)直接调用该线程的stop()方法来结束该线程,但该方法容易导致死锁,不推荐使用。
判断当前线程是否处于活着状态
调用线程对象的isAlive()方法,当线程处于就绪、运行、阻塞时,返回true;当线程处于新建、死亡时,返回false。

以Thread.sleep(int n)方法作为阻塞方式的示例:
file
在i=20的时候,当前进程睡眠了5秒,这5秒程序在阻塞状态,5秒过后,程序 从阻塞到就绪,直到获得CPU执行权进入运行状态,运行结束线程死亡。
执行结果片段:
file

4、控制线程#
4.1 join线程#
Thread 提供了让一个线程等待另一个线程完成的方法——join()方法。当在某个程序执行过程中,调用了其他线程的join方法时,当前调用线程(join代码写在哪,哪个就是调用线程)将被阻塞,直到被join()方法加入的join线程(谁调用的join 代码,谁就是join线程)执行完成。
join()方法的作用,是当一个线程需要等待另一个线程处理结果的时候应用。这也算是一种线程同步。
举个例子:
file
file
上面的代码输出结果:
file

4.2 sleep 和 yield#
4.2.1 线程睡眠:sleep
通过调用Thread 类的静态sleep()方法,可以让正在执行的线程暂停一段时间,并进入阻塞状态。
常用语法格式:
static void sleep(long millis) ;
让当前正在执行的线程 暂停millis 毫秒,并进入阻塞状态。

4.2.2 线程让步:yield#
通过调用Thread 类的静态yield()方法,可以让正在执行的线程暂停,并将该线程转入就绪状态。
常用语法格式:
static void yield();
当某个线程调用了yield()方法暂停之后,只有优先级与当前线程相同,或者优先级高于当前线程的处于就绪状态的线程才会获得执行机会。

优先级:
每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。
每个线程默认的优先级都与创建它的父线程的优先级相同,默认情况下,main 线程具有普通优先级,由main 线程创建的子线程也具有普通优先级。
设置优先级
setPriority(int newPriority)
设置指定线程的优先级。范围是1-10之间。Thread 类提供了三个静态常量
MAX_PRIORITY:其值是10。
MIN_PRIORITY:其值是1。
NORM_PRIORITY:其值是5。
示例:
file
执行结果不容易看明白。

5、线程同步#
5.1 什么是线程同步?#
同步:就是“排队”,排队的意思就是不让一起同时进行。
线程同步:几个线程之间进行排队,一个一个对共享资源进行操作。线程同步的目的就是避免线程同时执行。

5.2 线程安全问题#
银行取钱问题:
假设账户有账号和余额(为了简便,不判断密码)。用户要取钱,系统判断账户余额是否大于取款金额,如果大于则取款成功,如果余额小于取款金额,则取款失败。
1)建个账户对象,其中有账号和余额两个属性
file

2)创建个取款的线程
该线程类有账户、取款账号、取款金额三个属性。
进行取款操作,先判断账号,再判断余额。余额充足则取款成功,更新余额;取款失败提示失败原因。
file

创建测试类,main方法中创建三个子线程
file
按照我们期望的结果,应该是
第一次扣款成功
第二次扣款失败,原因:账号不符
第三次扣款失败,原因:余额不足
但实际运行结果:
file
可见,实际的运行结果不是我们所期望的结果,这正是多线程编程突然出现的“偶然”错误——因为线程调度的不确定性。
综上所述,多线程可能带来的问题
多个线程执行的不确定性引起执行结果的不稳定
多个线程对资源的共享,会造成操作的不完整性,会破坏数据

5.3 如何做线程同步#
针对5.2节的安全问题,是因为run()方法体不具有同步安全性,程序中有两个并发线程在修改Account对象,而且系统恰好在那个代码处执行线程切换,切换给另一个修改Account对象的线程,所以出现了问题。
为了解决这个问题,Java引入了锁机制。

5.3.1 什么是锁?#
一段同步的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在Java里边就是拿到某个同步对象的锁(一个对象只有一把锁); 如果这个时候同步对象的锁被其他线程拿走了,他(这个线程)就只能等了(线程阻塞在锁池等待队列中)。 取到锁后,他就开始执行同步代码(被 synchronized 修饰的代码);线程执行完同步代码后马上就把锁还给同步对象,其他在锁池中等待的某个线程就可以拿到锁执行同步代码了。这样就保证了同步代码在统一时刻只有一个线程在执行。
就好比,有一个房间,房间与外面连接的是个门,门里面有个锁,门外面有5个猴子排队进入吃香蕉。这个规则是一只猴子吃一根香蕉,无论多少只猴子同时进入,房间只提供一根香蕉。每次只有一只猴子进来,进来后从里面把门锁上(加锁),吃一根香蕉(抢的这个公共资源),吃完后把里面的锁打开(释放锁)出去,房间再提供一只香蕉,然后再放一只猴子进来吃香蕉……
上面的例子中,里面的猴子把门锁上后,外面的猴子打不开,只能等里面的猴子把锁打开(锁等待)。
如果猴子进房间后不锁门,后果可能就是剩下的4个猴子都进来抢同一根香蕉。这就好多个线程同时抢这个公共资源,可能出现安全问题。
file
Java用下面几种方式实现了锁机制

5.3.2 同步代码块#
语法定义:
file
synchronized 关键字后面跟一个圆括号,括号中的是某一个引用,这个引用应当指向某一个对象。后面紧跟一个代码块,这个代码块被称为“同步代码块”。
这种语法的含义是,如果某一个线程想要执行代码块中的代码,必须要先获得 obj 所指向对象的互斥锁标记。也就是说,如果有一个线程 t1 要想进入同步代码块,必须要获得 obj对象的锁标记;而如果 t1 线程正在同步代码块中运行,这意味着 t1 有着 obj 对象的互斥锁标记;而这个时候如果有一个 t2 线程想要访问同步代码块,会因为拿不到 obj 对象的锁标记而无法继续运行下去。
注意:
在分析、编写同步代码块时,一定要搞清楚,同步代码块锁的是哪个对象。只有把这个问题搞清楚了之后,才能正确的分析多线程以及同步的相关问题。

下面来改写5.2例子中取款的线程
因为余额变动是变动account对象的balance属性,在同步代码块中给account对象加锁,限制其他线程
file

执行结果,与预期相同:
file

5.3.3 同步方法#
假设P1、P2是同一个类的不同对象
把 synchronized 当作方法修饰符,代码示例如下:
file
这时 synchronized 锁定的是哪个对象呢?他锁定的是调用这个同步方法对象。也就是说,当一个对象P1在不同的线程中执行这个同步方法时,他们之间会形成互斥,达到同步的效果。但是这个对象所属的Class所产生的另一对象P2却能够任意调用这个被加了 synchronized 关键字的方法。
同步方法最好放到你要进行锁定那个对象的类里。
上边的示例代码等同于如下代码:
file
其中:this指的是什么呢?他指的就是调用这个方法的对象,如P1。
修改上面的代码示例:
在Account中增加同步draw方法,将同步代码放入同步方法中。
file

在DrawMoneyThread中调用draw方法做取款动作。
file

5.3.4 释放synchronized锁#
synchronized 互斥锁是自动获取和释放
释放时机
当前线程的同步方法、同步代码块执行结束
当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行
当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁

不会释放锁的情况
线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器),应尽量避免使用suspend()和resume()来控制线程

5.3.5 同步锁(Lock)#
通过显示定义同步锁对象来同步,在这种机制下,同步锁使用Lock 对象充当。
Lock 提供了比 synchronized 方法和 synchronized 代码块更广泛的锁定操作。Lock 是控制多个线程对共享资源进行访问的工具。每次只能有一个线程对 Lock 对象进行加锁,线程开始访问共享资源之前应先获得 Lock 对象。
采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally 块中进行,以保证锁一定被被释放,防止死锁的发生。
比较常用的是ReentrantLock(可重入锁)
代码格式如下:
file
将5.2例子用Lock锁改写:
file
ReentrantLock 锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock 锁再次加锁,ReentrantLock 对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()方法加锁后,必须显示调用unlock()方法来释放锁,所以一段被锁保护的代码可以调用另一个被相同的锁保护的方法。
5.3.6 死锁

既然可以上锁,那么假如有2个线程,一个线程想先锁对象1,再锁对象2,恰好另外有一个线程先锁对象2,再锁对象1。
在这个过程中,当线程1把对象1锁好以后,就想去锁对象2,但是不巧,线程2已经把对象2锁上了,也正在尝试去锁对象1。
什么时候结束呢,只有线程1把2个对象都锁上并把方法执行完,并且线程2把2个对象也都锁上并且把方法执行完毕,那么就结束了,但是,谁都不肯放掉已经锁上的对象,所以就没有结果,这种情况就叫做线程死锁。
file

代码示例:
file

6 控制线程通信#
6.1 控制线程通信——wait() 、notify() 和notifyAll()#
wait()
Object类中的final方法。它的作用是导致当前的线程等待,直到其它线程调用此对象的notify方法或者notifyAll方法,wait还有一些重用方法,传参数,比如说时间长度。
当前的线程必须拥有此对象监视器,然后该线程发布对此监视器的所有权并且开始等待,直到其它线程通过调用notify方法或者notifyAll方法,通知在此对象的监视器上等待的线程醒来,然后该线程将等到重新获得对监视器的所有权后才能开始执行。
调用wait() 方法的当前线程会释放对该同步监视器的锁定。

notify()
是Object类中的方法,用于唤醒在此对象上等待着的某一个线程,如果有很多线程挂起的话,就随机地决定哪一个。
只有当前线程放弃对该监视器锁定后(使用wait() 方法),才可以执行被唤醒的线程。

notifyAll()
是Object类中的方法,用于唤醒在此对象上等待着的所有线程。
只有当前线程放弃对该监视器锁定后(使用wait() 方法),才可以执行被唤醒的线程。

这三个方法必须由同步监视器对象来调用,分为以下两种情况:
对于使用synchronized 修饰的同步方法,因为该类的默认实例是同步监视器,所以可以在类的同步方法中直接调用这三个方法。
对于使用 synchronized 修饰的同步代码块,同步监视器是 synchronized 后括号里的对象,所以必须使用该对象调用这三个方法。

代码示例:
启动两个waiter对象,并对两个都调用wait() 方法,如果只调用一个notify()方法,线程则不会结束。只有调用notifyAll() 方法线程才结束。
创建Message 类,类里有同步方法doWait() 和 doNotify()
file
创建Waiter 线程,调用Message对象的doWait() 同步方法
file
创建Notify 线程,调用Message 对象的doNotify() 同步方法
file

测试类
file

运行结果:
file

6.2 控制线程——Condition#
当使用Lock 对象来保证同步时,Java 提供了一个 Condition 类来保持协调,使用 Condition 可以让那些已经得到Lock 对象却无法继续执行的线程释放Lock对象,Condition 对象也可以唤醒其他处于等待的线程。
Condition 实例实质上被绑定到一个锁上。要为特定 Lock 实例获得 Condition 实例,请使用其 newCondition() 方法。
void await()
造成当前线程在接到信号或被中断之前一直处于等待状态。 直到线程调用此 Condition 的 signal() 方法或 signalAll() 方法来唤醒该线程。
void signal()
唤醒在此Lock 对象上等待的单个线程。
void signalAll()
唤醒在此Lock 对象上等待的所有线程。
代码示例:

file

6.3 控制线程——阻塞队列(BlockingQueue)#
队列:
file

    Java5提供了阻塞队列的接口BlockingQueue,阻塞队列的概念是,一个指定长度的队列,如果队列满了,添加新元素的操作会被阻塞等待,直到有空位为止。同样,当队列为空时候,请求队列元素的操作同样会阻塞等待,直到有可用元素为止。
   Java阻塞队列应用于生产者消费者模式、消息传递、并行任务执行和相关并发设计的大多数常见使用上下文。
   程序的两个线程通过交替向BlockingQueue 中放入元素、取出元素,即可很好地控制线程的通信。
BlockingQueue 提供了两个支持阻塞的方法:
1)put(E e)
尝试把E元素放入BlockingQueue 中,如果该队列的元素已满,则阻塞该线程。
2)take()
尝试从BlockingQueue 的头部取出元素,如果该队列的元素已空,则阻塞该线程。

BlockingQueue定义的常用方法如下:
1)add(anObject):把anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则招聘异常
2)offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false。
3)put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.
4)poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null
5)take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到Blocking有新的对象被加入为止
BlockingQueue 包含的方法之间的对应关系
file
代码示例:
file

BlockingQueue有四个具体的实现类,根据不同需求,选择不同的实现类
1)ArrayBlockingQueue:规定大小的BlockingQueue,其构造函数必须带一个int参数来指明其大小.其所含的对象是以FIFO(先入先出)顺序排序的.
2)LinkedBlockingQueue:大小不定的BlockingQueue,若其构造函数带一个规定大小的参数,生成的BlockingQueue有大小限制,若不带大小参数,所生成的BlockingQueue的大小由Integer.MAX_VALUE来决定.其所含的对象是以FIFO(先入先出)顺序排序的
3)PriorityBlockingQueue:类似于LinkedBlockQueue,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数的Comparator决定的顺序.
4)SynchronousQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成的。

代码示例:
生产者线程每300ms 生产一个苹果(往队列里放), 消费者线程每 1000ms 消费一个苹果(从队列里取出),设定生产消费10个后,自动停止生产、消费线程。
file #
file

file

执行结果部分截图:
file

7 线程池#
多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。
假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。
如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。
线程池技术正是关注如何缩短或调整T1,T3 时间的技术,从而提高服务器程序性能的。它把T1,T3 分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1,T3的开销了。

线程池在系统启动时创建大量空闲的线程,程序将一个Runnable 对象或 Callable 对象传给线程池,线程池就会启动一个线程来执行它们的run() 或 call() 方法,当run() 或 call() 方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable 对象或 Callable 对象的run() 或 call() 方法。
除此之外,使用线程池可以有效地控制系统中并发线程的数量,当系统中包含大量并发线程时,会导致系统性能急剧下降,而线程池的最大线程数参数可以控制系统中并发线程数不超过此数。

在Executors 类里面提供了一些静态工厂,生成一些常用的线程池。
1)static ExecutorService newSingleThreadExecutor()
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

2)static ExecutorService newFixedThreadPool(int nThreads)
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

3)static ExecutorService newCachedThreadPool()
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

4)static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

5)static ScheduledExecutorService newSingleThreadScheduledExecutor():
创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。
前三个方法可以返回一个ExecutorService 对象,该对象代表一个线程池,它可以执行Runnable 对象或 Callable 对象所代表的线程;后两个方法返回一个ScheduledExecutorService 线程池,它是ExecutorService 的子类,它可以指定延迟后执行线程任务。‘

ExecutorService 在Executor 的基础上增加了一些方法,其中有两个核心的方法:

Future<?> submit(Runnable task)
提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。该 Future 的 get 方法在成功 完成时将会返回 null。
Future submit(Callable task)
提交一个返回值的任务用于执行,返回一个表示任务的未决结果的 Future。该 Future 的 get 方法在成功完成时将会返回该任务的结果。

ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit) 创建并执行在给定延迟后启用的 ScheduledFuture。 ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) 创建并执行在给定延迟后启用的一次性操作。 ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) 创建并执行一个在给定初始延迟后首次启用的定期操作,后续操作具有给定的周期;也就是将在 initialDelay 后开始执行,然后在 initialDelay+period 后执行,接着在 initialDelay + 2 * period 后执行,依此类推。 ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) 创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟。

**void shutdown() ** 启动一次顺序关闭,执行以前提交的任务,但不接受新任务。 **List shutdownNow() ** 试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。

使用线程池执行线程任务的步骤如下: 1)调用Executors 类的静态工厂方法创建一个ExecutorService 对象,该对象代表一个线程池。 2)创建Runnable 实现类或 Callable 实现类的实例,作为线程执行任务。 3)调用 ExecutorService 对象的submit() 方法来提交Runnable 实例或 Callable 实例。 4)当不想提交任何任务时,调用 ExecutorService 对象的shutdown() 方法来关闭线程池。 代码示例: 用线程池改造生产消费测试类 ![file](http://hainiubl.com/uploads/images/201707/17/426/vdgip4ORVi.png) ---------------------------------------------------------------------------------------------------------- ![file](http://hainiubl.com/uploads/images/201707/17/426/FwCy5jrQOJ.png) ###8 ThreadLocal 类 ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLocal类接口的4个方法: **void set(T value)** 设置此线程局部变量中当前线程副本中的值。 **public T get()** 该方法返回此线程局部变量中当前线程副本中的值。 **public void remove()** 删除此线程局部变量中当前线程副本中的值,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可 以加快内存回收的速度。 **protected T initialValue()** 返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

**Thread同步机制的比较** ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。 在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。 而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写 多线程代码时,可以把不安全的变量封装进ThreadLocal。

代码示例: 不带有ThreadLocal ![file](http://hainiubl.com/uploads/images/201707/17/426/F9eVQuFBdE.png)

带有ThreadLocal ![file](http://hainiubl.com/uploads/images/201707/17/426/EsfCBToJkg.png) 运行结果: ![file](http://hainiubl.com/uploads/images/201707/17/426/hAfsrBpKv1.png)

版权声明:原创作品,允许转载,转载时务必以超链接的形式表明出处和作者信息。否则将追究法律责任。来自海牛部落-海牛博士,http://hainiubl.com/topics/142
楼主热帖
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 转播转播 分享分享 分享淘帖 赞 踩

168大数据 - 论坛版权1.本主题所有言论和图片纯属网友个人见解,与本站立场无关
2.本站所有主题由网友自行投稿发布。若为首发或独家,该帖子作者与168大数据享有帖子相关版权。
3.其他单位或个人使用、转载或引用本文时必须同时征得该帖子作者和168大数据的同意,并添加本文出处。
4.本站所收集的部分公开资料来源于网络,转载目的在于传递价值及用于交流学习,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。
5.任何通过此网页连接而得到的资讯、产品及服务,本站概不负责,亦不负任何法律责任。
6.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源,若标注有误或遗漏而侵犯到任何版权问题,请尽快告知,本站将及时删除。
7.168大数据管理员和版主有权不事先通知发贴者而删除本文。

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

站长推荐上一条 /1 下一条

关于我们|小黑屋|Archiver|168大数据 ( 京ICP备14035423号|申请友情链接

GMT+8, 2024-4-26 04:34

Powered by BI168大数据社区

© 2012-2014 168大数据

快速回复 返回顶部 返回列表