进程和线程
进程
所有运行中的任务通常对应一个进程,当一个程序进入内存运行时,即变成一个进程.进程是处于运行过程中的程序,并且具有一定独立的功能,进程是系统进行资源分配和调度的一个独立单位.
进程的特性
独立性
动态性
并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会相互影响.
并发和并行的区别
并行(parellel
)指的是在同一时刻,有多条指令在多个处理器上同时被执行;
并发指的是在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得宏观上具有多个进程同时执行的结果.
多线程
多线程扩展了多进程的概念,使得同一进程可以同时并发处理多个任务.线程也被称为轻量级进程,线程时进程的执行单元.线程在程序中是独立的并发的执行流.当进程被初始化之后,主线程就被创建了.
线程是进程的组成部分,一个进程可以有多个线程,但一个线程必须有一个父进程.线程可以拥有自己的栈,自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源.因为多个线程共享父进程的所有资源,因此编程比较方便,但必须更加小心,需要确保线程不会妨碍到同一进程里的其他线程.
线程是独立运行的,它并不知道进程中是否还有其他的线程存在.线程的执行是抢占式的:当前运行的线程在任何时候都可能被挂起,以便另一个线程可以运行.
一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行.
从逻辑角度来看,多线程存在于一个应用程序中,让一个应用程序可以有多个执行部分同时进行,但操作系统无须将多个线程看做多个独立的应用,对多线程实现调度和管理以及资源分配.线程的调度和管理由进程本身负责完成.
总结:
1.操作系统可以同时执行多个任务,每个任务就是进程;
2.进程可以同时执行多个任务,每个任务就是线程.
多线程的优势
- 1.进程之间不能贡献内存,但线程之间贡献内存很容易
- 2.系统创建进程需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高.
- 3.Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程.
多线程的应用是很广泛的,比如一个浏览器必须能同时下载多个图片,一个web服务器必须能同时响应多个用户请求;Java虚拟机本身就在后台提供了一个超级线程来进行垃圾回收.....
线程的创建和启动
Java
使用Thread
类代表线程,每个线程对象都必须是Thread
类或其子类的实例.每个线程的作用是完成一定的任务,实际上是执行一段程序流.
继承Thread类创建线程类
- 1.定义
Thread
类的子类,并重写该类的run()
方法,该run()
方法的方法体就代表了线程需要完成的任务.因此把run()
方法称为线程执行体 - 2.创建
Thread
子类的实例,即创建了线程对象 - 3.调用线程对象的
start()
方法来启动该线程.
// 通过继承Thread类来创建线程类
public class FirstThread extends Thread
{
private int i ;
// 重写run方法,run方法的方法体就是线程执行体
public void run()
{
for ( ; i < 100 ; i++ )
{
// 当线程类继承Thread类时,直接使用this即可获取当前线程
// Thread对象的getName()返回当前该线程的名字
// 因此可以直接调用getName()方法返回当前线程的名
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)
{
for (int i = 0; i < 100; i++)
{
// 调用Thread的currentThread方法获取当前线程
System.out.println(Thread.currentThread().getName()
+ " " + i);
if (i == 20)
{
// 创建、并启动第一条线程
new FirstThread().start();
// 创建、并启动第二条线程
new FirstThread().start();
}
}
}
}
Java
程序运行时默认的主线程,main()
方法的方法体就是主线程的线程执行体.
可以看到Thread-0
和Thread-1
两个线程的输出的i变量不连续-----注意:i
变量是FirstThread
的实例变量,而不是局部变量,但是因为程序每次创建线程对象都需要创建一个FirstThread
对象,所以Thread-0
和Thread-1
不能共享该实例变量.
使用继承Thread
类的方法来创建线程类时,多个线程之间是无法共享线程类的实例变量.
实现Runnable接口创建线程类
- 1.定义
Runnable
接口的实现类,并重写该接口的run()
方法,该run()
方法的方法体同样是该线程的线程执行体. - 2.创建
Runnable
实现类的实例,并以此实例作为Thread
的target
来创建Thread
对象,该Thread
对象才是真正的线程对象 - 3.调用线程对象的
start()
方法来启动该线程
// 通过实现Runnable接口来创建线程类
public class SecondThread implements Runnable
{
private int i ;
// run方法同样是线程执行体
public void run()
{
for ( ; i < 100 ; i++ )
{
// 当线程类实现Runnable接口时,
// 如果想获取当前线程,只能用Thread.currentThread()方法。
System.out.println(Thread.currentThread().getName()
+ " " + i);
}
}
public static void main(String[] args)
{
for (int i = 0; i < 100; i++)
{
System.out.println(Thread.currentThread().getName()
+ " " + i);
if (i == 20)
{
SecondThread st = new SecondThread(); // ①
//通过创建Runnable实现类的对象SecondThread ,
//以Runnable实现类的对象SecondThread 作为Thread的target来创建Thread对象
// 通过new Thread(target , name)方法创建新线程
new Thread(st , "新线程1").start();
new Thread(st , "新线程2").start();
}
}
}
}
当线程类实现Runnable
接口时,如果想获取当前线程,只能用Thread.currentThread()
方法
可以看到两个子线程的i
变量是连续的这是因为采用Runnable
接口的方式创建的多个线程可以共享线程类的实例变量.是因为:程序创建的Runnable
对象只是线程的target
,而多个线程可以共享一个target
,所以多个线程可以共享一个线程类(实际上应该是线程的target
类)的实例变量.
使用Callable和Future创建线程
通过实现Runnable
接口创建多线程时,Thread
类的作用就是把run()
方法包装成线程执行体.从Java5
开始,Java
提供了Callable
接口,该接口可以理解为是Runnable
接口的增强版,Callable
接口提供了一个call()
方法可以作为线程执行体,但call()
方法比run()
方法功能更强大,call()
方法可以有返回值.call()
方法可以声明抛出的异常.
但是Callable
接口并不是Runnable
接口的子接口,所以Callable
对象不能直接作为Thread
的target
.而且call()
方法还有一个返回值,call()
方法并不是直接调用的,它是作为线程执行体被调用的.好在Java提供了Future
接口来代表Callable
接口里的Call()
方法的返回值,并为Future
接口提供了一个FutureTask
实现类,该实现类既实现了Future
接口,并实现了Runnable
接口----可以作为Thread
类的target
.
在Future接口里定义了几个公共方法来控制它关联的Callable任务.
Callable
接口有泛型限制,并且Callable
接口里的泛型形参类型与call()
方法返回值类型相同.而且Callable
接口是函数式接口,可以用Lambda
表达式创建Callable
对象
创建并启动具有返回值的线程的步骤如下:
- 1.创建
Callable
接口的实现类,并实现call()
方法,该call()
方法将作为线程执行体,且该call()
方法有返回值,再创建Callable
实现类的实例. - 2.使用
FutureTask
类来包装Callable
对象,该FutureTask
对象封装了该Callable
对象的call()
方法的返回值 - 3.使用
FutureTask
对象作为Thread
对象的target
创建并启动新线程 - 4.调用
FutureTask
对象的get()
方法来获得子线程执行结束后的返回值.
public class ThirdThread
{
public static void main(String[] args)
{
// 创建Callable对象
ThirdThread rt = new ThirdThread();
// 先使用Lambda表达式创建Callable<Integer>对象
// 使用FutureTask来包装Callable对象
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)() -> {
int i = 0;
for ( ; i < 100 ; i++ )
{
System.out.println(Thread.currentThread().getName()
+ " 的循环变量i的值:" + i);
}
// call()方法可以有返回值
return i;
});
for (int i = 0 ; i < 100 ; i++)
{
System.out.println(Thread.currentThread().getName()
+ " 的循环变量i的值:" + i);
if (i == 20)
{
// 实质还是以Callable对象来创建、并启动线程
new Thread(task , "有返回值的线程").start();
}
}
try
{
// 获取线程返回值
System.out.println("子线程的返回值:" + task.get());
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
创建线程的三种方式对比
采用实现Runnable,Callable
接口的方式创建多线程的优缺点:
- 1.线程类只是实现了
Runnable
接口或Callable
接口,还可以继承其他类 - 2.多个线程可以共享同一个
target
对象,非常适合多个相同线程来处理同一份资源的情况,较好的体现了面向对象的思想 - 3.需要访问当前线程,则必须使用
Thread.currentThread()
方法
采用继承Thread类的方式创建多线程的优缺点:
- 1.因为该线程已经继承了
Thread
类,所以不能在继承其他父类 - 2.编写简单,如果需要访问当前线程,则无需使用
Thread.currentThread()
方法,直接使用this
即可获得当前线程.
线程的生命周期
线程的生命周期中,需要经历新建(New
),就绪(Runnable
),运行(Running
),堵塞(Blocked
),死亡(Dead
)5种状态.
新建和就绪状态
当程序new
关键字创建了一个线程之后,该线程就处于新建状态.此时它和其他java
对象一样,仅仅由java
虚拟机为其分配内存,并初始化其成员变量的值,此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体了.
当线程对象开始执行start()
方法之后,该线程就处于就绪状态,Java
虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了.
注意:启动线程使用的是start()
方法,而不是run()
方法!永远都不要调用线程对象的run()
方法!!!调用start()
方法来启动线程,系统会把该run()
方法当成线程来处理;如果直接调用线程对象的run()
方法,系统会把线程对象当做普通对象来处理,而run()
方法也是一个普通方法,而不是线程执行体.
public class InvokeRun extends Thread
{
private int i ;
// 重写run方法,run方法的方法体就是线程执行体
public void run()
{
for ( ; i < 100 ; i++ )
{
// 直接调用run方法时,Thread的this.getName返回的是该对象名字,
// 而不是当前线程的名字。
// 使用Thread.currentThread().getName()总是获取当前线程名字
System.out.println(Thread.currentThread().getName()
+ " " + i); // ①
}
}
public static void main(String[] args)
{
for (int i = 0; i < 100; i++)
{
// 调用Thread的currentThread方法获取当前线程
System.out.println(Thread.currentThread().getName()
+ " " + i);
if (i == 20)
{
// 直接调用线程对象的run方法,
// 系统会把线程对象当成普通对象,run方法当成普通方法,
// 所以下面两行代码并不会启动两条线程,而是依次执行两个run方法
new InvokeRun().run();
new InvokeRun().run();
}
}
}
}
结果如下(截取部分):
如果直接调用线程对象的
run()
方法,则run()
方法不能直接通过getName()
方法来获取到当前线程的名字,而是需要使用Thread.currentThread()
方法先获得当前线程,再调用线程对象的getName()
方法来获取线程的名字.
不难看出,启动线程的正确方法是调用Thread
对象的start()
方法,而不是直接调用run()
方法,否则就变成单线程程序了.
需要指出的是:当调用了线程的run()
方法之后,该线程便不再处于新建状态,不要再次调用线程对象的start()
方法.
只能对处于新建状态的线程调用start()
方法,否则将引发IllegalThreadStateException
异常
调用线程对象的start()
方法之后,该线程立即进入就绪状态-------就绪状态相当于"等待执行",但该线程并未真正进入运行状态.
比如之前我们演示的secondThread那个程序:并不是到20就马上开启一个新线程的.
如果希望调用子线程的start()
方法后子线程立即开始执行,程序可以使用Thread.sleep(1)
来让当前运行的线程(主线程)睡眠1毫秒----1毫秒就够了
运行和堵塞状态
如果处于就绪状态的线程获得了CPU
,开始执行run()
方法的线程执行体,则该线程处于运行状态.如果计算机只有一个CPU
,那么任何时刻都只有一个线程处于运行状态,如果一个多处理器的机器上,将会有多个线程并行(parallel
)执行;当线程数大于处理器数时,依然会存在多个线程在同一个CPU
上轮换的现象.
抢占式调度和协作式调度策略
抢占式调度:线代桌面和服务器操作系统一般采取抢占式调度策略,系统会给每个可执行的线程一个小时间段来处理任务;当该时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会.在选择下一个进程的时候,系统会考虑线程的优先级
协作式调度:小型设备如手机则采取协作式调度策略,只有当一个线程调用了它的sleep()
或yeid()
方法后才会放弃所占用的资源-----即必须由该线程主动放弃所占用的资源
线程将会进入堵塞状态
- 1.线程调用
sleep()
方法主动放弃所占用的处理器资源 - 2.线程调用了一个堵塞式
IO
方法,在该方法返回之前,该线程被堵塞 - 3.线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有
- 4.线程在等待某个通知(
notify
) - 5.线程调用了线程的
suspend()
方法将该线程挂起,这个方法容易引起死锁(要尽量避免!!!)
如果当前线程被堵塞之后,其他线程就可以获得执行的机会,被堵塞的线程会在合适的时候重新进入就绪状态,注意是就绪状态而不是运行状态.被堵塞线程的堵塞解除后,必须重新等待线程调度器再次调用它.
解除上面的堵塞
- 1.调用
sleep()
方法的线程经过了指定的时间 - 2.线程调用的堵塞式
IO
方法已经返回 - 3.线程成功地获得了试图取得的同步监视器
- 4.线程正在等待某个通知时,其他线程发出了一条通知
- 5.处于挂起状态的线程被调用了
resume()
恢复方法
不难看出,线程从堵塞状态进入就绪状态,无法直接进入运行状态.而就绪和运行状态之间的转换通常不受程序控制,而是由系统线程调度所决定,当处于就绪状态的线程获取到CPU的资源时,该线程进入运行状态;当处于运行状态的线程失去处理器资源时,该线程进入就绪状态. 有一个方法例外:可以调用yield()
方法可以让运行状态的线程转入就绪状态.
线程死亡
线程会以下列三种方式结束,结束后就处于死亡状态
- 1.
run()
或call()
方法执行完成,线程正常结束 - 2.线程抛出一个未捕获的
Exception
或Error
- 3.直接调用该线程的
stop()
方法来结束该线程-----该方法容易引起死锁(不推介!!!)
当主线程结束时,其他线程不受任何影响,并不会随之结束.一旦子线程启动起来,它就拥有和主线程相同的地位,它不会受主线程的影响
为了测试某个线程是否已经死亡,可以调用该对象的isAlive()
方法,当线程处于就绪,运行,堵塞三种状态时,该方法返回true
,当线程处于新建和死亡两种状态时,该方法将返回false
不要对一个已经死亡的线程再调用start()
方法来让它重新启动,死亡就是死亡,该线程将不可再次作为线程执行.如果依然对一个已经死亡的线程再次调用start()
方法来启动该线程,将会引发IllegalThreadStateException
异常,这表明处于死亡状态的线程已经无法再次运行了.
如下程序可以说明上述现象:
public class StartDead extends Thread
{
private int i ;
// 重写run方法,run方法的方法体就是线程执行体
public void run()
{
for ( ; i < 100 ; i++ )
{
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)
{
// 创建线程对象
StartDead sd = new StartDead();
for (int i = 0; i < 300; i++)
{
// 调用Thread的currentThread方法获取当前线程
System.out.println(Thread.currentThread().getName()
+ " " + i);
if (i == 20)
{
// 启动线程
sd.start();
// 判断启动后线程的isAlive()值,输出true
System.out.println(sd.isAlive());
}
// 只有当线程处于新建、死亡两种状态时isAlive()方法返回false。
// 当i > 20,则该线程肯定已经启动过了,如果sd.isAlive()为假时,
// 那只能是死亡状态了。
if (i > 20 && !sd.isAlive())
{
// 试图再次启动该线程
sd.start();
}
}
}
}
总结:不要对一个已经死亡的线程再调用start()
方法,程序只能对新建状态的线程调用start()
方法,对新建状态的线程两次调用start()
方法也是错误的,上述两种情况都会引发IllegalThreadStateException
异常
控制线程
join线程
Thread
提供了让一个线程等待另一个线程完成的方法-------join()
方法.当某个执行流中调用其他线程的join()
方法时,调用线程将被堵塞,直到被join()
方法加入的join
线程执行完为止.
比如下面程序中的mian
线程即主线程,主线程中调用了其他线程(jt
线程)的join()
方法,此时调用线程(main
线程)将被堵塞,直到被join()
方法加入的join
线程执行完毕为止.
join()
方法通常由使用线程的程序调用,目的是:将大问题划分为许多小问题,每个小问题分配一个线程.当所有的小问题都得到解决处理后,再调用主线程来进一步操作.
public class JoinThread extends Thread
{
// 提供一个有参数的构造器,用于设置该线程的名字
public JoinThread(String name)
{
super(name);
}
// 重写run()方法,定义线程执行体
public void run()
{
for (int i = 0; i < 100 ; i++ )
{
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)throws Exception
{
// 启动子线程
new JoinThread("新线程").start();
for (int i = 0; i < 100 ; i++ )
{
if (i == 20)
{
JoinThread jt = new JoinThread("被Join的线程");
jt.start();
// main线程调用了jt线程的join()方法,main线程
// 必须等jt执行结束才会向下执行
jt.join();
}
System.out.println(Thread.currentThread().getName()
+ " " + i);
}
}
}
上述代码中
main
线程中调用了jt
线程的join()
方法,main
线程必须要等jt
线程执行完毕之后才会向下执行
join()
方法有如下三种重载的方法:
- 1.
join()
:等待被join
的线程执行完成. - 2.
join(long millis)
:等待被join
的线程的时间最长为millis毫秒. - 3.
join(long millis,int nanos)
:等待被join
的线程的事件最长为millis毫秒加nanos毫微秒(这个方法很少用!!!)
后台线程
有一种线程,它是在后台运行的,它的任务是为其他的线程提供服务的,这种线程称为后台线程(Daemon Thread
).JVM
的垃圾回收线程就是典型的后台线程.
后台线程的特征:如果所有的前台线程都死亡,后台线程自动死亡.
调用Thread
对象的setDaemon(true)
方法可将指定线程设置成后台线程.
public class DaemonThread extends Thread
{
// 定义后台线程的线程执行体与普通线程没有任何区别
public void run()
{
for (int i = 0; i < 1000 ; i++ )
{
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)
{
DaemonThread t = new DaemonThread();
// 将此线程设置成后台线程
t.setDaemon(true);
// 启动后台线程
t.start();
for (int i = 0 ; i < 10 ; i++ )
{
System.out.println(Thread.currentThread().getName()
+ " " + i);
}
// -----程序执行到此处,前台线程(main线程)结束------
// 后台线程也应该随之结束
}
}
本来该线程应该执行到
i=999
才会结束,但运行程序时不难发现该后台线程无法运行到999,因为当主线程也就是程序中唯一的前台线程运行结束后,JVM
会主动退出,因而后台线程也就被结束了.
Thread
类还提供了一个isDaemon()
方法来判断当前线程是否为后台线程.
上面程序中:主线程默认是前台线程,t线程默认是后台线程.并不是所有的线程默认都是前台线程,有些线程默认就是后台线程-----------前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程.
前台线程死亡之后,JVM
会通知后台线程死亡,但从它接收到指令到做出相应,需要一定时间(这也是为什么上图中:在main
线程死亡之后Thread-0
还进行了一会才死亡的原因).而且将某个线程设置为后台线程,必须要在该线程启动之前设置,即setDaemon(true)
必须在start()
方法之前调用,否则会引发IllegalThreadStateException
异常
线程睡眠sleep
如果需要让当前正在执行的线程暂停一段时间,并进入堵塞状态,则可以通过调用Thread
类的静态sleep()
方法来实现.
sleep()
方法有两种重载形式:
- 1.
static void sleep(long millis)
:让当前正在执行的线程暂停millis
毫秒,并进入堵塞状态 - 2.static void sleep(long millis,intnanos):让当前正在执行的线程暂停millis毫秒加nanos毫微秒,并进入堵塞状态(很少用)
当当前线程调用sleep()
方法进入堵塞状态后,在其睡眠时间段内,该线程不会获得执行的机会,即使系统中没有其他可执行的线程,处于sleep()
中的线程也不会执行,因此sleep()
方法常用来暂停程序的执行.
public class SleepTest
{
public static void main(String[] args)
throws Exception
{
for (int i = 0; i < 10 ; i++ )
{
System.out.println("当前时间: " + new Date());
// 调用sleep方法让当前线程暂停1s。
Thread.sleep(1000);
}
}
}
程序依次输出10条字符串,输出2条字符串之间的时间间隔为1秒
线程让步:yeid
yeid()
方法也是Thread
类提供的一个静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是让该线程转入就绪状态.yield()
只是让当前线程暂停一下,让系统的线程调度器重新调度一次.完全可能的情况是:当某个线程调用了yield()
方法暂停之后,线程调度器又将其调度出来重新执行.
当某个线程调用了yield()
方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会.
public class YieldTest extends Thread
{
public YieldTest(String name)
{
super(name);
}
// 定义run方法作为线程执行体
public void run()
{
for (int i = 0; i < 50 ; i++ )
{
System.out.println(getName() + " " + i);
// 当i等于20时,使用yield方法让当前线程让步
if (i == 20)
{
Thread.yield();
}
}
}
public static void main(String[] args)throws Exception
{
// 启动两条并发线程
YieldTest yt1 = new YieldTest("高级");
// 将ty1线程设置成最高优先级
yt1.setPriority(Thread.MAX_PRIORITY);
yt1.start();
YieldTest yt2 = new YieldTest("低级");
// 将yt2线程设置成最低优先级
yt2.setPriority(Thread.MIN_PRIORITY);
yt2.start();
}
}
如果使用多CPU来运行上述程序,可能效果不是很明显因为并发在多核CPU上效果不明显单核CPU比较明显
sleep()
和yield()
方法的区别
- 1.
sleep()
方法暂停当前线程后,会给其他线程机会,不会理会其他线程的优先级:但yield()
方法只会给优先级相同,或优先级更高的线程执行机会 - 2.
sleep()
方法会使线程进入堵塞状态,知道经过堵塞时间才会转入就绪状态;而yield()
不会将线程转入堵塞状态,它只是强调当前线程进入就绪状态.因此完全有可能某个线程调用yield()
方法暂停之后,立即重新获得处理器资源而被执行 - 3.
sleep()
方法声明抛出了InterruptedException
异常,所以调用sleep()
方法时要么捕捉该异常,要么显式声明抛出该异常;而yield()方法则没有声明抛出任何异常 - 4.
sleep()
方法比yield()
方法有更好的移植性,通常不建议用yield()
方法来控制并发线程的执行.
改变线程的优先级
每个线程执行都有一定的优先级,优先级越高的线程将获得较多的执行机会,而优先级低的线程则获得较少的机会.每个线程默认的优先级都与创建它的父类线程的优先级相同,main线程具有普通优先级,由main线程创建的子线程的优先级也具有普通优先级.
Thread类提供了setPriority(int newPriority),getPriority()方法来设置和返回指定的线程的优先级,setPriority()方法的参数可以是一个整数,范围是1~10之间,也可以使用如下三个静态常量:
MAX_PRIORITY
:其值是10
MIN_PRIORITY
:其值是1
NORM_PRIORITY
:其值是5
public class PriorityTest extends Thread
{
// 定义一个有参数的构造器,用于创建线程时指定name
public PriorityTest(String name)
{
super(name);
}
public void run()
{
for (int i = 0 ; i < 50 ; i++ )
{
System.out.println(getName() + ",其优先级是:"
+ getPriority() + ",循环变量的值为:" + i);
}
}
public static void main(String[] args)
{
// 改变主线程的优先级
Thread.currentThread().setPriority(6);
for (int i = 0 ; i < 30 ; i++ )
{
if (i == 10)
{
PriorityTest low = new PriorityTest("低级");
low.start();
System.out.println("创建之初的优先级:"
+ low.getPriority());
// 设置该线程为最低优先级
low.setPriority(Thread.MIN_PRIORITY);
}
if (i == 20)
{
PriorityTest high = new PriorityTest("高级");
high.start();
System.out.println("创建之初的优先级:"
+ high.getPriority());
// 设置该线程为最高优先级
high.setPriority(Thread.MAX_PRIORITY);
}
}
}
}
遗憾的是Java虽然提供了10个优先级,但这10个优先级并不都与操作系统兼容,比如win2000只提供了7个优先级所以尽量避免直接为线程指定优先级,而应该采用MAX_PRIORITY,MIN_PRIORITY,NORM_PRIORITY
三个静态常量来设置优先级,这样才能保证程序具有良好的可移植性.
线程同步
由系统的线程调度具有一定的随机性造成的,不过即使程序偶然出现问题,那也是由于编程不当引起的.当多个线程来访问同一个数据时,很容易"偶然"出现安全性问题.
线程安全问题:
银行取钱问题:
因为线程调度具有不确定性,假设系统线程调度器在粗体字代码处暂停,让另一个线程执行------为了强制暂停,只要取消上面程序中的粗体字代码的注释即可.
同步代码块:
因为run()方法的方法体不具有同步安全性------程序中有两个并发线程在修改Account对象;而且系统恰好在粗体字代码处执行线程切换,切换给另一个修改Account对象的线程,所以就出现了问题.就跟以前讲的文件并发访问,当有两个进程并发修改同一个文件时就有可能造成异常.
为了解决上述问题,Java引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块
//synchronized后括号里的obj就是同步监视器
synchronized(obj)
{
......
//此处的代码就是同步代码块
}
上述代码的含义是:在线程开始执行同步代码块之前,必须先获得对同步监视器的锁定.
任何时刻只能有一个线程可以获得同步监视器的锁定,当同步代码块执行完成之后,该线程会释放该同步监视器的锁定.
同步监视器的目的:阻止两个线程对同一个共享资源进行并发访问,推介使用可能被并发访问的共享资源充当同步监视器
public class DrawThread extends Thread
{
// 模拟用户账户
private Account account;
// 当前取钱线程所希望取的钱数
private double drawAmount;
public DrawThread(String name , Account account
, double drawAmount)
{
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
// 当多条线程修改同一个共享数据时,将涉及数据安全问题。
public void run()
{
// 使用account作为同步监视器,任何线程进入下面同步代码块之前,
// 必须先获得对account账户的锁定——其他线程无法获得锁,也就无法修改它
// 这种做法符合:“加锁 → 修改 → 释放锁”的逻辑
synchronized (account)
{
// 账户余额大于取钱数目
if (account.getBalance() >= drawAmount)
{
// 吐出钞票
System.out.println(getName()
+ "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t余额为: " + account.getBalance());
}
else
{
System.out.println(getName() + "取钱失败!余额不足!");
}
}
// 同步代码块结束,该线程释放同步锁
}
}
这种做法符合"加锁---修改---释放锁"的逻辑,任何线程在修改指定资源之前,首先对该资源加锁,在加锁期间其他线程无法修改该资源,当线程修改完成后,该线程释放对该资源的锁定.
通过这种方式可以保证并发线程在同一时刻只有一个线程可以进入修改共享资源的代码区(也被称为临界区),所以同一时刻最多只有一个线程处于临界区内,从而保证了线程的安全性.
同步方法
Java多线程还提供了同步方法来和同步代码块相对应,使用synchronized字来修饰某个方法,该方法称为同步方法.对于synchronized关键字修饰的实例方法,无须显式指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象.
通过同步方法可以非常方便的实现线程安全的类,线程安全的类具有如下特征:
- 1.该类的对象可以被多个线程安全地访问
- 2.每个线程调用该对象的任意方法之后都能得到正确结果
- 3.每个线程调用该对象的任意方法之后,该对象状态依然保持合理的状态
不可变类总是线程安全的,因为它的对象时不可变的;但可变对象需要额外的方法来保证其线程安全.
public class Account
{
// 封装账户编号、账户余额两个成员变量
private String accountNo;
private double balance;
public Account(){}
// 构造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
public double getBalance()
{
return this.balance;
}
// 提供一个线程安全draw()方法来完成取钱操作
public synchronized void draw(double drawAmount)
{
// 账户余额大于取钱数目
if (balance >= drawAmount)
{
// 吐出钞票
System.out.println(Thread.currentThread().getName()
+ "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
balance -= drawAmount;
System.out.println("\t余额为: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName()
+ "取钱失败!余额不足!");
}
}
// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
增加了一个代表取钱的draw()方法,并使用synchronized关键字来修饰该方法,把该方法编程同步方法,该同步方法的同步监视器是this,对于同一个Account账户而言,任意时刻只能有一个线程获得对Account对象的锁定,然后进入draw()方法执行取钱操作-----这样也可以保证多个线程并发取钱的线程安全.
注意:synvhronized
关键字可以修饰方法,可以修饰代码块,但不能修饰构造器,成员变量等等.
public class DrawThread extends Thread
{
// 模拟用户账户
private Account account;
// 当前取钱线程所希望取的钱数
private double drawAmount;
public DrawThread(String name , Account account
, double drawAmount)
{
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
// 当多条线程修改同一个共享数据时,将涉及数据安全问题。
public void run()
{
// 直接调用account对象的draw方法来执行取钱
// 同步方法的同步监视器是this,this代表调用draw()方法的对象。
// 也就是说:线程进入draw()方法之前,必须先对account对象的加锁。
account.draw(drawAmount);
}
}
在上面的示例中,调用draw()方法的对象是account,多个线程并发修改同一份account之前,必须先对account对象加锁,这也符合"加锁---修改---释放锁"的逻辑
面向对象中的一种流行的设计模式:
DDD
(领域驱动设计):这种方式认为每个类都应该是完备的领域对象,比如:Account代表用户账户,应该提供用户账户的相关方法;通过draw()方法来执行取钱操作(实际上还应该提供transfer()等方法来完成转账等操作),而不是直接将setBalance()方法暴露出来任人操作,这样才能保证Account对象的完整性和一致性.
可变类的线程安全是以降低程序的运行效率作为代价的.
- 1.不要堆线程安全类的所有方法进行同步,只对那些会改变竞争资源(竞争资源也就是共享资源)的方法进行同步.
- 2.可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本,即线程不安全版本和线程安全版本.在单线程环境中使用线程不安全版本以保证性能(
StringBuilder
);在多线程中使用线程安全的版本(StringBuffer
)
释放同步监视器的锁定
程序无须显式释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定
- 1.当前线程的同步方法,同步代码块执行结束
- 2.当前线程在同步代码块,同步方法中遇到
break,return
终止了代码块导致其异常结束 - 3.当前线程在同步代码块,同步方法中出现了未处理的
Error
和Exception
- 4.当前线程执行同步代码块和同步方法时,程序执行了同步监视器对象的
wait()
方法,当前线程暂停,并释放同步监视器
下面出现的情况,线程不会释放同步监视器
- 1.当前线程在执行同步代码块,同步方法时,程序调用了
Thread.sleep()
,Thread.yield()
方法来暂停当前线程的执行,当前线程并不会释放同步监视器 - 2.线程在执行同步代码块时,其他线程调用了该线程的
suspend()
方法将该线程挂起,该线程不会释放同步监视器.程序应该尽量避免使用suspend()
和resume()
方法来控制线程.
同步锁
通过显式定义同步锁对象来实现同步-----同步锁对象由Lock对象充当.(这是一种更为强大的线程同步机制)
Lock是控制多个线程对共享资源进行访问的工具,每次只能有一个线程对Lock对象加锁,程序开始访问共享资源之前首先要先获得Lock对象
某些锁可能允许对共享资源并发访问,如ReadWriteLock
(读写锁);
Lock,ReadWriteLock
是Java5
提供的两个根接口,并为Lock
提供了ReentrantLock
(可重入锁)实现类,为ReadWriteLock
提供了ReentrantReadWriteLock
实现类
ReentrantReadWriteLock
为读写提供了三种锁模式:Writing,ReadingOptimistic,Reading
在实现线程安全的控制中比较常用的是ReentrantLock
(可重入锁).使用该Lock
对象可以显式地释放锁,加锁.
import java.util.concurrent.locks.*;
public class Account
{
// 定义锁对象
private final ReentrantLock lock = new ReentrantLock();
//.......
// 提供一个线程安全draw()方法来完成取钱操作(定义一个保证线程安全的方法)
public void draw(double drawAmount)
{
// 加锁
lock.lock();
try
{
// 账户余额大于取钱数目
if (balance >= drawAmount)
{
// 吐出钞票
System.out.println(Thread.currentThread().getName()
+ "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 修改余额
balance -= drawAmount;
System.out.println("\t余额为: " + balance);
}
else
{
System.out.println(Thread.currentThread().getName()
+ "取钱失败!余额不足!");
}
}
finally
{
// 修改完成,释放锁
lock.unlock();
}
}
// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
使用ReentrantLock
对象来进行同步,加锁和释放锁出现在不同的作用范围内时,通常建议使用finally
块来确保在必要时释放锁.
程序中实现draw()
方法时,进入方法开始执行后立即请求对ReentrantLock
对象进行加锁,当执行完draw()
方法的取钱逻辑后,程序使用finally
块确保释放锁.
使用Lock
时是显式调用Lock
对象作为同步锁,而使用同步方法时系统隐式地使用当前对象作为同步监视器,同样都符合"加锁---修改---释放锁"的操作模式,而且Lock
对象时每个Lock
对象都对应一个Account
对象,一样可以保证对于同一个Account
对象,同一时刻只能有一个线程能进入临界区
ReentrantLock
锁具有可重入性,一个线程可以对已加锁的ReentrantLock
锁再次加锁,ReentrantLock
对象会维持一个计数器来追踪lock()
方法的嵌套使用,线程在每次调用lock()
方法加锁后,必须显式调用unlock()
方法来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法.
死锁
当两个线程互相等待对方释放同步监视器时就会发生死锁.Java没有提供任何检测措施来处理死锁的情况,所以多线程编程时应该尽量采取措施来避免死锁的出现.一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于堵塞状态,无法继续.
死锁很容易发生,尤其是在系统中出现多个同步监视器的情况下:
class A
{
public synchronized void foo( B b )
{
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了A实例的foo()方法" ); // ①
try
{
Thread.sleep(200);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用B实例的last()方法"); // ③
b.last();
}
public synchronized void last()
{
System.out.println("进入了A类的last()方法内部");
}
}
class B
{
public synchronized void bar( A a )
{
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了B实例的bar()方法" ); // ②
try
{
Thread.sleep(200);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用A实例的last()方法"); // ④
a.last();
}
public synchronized void last()
{
System.out.println("进入了B类的last()方法内部");
}
}
public class DeadLock implements Runnable
{
A a = new A();
B b = new B();
public void init()
{
Thread.currentThread().setName("主线程");
// 调用a对象的foo方法
a.foo(b);
System.out.println("进入了主线程之后");
}
public void run()
{
Thread.currentThread().setName("副线程");
// 调用b对象的bar方法
b.bar(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args)
{
DeadLock dl = new DeadLock();
// 以dl为target启动新线程
new Thread(dl).start();
// 调用init()方法
dl.init();
}
}
Thread
类的suspend()
方法也容易导致死锁,Java
不推介使用该方法来暂停线程的执行.
线程通信
程序通常无法准确控制线程的轮换执行,但Java也提供了一些机制来保证线程协调运行.
传统的线程通信
Object
类提供的三个方法(这三个方法必须由同步监视器对象来调用):
同步监视器对象可以分为下列两种情况:
- 1.使用
synchronized
修饰的同步方法,该类的默认实例(this
)就是同步监视器. - 2.使用
synchronized
修饰的同步代码块,同步监视器是synchronized
后括号里的对象
这三个方法解释如下:
wait()
:导致当前线程等待,直到其它线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程.调用wait()方法的当前线程会释放对该同步监视器的锁定.
notify()
:唤醒此同步监视器上等待的单个线程.只有当前线程放弃对该同步监视器的锁定后(使用wait()方法),才可以执行被唤醒的线程.
notifyAll()
:唤醒在此同步监视器上等待的所有线程.只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程.
public class Account
{
// 封装账户编号、账户余额的两个成员变量
private String accountNo;
private double balance;
// 标识账户中是否已有存款的旗标
private boolean flag = false;
public Account(){}
// 构造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
public double getBalance()
{
return this.balance;
}
public synchronized void draw(double drawAmount)
{
try
{
// 如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞
if (!flag)
{
wait();
}
else
{
// 执行取钱
System.out.println(Thread.currentThread().getName()
+ " 取钱:" + drawAmount);
balance -= drawAmount;
System.out.println("账户余额为:" + balance);
// 将标识账户是否已有存款的旗标设为false。
flag = false;
// 唤醒其他线程
notifyAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
}
public synchronized void deposit(double depositAmount)
{
try
{
// 如果flag为真,表明账户中已有人存钱进去,则存钱方法阻塞
if (flag) //①
{
wait();
}
else
{
// 执行存款
System.out.println(Thread.currentThread().getName()
+ " 存款:" + depositAmount);
balance += depositAmount;
System.out.println("账户余额为:" + balance);
// 将表示账户是否已有存款的旗标设为true
flag = true;
// 唤醒其他线程
notifyAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
}
// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
public class DrawThread extends Thread
{
// 模拟用户账户
private Account account;
// 当前取钱线程所希望取的钱数
private double drawAmount;
public DrawThread(String name , Account account
, double drawAmount)
{
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
// 重复100次执行取钱操作
public void run()
{
for (int i = 0 ; i < 100 ; i++ )
{
account.draw(drawAmount);
}
}
}
public class DepositThread extends Thread
{
// 模拟用户账户
private Account account;
// 当前取钱线程所希望存款的钱数
private double depositAmount;
public DepositThread(String name , Account account
, double depositAmount)
{
super(name);
this.account = account;
this.depositAmount = depositAmount;
}
// 重复100次执行存款操作
public void run()
{
for (int i = 0 ; i < 100 ; i++ )
{
account.deposit(depositAmount);
}
}
}
public class DrawTest
{
public static void main(String[] args)
{
// 创建一个账户
Account acct = new Account("1234567" , 0);
new DrawThread("取钱者" , acct , 800).start();
new DepositThread("存款者甲" , acct , 800).start();
new DepositThread("存款者乙" , acct , 800).start();
new DepositThread("存款者丙" , acct , 800).start();
}
}
上图所示的是堵塞而不是死锁,取钱者的线程已经执行结束,但是存钱者的线程只是在等待其他线程来取钱而已,并不是等待其他线程释放同步监视器,不要把死锁和程序堵塞等同起来.
使用Condition控制线程通信
如果程序使用Lock
对象保证同步,则系统中不存在隐式地同步监视器,也就不能用wait(),notify(),notifyAll()
方法进行线程通信了.
当使用Lock
对象来保证同步时,Java
提供了一个Condition
类来保持协调,使用Conditon
可以让那些已经得到Lock
对象却无法继续执行的线程释放Lock
对象,Conditon
对象也可以唤醒其它处于等待的线程.
Conditon
将同步监视器方法(wait(),notify(),notifyAll()
)分解成不同的对象,以便通过将这些对象和Lock
对象组合使用,为每个对象提供多个等待集(wait-set
).Lock
替代了同步方法或同步代码块,Conditon
替代了同步监视器的功能.
Conditon
实例绑定在一个Lock
对象上,要获得特定Lock
实例的Conditon
实例,调用Lock
对象的newConditon()
方法即可.
Conditon
类提供了如下三个方法:
await()
:类似于隐式同步器上的wait()
方法,导致当前线程等待,直到其它线程调用该Conditon
的signal()
方法或signalAll()
方法来唤醒线程.
signal()
:唤醒在此Lock
对象上等待的单个线程.只有当前线程放弃对该Lock
对象的锁定后(使用await()
方法),才可以执行被唤醒线程
signalAll()
:唤醒在此Lock
对象上等待的所有线程.只有当前线程放弃对该Lock
对象的锁定后(使用await()
方法),才可以执行被唤醒线程
下面程序通过Account
使用Lock
对象来控制同步,并使用Conditon
对象来控制线程的协调运行.
public class Account
{
// 显式定义Lock对象
private final Lock lock = new ReentrantLock();
// 获得指定Lock对象对应的Condition
private final Condition cond = lock.newCondition();
// 封装账户编号、账户余额的两个成员变量
private String accountNo;
private double balance;
// 标识账户中是否已有存款的旗标
private boolean flag = false;
public Account(){}
// 构造器
public Account(String accountNo , double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
// accountNo的setter和getter方法
public void setAccountNo(String accountNo)
{
this.accountNo = accountNo;
}
public String getAccountNo()
{
return this.accountNo;
}
// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
public double getBalance()
{
return this.balance;
}
public void draw(double drawAmount)
{
// 加锁
lock.lock();
try
{
// 如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞
if (!flag)
{
cond.await();
}
else
{
// 执行取钱
System.out.println(Thread.currentThread().getName()
+ " 取钱:" + drawAmount);
balance -= drawAmount;
System.out.println("账户余额为:" + balance);
// 将标识账户是否已有存款的旗标设为false。
flag = false;
// 唤醒其他线程
cond.signalAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 使用finally块来释放锁
finally
{
lock.unlock();
}
}
public void deposit(double depositAmount)
{
lock.lock();
try
{
// 如果flag为真,表明账户中已有人存钱进去,则存钱方法阻塞
if (flag) // ①
{
//导致当前线程等待,知道其他线程调用该Conditon的signal()或signalAll()方法来唤醒该线程
cond.await();
}
else
{
// 执行存款
System.out.println(Thread.currentThread().getName()
+ " 存款:" + depositAmount);
balance += depositAmount;
System.out.println("账户余额为:" + balance);
// 将表示账户是否已有存款的旗标设为true
flag = true;
// 唤醒其他线程
cond.signalAll();
}
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
// 使用finally块来释放锁
finally
{
lock.unlock();
}
}
// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode()
{
return accountNo.hashCode();
}
public boolean equals(Object obj)
{
if(this == obj)
return true;
if (obj !=null
&& obj.getClass() == Account.class)
{
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
这里只不过是现在显式使用Lock
对象来充当同步监视器,需要使用Condition
对象来暂停,唤醒指定的线程.
使用堵塞队列(BlockingQueue)控制线程通信
Java5
提供了一个BlockingQueue
接口,虽然BlockingQueue
也是Queue
的子接口,但它的主要作用不是作为容器,而是作为线程同步的工具.
BlockingQueue
具有一个特征:当生产者线程试图向BlockingQueue
中放入元素时,如果该队列已满,则线程堵塞;当消费者线程试图从BlockingQueue
中取出元素时,如果该队列已经已空,则该线程堵塞.
程序中两个线程通过交替向BlockingQueue中放入元素取出元素,即可实现控制线程通信.
BlockingQueue
提供下面两个支持堵塞的方法:
put(E e)
:尝试把e
元素放入BlockingQueue
中,如果该队列的元素已满,则堵塞线程
take()
:尝试从BlockingQueue
的头部取出元素,如果该队列的元素已空,则堵塞该线程.
BlockingQueue
继承了Queue
接口,当然也可以使用Queue
接口中的方法
在队列尾部插入元素:add(E e),offer(E e)
和put(E e)
方法,当该队列已满时,这三个方法分别抛出异常,返回false
,堵塞队列.
在队列头部删除并返回删除的元素:remove(),poll()
,和take()
方法.当该队列已空时,这三个方法分别会抛出异常,返回false
,堵塞队列.
在队列头部取出但不删除元素:包括element()
和peek()
方法,当队列已空时,这两个方法分别抛出异常,返回false
.
BlockingQueue
包含如下5个实现类:
ArrayBlockingQueue
:基于数组
LinkedBlockingQueue
:基于链表
PriotityBlockingQueue
:跟之前的PriotityQueue
有点类似,在这里不做过多的介绍,自然顺序
SynchronousQueue
:同步队列,该队列的存取操作必须交替进行
DelayQueue
:底层基于PriotityBlockingQueue
实现,不过DelayQueue
要求集合元素都实现Delay
接口(该接口里有一个long getDelay()
方法),DelayQueue
根据集合元素的getDelay()
方法的返回值进行排序.
下面使用ArrayBlockingQueue为例来介绍堵塞队列的功能和用法.
import java.util.concurrent.*;
public class BlockingQueueTest
{
public static void main(String[] args)
throws Exception
{
// 定义一个长度为2的阻塞队列
BlockingQueue<String> bq = new ArrayBlockingQueue<>(2);
bq.put("Java"); // 与bq.add("Java"、bq.offer("Java")相同
bq.put("Java"); // 与bq.add("Java"、bq.offer("Java")相同
bq.put("Java"); // ① 阻塞线程。
//bq.add("Java");//抛出异常
//bq.offer("Java");//返回false,元素不会被放入
}
}
与此类似的是:BlockingQueue
已空的情况下:
使用take()
方法取出元素会堵塞线程;
使用remove()
方法尝试取出元素将引发异常;
使用poll()
方法取出元素将会返回false,元素不会被删除.
下面程序利用BlockingQueue来实现线程通信
import java.util.concurrent.*;
class Producer extends Thread
{
private BlockingQueue<String> bq;
public Producer(BlockingQueue<String> bq)
{
this.bq = bq;
}
public void run()
{
String[] strArr = new String[]
{
"Java",
"Struts",
"Spring"
};
for (int i = 0 ; i < 999999999 ; i++ )
{
System.out.println(getName() + "生产者准备生产集合元素!");
try
{
Thread.sleep(200);
// 尝试放入元素,如果队列已满,线程被阻塞
bq.put(strArr[i % 3]);
}
catch (Exception ex){ex.printStackTrace();}
System.out.println(getName() + "生产完成:" + bq);
}
}
}
class Consumer extends Thread
{
private BlockingQueue<String> bq;
public Consumer(BlockingQueue<String> bq)
{
this.bq = bq;
}
public void run()
{
while(true)
{
System.out.println(getName() + "消费者准备消费集合元素!");
try
{
Thread.sleep(200);
// 尝试取出元素,如果队列已空,线程被阻塞
bq.take();
}
catch (Exception ex){ex.printStackTrace();}
System.out.println(getName() + "消费完成:" + bq);
}
}
}
public class BlockingQueueTest2
{
public static void main(String[] args)
{
// 创建一个容量为1的BlockingQueue
BlockingQueue<String> bq = new ArrayBlockingQueue<>(1);
// 启动3条生产者线程
new Producer(bq).start();
new Producer(bq).start();
new Producer(bq).start();
// 启动一条消费者线程
new Consumer(bq).start();
}
}
本程序的BlockingQueue集合容量时1,因此3个生产者线程无法连续放入元素,必须等待消费者线程取出一个元素后,3个生产者线程的其中之一才能放入一个元素.
线程组合未处理异常
Java
使用ThreadGroup
来表示线程组,它可以对一批线程进行分类管理.Java
允许程序直接对线程组进行控制.对线程组的控制相当于同时控制这批线程.
用户创建的所有线程都属于指定线程组,如果程序没有显示指定线程属于哪个线程组,那么该线程属于默认线程组.在默认情况下,子线程和创建它的父线程都处于同一线程组内,比如:A线程创建了B线程,并且没有指定B线程属于哪一个线程组,那么B线程属于A线程所在的那个线程组.
一旦某个线程加入了指定的线程组之后,该线程将一直属于该线程组,直到该线程死亡.线程运行中途不能改变它所属的线程组.
Thread
类提供了几个构造器来设置新创建的线程属于哪个线程组
Thread(ThreadGroup group, Runnable target)
:以target
的run()
方法作为线程执行体创建新线程,属于group
线程组.
Thread(ThreadGroup group, Runnable target,String name)
:以target
的run()
方法作为线程执行体创建新线程,该线程属于group
线程组,且线程名为name
Thread(ThreadGroup group, String name)
:创建新线程,新线程名字为name
,属于group
线程组.
Thread
类提供了一个getThreadGroup()
方法来返回线程所属的线程组,getThreadGroup()
方法的返回值是ThreadGroup
对象,表示一个线程组.
ThreadGroup
类提供了下面两个简单的构造器来创建实例:
ThreadGroup(String name):
以指定的线程组名字来创建新的线程组
ThreadGroup(ThreadGroup parent,String name):
以指定的名字,指定的父线程组创建一个新线程组.
线程组总会有一个名字字符串类型的名字,该名字可以通过ThreadGroup
的getName()
方法来获取,但是不允许改变线程组名字.
ThreadGroup类提供了如下几个常用的方法来操作整个线程组里的所有线程:
int activeCount()
:返回此线程组中活动线程总数
interrupt()
:中断此线程组中所有线程
isDaemon()
:判断该线程组是否是后台线程组
setDaemon(boolean daemon)
:把该线程组设置成后台线程组.
setMaxPriority(int pri)
:设置线程组的最高优先级
class MyThread extends Thread
{
// 提供指定线程名的构造器
public MyThread(String name)
{
super(name);
}
// 提供指定线程名、线程组的构造器
public MyThread(ThreadGroup group , String name)
{
super(group, name);
}
public void run()
{
for (int i = 0; i < 20 ; i++ )
{
System.out.println(getName() + " 线程的i变量" + i);
}
}
}
public class ThreadGroupTest
{
public static void main(String[] args)
{
// 获取主线程所在的线程组,这是所有线程默认的线程组
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
System.out.println("主线程组的名字:"
+ mainGroup.getName());
System.out.println("主线程组是否是后台线程组:"
+ mainGroup.isDaemon());
new MyThread("主线程组的线程").start();
ThreadGroup tg = new ThreadGroup("新线程组");
tg.setDaemon(true);
System.out.println("tg线程组是否是后台线程组:"
+ tg.isDaemon());
MyThread tt = new MyThread(tg , "tg组的线程甲");
tt.start();
new MyThread(tg , "tg组的线程乙").start();
}
}
ThreadGroup
内还定义了一个void uncaughtException(Thread t,Throwable e)
:该方法可以处理该线程组内的任意线程所抛出的未处理异常.该方法中的t
代表出现异常的线程,e
代表该线程抛出的异常.void uncaughtException(Thread t,Throwable e)
该方法属于Thread.UncaughtExceptionHandler
接口里唯一的一个方法,该接口是Thread
类的一个静态内部接口
Thread类提供如下两个方法来设置异常处理器:
static setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
:为该线程类的所有线程实例设置默认的异常处理器.
setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
:为指定的线程实例设置异常处理器
ThreadGroup
类实现了Thread.UncaughtExceptionHandler
接口,所以每个线程所属的线程组都会作为默认的异常处理器.
当一个线程抛出未处理的异常时,JVM
会首先查找该异常所对应的异常处理器(setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
方法设置的异常处理器),如果找到该异常处理器,则将调用该异常处理器处理该异常;否则JVM
将会调用该线程所属的线程组对象的uncaughtExceptio()
方法来处理该异常.
线程组处理异常的默认流程如下:
- 1.如果该线程组有父线程组,则调用父线程组的
uncaughtException()
方法来处理该异常 - 2.如果该线程实例所属的线程类有默认的异常处理器(由
setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
方法设置的异常处理器),那么就调用该异常处理器来处理该异常. - 3.如果该异常对象是
ThreadDeath
的对象,则不做任何处理;否则,将异常跟踪栈的信息打印到System.err
错误输出流,并结束该线程
下面程序为主线程设置了异常处理器,当主线程运行抛出未处理的异常时,该异常处理器会起作用.
class MyExHandler implements Thread.UncaughtExceptionHandler
{
// 实现uncaughtException方法,该方法将处理线程的未处理异常
public void uncaughtException(Thread t, Throwable e)
{
System.out.println(t + " 线程出现了异常:" + e);
}
}
public class ExHandler
{
public static void main(String[] args)
{
// 设置主线程的异常处理器
Thread.currentThread().setUncaughtExceptionHandler
(new MyExHandler());
int a = 5 / 0; // ①
System.out.println("程序正常结束!");
}
}
结果为:
Thread[main,5,main] 线程出现了异常:java.lang.ArithmeticException: / by zero
说明异常处理器与通过catch
捕捉异常是不同的,当使用catch
捕捉异常时,通常不会向上传播给上一级调用者;但使用异常处理器对异常进行处理之后,异常依然会传播给上一级调用者.
线程池
系统启动一个新线程的成本是比价高的,因为涉及到与操作系统交互,在这种情况下,使用线程池可以很好的提高性能,尤其是当程序中需要创建大量生存期很短的线程时,更应该考虑使用线程池
线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个线程来执行它们的run()或call()方法,当run()或call()方法执行结束后,该线程并不会死亡,而是再次返回线程池中称为空闲状态,等待执行下一个Runnable对象的run()或call()方法.
使用线程池可以有效控制系统中并发线程的数量,当系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至JVM
崩溃,而线程池的最大线程数参数可以控制系统中并发线程数不超过此数.
Java8改进的线程池
Java5新增了一个Executors工厂类来产生线程池,该工厂类提供了如下几个静态工厂方法来创建线程池
使用线程池来执行线程任务的步骤如下:
- 1.调用Executor类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池.
- 2.创建Runnable实现类或Callable实现类的实例,作为线程执行任务.
- 3.调用ExecutorService对象的submit()方法提交Runnable实例或Callable实例
- 4.当不想提交任何任务时,调用ExecutorService对象的shutdown()方法来关闭线程池.
下面程序使用线程池来执行指定Runnable对象所代表的任务
import java.util.concurrent.*;
public class ThreadPoolTest
{
public static void main(String[] args)
throws Exception
{
// 创建足够的线程来支持4个CPU并行的线程池
// 创建一个具有固定线程数(6)的线程池
ExecutorService pool = Executors.newFixedThreadPool(6);
// 使用Lambda表达式创建Runnable对象
Runnable target = () -> {
for (int i = 0; i < 100 ; i++ )
{
System.out.println(Thread.currentThread().getName()
+ "的i值为:" + i);
}
};
// 向线程池中提交两个线程
pool.submit(target);
pool.submit(target);
// 关闭线程池
pool.shutdown();
}
}
Java8增强的ForkJoinPool
Java7提供了ForkJoinPool来支持将一个任务分解为多个"小任务"并行计算,再把多个"小任务"的结果合并成总的计算结果.ForkJoinPool是ExecutorService的实现类,因此是一种特殊的线程池.
ForkJoinPool提供如下两个常用的构造器:
ForkJoinPool(int parallelism)
:创建一个包含parallelism
个并行线程的ForkJoinPool
.
ForkJoinPool():以Runtime.availableProcessors()
方法的返回值作为parallelism参数来创建ForkJoinPool
Java8为ForkJoinPool
增加了通用池功能.ForkJoinPool
类通过如下两个静态方法提供通用池功能:
ForkJoinPool commonPool()
:该方法返回一个通用池,通用池的运行状态不会受shutdown()
或shutdownNow()
方法的影响.如果程序直接执行System.exit(0)
来终止虚拟机,通用池以及通用池中正在执行的任务都会被自动终止.
int getCommonPoolParallelism()
:该方法返回通用池的并行级别.
创建了ForkJoinPool实例之后,就可以调用ForkJoinPool的submit(ForkJoinTask task)或invoke(ForkJoinTask task)方法来执行指定的任务了.其中ForkJoinTask代表一个可以并行,合并的任务.ForkJoinTask是一个抽象类,它有两个抽象子类:RecursiveAction和RecursiveTask.其中RecursiveAction代表有返回值的任务,RecursiveTask代表没有返回值的任务.
执行没有返回值的大任务为例,下面程序示例将一个大任务拆分成多个小任务,并将任务交给ForkJoinPool来执行
import java.util.concurrent.*;
// 继承RecursiveAction来实现"可分解"的任务
class PrintTask extends RecursiveAction
{
// 每个“小任务”只最多只打印50个数
private static final int THRESHOLD = 50;
private int start;
private int end;
// 打印从start到end的任务
public PrintTask(int start, int end)
{
this.start = start;
this.end = end;
}
@Override
protected void compute()
{
// 当end与start之间的差小于THRESHOLD时,开始打印
if(end - start < THRESHOLD)
{
for (int i = start ; i < end ; i++ )
{
System.out.println(Thread.currentThread().getName()
+ "的i值:" + i);
}
}
else
{
// 如果当end与start之间的差大于THRESHOLD时,即要打印的数超过50个
// 将大任务分解成两个小任务。
int middle = (start + end) / 2;
PrintTask left = new PrintTask(start, middle);
PrintTask right = new PrintTask(middle, end);
// 并行执行两个“小任务”
left.fork();
right.fork();
}
}
}
public class ForkJoinPoolTest
{
public static void main(String[] args)
throws Exception
{
ForkJoinPool pool = new ForkJoinPool();
// 提交可分解的PrintTask任务
pool.submit(new PrintTask(0 , 300));
pool.awaitTermination(2, TimeUnit.SECONDS);
// 关闭线程池
pool.shutdown();
}
}
如果大任务是有返回值的任务,则可以让任务继承RecursiveTask<T>,其中泛型T代表该任务的返回类型.
import java.util.concurrent.*;
import java.util.*;
// 继承RecursiveTask来实现"可分解"的任务
class CalTask extends RecursiveTask<Integer>
{
// 每个“小任务”只最多只累加20个数
private static final int THRESHOLD = 20;
private int arr[];
private int start;
private int end;
// 累加从start到end的数组元素
public CalTask(int[] arr , int start, int end)
{
this.arr = arr;
this.start = start;
this.end = end;
}
@Override
protected Integer compute()
{
int sum = 0;
// 当end与start之间的差小于THRESHOLD时,开始进行实际累加
if(end - start < THRESHOLD)
{
for (int i = start ; i < end ; i++ )
{
sum += arr[i];
}
return sum;
}
else
{
// 如果当end与start之间的差大于THRESHOLD时,即要累加的数超过20个时
// 将大任务分解成两个小任务。
int middle = (start + end) / 2;
CalTask left = new CalTask(arr , start, middle);
CalTask right = new CalTask(arr , middle, end);
// 并行执行两个“小任务”
left.fork();
right.fork();
// 把两个“小任务”累加的结果合并起来
return left.join() + right.join(); // ①
}
}
}
public class Sum
{
public static void main(String[] args)
throws Exception
{
int[] arr = new int[100];
Random rand = new Random();
int total = 0;
// 初始化100个数字元素
for (int i = 0 , len = arr.length; i < len ; i++ )
{
int tmp = rand.nextInt(20);
// 对数组元素赋值,并将数组元素的值添加到sum总和中。
total += (arr[i] = tmp);
}
System.out.println(total);
// 创建一个通用池
ForkJoinPool pool = ForkJoinPool.commonPool();
// 提交可分解的CalTask任务
Future<Integer> future = pool.submit(new CalTask(arr , 0 , arr.length));
System.out.println(future.get());
// 关闭线程池
pool.shutdown();
}
}
线程相关类
ThreadLocal类
ThreadLocal它代表一个线程局部变量,通过把数据放在ThreadLocal中就可以让每个线程创建一个该变量的副本,从而避免并发访问的线程安全问题.
ThreadLocal
类支持泛型支持.通过使用ThreadLocal
类可以简化多线程编程中的并发访问,使用这个工具类可以简捷地隔离多线程程序的竞争资源.
线程局部变量(ThreadLocal
)的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,使每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突.
ThreadLocal类的用法非常简单,它只提供如下三个public方法.
T get()
:返回此线程局部变量中当前线程副本中的值
void remove()
:删除次线程局部变量中当前线程的值
void set(T value)
:设置此线程局部变量中当前线程副本中的值
class Account
{
/* 定义一个ThreadLocal类型的变量,该变量将是一个线程局部变量
每个线程都会保留该变量的一个副本 */
private ThreadLocal<String> name = new ThreadLocal<>();
// 定义一个初始化name成员变量的构造器
public Account(String str)
{
this.name.set(str);
// 下面代码用于访问当前线程的name副本的值
System.out.println("---" + this.name.get());
}
// name的setter和getter方法
public String getName()
{
return name.get();
}
public void setName(String str)
{
this.name.set(str);
}
}
class MyTest extends Thread
{
// 定义一个Account类型的成员变量
private Account account;
public MyTest(Account account, String name)
{
super(name);
this.account = account;
}
public void run()
{
// 循环10次
for (int i = 0 ; i < 10 ; i++)
{
// 当i == 6时输出将账户名替换成当前线程名
if (i == 6)
{
account.setName(getName());
}
// 输出同一个账户的账户名和循环变量
System.out.println(account.getName()
+ " 账户的i值:" + i);
}
}
}
public class ThreadLocalTest
{
public static void main(String[] args)
{
// 启动两条线程,两条线程共享同一个Account
Account at = new Account("初始名");
/*
虽然两条线程共享同一个账户,即只有一个账户名
但由于账户名是ThreadLocal类型的,所以每条线程
都完全拥有各自的账户名副本,所以从i == 6之后,将看到两条
线程访问同一个账户时看到不同的账户名。
*/
new MyTest(at , "线程甲").start();
new MyTest(at , "线程乙").start ();
}
}
ThreadLocal和其他同步机制一样,都是为了解决多线程找那个对同一变量的访问冲突,在普通的同步机制中,是通过对象加锁来实现多个线程对同一个变量的安全访问的,该变量是多个线程共享的,所以要使用这种同步机制,要很细致的分析在什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放该对象的锁等等,在这种情况下系统并没有将这份资源复制多份,只是采用安全机制来控制对这份资源的访问而已.
ThreadLocal从另一个角度来解决多线程的并发访问,ThreadLocal将需要并发访问的资源复制多份,每个线程哟拥有一份资源,每个线程拥有自己的资源副本,从而也就没有必要对该变量进行同步了.ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的整个变量封装进ThreadLocal,或者把该对象与线程相关的状态使用ThreadLocal保存.
ThreadLocal并不能代替同步机制,同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式,而ThreadLocal是为了隔离多个线程的数据共享,从根本上上避免多个线程之间对共享资源(变量)的竞争,也就不需要对多个线程进行同步了.
如果多个线程之间需要共享资源,达到线程通信的功能,那么就使用同步机制;如果仅仅需要隔离多个线程之间的共享冲突,可以使用ThreadLocal
包装线程不安全的集合
前面提到了ArrayList,LinkedList,HashSet,TreeSet,HashMap,TreeMap
都是线程不安全的集合,当多个并发线程访问这些集合存取元素时,就可能会破坏这些集合的数据完整性.
可以使用Collections
提供的类方法把这些集合编程线性安全的集合.
如果需要把某个集合包装成线性安全的集合,应该在创建之后立即包装
//使用Collections的synchronizedMap方法将一个普通的HashMap包装成一个线程安全的类
HashMap m=Collections.synchronizedMap(new HashMap());
线程安全的集合类
从Java5开始,在java.util.concurrent包下提供了大量支持高效并发访问的集合接口和实现类