Java 线程概述
阅读原文时间:2023年07月16日阅读:1

1 进程与线程基本概念

1.1 进程:执行中的程序

每个进程都有独立的代码和数据空间(进程上下文),进程空间切换会有较大的开销,一个进程包含1-n个线程。进程是资源分配的最小单位。

1.2 线程:进程的执行单元,线程依靠进程运行,只能使用分配给进程的资源

同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器,线程切换开销小。线程是cpu调度的最小单位。

1.3 单线程

程序中只存在一个线程

1.4 多线程

一个程序中运行多个任务(同一程序中有多个顺序流在执行)

1.5 线程与进程的五个状态(生命周期)

新建、就绪、运行、阻塞、死亡。

2 线程实现

2.1 继承Thread类

① 通过继承Thread类定义它的子类,并重写该类的run()方法,该方法的方法体代表线程需要完成的任务。因此把run方法称为线程执行体。

② 创建Thread类子类的实例,即创建线程对象。

③ 调用线程对象start()方法来启动线程。

创建一个线程类

public class Thread4 extends Thread {

private int i = 0;

public Thread4(String name)  
{  
    super(name);  
}

@Override  
public void run()  
{  
    for(; i<10; i++)  
    {  
        //当线程类继承Thread类时,直接使用this即可获得当前线程  
        //Thread对象的getName()返回当前线程名字  
        //直接调用getName()获取当前线程名字  
        System.out.println("Thread"+getName()+" "+i);  
    }  
}

}

测试

@Test  
public void testRun() {

    for(int i=0; i<10; i++)  
    {  
        System.out.println(Thread.currentThread().getName()+" "+i);

        if(i==5)  
        {  
            new Thread4(i+"-1").start();  
            new Thread4(i+"-2").start();  
        }  
    }  
}

2.2 实现Runnable接口

① 定义Runnable 接口的实现类,并重写该接口的run()方法,该方法的方法体是线程的执行体。

② 创建Runnable实现类的实例,并以此实例为Thread的target来创建Thread类对象,该Thread对象才是真正的线程对象。

③ 调用线程对象的start()方法启动线程。

public class Runnable2 implements Runnable {

private String name;

public Runnable2(String name)  
{  
    this.name = name;  
}

@Override  
public void run() {

    for(int i=0; i<10; i++)  
    {  
        System.out.println(this.name+" "+i);  
    }  
}

public void start()  
{  
    new Thread(new Runnable2(this.name)).start();  
}

}

测试

@Test  
public void testRun() {

    for(int i=0; i<10; i++)  
    {  
        System.out.println(Thread.currentThread().getName()+" "+i);

        if(i==4)  
        {  
            new Runnable2(i+"-1").start();  
            new Runnable2(i+"-2").start();  
            new Runnable2(i+"-3").start();  
        }  
    }  
}

2.3 Callable 与 FutureTask 创建线程

① 创建Callable 接口的实现类,并实现call()方法,该call()方法作为线程执行体,且该方法有返回值,再创建Callable实现类的实例。

② 使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值。

③ 使用FutureTask对象作为Thread对象的target创建并启动新线程。

④ 调用FutureTask对象的get()方法获取子线程执行结束后的返回值。

//创建Callable 接口的实现类,并实现call()方法
public class Callable1 implements Callable{

@Override  
public Integer call() throws Exception {

    int sum = 0;

    for(int i=1; i<50; i++)  
    {  
        sum+=i;  
        System.out.println(i);  
    }

    return sum;  
}

}

@Test  
public void testCall() throws InterruptedException, ExecutionException {

    //使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值  
    Callable1 call = new Callable1();  
    FutureTask<Integer> task = new FutureTask<Integer>(call);  
    // 使用FutureTask对象作为Thread对象的target创建并启动新线程  
    new Thread(task).start();  
    //调用FutureTask对象的get()方法获取子线程执行结束后的返回值  
    System.out.println(task.get());  
}

3 线程生命周期

3.1 新建与就绪

当程序使用new 关键字创建一个线程之后,该线程就处于新建状态,此时它与其他Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。

当线程调用了start()方法之后,该线程就处于就绪状态,Java虚拟机为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有立即开始运行,只是表示可以运行了。至于该线程该何时开始运行,取决于JVM里的线程调度器。

