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,如需转载请自行联系原作者


相关文章
|
2月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
158 1
|
2月前
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
173 1
|
3月前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
Java 数据库 Spring
142 0
|
3月前
|
算法 Java
Java多线程编程:实现线程间数据共享机制
以上就是Java中几种主要处理多线程序列化资源以及协调各自独立运行但需相互配合以完成任务threads 的技术手段与策略。正确应用上述技术将大大增强你程序稳定性与效率同时也降低bug出现率因此深刻理解每项技术背后理论至关重要.
229 16
|
4月前
|
缓存 并行计算 安全
关于Java多线程详解
本文深入讲解Java多线程编程,涵盖基础概念、线程创建与管理、同步机制、并发工具类、线程池、线程安全集合、实战案例及常见问题解决方案,助你掌握高性能并发编程技巧,应对多线程开发中的挑战。
|
4月前
|
数据采集 存储 前端开发
Java爬虫性能优化:多线程抓取JSP动态数据实践
Java爬虫性能优化:多线程抓取JSP动态数据实践
|
5月前
|
Java API 调度
从阻塞到畅通:Java虚拟线程开启并发新纪元
从阻塞到畅通:Java虚拟线程开启并发新纪元
355 83
|
5月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
202 0
|
5月前
|
存储 Java 调度
Java虚拟线程:轻量级并发的革命性突破
Java虚拟线程:轻量级并发的革命性突破
332 83