Java之多线程
进程
进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单元。
1.独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址
空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
2.动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统活动的指令集合。
在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都
是不具备的。
3.并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。
线程
1.线程是进程的执行单元,是进程的组成部分。
2.一个进程可以拥有多个线程,一个线程必须有一个父进程。
3.线程可以有自己的堆栈,自己的程序计数器和自己的局部变量,但不拥有系统资源。
4.它与父进程的其他线程共享该进程所拥有的全部资源。
5.线程是独立运行的,他并不知道其他线程的存在。
6.线程的执行是抢占式的。
7.线程可以创建和销毁另一个线程,同一个进程中的多个线程之间可以并发执行。
8.线程的调度和管理由进程本身负责完成。
多线程的优势
1.进程之间不能共享内存,但线程之间可以共享内存。
2.系统创建进程时需要为该进程重新分配系统资源,但创建栈则代价小得多,因此使用多线程来实现多任务
并发比多线程的效率高。
3.Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化Java的多线程编程。
线程的创建
1.继承Thread创建线程类
1
2
3
4
5
6
7
8
9
10
11
|
public class ThreadTest extends Thread{ // 1.继承Thread线程类
public void ThreadWork(){ // 2.编写Thread执行体
// 线程执行体
}
public static void main(String[] args){
for ( int i= 0 ;i< 10 ;i++){ // 启动10个线程
new ThreadTest().start(); // 3.调用线程对象的start()启动线程
}
}
} |
2.实现Runnable接口创建线程类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class ThreadTest implements Runnable{ // 1.继承Runnable
// 2.重写run方法
public void run(){
// 线程执行体
}
public static void main(String[] args){
// 3.创建Runnable实例,并以为实体作为Thread的target来创建线程对象
ThreadTest st = new ThreadTest();
for ( int i= 0 ;i< 10 ;i++){
new ThreadTest(st, "新线程" + i).start(); // 4.启动线程
}
}
} |
3.使用Callable和Future创建线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public class ThreadTest implements Callable<Integer>{ // 1.继承Callable
// 2.实现call方法,他可以有返回值和抛出异常功能
public Integer call(){
// 线程执行体
try {
Integer a = new Integer( 55 );
} catch (Exception e){
}
return a;
}
public static void main(String[] args){
// 3.创建Callable实例,并以为实体作为Thread的target来创建线程对象
ThreadTest st = new ThreadTest();
// 4.使用FutureTask来包装Callable对象
FutureTask<Integer> task = new FutureTask<Integer>(st);
for ( int i= 0 ;i< 10 ;i++){
new ThreadTest(st, "新线程" + i).start(); // 5.启动线程
}
}
} |
线程的生命周期
1.新建:当程序使用new关键字创建一个线程之后,分配了内存和初始化其成员变量的值。
2.就绪:调用了start()方法后,虚拟机为其创建方法调用栈和程序计数器。
3.运行:如果处于就绪状态后获得了CPU,开始执行线程体内代码。
4.阻塞:当发生如下情况时,线程将会进入阻塞状态
⑴.调用了sleep()方法主动放弃所占用的处理器资源
⑵.调用了一个阻塞式IO方法
⑶.试图获得一个同步监视器,但该同步监视器整备其他线程所持有
⑷.等待某个通知
⑸.调用了线程的suspend()方法将该线程挂起-----该方法容易导致死锁,通常不推荐使用。
5.死亡:以下3种方式结束,结束后就处于死亡状态。
⑴.run()或call()方法执行完成,线程正常结束。
⑵.线程抛出一个未捕获的Exception或Error。
⑶.直接调用该线程的stop()方法来结束该线程-----该方法容易导致死锁,通常不推荐使用。
线程方法
join(): 让一个线程等待另一个线程完成的方法----join()方法,当在某个程序执行流中调用其他线程的join()方
法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。
sleep():需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread带的静态sleep()
方法。
yield():和sleep()类似,但它不会阻塞该线程,只是将该线程转入就绪状态,它只是让当前线程暂停一下,让系
统的线程调度器重新调度一次。
改变线程优先级
使用setPriority(int newPriority)来设置线程的优先级。优先级越高越有可能抢占到CPU,其参数为:
1.MAX_PRIORITY: 其值是10
2.MIN_PRIORITY: 其值是1
3.NORM_PRIORITY:其值是5
线程同步
为什么要同步
因为线程调度的不确定性,当多个并发线程修改了同一个份资源,可能会造成不确定错误。
同步工具
任何线程在修改资源之前,首先对资源加锁,在加锁期间其他线程无法修改该资源,当该线程修改完成后,
该线程释放对该资源的锁定。通过这种方式就可以保证并发线程在任一时刻只有一个线程可以进入修改共享
资源的代码区(也被称为临界区),所以同一时刻最多只有一个线程处于临界区内,从而保证了线程的安全
性。
1.同步代码块
1
2
|
synchronized (obj){ //
} |
2.同步方法
1
2
3
4
|
public class ThreadTest extend Thread{
public synchronized void work(){
}
} |
3.同步锁
1
2
3
4
5
6
7
8
|
public class ThreadTest{
private final ReentrantLock lock = new ReentrantLock();
public void work(){
lock.lock(); // 加锁
.....
lock.unlock(); // 解锁
}
} |
释放同步监视器的锁定
1.当前线程的同步方法,同步代码块执行结束,当前线程即释放同步监视器。
2.当前线程在同步代码块,同步方法中遇到break,return终止了该代码块,该方法的继续执行,当前线程将会
释放同步监视器。
3.当前线程在同步代码块,同步方法中出现了未处理的Error或Exception,导致了该代码块,该方法异常结束时
,当前线程将会释放同步监视器。
4.当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放
同步监视器。
线程通信
程序无法准确控制线程的轮换执行,但可以通过一些机制来保证线程的协调运作,从而达到通信效果。
我们可以通过设定一个旗标来标识线程是否继续执行,当旗标为false时,线程A就继续执行,当线程A的工作做
完,让旗标为true,并调用通知函数来唤醒其他线程,如果这时线程A再次进入到此线程执行体时,发现旗标为
true则调用等待函数来等待其他线程工作。这时线程B进入线程执行体发现旗标为true,这时他就开始工作,线
程B工作完毕后,将旗标设置为false,并调用通知函数通知其他线程,如此循环,进而控制线程协调工作。
1.使用Condition控制线程通信
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public class ThreadTest{
// 定义lock
private final ReentrantLock lock = new ReentrantLock();
// 获得Lock对象对应的Condition
private final Condition cond = lock.newCondition();
public void work(){
lock.lock(); // 加锁
if (!flag){
cond.await(); // 等待函数
}
else {
// work
flag = false ;
cond.signalAll(); // 唤醒函数
}
lock.unlock(); // 解锁
}
} |
2.使用阻塞队列(BlockingQueue)控制线程通信
BlockingQueue的特征
1.当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程
试图从BlockingQueue中取出元素时,如果该队列为空,则该线程被阻塞。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
class Warehouse extends Thread{
private BlockingQueue<String> bq;
public Warehouse(BlockingQueue<String> bq){
this .bq = bq;
}
public void run(){
String[] strArr = new String[]{
"Watermelon" , "Banana" , "Apple"
};
for ( int i= 0 ;i< 10000 ;i++){
System.out.println(getName() + "开始向仓库存放货物!" );
try {
Thread.sleep( 100 );
bq.put(strArr[i% 3 ]); // 如果该队列已满,则该线程被阻塞
} catch (Exception e){
e.printStackTrace();
}
System.out.println(getName() + "存放完成" );
}
}
} class Worker extends Thread{
private BlockingQueue<String> bq;
public Worker(BlockingQueue<String> bq){
this .bq = bq;
}
public void run(){
while ( 1 ){
System.out.println(getName() + "工人开始取出货物!" );
try {
Thread.sleep( 200 );
bq.take(); // 如果该队列为空,则该线程被阻塞
} catch (Exception e){
e.printStackTrace();
}
System.out.println(getName() + "取出完成" );
}
}
} public class BlockingQueueTest{
public static void main(String[] args){
BlockingQueue<String> bq = new ArrayBlockingQueue<>( 2 );
// 1个仓库
new Warehouse(bq).start();
// 2个工人
new Worker(bq).start();
new Worker(bq).start();
}
} |
线程组
使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,允许直接对线程组进行控制,可以同时控制
线程组中的所有线程。
线程组的创建
所有用户创建的所有线程都属于指定线程组,如果程序没有显示指定线程属于哪个线程组,则该线程属于默
认线程组。在默认情况下,子线程和创建它的父线程处于同一个线程组内。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
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();
}
} |
线程未处理的异常
如果线程执行过程中抛出异常,JVM会去查找是否有对应的Thread.uncaughtExceptionHandler对象,如果
找到该处理器对象,则会调用该对象的uncaughtException(Thread t,Throwable e)方法来处理该异常。
线程组处理异常默认流程:
1.查找该组是否有父线程组,则调用父线程组的uncaughtException方法来处理。
2.如果该线程组实例所属的线程类有默认异常处理器,则调用来处理。
3.如果该异常对象是ThreadDeath对象,则不作任何处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// 定义自己的异常处理器 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( "程序正常结束!" );
}
} |
线程池
使用线程池可提高性能,尤其是程序中需要创建大量生存期很短暂的线程时。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
// 实现Runnable接口来定义一个简单的线程类 class MyThread implements Runnable
{ public void run()
{
for ( int i = 0 ; i < 100 ; i++ )
{
System.out.println(Thread.currentThread().getName()
+ "的i值为:" + i);
}
}
} public class ThreadPoolTest
{ public static void main(String[] args)
throws Exception
{
// 创建一个具有固定线程数(6)的线程池
ExecutorService pool = Executors.newFixedThreadPool( 6 );
// 向线程池中提交两个线程
pool.submit( new MyThread());
pool.submit( new MyThread());
// 关闭线程池
pool.shutdown();
}
} |
ForkJoinPool
支持将一个任务拆分成多个小任务并行计算,再把多个小任务的结果合并成总的计算结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
// 继承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();
}
} |
其他线程类
ThreadLocal:线程局部变量,通过把数据放在ThreadLocal中就可以让每个线程创建一个该变量的副本,从而
避免并发访问的线程安全问题。