3.2 运行与阻塞

如果处于就绪状态的线程获得了CPU资源,开始执行run()方法的线程执行体,则该线程处于运动状态,如果计算机只有一个CPU,那么在任何时刻只有一个线程处于运行状态。

当如下情况发生,线程将会进入阻塞状态:

① 线程调用sleep()方法主动放弃所占用的CPU资源;

② 线程调用了一个阻塞式IO方法,在该方法返回前,该线程被阻塞;

③ 线程试图获取一个同步监视器(同步锁),但是该同步锁正在被其他线程所持有;

④ 线程在等待某个通知(notify);

⑤ 程序调用了线程的suspend()方法将它挂起;

3.3 死亡

线程会以如下三种方式结束,结束后就处于死亡状态:

① run()或call()执行完成,线程正常结束;

② 线程抛出一个未捕获的Exception或者Err;

③ 直接调用线程的stop()方法;

④ 使用标志位控制线程结束;

4 线程控制(常用方法)

4.1 join()

Thread 提供了让一个线程等待另一个线程完成的方法:join()方法。当某个程序执行流中调用其他线程的join()方法时,该线程将被阻塞,直到调用join()方法的线程执行完为止。join()方法有三种重载形式:

① join():等待被join的线程执行完成。

② join(long mills):等待被join()的线程的时间最长为millis毫秒,如果millis毫秒内,被join()的线程还没有执行结束,则不再等待。

③ join(long mills, int nanos):等待被join()的线程的时间最长为millis毫秒+nanos毫微秒。

public class JoinThread1 extends Thread{

private String name;

public JoinThread1(String name)  
{  
    this.name = name;  
}

@Override  
public void run() {

    for(int i=0; i<20; i++)  
    {  
        System.out.println(this.name+i);

    }

}

}

@Test  
public void testRun() throws InterruptedException {

    //启动一个子线程  
    JoinThread1 jt1 = new JoinThread1("A Thread");  
    jt1.start();

    for(int i=0; i<20; i++)  
    {  
        if(i==10)  
        {  
            JoinThread1 jt2 = new JoinThread1("B Thread");  
            jt2.start();  
            jt2.join();  
        }

        System.out.println(Thread.currentThread().getName()+i);  
    }  
}

4.2 sleep()

sleep()方法是Thread类的静态方法,调用它会让当前正在执行的线程暂停一段时间,并进入阻塞状态,但是不会释放锁,sleep()有两种重载形式:

sleep(long mills):让当前正在执行的线程暂停millis毫秒,并进入阻塞状态。

sleep(long mills, int nanos):让当前正在执行的线程暂停millis毫秒+nanos毫微秒,并进入阻塞状态。

当前线程调用sleep()方法进入阻塞状态时,在其睡眠时间段内,该线程不会获得执行机会,即使系统中没有其他可执行的线程,处于sleep()中的线程也不会执行,因此sleep()方法常用来暂停程序。

4.3 yield()

yield()方法与sleep() 有点类似,它是Thread类的静态方法,调用该方法会让线程暂停,但是线程不会进入阻塞状态,而是将线程转入就绪状态。yield()方法只是让当前线程暂停一下,让系统调度器重新调度一次。有可能出现:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行,该线程又立刻进入运行状态。

当某个线程调用了yield()方法暂停之后,只有优先级大于或者等于当前线程,并且处于就绪状态的线程才能获得执行的机会。

sleep()与yield()区别

① sleep()方法暂停当前线程后,会给其他线程执行机会,对其他线程的优先级没有要求;yield()方法暂停线程后只会给优先级大于等于它的其他线程执行机会。

② sleep()方法将线程转入阻塞状态,直到阻塞时间结束才会转入就绪状态;yield()直接将线程转入就绪状态。

③ sleep()方法声明抛出了InterruptedException 异常,调用它时需要捕捉该异常或者抛出该异常;yield()方法没有抛出异常。

4.4 setPriority() 与 getPriority()

每个线程执行时都有一定优先级,优先级高则获取较多的执行机会,优先级低则获取较少的执行机会。

Thread类提供了setPriority()与getPriority()方法来设置获取指定线程优先级,setPriority()的参数可以是一个整数,范围是1-10。也可以使用Thread类的如下三个静态常量

