Java的多线程(上)
多线程的优点和必要性是不言而喻的。
三种方法实现多线程
1. 继承Thread
- class A extends Thread{
- public void run() {...}
- }
使用时,
- new A().start();
2. 实现Runnable
(1)定义Runnable接口的实现类,并重写该接口的run()方法。
(2)创建该实现类的实例,并以此实例作为Thread的target来创建Thraed,这个Thread才是真正的线程对象。
- class A implements Runnable{
- public void run(){...}
- }
- ...
- Thread(new A()).start();
- Thread(new A(),"指定一个名字").start();
3. 利用Callable和Future
前面指出,通过实现Runnable接口创建多线程,Thread类把run()方法包装成线程的执行体,但是只能是这个run方法。C#支持把任意方法包装成线程执行体。
Java5后,Java提供了Callable接口,它的call()方法可以作为线程的执行体,较之run,它的优点在于能有返回值和能抛出异常。但是,Callable并不是Runnable的子接口,所以它不能被Thread类直接做target对象。和Callable接口同时推出的Future接口是用来代表call()方法的返回值的;特别的,有一个FutureTask的实现类,该类同时实现了Future接口和Runnable接口,因而可以作为Thread类的target。(注意体会此处精良的设计模式)
这里给出Future接口定义的方法:
- boolean cancel(boolean mayInterruptIfRunning):试图取消该Future关联的Callable任务。
- V get():返回call方法的返回值。该方法的调用会导致程序阻塞,必须等到子线程结束后才会得到返回值。
- V get(long timeout , TimeUnit unit):和上面方法的区别在于是让程序阻塞最多timeout,unit指定的时间,如果在指定时间后Callable任务依然没有返回值,会抛出异常。
- boolean isCancelled():如果在Callable任务正常完成前被取消,则返回true.
- boolean isDone():如果Callable任务已完成,则返回true。
下面是使用Callable的示例。
- public class MyThread implements Callable<Integer>
- {
- public Integer call() {...}
- public static void main(String[] args)
- {
- MyThread mt = new MyThread();
- FutureTask<Integer> task = new FutureTask<Integer>(mt);
- new Thread(task,"线程名").start();
- }
- }
我们可以用task.get()得到子线程的返回值。
线程的生命周期
熟悉操作系统的读者对线程、进程的生命周期一定很理解。一个线程要经过新建(new)、就绪(Runnable)、运行(Running)、阻塞(Block)和死亡(Dead)。并且一个线程也不可能一直占有CPU,所以它会多次在运行、阻塞之间切换。
1. start() vs run()
值得强调的是,启动线程使用start方法,而不是run方法。因为你调用了start启动线程,系统会把run方法当成线程的执行体,但是如果你自己调用run,在这个run执行结束前其他线程无法并发执行,也就是说,你多次调run,只是相当与在调用一个普通方法,和多线程无法。调用start就会进行就绪状态。
2. 运行、阻塞
现在的操作系统都是采用抢占式的调度策略,也就是系统可每个可执行的线程一个小时间段,结束后就剥夺资源。但一些嵌入式设备可能采用协作调度,即只有调用sleep()或yield()才会放弃作占的资源。
3. 死亡
一般死亡的方式有三种:正常结束;抛出未捕获的异常;调用stop()。我们一般不建议使用线程的stop,因为这样很容易导致死锁。
要注意的是,子线程在启动后就和主线程有相同地位,不会因为主线程结束而死亡。还有,不要试图对已经死亡的线程使用start方法,这会导致IllegalThreadState异常。实际上,start只能使用一次,也就是在线程新建状态下使用。
线程的控制
1. join 方法
Thread的join方法,可以让一个线程等待另一个线程完成。简单的说,就是线程A调用了b.join(),等b这个线程死亡后,A才继续向下执行。
- public class JoinThread extends Thread
- {
- public JoinThread(String name)
- {
- super(name);
- }
- public void run()
- {
- for(int i = 0;i<10;i++)
- {
- System.out.println(getName()+" "+i);
- }
- }
- public static void main(String[] args) throws Exception
- {
- // new JoinThread("线程1").start();
- for(int i = 0;i<10;i++)
- {
- if(i==5)
- {
- JoinThread jt = new JoinThread("被join");
- jt.start();
- jt.join();
- }
- System.out.println(Thread.currentThread().getName()+" "+i);
- }
- }
- }
结果的截图:
2.后台进程
所谓后台进程就是在后台运行、为其他进程服务的。它的特征是前台进程死亡时后台进程也会死亡。指定后台进程只需调用Thread对象的setDeamon(true)。其中,deamon是守护的意思,因此后台进程有时也称守护进程。
- 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<5;i++)
- System.out.println("Current "+Thread.currentThread().getName()+" "+i);
- }
- }
需要注意的是,setDaemon(true)要在start之前调用。
3 休眠 sleep、让步yield
执行Thread的静态方法sleep()可以正在运行的线程暂停一会儿。有两种重载:
- static void sleep(long millis):单位毫秒
- static void sleep(long millis,int nanos):相当于暂停millis后再暂停nanos。考虑到计算机本身时钟问题,这个方法并不常用。
yield方法和sleep很相似,都是Thread类的静态方法。但它不会阻塞该线程,而是让它回到就绪状态。我们使用yield的目的一般是为了让和当前线程优先级相同或更高的线程得到执行机会,但并不能得到完全的保证。虽然有的书上说,yiled()方法只会给优先级相同、更高的线程执行机会,但笔者利用下面代码测试,发现结果并不尽然:
- public class YieldTest extends Thread{
- public YieldTest(String name)
- {
- super(name);
- }
- public void run()
- {
- for(int i=0;i<30;i++)
- {
- System.out.println(getName()+" "+i);
- if(i==15)
- {
- Thread.yield();
- }
- }
- }
- public static void main(String[] args) throws Exception
- {
- YieldTest y = new YieldTest("High");
- y.setPriority(Thread.MAX_PRIORITY);
- y.start();
- YieldTest y2 = new YieldTest("Low");
- y2.setPriority(Thread.MIN_PRIORITY);
- y2.start();
- }
- }
查阅API文档发现这么一句话,"a hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore this hint‘。不难发现,调用yield()只是个暗示放弃,但系统是否这样做是不确定。所以我们一般倾向于使用sleep而不是yield。
线程同步
这部分知识是线程学习的核心部分,尤其在网络编程,保证线程同步的安全问题是屡见不鲜的。
1. 同步代码块
线程安全的经典问题是银行取钱。这部分的代码写在这里显得很臃肿,有兴趣的朋友可以参考https://github.com/ChenZhongPu/GitJava。
为了解决这个问题,java的多线程支持同步监视器。使用同步监视器的通用语法是同步代码块,格式如下:
- synchronize(obj)
- {
- ...
- }
上面代码的含义是,线程开始执行同步代码之前,必须先获得对同步监视器的锁定。这样,任何一个时刻只有一个线程可以获得对同步监视器的锁定,完成后就释放锁定。
2.同步方法
同步方法就是使用synchronized修饰方法,它无需显式指定同步监视器,同步方法的同步监视器就是this。通过使用同步方法可以很方便的实现线程安全的类,
比如,我们在Account方法里添加一个同步的draw方法,而不是在run方法实现取钱逻辑。
需要说明的是,可变类的线程安全是以减低效率为代价的。因此我们不要对所有方法都进行同步,只对那些改变共享资源的方法进行同步。
我们注意到,程序是无法显式释放对同步监视器的锁定,一般线程会在下面情况释放对同步监视器的锁定:
- 同步方法、同步代码块执行结束。
- 在同步方法、代码块遇到break,return。
- 同步方法、代码块出现未处理的error,Exception。
- 执行同步监视器对象的wait()方法。
而下面的情况不会释放同步监视器:
- 在执行同步方法、代码块时调用Thread,sleep(),Thread.yield()。
- 其他线程调用该线程的suspendI()。
3. 同步锁
java5之后提供了更强大的线程同步机制,即显式定义同步锁对象。
Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只有一个线程对Lock对象进行加锁。某些锁可能允许对共享资源并发访问,如ReadWriteLock。
找线程安全的控制中,常用的是ReentrantLock(可重入锁)。使用时代码格式如下:
- class A{
- private final ReentrantLock lock = new ReentrantLock();
- //...
- // 定义需要保证线程安全的方法
- public void m ()
- {
- lock.lock();//加锁
- try{
- //...
- }
- finally
- {
- lock.unlock();//释放
- }
- }
- }
现在简要解释可重入的含义,一个线程可以对已被加锁的ReentrantLock锁进行再次加锁,有一个计数器负责维护嵌套次数,线程在每次调用lock加锁后,必须显式调用unlock解锁,所以一段被锁保护的代码可以调用另一个被锁保护的方法。
4. 死锁
死锁现象发生在两个进程相互等待对方释放同步监视器,而JVM没有监测。一旦发生死锁,程序既无异常,也不提示。
死锁发生是很常见的,尤其是系统出现多个同步监视器。有兴趣朋友可以参考,https://github.com/ChenZhongPu/GitJava下面的DeadLock.java。
Java的多线程(上)的更多相关文章
- Java 多线程(上)
启动一个多线程 多线程即在同一时间,可以做多件事情,创建多线程有3种方式,分别是继承线程类,实现Runnable接口,匿名类 线程概念 首先要理解进程(Processor)和线程(Thread)的区别 ...
- java 网络编程(五)Socket多线程上传文件
客户端: package cn.sasa.socketUploadFileDemo; import java.io.FileInputStream; import java.io.IOExceptio ...
- Java的多线程机制系列:不得不提的volatile及指令重排序(happen-before)
一.不得不提的volatile volatile是个很老的关键字,几乎伴随着JDK的诞生而诞生,我们都知道这个关键字,但又不太清楚什么时候会使用它:我们在JDK及开源框架中随处可见这个关键字,但并发专 ...
- Java的多线程机制系列:(一)总述及基础概念
前言 这一系列多线程的文章,一方面是个人对Java现有的多线程机制的学习和记录,另一方面是希望能给不熟悉Java多线程机制.或有一定基础但理解还不够深的读者一个比较全面的介绍,旨在使读者对Java的多 ...
- Java Thread 多线程 介绍
1.线程概述 几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个运行中的程序就是一个进程. 当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程. 2.线程 ...
- Java的多线程机制系列:(四)不得不提的volatile及指令重排序(happen-before)
一.不得不提的volatile volatile是个很老的关键字,几乎伴随着JDK的诞生而诞生,我们都知道这个关键字,但又不太清楚什么时候会使用它:我们在JDK及开源框架中随处可见这个关键字,但并发专 ...
- Java的多线程机制系列:(三)synchronized的同步原理
synchronized关键字是JDK5之实现锁(包括互斥性和可见性)的唯一途径(volatile关键字能保证可见性,但不能保证互斥性,详细参见后文关于vloatile的详述章节),其在字节码上编译为 ...
- JAVA之多线程的创建
转载请注明源出处:http://www.cnblogs.com/lighten/p/5967853.html 1.概念 老调重弹,学习线程的时候总会牵扯到进程的概念,会对二者做一个区分.网上有较多的解 ...
- Java中多线程原理详解
Java是少数的集中支持多线程的语言之一,大多数的语言智能运行单独的一个程序块,无法同时运行不同的多个程序块,Java的多线程机制弥补了这个缺憾,它可以让不同的程序块一起运行,这样可以让程序运行更加顺 ...
随机推荐
- p标签里面不能嵌套div
先申明本人代码水平为零起点,刚开始学习前端,所以就是小白.不过大神也是小白变身的么,所以要专心码代码,潜心钻研,haha~ 今天练习了段代码,发现效果和自己想象的不一样: 想了一下估计是<p&g ...
- C# 、winform 添加皮肤后(IrisSkin2) label设置的颜色 无法显示
C# .winform 添加皮肤后(IrisSkin2) label设置的颜色 无法显示 解决方法一:设置label的Tag属性值与skinEngine的DisableTag属性值相同即可.默认值是9 ...
- 【HAOI2007】理想的正方形
[问题描述] 有一个a*b的整数组成的矩阵,现请你从中找出一个n*n的正方形区域,使得该区域所有数中的最大值和最小值的差最小. [输入] 第一行为3个整数,分别表示a,b,n的值第二行至第a+1行每行 ...
- C++对象数组操作误区
由于语义上的需要导致语法的上缺陷,所以导致对象数组在C++中存在陷阱. C++语境:一个基类指针或引用是可以指向派生类对象的,以此可来表现C++对运行时多态的需求: 创建一个对象数组将返回首元素的首地 ...
- extjs combobox 设置下拉时显示滚动条 设置显示条数
extjs在点击下拉时如果没有限制它的高度,那么它的默认最大高度是300,显示的时候就会显示300的高度,知道选项内容超过这个高度时才会自动显示滚动条,往往在有些时候我们希望让combobox显示一个 ...
- oracle常用SQL总结
这里我们介绍的是 40+ 个非常有用的 Oracle 查询语句,主要涵盖了日期操作,获取服务器信息,获取执行状态,计算数据库大小等等方面的查询.这些是所有 Oracle 开发者都必备的技能,所以快快收 ...
- strtolower() strtoupper()等字符串大小写转换函数
$str = "Mary Had A Little Lamb and She LOVED It So"; string strtolower ( string $str )— 将字 ...
- python -- 函数传参
一.参数传入规则 可变参数允许传入0个或任意个参数,在函数调用时自动组装成一个tuple: 关键字参数允许传入0个或任意个参数,在函数调用时自动组装成一个dict: 1. 传入可变参数: def ca ...
- python学习第二课要点记录
字典使用时,使用for k,v in items():要将字典转换为元组,因此效率较低,如果数据量较大,就不建议使用这样的形式获取key和value的值,而要使用 for item in dict: ...
- poj1159 Palindrome
G - 回文串 Time Limit:3000MS Memory Limit:65536KB 64bit IO Format:%I64d & %I64u Descripti ...