`

感受Java中的多线程设计

    博客分类:
  • java
 
阅读更多

我就不说最初那个单核CPU时代了,我们从多进程编程开始讲。在引入多线程概念前,多进程是并发编程的唯一解决方案;多进程在解决并发问题的同时带来了一些问题:主要有以下几点,多线程也就是正因为多进程有许多不足才被设计出来:

多进程的特点:每个进程都独立拥有数据空间(堆、栈、代码区等),这是多线程跟多进程最本质的区别,这个区别是多线程与多进程优缺点的起因

多进程缺点:

 

  • 进程间数据共享困难
  • 进程调试时上下文切换开销大
  • 线程给异步设计提供了方便的设计模式

 

多进程优点:

 

  • 进程与进程间的执行没有相互干扰(暂不考虑进程间通讯问题),进程基本上可以随时终时,所以操作系统会提供给我们任务管理器来强制终止进程,或是提供kill命令。线程这样做往往很危险,线程的终止方法也很特殊,后面我们会讲到;因为强制终止线程不能保证数据的一致性、完整性。
  • 进程能非常方便地应用于分布式环境

 

结论:多线程是不能完全替换多进程,但多线程带来的革命也是明显的,我程序设计中,我们要跟据实际情况,选择合理的并发方案;其实好多时候,我们往往是两者结合来用。好了,对多线程的基本概念讲到这里,我们以下要讲讲Java对多线程的支持。

Java多线程

线程对象设计

如果Java不是一门面象对象语言,那么我们要学习的东西可能少得多,但使用起来并不一定方便。我们先来讲一下Java中多线程的设计。Java中一切都是对像,线程也是,线程是CPU调度的单位,它本身跟具体的程序功能无关;所以Java把程序的功能又独立提取出来,放到Runnable或Callable接口中;最后Java添加了一个管理类ExecotorSerivce,用于管理多个线程的执行。

Thread类

一个Thread类表示一个线程,其实一个线程可以抽象为一个程序指针,保存了程序当前的执行进度;当然,其实一个线程的实现相对复杂,还包括锁的实现等,这个说法仅供理解。

一个thread对象包含了一些关键信息

 

  • 线程的状态:线程栈信息(每个线程都有个独立的栈)、中断标记信息(用于停止线程)、守护线程标记、名字、优先级等
  • 线程的一个协作方法:t.join(); //让调用者等待t线程的执行,并阻塞调用线程。
  • run(); //重新调用new Runnable().run(); 语法上讲,可以在这个方法里直接添加线程功能,但不推介。
  • start(); //启动线程
Thread类方法:
Thread类里有一些特殊的方法,这些方法都默认针对“当前运行”的线程,其它是一些所有线程的总体信息,以下是一些关键方法
  • currentThread(); //获得当前线程
  • sleep(); //让当前线程等待一段时间,些时不释放锁,仅不去抢占CPU。sleep并不直接干扰其它线程的执行。
  • yield(); //让当前线程释放一下CPU。
  • interrupted(); //返回当前线程的中断状态,并重置中断标记。

 

Runnable & Callable类

线程执行的具体功能被放在这个接口类里,如果涉及到资源协作(锁或是synchronized),我一般不直接在这两个类里处理这些,因为Runnable及Callable是两个行为类,一般不在这两个类里直接包含资源,所以一般不涉及到资源的锁问题;这样做也是为了让自己的程序更加容易阅读;然而一般退出线程的逻辑却应放在这个类里。

Callable相对于Runnable给我们带来了两个额外功能

 

  • 线程执行返回执行结果 Callable<returnType>
  • 可抛出异常,表示取得返回值的过程中遇到了其它状况,而不至于让我们发现返回值莫名其妙地没了

 

线程返回结果其实在绝大多数情况下其它是没必要的,但这种情况也不算太少数;我想说明的是,相对于Runnable来说Callable并没有带来革命性的更改,所以喜欢优先用哪个,我们可以跟据需要来;我一般会优先用Runnable+ExecutorSerivce;因为即使突然发现需要返回值,把Runnable修改为Callable也不是一件复杂的事。

ExecutorService管理类

相对于Callable,我觉得这个类实用多了;这类有以下特点:

 

  • 个数限制:以池的方式管理线程,用户不再需要自己创建线程,并能对线程的总个数加以限制;
  • 线程复用:除此之外,我们知道一般情况下我们都是一个线程对应一个Task,这样的线程是不能复用的,虽然我们说创建线程的开销远小于创建进程,但创建一个线程的开销相对于一般的编程还是要大得多的,默认Thread对象也没有给我们提供支持多个Task的方法,原因是需要满足太多的条件,这样做很危险;而ExecutorSerivce的池机制已经帮我们实现了这个功能。
  • 最后,ExecutorService提供了submit方法;这个方法有两个功能1、Runnable与Callable统一创建接口;2、跟踪线程的执行状态Future<return type>。

 

异常处理

异常是与线程对应的,我们知道当主线程抛出uncatched异常时,会造成jvm挂掉,并打印出栈信息。虽然这个功能也就这样,但至少我们知道自己的程序已经出状况了,需要重新维护。

子线程默认不会通知uncached异常信息,出现异常时,子线程挂掉,但我们什么也不知道。我们可以通过以下方法来外理这种异常

 

Thread.UncaughtExceptionHandler eh = new Thread.UncaughtExceptionHandler() {
			
			@Override
			public void uncaughtException(Thread t, Throwable e) {
				// TODO Auto-generated method stub
				
			}
		};
		t.setUncaughtExceptionHandler(eh); //一个线程对应一个异常控制器
		Thread.setDefaultUncaughtExceptionHandler(eh); //默认所有线线程都用这个异常控制器

 

其它:守护线程及优先级

设置守护线程方法

 

t.setDaemon(true);//请注意顺序
t.start();
守护线程概念

 

有这样一些线程,它们仅给其它线程提供后台服务,它们独立存在的话没有任何意义,我们往往把它们设置为守护线程;我们再说一点,一个Java进程都对应一个运行时的jvm,那么什么时候决定这个java进程已经执行完了可以退出jvm呢?回答是除了守护线程之外已经没有其它线程了,换句话说,程序已经不对外执行计算任务了,那么这个时候进程就可以结束了。

优先级

优先级仅是CPU调度的一个指标,其实影响CPU调度的原因很多。知道这点就可以了。

CPU协调

线程是CPU调度的基本单位,一个线程可以用以下方法直接影响CPU调度,且不涉及到资源及锁。CPU协调就是一般说的CPU竞争,我觉得用协调更好一些,因为现在我们程序员的素质高,不会占着CPU不放了;不像一些软件,比如,卡巴死机。

sleep()方法

sleep原理:创建并启动一个闹钟,并传入回调(回调用于继续执行),然后挂起当前线程;sleep并不释放锁,所以尽量不要在获得锁的时候用sleep,那就是占着茅坑不...了,呵呵。

 

Thread.sleep(millsec); //让当前线程sleep一会

Yield()方法

 

暂停当前线程,使其它线程能获得cpu。

 

Thread.yield();

 

资源协调

多线程真正有意思的地方我想就是这部分了,我们程序员不是干体力活的;这部分是最能体现我们程序员实力的地方了。

多线程里有句老话,大致这个意思:一切多线程问题的根源是对共享数据的访问。这句话绝对是95%正确的。5%我忽略不计了,算买个保险吧。

Java中多线程间的资源共享是通过“互斥锁”来实现的,它分为两种语法:

 

  • synchronized关键字为主的块状锁定语法。它的原理是锁定目标对象对应的monitor,之后我们会比较详细的说明这一部分
  • Lock接口为基础的显式锁

 

原子性 & volatile关键字

在讲具体锁之前,我想先讲两个非常重要的,但实际使用时很少使用(至少我是这样的)

原子性

理解原子性的概念非常重要。原子性指一个操作是CPU执行的最小单位,不能被打断。对具有原子性的操作,原理上讲我们是不用给它们加锁的。

volatile关键字作用

Java线程一般会把共享变量等拷贝一份副本到线程“本地栈”,从而提高性能;拷由的时机往往是这样的:在获得锁时拷贝,在释放锁的时候写回(包含wait,notify前后那些隐藏的锁交换);

用volatile修饰的变量表示这样一个意思:不对共享变量进行拷贝,直接访问共享变量,以达到变量值的一致性

实站:我们很难确定一个操作的原子性,这往往跟不同的JVM实现有关;举两个例子:i++,不是原子的。即使long l=5;这样一个操作在一个32位机子上也不是原子的,我们知道java中的长整型应该是64位的,而32位机器一次仅能处理32位,那么有可能,这个一赋值操作会分为两个原子操作,先赋低32位,再赋高32位。当然如果操作系统做了特殊处理的话,那另当别论。关于volatile也一样,即使加了volatile我们还得考虑原子性,而原子性往往是得不到保障的,所以volatile的使用也比较难。

我觉得一般的做法是不要考虑这两个概念,尽量给共享的资源加上锁。即使在我们认为是原子操作的地方也加上锁,因为原子操作往往非常“精细”,锁定的时间间隔自然也相对较短。

块状隐式锁synchronized

先说一下锁的概念:一个目标锁同时仅能被一个线程占用,线程要访问锁之间的代码的话必须先获取锁;(这里先不考虑特殊的ReadWriteLock)

每个java对象都对应有一个monitor。synchronized就是通过锁定这个monitor对象来达到锁定的目的;相对于显式的lock,synchronized语法的最大好处应该是会自动释放锁。synchronized有以下三种用法

 

  • 在类方法前加锁:表示锁定的是当前类对象(ClassName.class)的monitor;
  • 在实例方法前加锁:表示锁定的是当前的实例对象;
  • 在代码块里加锁:可以指定具体的目标对象,语法synchronized(o){...}
这样的块状锁非常容易使用,但在处理一些复杂情况时却真的无能为力,以显式锁部分我们会再讲到。

 

显式锁Lock

相对于用synchronized方法及语句,Lock提供了更加灵活的、可扩展的锁操作。它提供了更加灵活的语法结构,有的(指Lock的实现类)可能拥有一些特性,如它有可能支持多个与之关联的 Condition 对象

synchronized方式的一些不足

synchronized方法及语句提供了访问隐藏锁的方法,每个对象都关联着一个隐藏锁,但限制了锁的获得及释放必须发生在一个块状结构里:当同时获得多个锁时,这些锁必须以相反的顺序释放,而且,所有的锁的释放与锁的获取必须在同一个文法范围(可以理解为同一层缩进,同一个方法,同一个块里)。

虽然syncronized方法及语句的块式机制让我们能够轻松地运用隐藏锁机制来进行多线程编程,并且让我们避免了锁编程中的一些普遍编程问题。但还是有一些情况你不得不使用更加灵活的Lock方式(显式的Lock)。

一个必须得用显式Lock的实例

比如:一些并行遍历数据结构的算法,需要一种“一步步地”或“链接式的锁定”:先获得节点A的锁,再获得节点B的锁,然后释放对A的锁,获得对C的锁,释放对B的锁,再获得对D的锁 ...。Lock接口的实现类就能够让这些技法的使用变为可能,它实现了让锁能在不同的块结构里获得并释放,并允许多个锁可以以任何顺序获得或释放。

显式锁的特点(还是与实现类有关,并不是说每种显式锁都一定支持这些特点):

 

  • 能关联多个Conditioin对象
  • 可以随时随地加锁,锁与锁之前不一定要“嵌套”。
  • 获得锁时可以被中断(synchronized在获取锁时是不能被中断的),这点在处理死锁时非常有用(解除死锁仅需发一个中断 )
  • 其它还有其它一些有用的方法,这里不一一例举了

 

继承结构

java.util.concurrent.locks.Lock

  `-ReentrantLock

  `-ReentrantReadWriteLock

       `-ReadLock

       `-WriteLock

说实话,我不知道他这里为什么要把类名取为Reentrant(可重入),我觉得这根本与可重入的概念没有什么关系。有关可重入的概念大家要以自行从网上搜索。

wait & notify & notifyAll;

wait概念

由于程序执行需要的资源没有达到程序要求,而主动放弃已经获得的对象锁(在synchronized里对象指的是monitor),此时线程进入目标对象(monitor)的等待队列,让其它线程准备完资源后再重新通知锁定的对象(monitor),并让对象(monitor)进一步通知在等待队列里的线程。

notify & notifyAll

一般情况下,我们都用nofityAll(),它会通知所有等待的线程,都去尝试获得锁;而notify();仅会从等待队列中挑选一个线程,如果被挑选的线程,但如果被挑选的线程此时并没有满足执行的条件,进入wait,那么就可能造成所有的线都都进入了wait状态。

锁的目标对象与资源的关系

其实两者并没有直接的关系,但目标对象的选择必需与想要锁定的资源在逻辑上要能大约等同。这个在Java语言里比较好选择,一般把资源封装在对象里,然后把对象(或对象的monitor)当成目标对象。

线程式局部变量

比如:LocalThread<Integer> locThd = ...;做为一个共享变量的话,它会自动为每个线程自动创建一份拷贝,线程与线程内的数据不影响。这个类在线程内传递参数非常有用,我们可以把一个线程需要的一些公共信息放在LocalThread里。

终止线程

这个是个很有意思的话题,网上有一些终止线程的方法,比如判断标记,用interrupt中断,也有说中断并不能用来终止线程;那么到底是怎么回事?

线程是没法直接强制终止的

直接强制终止一个线程,会造成数据的丢失,不一致等,这对一个程序来说是非常致命的,所以Java里希望线程都能够“主动退出”;

判断标记的缺点

判断标记法自然是终止线程的一个可行的方法,但有一个不足:当线程被堵塞时(sleep, wait, join)线程无法进入判断,更无法即时退出;尤其是当线程进入DeadLock时,就更没有办法了。

再说说interrupt

interrupt有两个作用,一个是给线程添加interrupt标记,另一个是终止正被堵塞的线程,并让它们抛出一个InterruptedException,抛出interruptException之后,线程会自动清除interrupt标记。interrupt虽然不能直接退出所有线程,但至少当前已经堵塞的线程能立马退出,那些未堵塞的线程也能比较顺利地进入一个“判断标记”,然后自动退出线程。所以总的来说,结合interruptException来退出线程是很不错的,也可以直接用isInterrupt()函数来获取interrupt标记直接判断,其它判断标记的添加可以以语镜为准。

如果你使用是的ExecutorService,那么,有两个方法与线程关闭相关

 

  • shutdown(); //表示ExecutorSerivce对象不再继续接受其它对象的执行,但不影响已经提供的线程。
  • shutdownNow(); //在shutdown的语义上,再分别对已经提交的线程发起interrupt中断,试图中断每一个线程,线程的最终退出逻辑需线程自己判断interrupt标记。
  • awaitTermination(timeout, unit); //使当前线程堵塞并等待ExecutorSerivce执行完所有已经提供的线程,并返回结果。

 

 

来个实例

  1. package test;  
  2.   
  3. import java.io.IOException;  
  4. import java.util.concurrent.ExecutorService;  
  5. import java.util.concurrent.Executors;  
  6.   
  7.   
  8. public class MultiThreadDemo {  
  9.       
  10.     /** 
  11.      * 这里我们定义一个线程安全的的ResHolder类 
  12.      * add 用于添加某个资源,为了保证资源耗尽,我们给它设定一个上限Max,如果大于这个上限,那么就让clear方法重置一下. 
  13.      * @author Mr.He 
  14.      */  
  15.     static class ResHolder{  
  16.           
  17.         public static final int Max = 10000//read only   
  18.         private int i = 0;//把这个当成共享变量   
  19.           
  20.         /** 
  21.          * 我们一般不会在这个类里处理interruptedException,因为我们在这里不能确认是否要退出线程. 
  22.          * 而synchronized等锁操作一般都放在这个类里 
  23.          * @throws InterruptedException 
  24.          * @author Mr.He 
  25.          */  
  26.         public synchronized void add() throws InterruptedException{  
  27.             while(true){  
  28.                 if(i<Max){  
  29.                     i++;  
  30.                 }else{  
  31.                     notifyAll();  
  32.                     wait();  
  33.                 }  
  34.             }  
  35.         }  
  36.           
  37.           
  38.         public synchronized void clear() throws InterruptedException{  
  39.             while(true){  
  40.                 if(i<Max){  
  41.                     wait();  
  42.                 }else{  
  43.                     System.out.println("Clear value from 100 to 0 [Input ENTER to exit]");  
  44.                     i = 0;  
  45.                     notifyAll();  
  46.                 }  
  47.             }  
  48.         }  
  49.           
  50.     }  
  51.       
  52.     /** 
  53.      * 在Runnable的接口实现类里,是处理Interrupted异常最好的地方. 
  54.      * @author Mr.He 
  55.      */  
  56.     static class TaskAdd implements Runnable{  
  57.   
  58.         private ResHolder resHolder;  
  59.           
  60.         private TaskAdd(ResHolder resHolder) {  
  61.             super();  
  62.             this.resHolder = resHolder;  
  63.         }  
  64.   
  65.         @Override  
  66.         public void run() {  
  67.             try {  
  68.                 resHolder.add();  
  69.             } catch (InterruptedException e) {  
  70.                 System.out.println("Exit for interrupt! "+Thread.currentThread());  
  71.             }  
  72.         }  
  73.     }  
  74.       
  75.     static class TaskClear implements Runnable{  
  76.   
  77.         private ResHolder resHolder;  
  78.           
  79.         private TaskClear(ResHolder resHolder) {  
  80.             super();  
  81.             this.resHolder = resHolder;  
  82.         }  
  83.   
  84.         @Override  
  85.         public void run() {  
  86.             try {  
  87.                 resHolder.clear();  
  88.             } catch (InterruptedException e) {  
  89.                 System.out.println("Exit for interrupt! "+Thread.currentThread());  
  90.             }  
  91.         }  
  92.     }  
  93.       
  94.     public static void main(String[] args) throws IOException {  
  95.           
  96.         ResHolder resHolder = new ResHolder();  
  97.           
  98.         ExecutorService exec = Executors.newCachedThreadPool();  
  99.         exec.submit( new TaskAdd(resHolder) );  
  100.         exec.submit( new TaskAdd(resHolder) );  
  101.         exec.submit( new TaskClear(resHolder) );  
  102.           
  103.         //退出~   
  104.         if(System.in.read()!=-1){  
  105.             exec.shutdownNow();  
  106.         }  
  107.           
  108.     }  

分享到:
评论

相关推荐

    Java Swing多线程死锁问题解析

    Java Swing多线程死锁问题解析 Java Swing多线程死锁问题解析是...在实际开发中,我们需要遵循Java Swing多线程编程的基本原则,正确地使用多线程技术,避免死锁和其他问题的出现,使我们的程序更加稳定、高速和高效。

    java整理的一些资料

    "感受Java中的多线程设计.doc" 和 "多线程编程您不知道的5件事.doc" 这两份文档可能详细阐述了Java中多线程的设计模式和最佳实践,包括同步、互斥、守护线程、线程池等概念。学习多线程不仅需要理解Thread类和...

    多线程实验

    【多线程实验】是计算机科学中的一个重要实践环节,尤其在Java编程中,多线程技术是实现并发处理和高效能应用的关键。本实验旨在帮助学生深入理解和掌握线程的相关概念,包括线程调度机制、线程同步机制,以及如何在...

    java-坦克大战-涉及多线程,IO流的操作

    《Java坦克大战:多线程与IO流的实战解析》 在编程世界中,Java以其强大的跨平台能力和丰富的库支持,成为了开发各种类型游戏的热门选择。本篇将深入探讨一款名为“坦克大战”的Java游戏,它巧妙地运用了多线程和IO...

    java课程设计广工 俄罗斯方块

    Java内置了多线程支持,可以创建Thread对象或者使用Runnable接口来实现并发操作。 3. **事件处理**:为了响应用户的键盘输入和鼠标点击,需要设置事件监听器。Java的EventListener接口和相关的事件类,如...

    【课程思政案例】《Java语言程序设计》:引入抗疫案例,启发工程思维,牢记责任使命.docx

    课程内容涵盖了Java语言的基本概念、语法、面向对象特性、异常处理、输入输出(I/O)、图形用户界面(Swing GUI)、多线程和数据库连接(JDBC)等核心主题。 在课程中,教师通过讲述Java的发展历程,强调科技发展的曲折性...

    java飞机大战小游戏(网络数据库线程).rar

    《Java飞机大战小游戏:网络数据库线程...通过这个项目,开发者可以深入理解Java的面向对象编程、事件驱动、JDBC、Socket编程以及多线程等核心概念,同时也能感受到编程的乐趣和挑战,为今后的软件开发奠定坚实基础。

    通俗易懂的java设计模式

    在多线程环境下,实现线程安全的单例是一个挑战,书中可能涵盖双重检查锁定(Double-Checked Locking)和静态内部类等经典实现方式。 2. **工厂方法模式**:定义一个用于创建对象的接口,让子类决定实例化哪一个类...

    Java学习总结[C程序员的感悟]

    多线程是Java中的一个高级主题,它允许同时执行多个任务,充分利用现代多核处理器的能力。Java通过`Thread`类和`Runnable`接口支持线程的创建和管理,通过`synchronized`关键字和锁机制实现线程之间的同步和通信。 ...

    Java程序性能优化

    一个优秀的程序员,不仅要会编写程序,更要会编写高质量的程序感受Java开发中的大智慧,让你的Java程序更优美。专 注于Java应用程序的优化方法、技巧和思想,深入剖析软件设计层面、代码层面、JVM虚拟机层面的优化...

    java_一本糊涂账

    6. 多线程:如果项目中涉及并发处理,如多个用户同时记账,那么Java的多线程技术将派上用场。Thread类和Runnable接口是实现多线程的基础。 三、项目实践 1. 设计模式:虽然项目可能较为基础,但设计模式的理念仍...

    java面试题目精选

    - **9.1 Java的多线程机制** - **进程与线程的区别**:解释了进程和线程的概念及其差异。 - **线程状态**:概述了Java中线程的各种状态及其转换。 - **创建线程的方法**:介绍了通过继承Thread类和实现Runnable...

    微信飞机大战,JAVA版,设计完美,100%还原真实并加入各种道具,很耐玩。

    同时,JAVA的内存管理和多线程支持确保了游戏运行的流畅性,使得玩家在激烈的空战中感受不到任何卡顿。 在游戏的代码结构方面,开发者注重代码的清晰性和可读性,提供了详细的注释。这不仅是对自身工作的负责,也是...

    Java并发实战

    在现代软件开发中,多线程和并发控制是提升程序性能的关键技术,尤其是在服务器端开发中尤为重要。Java作为一门成熟的编程语言,其在并发控制方面提供了丰富而强大的工具和API,但是这也给开发者带来了一定的学习和...

    java核心技术

    义的应用实例,详细介绍了Java语言基础知识、面向对象程序设计、接口, 与内部类、事件监听器模型、swing图形用户界面程序设计、打包应用程序, 、异常处理、登录与调试、泛型程序设计、集合框架、多线程等内容。...

    华为综合面试题集及感受

    华为面试题集及感受 ...华为的面试题集及感受涵盖了JAVA基础知识、面向对象编程、多线程编程、网络编程、数据库编程、Servlet和JSP、异常处理、泛型编程和设计模式等多个方面,旨在考查应聘者的JAVA知识和编程能力。

    Java 入门 基础 代码

    14. **多线程**:Java支持多线程编程,理解Thread类和Runnable接口,以及同步机制如synchronized关键字和wait()、notify()方法。 15. **枚举与注解**:枚举用于定义固定的常量集合,注解提供元数据,对代码进行标记...

    Java程序设计课程教学案例平台的设计.pdf

    - 知识点的整合:设计案例平台时,需要对Java编程中复杂的概念如接口、多态性、多线程和抽象类等进行合理的整合和分类,便于学生理解和学习。 - 针对性解决学生问题:针对学生在学习过程中出现的对知识点理解不...

    java项目实践之电梯模拟调度算法

    在Java项目实践中,电梯模拟调度算法是一个典型的多线程与图形...通过这个项目,开发者不仅可以提升对Java多线程和GUI编程的理解,还能学习到如何设计和实现复杂的调度算法,这对于开发实际的并发系统是非常有价值的。

    基于JAVA开发的2048小游戏

    6. **多线程**:虽然2048游戏通常不需要多线程,但如果想增加复杂性,如添加动画效果,那么可能需要使用到线程来同步UI更新和游戏逻辑。 7. **PPT介绍**:项目附带的PPT可能包含了游戏设计思路、实现步骤以及可能...

Global site tag (gtag.js) - Google Analytics