MAX_PRIORITY:其值是10。

MIN_PRIORITY:其值是1。

NORMAL_PRIORITY:其值是5。

4.5 setName(String name)

设置线程的名字

4.6 setDaemon(boolean on)

线程设置为守护线程

4.7 isAlive()

线程是否活着

5 线程同步

5.1 银行取钱问题

① 用户插入卡号,输入密码;

② 输入金额,判断金额是否大于余额;

③ 输入金额小于余额则取款成功,否则失败;

上面的流程在多线程并发的场景下,就可能会出问题。

5.2 用代码实现上述流程

① 定义一个Account 类

public class Account {

private String accountNo;  
private double balance;

public Account(){}

public Account(String accountNo, double balance)  
{  
    this.accountNo = accountNo;  
    this.balance = balance;  
}

public String getAccountNo() {  
    return accountNo;  
}

public void setAccountNo(String accountNo) {  
    this.accountNo = accountNo;  
}

public double getBalance() {  
    return balance;  
}

public int hashCode()  
{  
    return accountNo.hashCode();  
}

public boolean equals(Object obj)  
{  
    if(this == obj)  
        return true;  
    if(obj!=null && obj.getClass() == Account.class)  
    {  
        Account target = (Account) obj;  
        return target.getAccountNo().equals(accountNo);  
    }

    return false;  
}  

}

② 定义一个取钱的线程类

package com.latiny.concurrent;

import com.latiny.concurrent.Account;

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()>this.drawAmount)  
    {  
        System.out.println(getName()+" 取了"+drawAmount+"元");

        try  
        {  
            Thread.sleep(1000);  
        }  
        catch (InterruptedException e)  
        {  
            e.printStackTrace();  
        }

        account.setBalance(account.getBalance()-drawAmount);  
        System.out.println(getName()+" 取了之后余额为:"+account.getBalance()+"元");  
    }  
    else  
    {  
        System.out.println(getName()+" 余额不足");  
    }  
}

}

③ Testing

import com.latiny.concurrent.*;

public class DrawThreadTest {

public static void main(String\[\] args)  
{  
    Account account1 = new Account("1001",1000);  
    DrawThread dt1 = new DrawThread("Latiny1", account1, 800);  
    DrawThread dt2 = new DrawThread("Latiny2", account1, 800);  
    DrawThread dt3 = new DrawThread("Latiny3", account1, 800);

    dt1.start();  
    dt2.start();  
    dt3.start();  
}  

}

Testing result:

Latiny2 取了800.0元
Latiny3 取了800.0元
Latiny1 取了800.0元
Latiny3 取了之后余额为:-600.0元
Latiny1 取了之后余额为:-600.0元
Latiny2 取了之后余额为:-1400.0元

多次运行取钱线程类,出现了问题,账户只有1000元,取出了2400元,账户余额出现了负值。

5.3 同步代码块

为了解决线程不安全问题,Java多线程支持引入同步监视器来解决这个问题

synchronized(obj)
{

  //同步块代码
}

上面语法格式synchronized 后括号里的obj就是同步监视器,含义:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。

Java允许对任何对象作为同步监视器,但是同步监视器的目的:阻止多个线程对同一个共享资源进行并发访问,所以一般只对可能被并发访问的共享资源充当同步监视器。对于上列取钱模拟程序,应该考虑使用账户account作为同步监视器,修改上列的DrawThread代码:

   public void run()
{
//使用account作为同步监视器,任何线程进入线面同步代码块之前必须先获得对account账户的锁,
//获得锁之后,其他线程无法获得锁,也就无法修改它。直到获得account 锁的线程执行完程序体,释放锁,其他线程才能获得锁,继而执行代码同步块。
synchronized(account)
{
if(account.getBalance()>this.drawAmount)
{
System.out.println(getName()+" 取了"+drawAmount+"元");

            try  
            {  
                Thread.sleep(1000);  
            }  
            catch (InterruptedException e)  
            {  
                e.printStackTrace();  
            }

            account.setBalance(account.getBalance()-drawAmount);  
            System.out.println(getName()+" 取了之后余额为:"+account.getBalance()+"元");  
        }  
        else  
        {  
            System.out.println(getName()+" 余额不足");  
        }  
    }  
    //同步代码块结束,该线程释放同步锁。  
}

