多线程

线程和进程

1.进程是处于运行过程中的程序,其具有一定的独立功能,有以下3个特点:

  • 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间,在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
  • 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合,在进程中加入了时间的概念,进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
  • 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。
    2.并发性和并行性的概念区别:并行只在同一时刻,有多条指令在多个处理器上同时执行,并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
    3.一个进程可以拥有多个线程,但一个线程必须有一个父进程,线程可以拥有自己的堆栈,自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源,同一个进程中的多个线程之间可以并发执行。
    4.多线程主要有以下两个优点:
  • 进程之间不能共享内存,但线程之间共享内存非常容易。
  • 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高。

线程的创建和启动

1.在Java语言中,创建多线程有三种方式,分别是继承Thread类或实现Runnable,Callable接口来创建多线程,这三种创建方式的特征分别有以下几点:
- 实现Runnable,Callabe接口的方式创建多线程 ——线程类实现了Runnable接口或Callable接口(不过这接口定义的线程方法不但可以有返回值,也可以声明抛出异常),还可以继承其他类;在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源情况,从而可以将CPU,代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想,不过编程稍微复杂,访问当前现场,必须使用Thread.currentThread()方法。
- 采用继承Thread类的方式创建多线程——因为线程类已经继承了Thread类,所以不能再继承其他父类,也不能很好共享同一个线程类的实例变量,不过编程较简单。

代码例子:
通过继承Thread类来创建线程类

public class FirstThread extends Thread
{
    private int i ;
    // 重写run方法,run方法的方法体就是线程执行体
    public void run()
    {
        for ( ; i < 100 ; i++ )
        {
            // 当线程类继承Thread类时,直接使用this即可获取当前线程
            // Thread对象的getName()返回当前该线程的名字
            // 因此可以直接调用getName()方法返回当前线程的名
            System.out.println(getName() +  " " + i);
        }
    }
    public static void main(String[] args) 
    {
        for (int i = 0; i < 100;  i++)
        {
            // 调用Thread的currentThread方法获取当前线程           System.out.println(Thread.currentThread().getName()
                +  " " + i);
            if (i == 20)
            {
                // 创建、并启动第一条线程
                new FirstThread().start();
                // 创建、并启动第二条线程
                new FirstThread().start();
            }
        }
    }
}

运行结果:
main 0
main 1
main 2
main 3
main 4
main 5
main 6
main 7
main 8
main 9
main 10
main 11
main 12
main 13
main 14
main 15
main 16
main 17
main 18
main 19
main 20
Thread-0 0
main 21
Thread-0 1
Thread-1 0
Thread-0 2
main 22
Thread-0 3
Thread-1 1
Thread-0 4
main 23
Thread-0 5
Thread-1 2
Thread-0 6
main 24
Thread-0 7
Thread-1 3
Thread-0 8
main 25
Thread-0 9
Thread-1 4
Thread-0 10
main 26
Thread-0 11
Thread-1 5
Thread-0 12
main 27
Thread-0 13
Thread-1 6
Thread-0 14
main 28
Thread-0 15
Thread-1 7
Thread-0 16
main 29
Thread-0 17
Thread-1 8
Thread-0 18
main 30
Thread-0 19
Thread-1 9
Thread-0 20
main 31
Thread-0 21
Thread-1 10
Thread-0 22
main 32
Thread-0 23
Thread-1 11
Thread-0 24
main 33
Thread-0 25
Thread-1 12
Thread-0 26
main 34
Thread-0 27
Thread-1 13
Thread-0 28
main 35
Thread-0 29
Thread-1 14
Thread-0 30
main 36
Thread-0 31
Thread-1 15
Thread-0 32
main 37
Thread-0 33
Thread-1 16
Thread-0 34
main 38
Thread-0 35
Thread-1 17
Thread-0 36
main 39
Thread-0 37
Thread-1 18
Thread-0 38
main 40
Thread-0 39
Thread-1 19
Thread-0 40
main 41
Thread-0 41
Thread-1 20
Thread-0 42
main 42
Thread-0 43
Thread-1 21
Thread-1 22
Thread-1 23
Thread-0 44
main 43
Thread-0 45
Thread-1 24
Thread-0 46
main 44
Thread-0 47
Thread-1 25
Thread-0 48
main 45
Thread-0 49
Thread-1 26
main 46
Thread-1 27
main 47
Thread-1 28
main 48
Thread-1 29
main 49
Thread-1 30
Thread-1 31
Thread-1 32
Thread-1 33
Thread-1 34
Thread-1 35
Thread-1 36
Thread-1 37
Thread-1 38
Thread-1 39
Thread-1 40
Thread-1 41
Thread-1 42
Thread-1 43
Thread-1 44
Thread-1 45
Thread-1 46
Thread-1 47
Thread-1 48
Thread-1 49
通过实现Runnable接口来创建线程类

