文章目录
线程的基础知识
一个任务通常就是一个程序,每个运行的程序就是一个线程.当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程.
理解线程和进程的区别和联系
什么是进程
当一个程序进入内存运行,即变成一个进程.
- 进程是处于运行过程中的程序,并且具有一定独立功能,
- 进程是系统进行资源分配和调度的一个独立单位.
进程的三个特征
独立性
进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间.在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间.
动态性
进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合.在进程中加入了时间的概念.进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的.
并发性
多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响.
什么是线程
线程是进程的执行单元,线程在程序中是独立的、并发的执行流。线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不再拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。
多线程编程的优势
- 进程间不能共享内存,但线程间共享内存。
- 系统创建进程需要为该进程分配系统资源,但创建线程则代价小的许多(共享进程空间),因此使用多线程来实现多任务并发比多进程的效率高。
- Java语言内置多线程功能的支持是我们更加轻易的使用多线程编程。
两种创建线程的方式
Thread类是所有Java线程类的父类,每条线程的作用是完成一定的任务,实际上就是执行一段程序流(一段顺序执行的代码)。Java使用run()来封装这样一段程序流。
继承Thread类创建线程类
- 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就是代表了线程需要完成的任务。因此,我们经常把run方法称为线程执行体。
- 创建Thread子类的实例,即使创建了线程对象。
- 用线程对象的start方法来启动该线程。
写一个通过继承Thread类创建线程的例子:
public class FirstThread extends Thread{
private int i;
//重写run方法
public void run() {
for(;i<100;i++) {
//当线程类继承Thread类时,可以直接调用getName()方法来返回当前线程的名
//如果想获取当前线程,直接使用this
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
if (i==20) {
new FirstThread().start();
new FirstThread().start();
}
}
}
}
执行后你会发现,不论是哪个线程,i都从0打印到100,这意味着使用继承Thread类的方法来创建线程类时,多条线程之间无法共享线程类的实例变量。
实现Runnable接口创建线程类
- 定义Runnable接口的实现类,并重写该接口的run方法,该run方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
//创建Runnable实现类的对象
SecondThread st= new SecondThread();
//以Runnable实现类的对象作为Thread的target来创建Thread对象,即线程对象
new Thread(st);
也可以在创建Thread对象时为该Thread对象指定一个名字
new Thread(st,"线程1");
Runnable对象仅仅作为Thread对象的target,runnable实现类里包含的run方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run方法。
下面展示一个完整的runnable实现多线程的例子:
public class RunnableThread implements Runnable {
private int i;
public static void main(String[] args) {
// TODO Auto-generated method stub
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
if (i==20) {
RunnableThread sThread=new RunnableThread();
new Thread(sThread,"新线程1").start();
new Thread(sThread,"新线程2").start();
}
}
}
@Override
public void run() {
// TODO Auto-generated method stub
for (; i < 100; i++) {
//当线程类实现Runnable接口时,
//如果想获取当前线程,只能用Thread.currentThread()方法
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
执行上面的代码你会惊奇的发现线程1和线程2竟然共享一个i变量,那是因为这里两个线程target了同一个实现了Runnable接口的类对象。
两种方式所建线程的对比
采用实现Runnable接口方式的多线程
优势
- 线程类只是实现了Runnable接口,还可以继承其他类
- 可以多个线程共享一个target类对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势
- 编程稍稍复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。
采用继承Thread类实现多线程
优势
- 编写简单,如果需要访问当前线程,无须使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势
- 因为线程类已经继承了Thread类,因此不能再继承其他父类。
线程的生命周期
生命周期的五种状态:New(新建)、Runnable(就绪)、Running(运行)、Blocked(阻塞)、Dead(死亡)。
新建和就绪状态
- 当程序使用new关键字创建一个线程后,该线程就会处于新建状态(new)。
- 当线程对象调用了start()方法后,线程就处于就绪状态(Runnable)。Java虚拟机会为其调用栈和程序计数器,处于这个状态的线程并没有开始运行,至于该线程何时运行将取决于JVM线程调度器的调度。
如果调用run()而不是start()会怎样
run方法会立即执行而不是等待调度,且在run方法执行完之前其他线程无法并发执行,也就是说系统把直接执行run方法的线程对象当成了普通对象。
运行和阻塞状态
- 如果处于就绪状态的线程获得了CPU,开始执行run方法的线程执行体,则该线程处于运行状态。
- 如果CPU资源被其它线程抢占,这个线程就会从运行状态编程阻塞状态。
进入阻塞状态的可能情况
- 线程调用sleep方法主动放弃所占用的处理器资源
- 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
- 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
- 线程在等待某个通知(notify)。
- 程序调用了线程的suspend方法将该线程挂起。不过这个方法容易导致死锁。
从阻塞状态恢复后是进入运行状态吗?
被阻塞的线程会在合适的时候进入就绪状态(runnable)而不是运行状态(running)。被阻塞的线程解除阻塞后必须重新等待线程调度器再次调度。
解除阻塞的可能情况
- 调用sleep方法的线程经过了指定的时间
- 线程调用的阻塞式方法已经返回
- 线程成功地获得了同步监视器。
- 线程在等待通知时收到了其他线程发出的通知
- 处于关闭状态地线程被调用了resume恢复方法。
能让线程从运行状态进入就绪状态地方法
调用yield()可以让线程从running变为runnable。
线程死亡
线程死亡的三种情况
- run()方法执行完成,线程正常结束
- 线程抛出一个未捕获地Exception或Error。
- 直接调用该线程的stop()方法来结束该线程。
- 主线程结束并不会导致其他线程的死亡,一旦子线程启动,它就拥有和主线程相同的地位,不会受到主线程的影响。
- 使用isAlive()方法,当线程处于就绪、运行、阻塞三种状态时,该方法返回true;当线程处于新建、死亡两种状态时,该方法返回false。
- 不要对一个已经死亡的线程调用start()使它重新启动,死亡的线程不能再次作为线程执行,否则会报IllegalThreadStateException异常。
控制线程的常用方法
join线程
- Thread提供了让一个线程等待另一个线程完成的方法:join()。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join方法加入的join线程完成为止。
- join()方法通常由使用线程的程序调用,以将大问题分为许多小问题,每一个小问题分配一个线程。当所有小问题得到处理后,在调用主线程来进一步操作。
举一个使用join方法的例子:
public class JoinThread extends Thread {
//提供一个有参数的构造器,用于设置该线程的名字
public JoinThread(String name) {
super(name);
}
//重写run方法
public void run() {
for(int i=0;i<100;i++) {
//当线程类继承Thread类时,可以直接调用getName()方法来返回当前线程的名
//如果想获取当前线程,直接使用this
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) throws InterruptedException {
// 启动子线程
new JoinThread("新线程").start();
for (int i = 0; i < 100; i++) {
if (i==20) {
JoinThread jThread=new JoinThread("被join的线程");
jThread.start();
//main线程调用了jThread的join方法,main线程必须等jThread执行结束才会继续执行
jThread.join();
}
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
join()方法有三种重载形式
- join():等待被join的线程执行完成
- join(long millis):等待被join的线程的时间最长为millis毫秒。如果在millis毫秒内,被join的线程还没有执行结束则不在等待。
- join(long millis,int nanos):等待被join的线程的时间最长为millis毫秒加上nanos微秒。
后台线程
后台线程又称为“守护线程”或“精灵线程”。JVM的垃圾回收线程就是典型的后台线程。
如果所有前台线程死亡,后台线程自动死亡。
调用Thread对象setDaemon(true)方法可将指定线程设置成后台线程。
写一个设置后台线程的例子:
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) {
// TODO Auto-generated method stub
DaemonThread thread=new DaemonThread();
thread.setDaemon(true);
thread.start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
//前台线程结束,后台线程也会跟着结束
}
}
线程睡眠:sleep
如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态方法sleep()。
sleep有两种重载方式:
- static void sleep(long millis):让当前正在执行的线程暂停millis毫秒,并进入阻塞状态,该方法受到系统计时器和线程调度器的精度和准确度影响。
- static void sleep(long millis,int nanos):让当前正在执行的线程暂停millis毫秒加nanos微秒,并进入阻塞状态。
线程在sleep时间段内不会获得执行机会。
线程让步:yield
yield()方法是一个和sleep方法有点相似的方法,它也是一个Thread类提供的一个静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,线程会进入就绪状态。
yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用yield方法暂停后,线程调度器又将其调度出来重新执行。
当某个线程调用了yield方法暂停之后,只有优先级与当前线程相同或者比当前线程更高的线程才能获得执行机会,如果都没有优先级大于等于这个线程的,该线程继续执行。
写一个使用yield()方法的例子:
public class TestYield extends Thread{
public TestYield() {
// TODO Auto-generated constructor stub
}
public TestYield(String name) {
super(name);
}
public void run() {
for(int i=0;i<50;i++) {
System.out.println(getName()+" "+i);
if (i==20) {
//使当前线程让步
Thread.yield();
}
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
TestYield tYield=new TestYield("高级");
//将线程设置最高级
tYield.setPriority(Thread.MAX_PRIORITY);
tYield.start();
TestYield tYield2=new TestYield("低级");
//将线程设置最低级
tYield2.setPriority(MIN_PRIORITY);
tYield2.start();
}
}
yield方法与sleep方法的区别
- yield方法只会给优先级相同或更高的线程执行机会,而sleep方法一定会暂停当前线程给其他线程执行。
- yield方法将线程从执行状态转为就绪状态,而sleep方法将线程从执行状态转为阻塞状态。
- yield方法不需要抛出任何异常,而sleep方法声明抛出了InterruptedException异常,因此调用sleep方法要么捕捉该异常,要么显式声明抛出该异常。
- sleep方法比yield方法具有更好的可移植性。
改变线程优先级
每个线程默认的优先级都与创建它的父线程具有相同的优先级。在默认情况下,main线程具有普通优先级,由main线程创建的子线程也有普通优先级。
Thread提供了setPriority(int newPriority)和getPriority()方法来设置和返回指定线程的优先级,优先级分为1~10,有三个静态常量:
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5
线程同步的概念和必要性
系统的线程调度是具有一定随机性的。,当使用多个线程来访问同一个数据时,非常容易出现线程安全问题.
用银行取钱的经典问题引出讨论
定义一个账户类:
public class Account {
//封装账户编号,账户余额两个属性
private String accountNo;
private double balance;
public Account() {
// TODO Auto-generated constructor stub
}
public Account(String accountNo,double balance) {
this.balance=balance;
this.accountNo=accountNo;
}
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//重写equals和hashcode
public boolean equals(Object object) {
if (object!=null && object.getClass()==Account.class) {
Account target=(Account)object;
return target.getAccountNo().equals(accountNo);
}
return false;
}
public int hashCode() {
return accountNo.hashCode();
}
}
创建操作线程:
public class DrawThread extends Thread{
//模拟用户账户
private Account account;
//当前取钱线程所希望取的钱数
private double drawAmount;
public DrawThread(String name,Account account,double drawAmount) {
super(name);
this.account=account;
this.drawAmount=drawAmount;
}
//当多条线程修改同一个共享数据时,将涉及数据安全问题
public void run() {
//若账户余额大于取钱数目
if (account.getBalance()>=drawAmount) {
System.out.println(getName()+"取钱成功!吐出钞票:"+drawAmount);
try {
//让当前线程暂停1毫秒,让另一个线程通过判断,会导致两个线程都能够扣钱.
Thread.sleep(1);
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
//修改余额
account.setBalance(account.getBalance()-drawAmount);
System.out.println("\t 余额为:"+account.getBalance());
}else {
System.out.println(getName()+"取钱失败!余额不足");
}
}
测试代码:
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
//账号余额1000
Account account=new Account("123",1000);
//扣800
new DrawThread("线程1", account, 800);
//扣800
new DrawThread("乙", account, 800);
}
}
运行后你会发现账号余额-600,当并发线程越多,哪怕不sleep(1)你也可能会发现余额为负的情况,因为这里只有两个线程,为了展示这个效果我才加的sleep方法.
使用synchronized控制线程同步
同步代码块
为了避免这种异常情况发生,Java多线程引入了同步监视器来解决这个问题,而使用同步监视器的通用方法就是同步代码块.
synchronized(obj){
...
//此处代码就是同步代码块
}
synchronized后括号里的obj就是同步监视器.
同步监视器的目的是:阻止两条线程对同一个共享资源进行并发访问.因此虽然任何对象都能做同步监视器,但是我们总是会选择那些可能被并发访问的共享资源作为同步监视器.
对上面的银行取钱线程进行修改:
public class DrawThread extends Thread{
//模拟用户账户
private Account account;
//当前取钱线程所希望取的钱数
private double drawAmount;
public DrawThread(String name,Account account,double drawAmount) {
super(name);
this.account=account;
this.drawAmount=drawAmount;
}
//当多条线程修改同一个共享数据时,将涉及数据安全问题
public void run() {
//若账户余额大于取钱数目
synchronized (account) {
if (account.getBalance()>=drawAmount) {
System.out.println(getName()+"取钱成功!吐出钞票:"+drawAmount);
try {
//让当前线程暂停1毫秒,让另一个线程通过判断,会导致两个线程都能够扣钱.
Thread.sleep(1);
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
//修改余额
account.setBalance(account.getBalance()-drawAmount);
System.out.println("\t 余额为:"+account.getBalance());
}else {
System.out.println(getName()+"取钱失败!余额不足");
}
}
}
}
其他线程若无法获取同步锁,就会等待锁释放.
同步方法
使用synchronized修饰的方法被称为同步方法
同步方法无须显式指定同步监视器,同步方法的默认同步监视器是this,也就是该对象本身.
通过使用同步方法可以将某类非常方便的变成线程安全的类,线程安全的类具有如下特征:
- 该类的对象可以被多个线程 安全的访问.
- 每个线程调用该对象的任意方法之后都将得到正确的结果.
- 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态.
我们试着把可变方法改为同步的
public class Account {
//封装账户编号,账户余额两个属性
private String accountNo;
private double balance;
public Account() {
// TODO Auto-generated constructor stub
}
public Account(String accountNo,double balance) {
this.balance=balance;
this.accountNo=accountNo;
}
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
//重写equals和hashcode
public boolean equals(Object object) {
if (object!=null && object.getClass()==Account.class) {
Account target=(Account)object;
return target.getAccountNo().equals(accountNo);
}
return false;
}
public int hashCode() {
return accountNo.hashCode();
}
public synchronized void draw(double drawAmount) {
//若账户余额大于取钱数目
if (balance>=drawAmount) {
System.out.println(Thread.currentThread().getName()+"取钱成功!吐出钞票:"+drawAmount);
try {
//让当前线程暂停1毫秒,让另一个线程通过判断,会导致两个线程都能够扣钱.
Thread.sleep(1);
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
//修改余额
balance=balance-drawAmount;
System.out.println("\t 余额为:"+balance);
}else {
System.out.println(Thread.currentThread().getName()+"取钱失败!余额不足");
}
}
}
这个draw()方法会锁住当前Account对象,当多个线程使用同一个Account对象并调用draw()方法时,同一时刻只能有一个线程执行.
synchronized只能修饰代码块和方法.
如何减少线程安全带来的负面影响
线程安全的实现是以降低程序运行效率作为代价的.
- 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(也就是共享资源)的方法进行同步.
- 如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本:线程不安全版和线程安全版.单线程使用线程不安全版,多线程使用线程安全版.
释放同步监视器的锁定 条件
- 当线程的同步方法,同步代码块执行结束,当前线程即释放同步监视器.
- 在同步方法或同步代码块遇到break,return终止执行,当前线程会释放同步监视器.
- 出现未处理的Error和Exception
- 执行同步监视器对象的wait()方法
不会释放同步监视器的可能情况
- 调用Thread.sleep(),Thread.yield()方法来暂停当前线程的执行.
- 其他线程调用正在执行同步代码块或同步方法的线程的suspend()将线程挂起.
使用Lock对象控制线程同步
同步锁实现同步.
Lock(同步锁)的优势:
- 提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock实现允许更灵活的结构,可以具有差别很大的属性,并且可以支持多个相关的Condition对象.
Lock提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象.
常见的Lock使用方法:
class X{
//定义锁对象
private final ReentrantLock lock=new ReentrantLock();
public void m(){
//加锁
lock.lock();
try{
//需要保证线程安全的代码
//... method body
}finally{
lock.unlock();
}
}
}
使用Object提供的方法实现线程通信
假设有这样一个场景,系统要求存款者和取钱者不断地重复执行存款,取款的动作,并且要求不能连续两次重复取款或存款,这就涉及到了线程的协调执行的问题.
那么现在我们可以借助Object类提供的wait(),notify()和notifyAll()三个方法.注意:这三个方法必须由同步监视器对象调用.
- 对于synchronized修饰的同步方法,因为该类的默认同步监视器就是this,所以可以在同步方法中直接调用这三个方法.
- 对于使用同步代码块实现同步的程序而言,必须由同步监视器对象执行这三个方法.
方法 | 作用 |
---|---|
wait() | 导致当前线程等待,直到其他线程调用该同步监视器的notify()或者notifyAll()方法来唤醒该线程 |
notify() | 唤醒在此同步监视器上等待的单个线程.如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程,选择是任意性的,执行notify()的线程放弃对同步监视器的锁定后被唤醒的线程才有可能执行 |
notifyAll() | 唤醒再次同步监视器上等待的所有线程 |
使用条件变量实现线程通信
如果程序不使用synchronized来保证同步,而是使用Lock对象来保证同步,则不能使用wait(),notify(),notifyAll()实现线程协调.
当使用Lock对象保证同步时,Java提供了Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程.
要获得特定Lock实例的Condition实例**,调用Lock对象newCondition()方法**即可.
Condition类的方法如下:
方法 | 作用 |
---|---|
await() | 导致当前线程等待直到其他线程调用该Condition的signal()方法或signalAll()来唤醒该线程,具有的变体很多,就不细说了 |
signal() | 唤醒在此Lock上等待的线程 |
signalAll() | 唤醒所有在此Lock对象上等待的线程 |
使用管道流实现线程通信
管道流有三种形式:PipedInputStream和PipedOutputStream(管道字节流),PipedReader和PipedWriter(管道字符流),Pipe.SinkChannel和Pipe.SourceChannel(NIO的管道Channel).
使用步骤:
- 使用new创建管道输入流和管道输出流
- 使用管道输入流或者管道输出流的connect方法把两个输入流和输出流连接起来.
- 将管道输入流,管道输出流分别传入两个线程
- 两个线程可以分别依赖各自的管道输入流,管道输出流进行通信.
import java.io.BufferedReader;
import java.io.PipedReader;
import java.io.PipedWriter;
class PipedCommunicationTest extends Thread{
public static void main(String[] args) {
PipedWriter pWriter=null;
PipedReader pReader=null;
try {
//分别创建两个独立的管道输出流,输入流
pWriter=new PipedWriter();
pReader=new PipedReader();
//连接管道输出流,输入流
pWriter.connect(pReader);
//将连接好的管道流分别传入两个线程
//就可以让两个线程通过管道流进行通信
new WriterThread(pWriter).start();
new ReaderThread(pReader).start();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
class WriterThread extends Thread{
String[] books=new String[] {"s1","s2","s3","s4"};
private PipedWriter pWriter;
public WriterThread() {};
public WriterThread(PipedWriter pWriter) {
this.pWriter=pWriter;
}
public void run() {
try {
//向管道输出流写入100个字符串
for (int i = 0; i < 100; i++) {
pWriter.write(books[i%4]+"\n");
}
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}//使用finally块来关闭管道输出流
finally {
try {
if (pWriter!=null) {
pWriter.close();
}
}catch(Exception e){
e.printStackTrace();
}
}
}
}
class ReaderThread extends Thread{
private PipedReader ps;
//用于包装管道流的bufferReader对象
private BufferedReader bReader;
public ReaderThread() {
// TODO Auto-generated constructor stub
}
//将管道绑进BufferedReader中
public ReaderThread(PipedReader ps) {
this.ps=ps;
this.bReader=new BufferedReader(ps);
}
public void run() {
String buf=null;
try {
//逐行读取管道输入流中的内容
while((buf=bReader.readLine())!=null) {
System.out.println(buf);
}
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}finally {
try {
if (bReader!=null) {
bReader.close();
}
} catch (Exception e2) {
// TODO: handle exception
e2.printStackTrace();
}
}
}
}
线程组
Java使用ThreadGroup来表示线程组,对线程组的控制相当于同时控制这批线程.
在默认情况下,子线程和创建它的父线程处于同一个线程组内.
Thread类给线程设置线程组的方法:
方法 | 作用 |
---|---|
Thread()ThreadGroup group,Runnable target | 以target的run()方法执行创建新线程,属于group线程组 |
实现Callable接口创建线程
自JDK5以后,除了实现Runnable接口和继承Thread类这两种方法外又新增了Callable接口,call()方法可以作为线程执行体,但call()方法比run()方法功能更强大.
- call()方法可以有返回值
- call()可以声明抛出异常
由于Callable接口不是Runnable接口的子接口,那就意味着Callable对象无法作为target给new Thread(Runnable target)创建线程.
为此,JDK5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该类既实现了Callable接口,又实现了Runnable接口.,FutureTask类可以作为Thread类的target使用.
方法 | 作用 |
---|---|
boolean cancel(boolean mayInterruptIfRunning) | 试图取消该future里关联的callable任务. |
V get() | 返回Callable任务里的call()方法的返回值.调用该方法将导致程序阻塞,必须等到子线程结束才会得到返回值 |
V get(long timeout,TimeUnit unit) | 返回Callable任务里call方法的返回值.该方法让程序最多阻塞timeout和unit指定的时间.如果经过指定时间后Callable任务依然没有返回值,将会抛出TimeoutException异常 |
boolean isCancelled() | 如果在Callable任务正常完成前被取消,则返回true |
boolean isDane() | 如果Callable任务完成,则返回true |
使用Callable创建线程的一般步骤
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call方法有返回值.
- 创建Callable实现类的实例对象,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象call()方法的返回值
- 使用FutureTask对象作为Thread对象的target创建,并启动新线程.
- 调用FutureTask对象的方法来获得子线程执行结束后的返回值.
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class RtnThread implements Callable<Integer>
{
//实现call方法,作为线程执行体
@Override
public Integer call() throws Exception {
// TODO Auto-generated method stub
int i=0;
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"的循环变量i的值"+i);
}
return i;
}
}
public class CallableTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
//创建callable对象
RtnThread rtnThread=new RtnThread();
//使用FutureTask来包装Callable对象
FutureTask<Integer> task=new FutureTask<>(rtnThread);
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"的循环变量i的值:"+i);
if (i==20) {
//实质还是以Callable对象来创建,并启动线程
new Thread(task,"有返回值的线程").start();
}
}
try {
//获取线程的返回值
System.out.println("子线程的返回值:"+task.get());
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
线程池的功能和用法
系统启动新线程的成本是比较高的,因为它涉及与操作系统相关的交互.在这种情况下,使用线程池技术可以很好的提升性能.尤其是程序需要创建大量生存周期很短暂的线程时,更应该考虑使用线程池.
使用Executors工厂类产生线程池
静态方法创建线程池
方法 | 作用 |
---|---|
newCachedThreadPool() | 创建一个具有缓存功能的线程池,系统根据需要创建线程池,这些线程将会被缓存在线程池 |
newFixedThreadPool(int nThreads) | 创建一个可重用的,具有固定线程数的线程池 |
newSingleThreadExcutor() | 创建一个只有单线程的线程池,它相当于newFixedThreadPool方法时传入参数为1 |
newScheduledThreadPool(int corePoolSize) | 创建具有指定线程数的线程池,可以在指定延迟后执行线程任务 |
newSingleThreadScheduledExecutor() | 创建只有一条线程的线程池,可延时执行线程任务 |
newCachedThreadPool(),newFixedThreadPool(int nThreads),newSingleThreadExcutor()这三个方法执行后会返回ExecutorService对象,该对象代表线程池可以执行Runnable对象或Callable对象所代表的线程.
newScheduledThreadPool(int corePoolSize),newSingleThreadScheduledExecutor()返回ScheduledExecutorService线程池对象,可延时执行线程.
ExecutorService线程池对象会将收到的线程任务尽快执行,有三个执行方法:
ScheduleExecutorService线程池对象可在指定延时时间后执行线程,有四个方法:
调用线程池对象的shutdown()方法后池内线程将不接受新任务,池内线程有任务的继续执行直到所有线程任务执行完毕后一起死亡.
调用线程池执行任务的步骤
- 调用Executors类的静态工厂方法创建一个ExecutorService(或者ScheduleExecutorService)对象,
- 创建Runnable或者Callable实现类的实例对象,作为线程执行任务
- 调用ExecutorService对象的submit方法来提交Runnable(或者Callable)实例
- 当不想提交任何任务时调用ExecutorService对象的shutdown方法来关闭线程池.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//实现Runnable接口来定义一个简单的线程
class TestThread implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"的i值为"+i);
}
}
}
public class ThreadPoolTest {
public static void main(String[] args) {
//创建一个具有固定线程数(6)的线程池
ExecutorService pool=Executors.newFixedThreadPool(6);
//向线程池中提交两个线程
pool.submit(new TestThread());
pool.submit(new TestThread());
//关闭线程池
pool.shutdown();
}
}
ThreadLocal类的功能和用法
ThreadLocal类可以将线程中的局部变量作为副本传给其他线程,让这个变量变成线程独立的资源(每个线程都有这个变量的独立存储空间),避免并发访问的线程安全问题.
方法 | 作用 |
---|---|
T get() | 返回此线程局部变量中当前线程副本中的值 |
void remove() | 删除此线程局部变量中当前线程的值 |
void set(T value) | 设置此线程局部变量中当前线程副本中的值 |
一个ThreadLocal的使用实例
//账号类
class Account{
/**
* 定义一个ThreadLocal类型的变量,该变量将是一个线程局部变量
* 每一个线程都会保留该变量的一个副本
*/
private ThreadLocal<String> name=new ThreadLocal<>();
//定义一个初始化name属性的构造器
public Account(String string) {
this.name.set(string);
System.out.println("------"+this.name.get());
}
//定义了name属性的setter和getter
public String getName() {
return name.get();
}
public void setName(String string) {
this.name.set(string);;
}
}
class MyTest extends Thread{
private Account account;
public MyTest(Account account,String name) {
super(name);
this.account=account;
}
public void run() {
for (int i = 0; i < 10; i++) {
//当i==6时输出将账户名替换为当前线程名
if (i==6) {
account.setName(getName());
}
System.out.println(account.getName()+"的账户的i值"+i);
}
}
}
public class ThreadLocalTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
//启动两条线程共享一个Account
Account account=new Account("初始名");
/**
* 虽然两条线程共享一个Account类对象,但是name确实独立的,不参与共享
* 每个线程都有name的副本
*/
new MyTest(account, "线程甲").start();
new MyTest(account, "线程乙").start();
}
}
使用线程安全的集合类
将线程不安全的集合类包装成线程安全的集合类
ArrayList、LinkedList、HashSet、TreeSet、HashMap等都是线程不安全的集合类,
我们可以使用Collections工具类提供的静态方法包装为线程安全的集合类。
方法 | 作用 |
---|---|
Collection< T > synchronizedCollection(Collection< T > c ) | 返回指定Collection接口对应的线程安全的Collection(比如hashMap对应的就是ConcurrentHashMap) |
static List synchronizedList(List list) | 返回指定List对应的线程安全的List对象 |
线程安全的集合类 ConcurrentHashMap和ConcurrentLinkedQueue
- 这两个类写入线程的所有操作都是线程安全(有锁)的,读取操作没有锁.
- 当多个线程共享访问一个集合类时,推荐使用ConcurrentLinkedQueue,多条线程并发访问这个集合类时无需进行等待。
- 当我们使用java.util包下的Collection实现类作为集合对象时,对该集合对象使用迭代器后集合元素发生改变,会引发ConcurrentModificationException异常,但是如果对上诉两个并发类建立迭代器并修改集合元素,不会抛出异常(可能你做的修改是无效的)。