上面程序使用synchronized将run()方法里的程序修改为同步代码块,该同步代码块的监视对象是account对象,这样就做到了:加锁 -> 修改 -> 释放锁 的逻辑,任何线程修改指定的资源之前,首先会对该资源加锁,在加锁期间其他线程无法修改该资源,当该线程修改完之后,释放对该资源的锁。通过这种方式就可以保证并发线程在任何时刻只有一个线程可以进入修改资源的代码块,这就就保证了线程的安全。修改程序之后,多次运行该程序,总能得到正确的结果:

Latiny1 取了800.0元
Latiny1 取了之后余额为:200.0元
Latiny2 余额不足
Latiny3 余额不足

5.4 同步方法

Java多线程安全提供了同步方法,同步方法就是使用synchronized关键字修饰方法,对于synchronized修饰的方法(非静态方法)而言,无需显示指定同步监视器,同步方法的同步监视器是this,即调用该方法的对象。

修改Account类,增加draw()方法:

   public synchronized void draw(double drawAmount)
{
if(balance>=drawAmount)
{
System.out.println("您成功取了"+drawAmount+"元");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

        balance-=drawAmount;  
        System.out.println("您的余额为:"+balance);  
    }  
    else  
    {  
        System.out.println("您的余额不足,当前余额为:"+balance);  
    }  
}

上面程序提供了一个取钱方法draw(),并使用了synchronized关键字修饰,把该方法变为了同步方法,该同步方法的同步监视器是this。

改写drawThread类:

public void run()  
{  
    //直接调用account对象的draw方法来执行取钱操作  
    //同步方法监视器是this,this代表调用draw()方法的对象  
    //也就是说,线程进入draw()方法之前,必须先对account对象加锁。  
    account.draw(getName(), drawAmount);  
}

线程安全是以降低程序的运行效率作为代价的,为了减少线程安全带来的负面影响,程序可以采用如下策略:

① 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源或者共享资源的方法进行同步;

② 如果类有两种运行环境:单线程环境与多线程环境,则应该为它提供两种版本;

5.5 释放同步监视器的锁

① 线程会在以下情况释放对同步监视器的锁:

当前线程的同步方法、同步代码块执行结束;

当前线程同步方法、同步代码块遇到break、return终止了该方法、代码块的执行;

当前线程同步方法、同步代码块出现了未处理的Error或者Exception,导致线程同步方法、同步代码块异常结束;

当前线程同步方法、同步代码块执行时,程序执行了同步监视器对象的wait()方法,则当前线程暂停;

② 线程在以下情况不会释放对同步监视器的锁:

线程执行同步代码块、同步方法时,程序调用Thread.sleep()、Thread.yield()方法来暂停当前线程的执行;

线程执行同步代码块、同步方法时,其他线程调用了该线程的suspend()方法将该线程挂起;

5.6 同步锁(lock)

显示定义同步锁对象实现同步 -- 一种更强大的线程同步机制,这种机制下,同步锁由Lock对象来实现。

Lock提供了比synchronized方法和synchronized代码更广泛的锁定操作,Lock允许实现更灵活的结构,可以具有差别很大的属性,并且支持多个相关的Condition对象。

线程安全的控制中,常用的是ReentrantLock锁,使用该锁可以显示的加锁、释放锁:

//定义锁对象  
private final ReentrantLock lock = new ReentrantLock();  
public void draw(String threadName, double drawAmount)  
{  
    lock.lock();  
    try  
    {  
        if(balance>=drawAmount)  
        {  
            System.out.println(threadName+"成功取了"+drawAmount+"元");

            Thread.sleep(10);

            balance-=drawAmount;  
            System.out.println(threadName+"取钱之后的余额为:"+balance);  
        }  
        else  
        {  
            System.out.println(threadName+"取钱的余额不足,当前余额为:"+balance);  
        }  
    }  
    catch(Exception e)  
    {  
        e.printStackTrace();  
    }  
    //使用finally块保证释放锁  
    finally  
    {  
        lock.unlock();  
    }

}

5.7 死锁

当两个线程互相等待对方释放同步监视器时就会发生死锁。Java虚拟机不会监测死锁情况,也不会处理。所以多线程编程时应该采取措施避免死锁。

