Head First 多线程

线程概述

进程与线程:几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个运行中的程序就是一个进程,当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程。线程是进程的组成部分,一个进程可以有多个线程,一个线程必须有一个父进程。它与父进程的其他线程共享该进程所拥有的全部资源。

线程的创建与启动

Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。创建线程的方法有三种:

继承Thread类创建线程

step1:定义Thread类,并重写该类的run()方法;
step2:创建Thread子类的实例,即创建了线程对象;
step3:调用start()方法来启动该线程。

public class FirstThread extends Thread{
    private int i;

    // 重写run方法,run方法的方法体就是线程的执行体
    public void run() {
        for (; i < 100; i++) {
            // getName()方法返回当前线程的名字
            System.out.println(getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        for(int i = 0;i < 100;i++){
            System.out.println(Thread.currentThread().getName() + " " + i);
            if(i == 20){
                //创建两个线程
                new FirstThread().start();
                new FirstThread().start();
            }
        }
    }
}

enter description here
注意:主线程的名字为main,用户启动的线程名字依次为Thread-0~Thread-n。使用继承Thread类创建线程,由于需要创建新线程对象,因此无法共享线程类的实例变量

实现Runnable接口创建线程类

step1:定义Runnable接口的实现类,重写run()方法;
step2:创建Runnable接口的实例,并作为Thread的target创建线程对象;
step3:调用start()方法来启动该线程。

public class SecondThread implements Runnable {
    private int i = 0;

    // run方法依然是线程执行体
    @Override
    public void run() {
        for (; i < 100; i++) {
            // Thread.currentThread().getName()方法返回当前线程的名字
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        for(int i = 0;i < 100;i++){
            System.out.println(Thread.currentThread().getName() + " " + i);
            if(i == 20){
                SecondThread st = new SecondThread();
                //创建两个线程,同时命名
                new Thread(st,"新线程1").start();
                new Thread(st,"新线程2").start();
            }
        }
    }
}

注意:新线程1,新线程2输出i变量是连续的,说明多线程可以共享同一个线程类的实例变量。Runnable接口是一个函数式接口。

使用Callable和Futrue创建线程

Callable接口提供了一个call()方法可以作为线程执行体,但call()方法比run()方法功能更强大:call()方法可以有返回值且call()方法可以声明抛出异常。
Callable接口不是Runnable接口的子接口,所以Callable对象不能直接作为Tread的target,需要使用Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runnable接口,所以可以作为Tread类的target。创建并启动有返回值的线程步骤:
step1:创建Callable接口的实现类,并实现call()方法;
step2:使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;
step3:使用FutureTask对象作为Tread对象的target创建并启动新线程;
step4:调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

public class ThirdThread {
    public static void main(String[] args) {
        ThirdThread rt = new ThirdThread();
        //使用Lambda表达式创建Callable对象,使用FutureTask包装Callable对象
        FutureTask<Integer> task = new FutureTask<>(() -> {
            int i = 0;
            for (; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
            return i;
        });

        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 20) {
                //创建线程,同时命名
                new Thread(task, "有返回值的新线程").start();
            }
        }
        try {
            System.out.println("子线程的返回值为" + task.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

注意:FutureTask对象的get方法将导致主线程被阻塞,直到call()方法结束并返回为止

创建线程的三种方式对比

对于实现Runnable、Callable接口的方法:

  • 优点:线程类只实现Runnable、Callable接口还可以继承其他类;在这种方式下,多个线程可以共享一个target对象,非常适合多个相同线程来处理同一份资源的情况。
  • 缺点:如果要访问当前线程,必须使用Tread.currentTread()方法,编程较为繁琐,如果继承Tread类的话直接使用this即可获得当前线程。

一般推荐采用实现Runnable、Callable接口的创建方法。

线程的生命周期

当线程被创建并启动以后,它既不是一启动就进入执行状态,也不是一直处于执行状态,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。当线程启动以后,CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。

新建和就绪状态

new出一个线程之后,该线程处于新建状态,此时和其他Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值,此时线程对象没有表现出任何线程的动态特征,程序也不会执行线程的执行体;当调用start()方法之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有开始运行,只是表示该线程可以运行了,至于何时开始运行,取决于JVM里线程调度器的调度。

注意:如果希望子线程在start()后立即执行,可以使用Thread.sleap(1)方法让当前主线程睡眠1毫秒。

运行和阻塞状态

如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态

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

  • 线程调用了sleep()方法主动放弃所占用的处理器资源;
  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞;
  • 线程试图获得一个同步监视器,但该同步监视器正在被其他线程所持有;
  • 线程在等待某个通知;
  • 程序调用了线程的suspend()方法将线程挂起。

当发生如下特定的情况时可以解除上面的阻塞,让该线程重新进入就绪状态:

  • 调用sleep()方法的线程经过了指定时间;
  • 线程调用的阻塞式IO方法已经返回;
  • 线程成功地获得了试图取得的同步监视器;
  • 线程正在等待某个通知时,其他线程发出了一个通知;
  • 处于挂起状态的线程被调用了resume()恢复方法。
    enter description here

线程死亡

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

  • run()或call()方法执行完成,线程正常结束;
  • 线程抛出一个未捕获的Exception或Error;
  • 直接调用该线程的stop()方法来结束该线程-该方法容易导致死锁,不推荐使用。

isAlive()方法可以测试某个线程是否已经死亡,当线程处于就绪、运行、阻塞三种状态时,该方法返回true;当线程处于新建、死亡状态时,返回false;若对已死亡的线程使用start()方法将会引发IllegalThreadStateException异常

控制线程

join线程

当在某个程序执行流中调用其它线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的线程执行完毕为止。

public class JoinThread extends Thread {

    // 提供一个有参构造器,用于设置线程名称
    public JoinThread(String name) {
        super(name);
    }

    public void run() {
        for (int i = 0; i < 100; i++) {
            // getName()方法返回当前线程的名字
            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 jt = new JoinThread("被Join的线程");
                jt.start();
                //main线程调用jt线程的join()方法
                jt.join();
				//调用后join后,main方法阻塞
            }
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

enter description here

后台线程

后台线程(Daemon Thread),又称守护线程或精灵线程,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) {
        DaemonThread dt = new DaemonThread();
        //将此线程设置为伴随线程
        dt.setDaemon(true);
        //启动后台线程
        dt.start();
        for(int i = 0;i < 10;i++){
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
		//主线程结束后,后台线程自动死亡,i不会执行到1000
    }
}

前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。

线程睡眠

Thread.sleep()方法可以让正在执行的线程暂停一段时间,并进入阻塞状态。在线程睡眠期间,将不会获得执行机会,即使系统中没有其它可执行的线程也不会执行。
sleep()的两种重载形式:

  • static void sleep(long millis)
  • static void sleep(long millis,int nanos)
public class SleepThread {
    public static void main(String[] args) throws InterruptedException {
        for(int i = 0;i < 10;i++){
            System.out.println("当前时间"+ Clock.systemUTC().instant());
            //调用sleep方法让当前线程暂停1s
            Thread.sleep(1000);
        }
    }
}

系统每隔1s输出一个当前时间。

Thread.yield()方法可以让正在执行的线程暂停,但它不会阻塞该线程,只是让线程转入就绪状态,可能很快又会继续执行;当某个线程调用了yield方法暂停后,只有优先级与当前线程相同或高于该线程的就绪状态线程才会有机会执行。

线程的优先级

每个线程默认的优先级都与创建它的父线程优先级相同,在默认情况下,main线程具有普通优先级。Thread类提供Tread.setPriority(int newPriority)、getPriority()方法可以设置和返回指定线程的优先级,范围是1~10之间。默认优先级为5。

public class PriorityTest extends Thread {
    public PriorityTest(String name) {
        super(name);
    }

    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println(getName() + "循环变量" + i);
        }
    }

    public static void main(String[] args) {
        //设置主线程优先级为6
        Thread.currentThread().setPriority(6);
        for (int i = 0; i < 30; i++) {
            if (i == 10) {
                PriorityTest low = new PriorityTest("低级");
                low.setPriority(Thread.MIN_PRIORITY);
                low.start();
            }
            if (i == 20) {
                PriorityTest high = new PriorityTest("高级");
                high.setPriority(Thread.MAX_PRIORITY);
                high.start();
            }
        }
    }
}

明显优先级更高的线程得到了更多的执行机会。

线程同步

线程安全问题

以两个人以同一账号在银行取钱问题为例:

class Account {
    private double balence;//账户余额

    public Account(double balence) {
        this.balence = balence;
    }

    public double getBalence() {
        return balence;
    }

    public void setBalence(double balence) {
        this.balence = balence;
    }
}

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.getBalence() >= drawAmount) {
            System.out.println("吐出钞票" + drawAmount);
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //修改余额
            account.setBalence(account.getBalence() - drawAmount);
            System.out.println("余额为" + account.getBalence());
        } else {
            System.out.println("余额不足");
        }
    }
}

public class DrawTest {
    public static void main(String[] args) {
        Account account = new Account(1000);

        DrawThread dt1 = new DrawThread("甲", account, 800);
        DrawThread dt2 = new DrawThread("乙", account, 800);

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

输出结果为:

吐出钞票800.0
吐出钞票800.0
余额为200.0
余额为-600.0

这就是多个线程对同一个对象进行修改时会产生的线程不安全问题,因为run方法不具有同步安全性。

同步代码块

可以使用关键字synchronized创建同步代码块,语法格式为:

synchronize(obj){
    // 同步代码块
}

obj就是同步监视器,任何时刻只有一个线程可以获得对同步代码块的锁定,阻止两个线程同时对一个共享资源(临界区)的并发访问。对银行取钱问题,可以使用account对象作为同步监视器。

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() {
        //对account进行锁定
        synchronized (account){
            if (account.getBalence() >= drawAmount) {
                System.out.println("吐出钞票" + drawAmount);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //修改余额
                account.setBalence(account.getBalence() - drawAmount);
                System.out.println("余额为" + account.getBalence());
            } else {
                System.out.println("余额不足");
            }
        }
        // 同步代码块结束,该线程释放同步锁
    }
}

加锁—修改—释放锁

同步方法

同步方法就是使用synchronized关键字修饰某个方法,这个方法就是同步方法。这个同步方法(非static方法)无须显式指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。.不可变类总是线程安全的,因为它的对象状态不可改变可变类需要额外的方法来保证其线程安全,在Account类中我们只需要把balance的方法变成同步方法即可:

public class Account {
    private String accountNo;
    private double balance;
    public Account(String accountNo,double balance){
        this.accountNo=accountNo;
        this.balance=balance;
    }
 
    //因为账户余额不可以随便更改,所以只为balance提供getter方法
    public double getBalance() {
        return balance;
    }
	
    //提供一个线程安全的draw()方法来完成取钱操作
    public synchronized void draw(double drawAmount){
        if(balance>=drawAmount){
            System.out.println(Thread.currentThread().getName()+"取钱成功!吐出钞票:"+drawAmount);
            try{
                Thread.sleep(1);
            }catch (InterruptedException ex){
                ex.printStackTrace();
            }
            balance-=drawAmount;
            System.out.println("\t余额为:"+balance);
        }else{
            System.out.println(Thread.currentThread().getName()+"取钱失败,余额不足");
        }
    }
}

可变类的线程安全是以降低程序的运行效率为代价的,不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(共享资源)的方法进行同步。

同步锁

Java5开始,Java提供了一种功能更加强大的线程同步机制——通过显式定义同步锁对象来实现同步,这里的同步锁由Lock对象充当。在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁)。代码格式如下:

class X{
    //定义锁对象
    private final ReentrantLock lock=new ReentrantLock();
    //定义需要保证线程安全的方法
    public void m(){
        //加锁
        lock.lock();
        try{
            //...method body
        }
        //使用finally块来保证释放锁
        finally{
            lock.unlock();
        }
    }
}

死锁

当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有检测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁出现。一旦出现死锁,程序既不会发生任何异常,也不会给出任何提示,只是所有线程都处于阻塞状态,无法继续。

class A{
    public synchronized void foo(B b){
        System.out.println("当前线程名为:"+Thread.currentThread().getName()+"进入了A实例的foo()方法");
        try{
            Thread.sleep(200);
        }catch(InterruptedException ex){
            ex.printStackTrace();
        }
        System.out.println("当前线程名为:"+Thread.currentThread().getName()+"试图调用B实例的last()方法");
        b.last();
    }
    public synchronized void last(){
        System.out.println("进入了A类的last()方法内部");
    }
}
class B{
    public synchronized void bar(A a){
        System.out.println("当前线程名为:"+Thread.currentThread().getName()+"进入了B实例的bar()方法");
        try{
            Thread.sleep(200);
        }catch(InterruptedException ex){
            ex.printStackTrace();
        }
        System.out.println("当前线程名为:"+Thread.currentThread().getName()+"试图调用A实例的last()方法");
        a.last();
    }
    public synchronized void last(){
        System.out.println("进入了B类的last()方法内部");
    }
}
public class DeadLock implements Runnable{
    A a =new A();
    B b=new B();
    public void init(){
        Thread.currentThread().setName("主线程");
        a.foo(b);
        System.out.println("进入了主线程之后...");
    }
    public void run(){
        Thread.currentThread().setName("副线程");
        b.bar(a);
        System.out.println("进入了副线程之后...");
    }
    public static void main(String[] args){
        DeadLock d1=new DeadLock();
        new Thread(d1).start();
        d1.init();
    }
}

结果:
当前线程名为:主线程进入了A实例的foo()方法
当前线程名为:副线程进入了B实例的bar()方法
当前线程名为:主线程试图调用B实例的last()方法
当前线程名为:副线程试图调用A实例的last()方法

在A实例中把B实例加锁,B实例中把A实例加锁,二者相互等待,造成死锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值