Java多线程感悟一

简介:

写在前面

有时候在项目开发中,时不时的会遇到多线程方面的问题。可是要想驾驭它,就必须了解Java多线程方面的知识,认识它的内在。结合我自己的开发经验以及对Java多线程的一些学习过程,将写几篇关于Java多线程的博客,分享给大家,如有错误,请大家指正。


目录

  1. 线程和进程

  2. Thread和Runnable

  3. 线程的状态

  4. 生产者和消费者模型:wait/notify

  5. 后台线程

  6. interrupt

  7. 线程合并join

  8. UncaughtExceptionHandler


线程和进程

线程和进程的概念相信大家都知道,我来说下我的理解。操作系统可以将系统的一些资源,如CPU,内存等交给一段运行起来的程序,即进程。进程之间一般是相互独立的,比如Nginx的工作模型中有一个master进程,多个worker进程,如果有worker进程“挂”了,并不会影响其他worker进程处理请求。在进程内部可以有多个并发执行流,即线程。线程的存在必须依赖进程,同时一个进程内部的多个线程,它们共享进程的资源,相互之间可以影响。线程是轻量级的进程,它的创建、销毁、切换都比进程要小。



Thread和Runnable

说起Java的多线程,我们脑海里面可能有如下的想法:

要么extends Thread,然后重写run(),调用start()

或者implements Runnable , 实现run(),将Runnable实例传递给Thread构造方法,调用start()


呵呵,其实,以前,我也是这么想的。


那么,在Java线程中,Thread和Runnable到底是什么,各自扮演着什么角色?


看一下Thread的类声明吧:

1
public  class  Thread  implements  Runnable


从上面的来看,好像它们是“一家人”。


我们可以扫一扫Thread的源码,发现:

1
2
3
4
5
/* What will be run. */
private  Runnable target;
public  Thread(Runnable target) {
init( null , target,  "Thread-"  + nextThreadNum(),  0 );
}

其实,Runnable接口扮演的是一个任务的角色,通过提供run()来明确这个任务要做什么。对于Thread而言,默认情况下,它调用的就是你传递给它的Runnable的run()方法。Thread类中的target就是用来接收你传递进来的任务的,这样任务是任务,线程是线程,线程可以执行任务,就这么简单!



线程的状态

线程的状态很重要!

就我自己的感觉而言,我们写多线程方面的代码,就是在进行线程的状态转换从而完成业务上的要求,如果我们不清楚线程的状态,那么我们将无从下手,更加看不懂别人的代码!


根据我的理解,画了张图,一起来看下吧。


wKioL1TDQY-hNnPdAACMYASm-2g174.jpg


线程有5个基本状态:新建、就绪、运行、阻塞、死亡。


当我们新建了一个Thread类的对象时,在JVM中只是存在了一个Thread对象而已,此时OS中并没有真正存在一个线程,只有一个操作线程的外壳,此时为新建状态。


如果我们调用了start()方法,那么将从新建状态转为就绪状态。start()方法的调用结束使得OS完成了内核调用,线程已经存在。需要注意的是,start()调用,run()方法一般并不会同步调用,因为run()的调用是CPU的选择,什么时候调用,我们无法准确干预。


当start()调用结束,系统相关资源准备完毕,那么就会进入运行状态,调用run()。如果在run过程中,失去了CPU时间片,将回到就绪状态。比如yield()方法,这个方法会让出CPU的使用权,希望给其他人用一用,当然这只是它的想法,也许yield()调用进入就绪状态后,马上CPU又调度他执行。


运行状态中,如果发生了sleep/等待锁/IO阻塞/wait等,将进入阻塞状态。如果在阻塞状态中,睡好了/锁来了/IO完成了/notify来了,那么就完成了阻塞状态的解除,从而进入到就绪状态,重新等待CPU的调度。


正常情况下,当run()结束或者发生了未捕获的异常,就会进入死亡状态。



生产者和消费者模型:wait/notify

前面说了那么多理论,下面我们就来点代码练练手吧!


业务场景:

