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的多线程机制弥补了这个缺憾,它可以让不同的程序块一起运行,这样可以让程序运行更加顺 ...
随机推荐
- C# 实现文件夹的复制以及删除
代码来源:http://blog.163.com/u_tommy_520/blog/static/20406104420147493933662/ http://www.cnblogs.com/lov ...
- eclipse打包jar时包含第三方jar包的相关问题
我用的是mars4.5版本的eclipse 需求:要把写好的工程打成jar包,并能直接运行.工程用了若干个第三方jar. 在打包的时候,eclipse提供的打包方法不能引用第三方jar包,导致了出现C ...
- Java面向对象的概念以及OOP思想的优点
传统面向过程程序设计的思路: 先设计一组函数用来解决一个问题,然后确定函数中需要处理的数据以及存储位置. 面向对象的设计的思路: 先确定处理的数据,然后确定处理数据的算法,最后将数据和算法封装在一起构 ...
- SQL利用临时表实现动态列、动态添加列
--方法一--------------------------------------------------------------------- declare @sql as varchar(1 ...
- flash文件运动节奏的控制
flash里面,比较难的是控制运动的节奏.参考了几个韩国网站的fla源文件,提出以下几个建议与参考. 1,flash文件里面,每秒的帧数 设置为 120,或者一个比较大的数字(90,60).普通的文件 ...
- iOS学习笔记(十四)——打电话、发短信
电话.短信是手机的基础功能,iOS中提供了接口,让我们调用.这篇文章简单的介绍一下iOS的打电话.发短信在程序中怎么调用. 1.打电话 [[UIApplication sharedApplicatio ...
- 34.Spring-Aop.md
http://blog.csdn.net/bigtree_3721/article/details/50759843 目录 [toc] --- 1.概念 1.1概念 AOP是Spring提供的关键特性 ...
- [转载]delete指针之后应该赋值NULL
首先,C++标准规定:delete空指针是合法的,没有副作用.但是,delete p后,只是释放了指针指向的内存空间.p并不会自动被置为NULL,而且指针还在,同时还指向了之前的地址. 问题来了,对一 ...
- Cloudera Manager(CentOS)安装介绍
相信通过这篇文章大家都对Cloudera Manager及CDH安装有一个整体的认识 目 录 1 准备工 作.................................... ...
- __file__ __name__ __doc__ argv详解
__file__:表示输出当前py文件的路径 __name__: 表示输出当前函数名称,是main()函数(入口函数),或者是其他函数 __doc__: 模块的对象,输出模块的版权信息,如:作者 ch ...