public class SecondThread implements Runnable
{
    private int i ;
    // run方法同样是线程执行体
    public void run()
    {
        for ( ; i < 100 ; i++ )
        {
            // 当线程类实现Runnable接口时,
            // 如果想获取当前线程,只能用Thread.currentThread()方法。
            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(target , name)方法创建新线程
                new Thread(st , "新线程1").start();
                new Thread(st , "新线程2").start();
            }
        }
    }
}

运行结果:
main 0
main 1
main 2
main 3
main 4
main 5
main 6
main 7
main 8
main 9
main 10
main 11
main 12
main 13
main 14
main 15
main 16
main 17
main 18
main 19
main 20
新线程1 0
新线程1 1
新线程1 2
新线程1 3
新线程1 4
新线程1 5
新线程1 6
新线程1 7
新线程1 8
新线程1 9
新线程1 10
新线程1 11
新线程1 12
新线程1 13
新线程1 14
新线程1 15
新线程1 16
新线程1 17
新线程1 18
新线程1 19
新线程1 20
新线程1 21
main 21
新线程1 22
新线程2 21
新线程1 23
main 22
新线程1 25
新线程2 24
新线程2 27
新线程1 26
main 23
新线程1 29
新线程2 28
新线程1 30
main 24
新线程1 32
新线程2 31
新线程1 33
main 25
新线程1 35
新线程2 34
新线程1 36
main 26
新线程1 38
新线程2 37
新线程1 39
main 27
新线程1 41
新线程2 40
新线程1 42
main 28
新线程1 44
新线程2 43
新线程1 45
main 29
新线程1 47
新线程2 46
新线程1 48
main 30
新线程2 49
main 31
main 32
main 33
main 34
main 35
main 36
main 37
main 38
main 39
main 40
main 41
main 42
main 43
main 44
main 45
main 46
main 47
main 48
main 49
实现Callable接口来实现线程

public class ThirdThread implements Callable<Integer>
{
    // 实现call方法,作为线程执行体
    public Integer call()
    {
        int i = 0;
        for ( ; i < 50; i++ )
        {
            System.out.println(Thread.currentThread().getName()
                + " 的循环变量i的值:" + i);
        }
        // call()方法可以有返回值
        return i;
    }