package com.latiny.concurrent;

import java.util.concurrent.locks.ReentrantLock;

public class DeadLockA implements Runnable {

A a = new A();  
B b = new B();

@Override  
public void run() {  
    a.test1(b);  
    b.test1(a);  
}

public static void main(String\[\] args)  
{  
    DeadLockA dl = new DeadLockA();  
    new Thread(dl).start();  
    new Thread(dl).start();  
}

}

class A
{
private final ReentrantLock lock = new ReentrantLock();

public void test1(B b)  
{  
    lock.lock();  
    try  
    {  
        System.out.println(Thread.currentThread().getName()+" 进入了A实例的test1方法");  
        Thread.sleep(1000);  
        b.last();  
    }  
    catch(Exception e)  
    {  
        e.printStackTrace();  
    }  
    finally  
    {  
        lock.unlock();  
    }  
}

public void last()  
{  
    lock.lock();  
    try  
    {  
        System.out.println("A实例的last()方法");  
    }  
    finally  
    {  
        lock.unlock();  
    }

}

}

class B
{
private final ReentrantLock lock = new ReentrantLock();

public void test1(A a)  
{  
    lock.lock();  
    try  
    {  
        System.out.println(Thread.currentThread().getName()+" 进入了B实例的test1方法");  
        Thread.sleep(1000);  
        a.last();  
    }  
    catch(Exception e)  
    {  
        e.printStackTrace();  
    }  
    finally  
    {  
        lock.unlock();  
    }  
}

public void last()  
{  
    lock.lock();  
    try  
    {  
        System.out.println("B实例的last()方法");  
    }  
    finally  
    {  
        lock.unlock();  
    }

}  

}

5.8 线程通信

Java 可以利用Object类的wait()、nofity()、nofityAll()方法完成线程之间的通信。

wait():导致当前线程等待,直到其他线程调用该同步监视器的nofity()或者nofityAll()方法来唤醒该线程,wait()方法有三种形式,无参数,带毫秒,毫秒微妙三种。

notify():唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器等待,则会选择唤醒其中一个线程。选择是任意的。

notifyAll():唤醒在此同步监视器上等待的所有线程。

三线程轮流打印ABC,使用上列的方法可以实现:

public class TurnAround extends Thread {

String name;  
Object self;  
Object prev;

public TurnAround(String name, Object self, Object prev)  
{  
    this.name = name;  
    this.self = self;  
    this.prev = prev;  
}  

  @Override
public void run()
{
int i=0;
while(i<10)
{
try
{
synchronized(prev)
{
synchronized(self)
{
System.out.println(name+i);
i++;
//唤醒下一个线程,如果
self.notify();
}

                //让前一个线程处于等待状态  
                prev.wait();

            }  
        }  
        catch(Exception e)  
        {  
            e.printStackTrace();  
        }

    }  
}  

}

    public static void main(String[] args) throws InterruptedException {

    Object a = new Object();  
    Object b = new Object();  
    Object c = new Object();

    TurnAround A = new TurnAround("A",a,c);  
    TurnAround B = new TurnAround("B",b,a);  
    TurnAround C = new TurnAround("C",c,b);

    A.start();  
    Thread.sleep(100);  
    B.start();  
    Thread.sleep(100);  
    C.start();  
    Thread.sleep(100);

}

5.9 condition控制线程通信

如果程序不使用synchronized 保证同步,而是直接使用Lock对象来控制,则系统中不存在隐式的同步监视器,也不能用wait()、notify()、nofityAll()方法进行通信了。

当使用Lock对象来控制同步时,Java提供了Condition类来保持协调,使用Condition对象可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。

Condition实例被绑定在一个Lock对象上,要获得Lock实例的Condition实例,调用Lock对象的newCondition()方法即可。Condition也提供了三个方法:

① await():类似于wait()方法,导致当前线程等待,直到其他线程调用该Condition的signale()方法或者signalAll()方法来唤醒该线程。

② signal():唤醒在此Lock对象上等待的单个线程。如果有多个线程在该Lock对象上等待,则会选择唤醒其中一个线程。选择是任意的。

③ signalAll():唤醒在此Lock对象上等待的所有线程。