多线程的优点和必要性是不言而喻的。

三种方法实现多线程

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的多线程(上)的更多相关文章

  1. Java 多线程(上)

    启动一个多线程 多线程即在同一时间,可以做多件事情,创建多线程有3种方式,分别是继承线程类,实现Runnable接口,匿名类 线程概念 首先要理解进程(Processor)和线程(Thread)的区别 ...

  2. java 网络编程(五)Socket多线程上传文件

    客户端: package cn.sasa.socketUploadFileDemo; import java.io.FileInputStream; import java.io.IOExceptio ...

  3. Java的多线程机制系列:不得不提的volatile及指令重排序(happen-before)

    一.不得不提的volatile volatile是个很老的关键字,几乎伴随着JDK的诞生而诞生,我们都知道这个关键字,但又不太清楚什么时候会使用它:我们在JDK及开源框架中随处可见这个关键字,但并发专 ...

  4. Java的多线程机制系列:(一)总述及基础概念

    前言 这一系列多线程的文章,一方面是个人对Java现有的多线程机制的学习和记录,另一方面是希望能给不熟悉Java多线程机制.或有一定基础但理解还不够深的读者一个比较全面的介绍,旨在使读者对Java的多 ...

  5. Java Thread 多线程 介绍

    1.线程概述 几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个运行中的程序就是一个进程. 当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程. 2.线程 ...

  6. Java的多线程机制系列:(四)不得不提的volatile及指令重排序(happen-before)

    一.不得不提的volatile volatile是个很老的关键字,几乎伴随着JDK的诞生而诞生,我们都知道这个关键字,但又不太清楚什么时候会使用它:我们在JDK及开源框架中随处可见这个关键字,但并发专 ...

  7. Java的多线程机制系列:(三)synchronized的同步原理

    synchronized关键字是JDK5之实现锁(包括互斥性和可见性)的唯一途径(volatile关键字能保证可见性,但不能保证互斥性,详细参见后文关于vloatile的详述章节),其在字节码上编译为 ...

  8. JAVA之多线程的创建

    转载请注明源出处:http://www.cnblogs.com/lighten/p/5967853.html 1.概念 老调重弹,学习线程的时候总会牵扯到进程的概念,会对二者做一个区分.网上有较多的解 ...

  9. Java中多线程原理详解

    Java是少数的集中支持多线程的语言之一,大多数的语言智能运行单独的一个程序块,无法同时运行不同的多个程序块,Java的多线程机制弥补了这个缺憾,它可以让不同的程序块一起运行,这样可以让程序运行更加顺 ...

随机推荐

  1. codevs1404字符串匹配

    /* 无奈我改了那么久还是看的题解 首先跑一边kmp 几下ans[p]表示总共匹配到长度p的次数 这些不一定都是恰好到p 所以在处理一下 ans[p]通过处理变成 所有的匹配到长度p的次数 最后答案就 ...

  2. 转载:C#之接口简介

    原文地址 http://www.cnblogs.com/michaelxu/archive/2007/03/29/692021.html 感谢博主分享! 什么是接口?其实,接口简单理解就是一种约定,使 ...

  3. ADLINK 8158控制程序-连续运动(VB.NET)

    运动平台:日脉的二维运动平台(一个旋转平台和一个滑动平台) 开发环境:VS2010 + .NET Framework + VB.NET 使用文件:pci_8158.vb motion_8158_2D. ...

  4. Java 数据类型转换(转换成字节型)

    package com.mystudypro.byteutil; import java.io.UnsupportedEncodingException; public class ConToByte ...

  5. ASP.NET中 分析器错误:发现不明确的匹配

    这是一个不好的代码习惯引起的发布后运行时的问题.错误原因为.net2.0无法正确识别服务器控件和变量的大小写区别,但是这个错误只有在iis中体现,在文件系统的调试中没有发生. 错误信息 引发错误的参考 ...

  6. Excel 2007中的新文件格式

    *.xlsx:基于XML文件格式的Excel 2007工作簿缺省格式 *.xlsm:基于XML且启用宏的Excel 2007工作簿 *.xltx:Excel2007模板格式 *.xltm:Excel ...

  7. (转)PHP数组的总结(很全面啊)

    一.什么是数组数组就是一组数据的集合,把一系列数据组织起来,形成一个可操作的整体.数组的每个实体都包含两项:键和值. 二.声明数据在PHP中声明数组的方式主要有两种:一是应用array()函数声明数组 ...

  8. Delphi 动态创建组件,单个创建、单个销毁

    效果图如下: 实现部分代码如下: var rec: Integer = 0; //记录增行按钮点击次数 implementation {$R *.dfm} //动态释放单个组件内存,即销毁组件 pro ...

  9. 理解线程的挂起,sleep还有阻塞

    线程是靠cpu来运行的,cpu要运行一个线程(不说别的)最起码就是要占用cpu时间,象Windows这样的多任务操作系统,可以允许多个线程同时运行,所谓的同时运行并不是真正的同时运行,而是轮流运行不同 ...

  10. MYSQL主从不同步延迟原理

    1. MySQL数据库主从同步延迟原理.   要说延时原理,得从mysql的数据库主从复制原理说起,mysql的主从复制都是单线程的操作,   主库对所有DDL和DML产生binlog,binlog是 ...