    public static void main(String[] args) 
    {
        // 创建Callable对象
        ThirdThread rt = new ThirdThread();
        // 使用FutureTask来包装Callable对象
        FutureTask<Integer> task = new FutureTask<Integer>(rt);
        for (int i = 0 ; i < 50 ; i++)
        {
            System.out.println(Thread.currentThread().getName()
                + " 的循环变量i的值:" + i);
            if (i == 20)
            {
                // 实质还是以Callable对象来创建、并启动线程
                new Thread(task , "有返回值的线程").start();
            }
        }
        try
        {
            // 运行到获取线程返回值get()方法时,会让主程序阻塞,直到该子线程程序结束返回结果后主程序才执行
            System.out.println("子线程的返回值:" + task.get());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

运行结果:
main 的循环变量i的值:0
main 的循环变量i的值:1
main 的循环变量i的值:2
main 的循环变量i的值:3
main 的循环变量i的值:4
main 的循环变量i的值:5
main 的循环变量i的值:6
main 的循环变量i的值:7
main 的循环变量i的值:8
main 的循环变量i的值:9
main 的循环变量i的值:10
main 的循环变量i的值:11
main 的循环变量i的值:12
main 的循环变量i的值:13
main 的循环变量i的值:14
main 的循环变量i的值:15
main 的循环变量i的值:16
main 的循环变量i的值:17
main 的循环变量i的值:18
main 的循环变量i的值:19
main 的循环变量i的值:20
main 的循环变量i的值:21
main 的循环变量i的值:22
有返回值的线程 的循环变量i的值:0
main 的循环变量i的值:23
有返回值的线程 的循环变量i的值:1
main 的循环变量i的值:24
有返回值的线程 的循环变量i的值:2
main 的循环变量i的值:25
有返回值的线程 的循环变量i的值:3
main 的循环变量i的值:26
有返回值的线程 的循环变量i的值:4
main 的循环变量i的值:27
有返回值的线程 的循环变量i的值:5
main 的循环变量i的值:28
有返回值的线程 的循环变量i的值:6
main 的循环变量i的值:29
有返回值的线程 的循环变量i的值:7
main 的循环变量i的值:30
有返回值的线程 的循环变量i的值:8
main 的循环变量i的值:31
有返回值的线程 的循环变量i的值:9
main 的循环变量i的值:32
有返回值的线程 的循环变量i的值:10
main 的循环变量i的值:33
有返回值的线程 的循环变量i的值:11
main 的循环变量i的值:34
有返回值的线程 的循环变量i的值:12
main 的循环变量i的值:35
有返回值的线程 的循环变量i的值:13
main 的循环变量i的值:36
有返回值的线程 的循环变量i的值:14
main 的循环变量i的值:37
有返回值的线程 的循环变量i的值:15
main 的循环变量i的值:38
有返回值的线程 的循环变量i的值:16
main 的循环变量i的值:39
有返回值的线程 的循环变量i的值:17
main 的循环变量i的值:40
有返回值的线程 的循环变量i的值:18
main 的循环变量i的值:41
有返回值的线程 的循环变量i的值:19
main 的循环变量i的值:42
有返回值的线程 的循环变量i的值:20
main 的循环变量i的值:43
有返回值的线程 的循环变量i的值:21
main 的循环变量i的值:44
有返回值的线程 的循环变量i的值:22
main 的循环变量i的值:45
有返回值的线程 的循环变量i的值:23
main 的循环变量i的值:46
有返回值的线程 的循环变量i的值:24
main 的循环变量i的值:47
有返回值的线程 的循环变量i的值:25
main 的循环变量i的值:48
有返回值的线程 的循环变量i的值:26
main 的循环变量i的值:49
有返回值的线程 的循环变量i的值:27
有返回值的线程 的循环变量i的值:28
有返回值的线程 的循环变量i的值:29
有返回值的线程 的循环变量i的值:30
有返回值的线程 的循环变量i的值:31
有返回值的线程 的循环变量i的值:32
有返回值的线程 的循环变量i的值:33
有返回值的线程 的循环变量i的值:34
有返回值的线程 的循环变量i的值:35
有返回值的线程 的循环变量i的值:36
有返回值的线程 的循环变量i的值:37
有返回值的线程 的循环变量i的值:38
有返回值的线程 的循环变量i的值:39
有返回值的线程 的循环变量i的值:40
有返回值的线程 的循环变量i的值:41
有返回值的线程 的循环变量i的值:42
有返回值的线程 的循环变量i的值:43
有返回值的线程 的循环变量i的值:44
有返回值的线程 的循环变量i的值:45
有返回值的线程 的循环变量i的值:46
有返回值的线程 的循环变量i的值:47
有返回值的线程 的循环变量i的值:48
有返回值的线程 的循环变量i的值:49
子线程的返回值:50

线程的声明周期

1.由于现代的桌面和服务器操作都采用抢占式调度策略,系统会给每个可执行的线程一个小时间段来处理任务,当时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。
2.线程的生命周期有五个阶段:新建,就绪,运行,阻塞,死亡,其中运行阶段是由JVM虚拟机线程调度器调度。,五个阶段之间可以互相转换,如图:
多线程
3.主线程结束时,其他线程不受任何影响,子线程已启动起来就拥有和主线程相同的地位,它不受主线程的影响,程序不要对已死亡状态线程或对新建状态的线程再次调用start()方法,否则会引发IllegalThreadStateException异常。

控制线程

1.后台线程:它是在后台运行的,如果所有的前台线程都死亡,后台线程会自动死亡,前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程,要将某个线程设置为后台线程,必须在该线程启动(start()方法)之前设置,否则会引发异常。
代码例子:

public class DaemonThread extends Thread
{
    // 定义后台线程的线程执行体与普通线程没有任何区别
    public void run()
    {
        for (int i = 0; i < 20 ; 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(Thread.currentThread().getName()
                + "  " + i);
        }
        // -----程序执行到此处,前台线程(main线程)结束------
        // 后台线程也应该随之结束, 这时方法体不一定执行完了
    }
}

运行结果:
main 0
main 1
main 2
main 3
main 4
Thread-0 0
Thread-0 1
2.sleep()方法是让当前线程暂停一段时间,进入阻塞状态,而yield()方法让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态,两个线程主要区别:

  • sleep()方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级,但yield()方法只会给优先级相同,或优先级更高的线程执行机会。
  • sleep()方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态,而yield()不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态,因此完全有可能某个线程调用yield()方法暂停之后,立即再次获得处理器资源被执行。
  • sleep()方法声明抛出了InterruptedException异常,所以调用sleep()方法时要么捕捉该异常,要么显示声明抛出该异常,而yield()方法则没有声明抛出任何异常。
  • sleep()方法比yield()方法有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。
    每个线程默认的优先级都与创建它的父线程的优先级相同,在默认情况下,main线程具有普通优先级,有main线程创建的子线程也具有普通优先级(线程优先级越高,其获得执行机会较多)。

线程同步

1.当我们用多线程对一个对象操作时,容易引起对象属性操作不安全问题,这时我们可以引入线程同步代码块(synchronized(obj){//代码实体}),其synchronized后括号里的obj就是同步监视器,任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定,用途:阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。同步代码块整个过程如图:
同步代码块
代码例子:

public class Account
{
    // 封装账户编号、账户余额两个Field
    private String accountNo;
    private double balance;
    public Account(){}
    // 构造器
    public Account(String accountNo , double balance)
    {
        this.accountNo = accountNo;
        this.balance = balance;
    }
    // 此处省略了accountNo和balance两个Field的setter和getter方法

    // accountNo的setter和getter方法
    public void setAccountNo(String accountNo)
    {
        this.accountNo = accountNo;
    }
    public String getAccountNo()
    {
        return this.accountNo;
    }

    // balance的setter和getter方法
    public void setBalance(double balance)
    {
        this.balance = balance;
    }
    public double getBalance()
    {
        return this.balance;
    }

    // 下面两个方法根据accountNo来重写hashCode()和equals()方法
    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;
    }
}
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()
    {
        // 使用account作为同步监视器,任何线程进入下面同步代码块之前,
        // 必须先获得对account账户的锁定——其他线程无法获得锁,也就无法修改它
        // 这种做法符合:“加锁 → 修改 → 释放锁”的逻辑
        synchronized (account)
        {
            // 账户余额大于取钱数目
            if (account.getBalance() >= drawAmount)
            {
                // 吐出钞票
                System.out.println(getName()
                    + "取钱成功!吐出钞票:" + drawAmount);
                try
                {
                Thread.sleep(1);
                }
                catch (InterruptedException ex)
                {
                ex.printStackTrace();
                }
                // 修改余额
                account.setBalance(account.getBalance() - drawAmount);
                System.out.println("\t余额为: " + account.getBalance());           
            }
            else
            {
                System.out.println(getName() + "取钱失败!余额不足!");
            }
        }
        //同步代码块结束,该线程释放同步锁
    }
}

2.Java也提供了同步方法,其同步方法是语法是(public synchronized void methodname(){}),该方法无须显示指定同步监视器,它的同步监视器就是this,代表对象本身。
3.可变类的为了实现线程安全,会用线程同步的方式来解决,但这会影响了程序运行效率,对于不可变类,可以采用如下策略:

  • 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(共享资源)的方法进行同步。
  • 如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本,即线程安全和线程不安全版本,在单线程环境中使用线程不安前版本以保证性能,在多线程中使用线程安全版本。
    4.线程在以下几种情况会释放对同步监视器的锁定:
  • 当前线程的同步方法,同步代码块执行结束,当前线程即释放同步代码块。
  • 当前线程在同步代码块,同步方法中遇到break,return终止了改代码块,该方法的继续执行,当前线程会释放同步监视器,当前线程在同步代码块,同步方法中出现了未处理的Error或Exception,导致了改代码块,该方法异常结束时,该当前线程将会释放同步监视器。
  • 当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。
    5.线程不会释放同步监视器,有以下几种情况:
  • 线程执行同步代码块或同步方法时,程序调用Thread.sleep(),Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器。
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器。
    6.死锁:当两个线程相互等待对方释放同步监视器时就会发生死锁,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方法
        a.foo(b);
        System.out.println("进入了主线程之后");
    }
    public void run()
    {
        Thread.currentThread().setName("副线程");
        // 调用b对象的bar方法
        b.bar(a);
        System.out.println("进入了副线程之后");
    }
    public static void main(String[] args)
    {
        DeadLock dl = new DeadLock();
        // 以dl为target启动新线程
        new Thread(dl).start();
        // 调用init()方法
        dl.init();
    }
}

线程通信

1.线程通信可以借助Object类提供的wait(),notify()和 notifyAll()3个方法来做到,只不过这3个方法必须有同步监视器对象来调用,这三个方法的特点:

  • wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程,调用wait()方法的当前线程会释放对该同步监视器的锁定。
  • notify():唤醒在此同步监视器上等待的单个线程,如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程,选择是任意性的,只有当前线程放弃对该同步监视器的锁定后(使用wait()方法),才可以执行被唤醒的线程。
  • notifyAll():唤醒在此同步监视器上等待的所有线程,只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。
    代码例子:
public class Account
{
    // 封装账户编号、账户余额两个Field
    private String accountNo;
    private double balance;
    //标识账户中是否已有存款的旗标
    private boolean flag = false;

    public Account(){}
    // 构造器
    public Account(String accountNo , double balance)
    {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    // accountNo的setter和getter方法
    public void setAccountNo(String accountNo)
    {
        this.accountNo = accountNo;
    }
    public String getAccountNo()
    {
        return this.accountNo;
    }
    // 因此账户余额不允许随便修改,所以只为balance提供getter方法,
    public double getBalance()
    {
        return this.balance;
    }

    public synchronized void draw(double drawAmount)
    {
        try
        {
            // 如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞
            if (!flag)
            {
                wait();
            }
            else
            {
                // 执行取钱
                System.out.println(Thread.currentThread().getName() 
                    + " 取钱:" +  drawAmount);
                balance -= drawAmount;
                System.out.println("账户余额为:" + balance);
                // 将标识账户是否已有存款的旗标设为false。
                flag = false;
                // 唤醒其他线程
                notifyAll();
            }
        }
        catch (InterruptedException ex)
        {
            ex.printStackTrace();
        }
    }
    public synchronized void deposit(double depositAmount)
    {
        try
        {
            // 如果flag为真,表明账户中已有人存钱进去,则存钱方法阻塞
            if (flag)             //①
            {
                wait();
            }
            else
            {
                // 执行存款
                System.out.println(Thread.currentThread().getName()
                    + " 存款:" +  depositAmount);
                balance += depositAmount;
                System.out.println("账户余额为:" + balance);
                // 将表示账户是否已有存款的旗标设为true
                flag = true;
                // 唤醒其他线程
                notifyAll();
            }
        }
        catch (InterruptedException ex)
        {
            ex.printStackTrace();
        }
    }

    // 下面两个方法根据accountNo来重写hashCode()和equals()方法
    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;
    }
}
//取钱线程
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;
    }
    // 重复100次执行取钱操作
    public void run()
    {
        for (int i = 0 ; i < 100 ; i++ )
        {
            account.draw(drawAmount);
        }
    }
}
//存款线程
public class DepositThread extends Thread
{
    // 模拟用户账户
    private Account account;
    // 当前取钱线程所希望存款的钱数
    private double depositAmount;
    public DepositThread(String name , Account account 
        , double depositAmount)
    {
        super(name);
        this.account = account;
        this.depositAmount = depositAmount;
    }
    // 重复100次执行存款操作
    public void run()
    {
        for (int i = 0 ; i < 100 ; i++ )
        {
            account.deposit(depositAmount);
        }
    }
}
public class DrawTest
{
    public static void main(String[] args) 
    {
        // 创建一个账户
        Account acct = new Account("1234567" , 0);
        //由于1个取钱线程只有100此操作,而3个存款线程共有300此操作
        //因此线程执行到最后,存款线程会被阻塞。
        new DrawThread("取钱者" , acct , 800).start();
        new DepositThread("存款者甲" , acct , 800).start();
        new DepositThread("存款者乙" , acct , 800).start();
        new DepositThread("存款者丙" , acct , 800).start();
    }
}