有一个篮子,容量是10个,生产者生产馒头放入其中,消费者从篮子里面拿馒头吃。如果生产者发现篮子里面的馒头已经满了,就不再生产,消费者可以消费。如果消费者发现篮子里面的馒头空了,就停止消费,生产者可以生产。


馒头Model:

1
2
class  ManTou{
}


篮子容器:

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  LanZhi{
private  int  size;
private  int  index =  0 ;
private  ManTou[] foods;
public  LanZhi( int  size){
this .size = size;
this .foods =  new  ManTou[size];
}
public  synchronized  void  produce(ManTou m){
if (index == size){
//不应该在生产馒头
try  {
this .wait();
catch  (InterruptedException e) {
e.printStackTrace();
}
}
//馒头放入篮子
foods[index] = m;
++index;
System.out.println( "生产一个馒头,现在馒头总共有:"  + index);
//发出通知:可以吃馒头了
this .notify();
}
public  synchronized  ManTou eat(){
ManTou m =  null ;
if (index ==  0 ){
//没得吃了
try  {
this .wait();
catch  (InterruptedException e) {
e.printStackTrace();
}
}
m = foods[--index];
System.out.println( "吃掉一个馒头,现在馒头总共有:"  + index);
//发出通知:可以放入馒头了
this .notify();
return  m;
}
}



生产任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class  ProduceRunnable  implements  Runnable{
private  LanZhi l;
public  ProduceRunnable(LanZhi l){
this .l = l;
}
@Override
public  void  run() {
while ( true ){
l.produce( new  ManTou());
try  {
//假设生产馒头耗时0.3S
Thread.sleep( 300 );
catch  (InterruptedException e) {
e.printStackTrace();
}
}
}
}



消费任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class  ConsumeRunnable  implements  Runnable{
private  LanZhi l;
public  ConsumeRunnable(LanZhi l){
this .l = l;
}
@Override
public  void  run() {
while ( true ){
l.eat();
try  {
//假设吃馒头耗时1S
Thread.sleep( 1000 );
catch  (InterruptedException e) {
e.printStackTrace();
}
}
}
}



主线程调用:

1
2
3
4
5
public  static  void  main(String[] args) {
LanZhi l =  new  LanZhi( 10 );
new  Thread( new  ProduceRunnable(l)).start();
new  Thread( new  ConsumeRunnable(l)).start();
}




说明:

wait/notify这两个方法都是在object中定义的方法,都应该在临界区域内发生调用(也就是synchronized区域),否则会发生异常。一个线程好不容易拿到了“门票”,为什么要调用wait呢?因为有些时候,我们拿到了锁,准备进行一些操作的时候,发现并不满足业务上的一些要求,就需要wati了。就好比,我们进入了卫生间,关上了门,蹲在了马桶上,此时发现,没有带纸,没有办法,只好退出卫生间!



后台线程

线程分为前台线程和后台线程。

首先需要注意的是,JVM认为一旦所有的前台线程运行完毕,只剩下后台线程的话,那么就意味着程序要结束了。


举个例子:

1
2
3
4
5
6
7
8
class  Daemon  extends  Thread{
@Override
public  void  run() {
for ( int  i =  0  ; i <  10  ; i++){
System.out.println( "i : "  + i);
}
}
}



主线程调用:

1
2
3
4
5
6
public  static  void  main(String[] args) {
Daemon d =  new  Daemon();
d.setDaemon( true );
d.start();
System.out.println( "hello" );
}
1
2
3
4
5
6
7
8
9
10
运行结果:
hello
i :  0
i :  1
i :  2
i :  3
i :  4
i :  5
i :  6
i :  7



hello的输出,意味着前台线程已经运行完毕了,那么JVM将会结束整个进程,并且不保证执行完后台线程。那么后台线程可以用来干嘛?比如Java的垃圾回收线程就是一个后台线程,而且它的调度优先级比较低,它不会随便去和其他线程争夺使用CPU。


interrupt

interrupt,即中断的意思,那中断意味着什么?


我们先看看下面的:

1
2
3
public  final  native  void  wait( long  timeout)  throws  InterruptedException;
public  static  native  void  sleep( long  millis)  throws  InterruptedException;
public  final  synchronized  void  join( long  millis)   throws  InterruptedException


我们知道有些方法会抛出中断异常,那什么时候抛呢?


再去扫一眼Thread源码,发现:

1
2
3
4
5
6
7
  public  static  boolean  interrupted() {
return  currentThread().isInterrupted( true );
  }
  public  boolean  isInterrupted() {
return  isInterrupted( false );
  }
  private  native  boolean  isInterrupted( boolean  ClearInterrupted);

在默认情况下,显然,线程的中断标志是false。当我们调用了interrupted()后,实际上是将线程中断标志设置为true。


看一个小例子:

1
2
3
4
5
6
7
8
9
10
11
12
class  Thread1  extends  Thread{
@Override
public  void  run() {
try  {
Thread.sleep( 10000 );
//其他一些业务操作
System.out.println( "sleep over..." );
catch  (InterruptedException e) {
System.out.println( "出现中断异常" );
}
}
}

主线程调用:

1
2
3
Thread1 t1 =  new  Thread1();
t1.start();
t1.interrupt();


运行结果:

出现中断异常



说明interrupt()的调用,只是做了一个标记并产生异常抛出而已,并没有真正的唤醒线程。



线程合并join

线程合并?为什么多个线程要合并?什么时候会出现需要合并呢?怎么合并?


假设有这么一个业务排序场景:


我们有N个这样的块,每个块中存放数字,而且块间有序、块内是无序的,如下:

第一块数字范围:[0,1000]

第二块数字范围:[1001,2000]

第三块数字范围:[2001,3000]

...


分析:

不应该对所有块的所有数字来一次全排序,很显然的是,我们想利用块间有序这个条件,能不能对每一块进行排序。要知道,第一块排序和第二块排序,他们是相互独立的,没有锁的限制,完全可以并行进行。那我们的初步想法是,启动N个线程对N个块进行排序,当N个线程都结束,就完成了排序了。但是要知道,我们无法保证每个排序线程的完成顺序,但是业务上,我们确实需要当排序完毕后进行XXX操作。

当然,我们可以写一个循环进行遍历,来获取N个排序线程的状态,从而决定排序完毕后的操作什么时候进行。其实,Java已经提供了join(),来完成这方面的需要。


实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class  SortRunnable  implements  Runnable{
int [] array;
public  SortRunnable( int [] array){
this .array = array;
}
@Override
public  void  run() {
for ( int  i =  0  ; i < array.length ; i++){
for ( int  j = i+ 1  ; j < array.length ; j++){
if (array[i] > array[j]){
try  {
Thread.sleep( 1000 );
catch  (InterruptedException e) {
e.printStackTrace();
}
int  tmp = array[i] ^ array[j];
array[i] = tmp ^ array[i];
array[j] = tmp ^ array[j];
}
}
}
}
}

主线程调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int [] array1 = { 1 , 9 , 8 , 7 };
int [] array2 = { 10 , 30 , 25 };
Thread t1 =  new  Thread( new  SortRunnable(array1));
Thread t2 =  new  Thread( new  SortRunnable(array2));
t1.start();
t2.start();
t1.join();
t2.join();
for ( int  tmp : array1){
System.out.print(tmp +  " , " );
}
System.out.println();
for ( int  tmp : array2){
System.out.print(tmp +  " , " );
}



UncaughtExceptionHandler

直接看一个例子,大家就能明白!

1
2
3
4
5
6
class  MyException  implements  UncaughtExceptionHandler{
@Override
public  void  uncaughtException(Thread t, Throwable e) {
System.out.println( "捕捉到线程中未处理的异常" );
}
}


主线程调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Thread t =  new  Thread(){
@Override
public  void  run() {
//业务操作
//...
System.out.println( "start..." );
Integer.parseInt( "a" );
//业务操作
//...
System.out.println( "end..." );
}
};
t.setUncaughtExceptionHandler( new  MyException());
t.start();

运行结果:

start...

捕捉到线程中未处理的异常



说明:

如果在run()中出现了我们没有处理的异常,那么我们还有一次“后悔药”可以吃~



本文转自zfz_linux_boy 51CTO博客,原文链接:http://blog.51cto.com/zhangfengzhe/1607712,如需转载请自行联系原作者


相关文章
|
9天前
|
存储 Java 数据库连接
java多线程之线程通信
java多线程之线程通信
|
9天前
|
安全 Java 开发者
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第9天】本文将深入探讨Java并发编程的核心概念,包括线程安全和性能优化。我们将详细解析Java中的同步机制,包括synchronized关键字、Lock接口以及并发集合等,并探讨它们如何影响程序的性能。此外,我们还将讨论Java内存模型,以及它如何影响并发程序的行为。最后,我们将提供一些实用的并发编程技巧和最佳实践,帮助开发者编写出既线程安全又高效的Java程序。
22 3
|
11天前
|
Java
Java 并发编程:深入理解线程池
【4月更文挑战第8天】本文将深入探讨 Java 中的线程池技术,包括其工作原理、优势以及如何使用。线程池是 Java 并发编程的重要工具,它可以有效地管理和控制线程的执行,提高系统性能。通过本文的学习,读者将对线程池有更深入的理解,并能在实际开发中灵活运用。
|
9天前
|
算法 Java 开发者
Java中的多线程编程:概念、实现与性能优化
【4月更文挑战第9天】在Java编程中,多线程是一种强大的工具,它允许开发者创建并发执行的程序,提高系统的响应性和吞吐量。本文将深入探讨Java多线程的核心概念,包括线程的生命周期、线程同步机制以及线程池的使用。接着,我们将展示如何通过继承Thread类和实现Runnable接口来创建线程,并讨论各自的优缺点。此外,文章还将介绍高级主题,如死锁的预防、避免和检测,以及如何使用并发集合和原子变量来提高多线程程序的性能和安全性。最后,我们将提供一些实用的性能优化技巧,帮助开发者编写出更高效、更稳定的多线程应用程序。
|
7天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第11天】 在Java中,高效的并发编程是提升应用性能和响应能力的关键。本文将探讨Java并发的核心概念,包括线程安全、锁机制、线程池以及并发集合等,同时提供实用的编程技巧和最佳实践,帮助开发者在保证线程安全的前提下,优化程序性能。我们将通过分析常见的并发问题,如竞态条件、死锁,以及如何利用现代Java并发工具来避免这些问题,从而构建更加健壮和高效的多线程应用程序。
|
11天前
|
Java
Java并发编程:深入理解线程池
【4月更文挑战第7天】在现代软件开发中,多线程编程已经成为一种不可或缺的技术。为了提高程序性能和资源利用率,Java提供了线程池这一强大工具。本文将深入探讨Java线程池的原理、使用方法以及如何根据实际需求定制线程池,帮助读者更好地理解和应用线程池技术。
15 0
|
2天前
|
存储 安全 Java
Java中的容器,线程安全和线程不安全
Java中的容器,线程安全和线程不安全
7 1
|
3天前
|
设计模式 运维 安全
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第15天】在Java开发中,多线程编程是提升应用程序性能和响应能力的关键手段。然而,它伴随着诸多挑战,尤其是在保证线程安全的同时如何避免性能瓶颈。本文将探讨Java并发编程的核心概念,包括同步机制、锁优化、线程池使用以及并发集合等,旨在为开发者提供实用的线程安全策略和性能优化技巧。通过实例分析和最佳实践的分享,我们的目标是帮助读者构建既高效又可靠的多线程应用。
|
4天前
|
Java 程序员 编译器
Java中的线程同步与锁优化策略
【4月更文挑战第14天】在多线程编程中,线程同步是确保数据一致性和程序正确性的关键。Java提供了多种机制来实现线程同步,其中最常用的是synchronized关键字和Lock接口。本文将深入探讨Java中的线程同步问题,并分析如何通过锁优化策略提高程序性能。我们将首先介绍线程同步的基本概念,然后详细讨论synchronized和Lock的使用及优缺点,最后探讨一些锁优化技巧,如锁粗化、锁消除和读写锁等。
|
6天前
|
Java
探秘jstack:解决Java应用线程问题的利器
探秘jstack:解决Java应用线程问题的利器
14 1
探秘jstack:解决Java应用线程问题的利器