运行结果:
取钱者 取钱:800.0
账户余额为:0.0
存款者丙 存款:800.0
账户余额为:800.0
取钱者 取钱:800.0
账户余额为:0.0
存款者乙 存款:800.0
账户余额为:800.0
取钱者 取钱:800.0
账户余额为:0.0
存款者丙 存款:800.0
账户余额为:800.0
取钱者 取钱:800.0
账户余额为:0.0
存款者乙 存款:800.0
账户余额为:800.0
取钱者 取钱:800.0
账户余额为:0.0
存款者丙 存款:800.0
账户余额为:800.0
…….
注:该结果运行到最后,程序并没有结束,而是存款线程会因wait()方法而阻塞是程序无法继续向下执行。
2.当使用Lock对象类来保证同步时,就不能使用wait(),notify(),notifyAll()方法进行线程通信了,这时你可以同Condition类来保持线程通信,该类里await(),signal(),signalAll()作用分别类似于上面Object对象的wait(),notify(),notifyAll()三个方法(虽然Condition也能调用wait(),notify(),notifyAll(),但会在运行时期抛出非监视器异常),该Condition类可以为同个Lock对象线程建立多个不同对象来唤醒相对应的线程。
3.Java提供了一个BlockingQueue接口,它主要用作线程同步的工具,其特点就是当程序向BolckingQueue中放入元素时,如果该队列已满,则该线程阻塞,当程序向BolckingQueue中取出元素时,如果该队列已空,则该线程被阻塞。
BlockingQueue包含的方法特点如图:
BlockingQueue方法图
BlockingQueue与其实现类的类图
BlockingQueue与其实现类
代码例子:

class Producer extends Thread
{
    private BlockingQueue<String> bq;
    public Producer(BlockingQueue<String> bq)
    {
        this.bq = bq;
    }
    public void run()
    {
        String[] strArr = new String[]
        {
            "Java",
            "Struts",
            "Spring"
        };
        for (int i = 0 ; i < 2 ; i++ )
        {
            System.out.println(getName() + "生产者准备生产集合元素!");
            try
            {
                Thread.sleep(200);
                // 尝试放入元素,如果队列已满,线程被阻塞
                bq.put(strArr[i % 3]);
            }
            catch (Exception ex){ex.printStackTrace();}
            System.out.println(getName() + "生产完成:" + bq);
        }
    }
}
class Consumer extends Thread
{
    private BlockingQueue<String> bq;
    public Consumer(BlockingQueue<String> bq)
    {
        this.bq = bq;
    }
    public void run()
    {
        while(true)
        {
            System.out.println(getName() + "消费者准备消费集合元素!");
            try
            {
                Thread.sleep(200);
                // 尝试取出元素,如果队列已空,线程被阻塞
                System.out.println("消费将要取元素了");
                bq.take();
            }
            catch (Exception ex){ex.printStackTrace();}
            System.out.println(getName() + "消费完成:" + bq);
        }
    }
}
public class Test
{
    public static void main(String[] args)
    {
        // 创建一个容量为1的BlockingQueue
        BlockingQueue<String> bq = new ArrayBlockingQueue<>(1);
        // 启动3条生产者线程
        new Producer(bq).start();
        new Producer(bq).start();
        new Producer(bq).start();
        // 启动一条消费者线程
        new Consumer(bq).start();
    }
}

运行结果:
Thread-0生产者准备生产集合元素!
Thread-1生产者准备生产集合元素!
Thread-2生产者准备生产集合元素!
Thread-3消费者准备消费集合元素!
Thread-1生产完成:[Java]
Thread-1生产者准备生产集合元素!
消费将要取元素了
Thread-3消费完成:[]
Thread-3消费者准备消费集合元素!
Thread-0生产完成:[Java]
Thread-0生产者准备生产集合元素!
消费将要取元素了
Thread-3消费完成:[]
Thread-3消费者准备消费集合元素!
Thread-2生产完成:[Java]
Thread-2生产者准备生产集合元素!
消费将要取元素了
Thread-1生产完成:[Struts]
Thread-3消费完成:[Struts]
Thread-3消费者准备消费集合元素!
消费将要取元素了
Thread-0生产完成:[Struts]
Thread-3消费完成:[Struts]
Thread-3消费者准备消费集合元素!
消费将要取元素了
Thread-3消费完成:[]
Thread-3消费者准备消费集合元素!
Thread-2生产完成:[Struts]
消费将要取元素了
Thread-3消费完成:[]
Thread-3消费者准备消费集合元素!
消费将要取元素了
由于多线程并发执行的影响,运行结果的消费和生产并未发生互相交替输出,但我们观察其结果可以发现的每次要输出“生产完成”时,前面一定会输出”消费将要取元素了”,结合总体结果来看,还是可以观察出其BlockingQueue系类是一个阻塞队列。

线程组和未处理的异常

1.Java使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制,对线程组的控制相当于同时控制这批线程,用户创建的所有线程都属于指定线程组,如果程序没有显示指定线程属于哪个线程组,则该线程属于默认线程组,一旦某个线程加入了指定线程组之后,该线程将一直属于该线程组,直到该线程死亡,线程运行中途不能改变它所属的线程组。
代码例子:

class MyThread extends Thread
{
    // 提供指定线程名的构造器
    public MyThread(String name)
    {
        super(name);
    }
    // 提供指定线程名、线程组的构造器
    public MyThread(ThreadGroup group , String name)
    {
        super(group, name);
    }
    public void run()
    {
        for (int i = 0; i < 2 ; i++ )
        {
            System.out.println(getName() + " 线程的i变量" + i);
        }
    }
}
public class ThreadGroupTest
{
    public static void main(String[] args) 
    {
        // 获取主线程所在的线程组,这是所有线程默认的线程组
        ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
        System.out.println("主线程组的名字:" 
            + mainGroup.getName());
        System.out.println("主线程组是否是后台线程组:" 
            + mainGroup.isDaemon());
        new MyThread("主线程组的线程").start();
        //创建一个新的线程组
        ThreadGroup tg = new ThreadGroup("新线程组");
        tg.setDaemon(true);
        System.out.println("tg线程组是否是后台线程组:" 
            + tg.isDaemon());
        //把这个线程指定新的线程组
        MyThread tt = new MyThread(tg , "tg组的线程甲");
        tt.start();
        //把这个线程指定新的线程组
        new MyThread(tg , "tg组的线程乙").start();
    }
}

运行结果:
主线程组的名字:main
主线程组是否是后台线程组:false
tg线程组是否是后台线程组:true
tg组的线程甲 线程的i变量0
主线程组的线程 线程的i变量0
主线程组的线程 线程的i变量1
tg组的线程甲 线程的i变量1
tg组的线程乙 线程的i变量0
tg组的线程乙 线程的i变量1
2.对于线程的异常,Java采用了ThreadGroup来管里整组线程的异常和单个异常管理自己抛出的异常,这里涉及了ThreadGroup的void uncaughtException(Thread t,Throwable e)方法,和Thread 的setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)(指定线程的异常处理器)以及setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)方法(该线程类的所有线程实例异常处理器)来设定对应的处理器,它们与catch捕获异常不同的是这些异常处理器对异常进行处理后,异常依然会传播给上一级调用者。
3.ThreadGroup类和Thread类的处理器的关系如图:
线程异常
代码例子:

// 定义自己的异常处理器
class MyExHandler implements Thread.UncaughtExceptionHandler 
{
    //实现uncaughtException方法,该方法将处理线程的未处理异常
    public void uncaughtException(Thread t, Throwable e)
    {
        System.out.println(t + " 线程出现了异常:" + e);
    } 
}
public class ExHandler
{
    public static void main(String[] args) 
    {
        // 设置主线程的异常处理器
        Thread.currentThread().setUncaughtExceptionHandler
            (new MyExHandler());
        int a = 5 / 0;     //①
        System.out.println("程序正常结束!");
    }
}

运行结果:
Thread[main,5,main] 线程出现了异常:java.lang.ArithmeticException: / by zero

线程池

1.线程池在系统启动时即创建大量空闲的程序,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个线程来执行它们的run()或call()方法,当run()或call()方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run()或call()方法。
2.使用一个线程池来执行线程任务的步骤如下:

  • 调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池。
  • 创建Runnable实现类或Callable实现类的实例,作为线程执行任务。
  • 调用ExecutorService对象的submit()方法来提交Runnable实例或Callable实例。
  • 当不想提交任何任务时,调用ExecutorService对象的shutdown()方法来关闭线程池。
  • 代码例子:
//实现Runnable接口来定义一个简单的线程类
class MyThread implements Runnable
{
    public void run()
    {
        for (int i = 0; i < 10 ; i++ )
        {
            System.out.println(Thread.currentThread().getName()
                + "的i值为:" + i);
        }
    }
}
public class ThreadPoolTest
{
    public static void main(String[] args) 
        throws Exception
    {
        // 创建一个具有固定线程数(6)的线程池
        ExecutorService pool = Executors.newFixedThreadPool(6);
        // 向线程池中提交两个线程
        pool.submit(new MyThread());
        pool.submit(new MyThread());
        // 关闭线程池
        pool.shutdown();
    }
}

运行结果:
pool-1-thread-2的i值为:0
pool-1-thread-1的i值为:0
pool-1-thread-2的i值为:1
pool-1-thread-1的i值为:1
pool-1-thread-2的i值为:2
pool-1-thread-1的i值为:2
pool-1-thread-2的i值为:3
pool-1-thread-1的i值为:3
pool-1-thread-2的i值为:4
pool-1-thread-2的i值为:5
pool-1-thread-2的i值为:6
pool-1-thread-2的i值为:7
pool-1-thread-2的i值为:8
pool-1-thread-2的i值为:9
pool-1-thread-1的i值为:4
pool-1-thread-1的i值为:5
pool-1-thread-1的i值为:6
pool-1-thread-1的i值为:7
pool-1-thread-1的i值为:8
pool-1-thread-1的i值为:9

线程相关类

1.Java提供了一个ThreadLocal类,其为了隔离多个线程的数据共享(主要通过创建共享资源的副本),从根本上避免多个线程之间对共享资源的竞争,也就不需要对个多个线程进行同步机制了,而同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式。
代码例子:

class Account
{
    /* 定义一个ThreadLocal类型的变量,该变量将是一个线程局部变量
    每个线程都会保留该变量的一个副本 */
    private ThreadLocal<String> name = new ThreadLocal<>();
    // 定义一个初始化name属性的构造器
    public Account(String str)
    {
        this.name.set(str);
        // 下面代码用于访问当前线程的name副本的值
        System.out.println("---" + this.name.get());
    }
    // name的setter和getter方法
    public String getName()
    {
        return name.get();
    }
    public void setName(String str)
    {
        this.name.set(str);
    }
}
class MyTest extends Thread
{
    // 定义一个Account属性
    private Account account;
    public MyTest(Account account, String name)
    {
        super(name);
        this.account = account;
    }
    public void run()
    {
        // 循环10次
        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) 
    {
        // 启动两条线程,两条线程共享同一个Account
        Account at = new Account("初始名");
        /*
        虽然两条线程共享同一个账户,即只有一个账户名
        但由于账户名是ThreadLocal类型的,所以每条线程
        都完全拥有各自的账户名副本,所以从i == 6之后,将看到两条
        线程访问同一个账户时看到不同的账户名。
        */
        new MyTest(at , "线程甲").start();
        new MyTest(at , "线程乙").start ();
    }
}

运行结果:
—初始名
null 账户的i值:0
null 账户的i值:1
null 账户的i值:2
null 账户的i值:3
null 账户的i值:4
null 账户的i值:5
线程乙 账户的i值:6
线程乙 账户的i值:7
线程乙 账户的i值:8
线程乙 账户的i值:9
null 账户的i值:0
null 账户的i值:1
null 账户的i值:2
null 账户的i值:3
null 账户的i值:4
null 账户的i值:5
线程甲 账户的i值:6
线程甲 账户的i值:7
线程甲 账户的i值:8
线程甲 账户的i值:9
2.Java提供了一个Collections静态的 Collection synchronizedXXX(XXX c)方法来包转那些线程并发访问不安全的容器(XXX表示容器类型).
例子:
HashMap m=Collections.synchronizedMap(new HashMap()).

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值