java笔记_多线程

Java是一个多线程的编程语言,因此我们可以利用java来编写多线程的程序。一个多线程程序可以包含两个或者多个同时运行的部分,每个部分可以处理不同的任务。多线程能够有效的利用资源,尤其是当你有多个CPU的时候,是你能够在同一个程序中编写多个执行任务,并且可以并行的运行。

线程与进程

进程是处于运行过程中的程序,具有独立性、动态性、并发性,是系统进行资源分配和调度的独立单位。操作系统可以同时执行多个任务,每一个任务就是一个进程;而进程中的每一个顺序执行流就是一个线程,一个进程可以有多个线程,每个线程可以共享进程的资源(如:变量和部分环境);线程的执行是抢占式的。

并发:同一时刻只能由一条指令执行,但多个进程轮换执行,使得看起来像是同时执行一样。

并行:同一时刻,有多条指令再多个处理器上同时执行。

线程的生命周期

一个线程会在它的生命周期中经历不同的阶段。例如:新建、可运行、运行、阻塞和死亡状态。下图显示则为一个完整的线程生命周期图:

通过new关键字创建对象后,该线程就处于新建状态;直到调用了Start()方法后进入可运行状态,此时线程处于等待JVM线程调度器的调度;当执行run()方法之后便处于运行状态,这期间如果执行了sleep()、suspend等方法,该线程则会进入阻塞状态;最后一个运行状态的线程完成任务后终止条件发生,那么该线程就终止了。

创建线程的三种方式

创建线程提供了三种方式;可以通过继承Thread类来创建线程,由于java支持多继承,所以当一个类已经继承而无法在继承Thread类时,可以使用实现接口的方式来创建线程。

通过继承Thread类创建线程

通过创建一个继承Thread类来创建线程的方式步骤如下:

  1. 继承Thread类创建一个新的线程

    public MyThread extends Thread{}
  2. 重写run()方法

    run();

Thread常用的两个构造方法:

  • public Thread(): 创建一个新的线程对象
  • public Thread(String threadName):创建一个名称为threadName的线程对象

示例:

class MyThread extends Thread{
    private Thread t;
    private String threadName;

    MyThread( String name){
        threadName = name;
        System.out.println("Creating " + threadName);
    }

    public void run() {
        System.out.println("Running " + threadName);
        try {
            for(int i=4; i>0; i--) {
                System.out.println("Thread " + threadName + ", " + i);
                Thread.sleep(50);
            }
        } catch(InterruptedException e) {
            System.out.println("Thread " + threadName + " interrupted.");
        }
        System.out.println("thread " + threadName + " exiting.");
    }

    public void start() {
        System.out.println("Starting " + threadName);
        if( t == null ) {
            t = new Thread(this, threadName);
            t.start();
        }
    }
}


public class createThread {

    public static void main(String[] args) {
        MyThread t1 = new MyThread("Thread-1");
        t1.start();
        MyThread t2 = new MyThread("Thread-2");
        t2.start();
    }

}

显示结果

Creating Thread-1
Starting Thread-1
Creating Thread-2
Starting Thread-2
Running Thread-1
Running Thread-2
Thread Thread-2, 4
Thread Thread-1, 4
Thread Thread-2, 3
Thread Thread-1, 3
Thread Thread-2, 2
Thread Thread-1, 2
Thread Thread-1, 1
Thread Thread-2, 1
thread Thread-1 exiting.
thread Thread-2 exiting. 

通过实现Runnable接口创建线程

如果你的类已经继承了一个父类,那么你可以通过实现Runnable接口来创建一个新的线程。步骤如下:

  1. 需要重写由Runnable接口提供的run()方法

    public void run()

  2. 实例化对象(threadObj是一个实现Runnable接口的类的实例化对象; threadName是新线程的名字)

    Thread(Runnable threadObj, String threadName);

  3. 启动创建的线程

    void start();

示例:

class RunnableDemo implements Runnable{
    private Thread t;
    private String threadName;

    RunnableDemo (String name){
        threadName = name;
        System.out.println("Creating " + threadName);
    }

    @Override
    public void run() {
        System.out.println("Running " + threadName);
        try {
            for(int i=4; i>0; i--) {
                System.out.println("Thread " + threadName + ", " + i);
                Thread.sleep(50);
            }
        }catch(InterruptedException e) {
            System.out.println("Thread " + threadName + " exiting.");
        }
    }

    public void start() {
        System.out.println("Starting " + threadName);
        if( t == null ) {
            t = new Thread(this, threadName);
            t.start();
        }
    }

}


public class TestThread  {

    public static void main(String[] args) {
        RunnableDemo r1 = new RunnableDemo("Thread-1");
        r1.start();
        RunnableDemo r2 = new RunnableDemo("Thread-1");
        r2.start();
    }

}

显示结果:

Creating Thread-1
Starting Thread-1
Creating Thread-1
Starting Thread-1
Running Thread-1
Thread Thread-1, 4
Running Thread-1
Thread Thread-1, 4
Thread Thread-1, 3
Thread Thread-1, 3
Thread Thread-1, 2
Thread Thread-1, 2
Thread Thread-1, 1
Thread Thread-1, 1

通过实现Callable接口创建线程

Callable是在jdk5加进来的,在java.util.concurrent包下。创建步骤如下:

  1. 自定义一个类实现java.utl.concurrent包下的Callable接口

  2. 重写call方法

  3. 将要在线程中执行的代码编写在call方法中

  4. 创建ExecutorService线程池

  5. 将自定义类的对象放入线程池里面

  6. 获取线程的返回结果

  7. 关闭线程池,不再接收新的线程,未执行的线程不会被关闭

    //1、自定义一个类实现java.util.concurrent包下的Callable接口
    class MyCallable implements Callable{

    private int count;
    
    public MyCallable(int count) {
        this.count = count;
    }
    // 2、重写call方法
    @Override
    public Integer call() throws Exception {
        // 3、将要在线程中执行的代码编写在call方法中
        int sum = 1;
        if( count != 0) {
            for(int i=1; i<=count; i++) {
                sum *= i;
            }
        } else {
            sum = 0;
        }
        return sum;
    }

}

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // 4、创建ExecutorService线程池
        // ExecutorService es = Executors.newFixedThreadPool(2);

        //创建一个线程池,里面的线程会根据任务数量进行添加
        ExecutorService es = Executors.newCachedThreadPool();

        // 5、将自定义类的对象放入线程池里面
        Future<Integer> f1 = es.submit(new MyCallable(5));
        Future<Integer> f2 = es.submit(new MyCallable(3));

        // 6、获取线程的返回结果
        //System.out.println(f1.get());
        //System.out.println(f2.get());

        // 判断线程中的任务是否执行完毕
        if(f1.isDone()) {
            System.out.println(f1.get());
        } else {
            System.out.println("f1线程中的任务还未执行完毕");
        }

        if(f2.isDone()) {
            System.out.println(f2.get());
        } else {
            System.out.println("f2线程中的任务还未执行完毕");
        }

        // 7、关闭线程池,不再接收新的线程,未执行的线程不会被关闭
        es.shutdown();
    }    
}

显示结果:

f1线程中的任务还未执行完毕
6

三种创建线程的优缺点

  1. 实现Runnable、Callable接口的方式创建

    优点:可以实现多继承;多个线程共享同一个target对象,适合多个线程处理同一份资源的情况。

    缺点:编程复杂,需用Thread.currentThread()方法访问当前线程

  2. 继承Thread类的方式创建

    优点:编程简单,用this可以直接获取当前线程

    缺点:不能实现多继承

线程控制

join线程

某个线程在执行时,另一个线程调用了join()方法,则需要等待调用join方法的线程执行完毕之后再继续执行。

join有三种重载形式:

1、join():等待调用线程执行完毕

2、join(long millis):等待调用join的线程的执行millis时长后,再执行

3、join(long millis, int nanos):等待时长为millis毫秒加nanos毫微秒

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

    public void run(){
        for(int i = 0; i < 100; i++){
            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();
                jt.join();
            }
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

Daemon线程

在后台运行的一种线程,主要为其它线程提供服务,例如:JVM的垃圾回收器;当其它线程都死亡,则守护线程自动死亡。利用setDaemon(boolean on)来设置守护线程,注意要在该线程启动前设置。

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 < 10; i++){
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

Sleep线程

调用sleep(long millis)方法的线程会暂停一段时间,并进入阻塞状态。yield()方法与sleep()方法类似,它会让当前执行的线程暂停,并进入就绪状态。

sleep()方法与yield()方法比较:

1)yield()方法只会给优先级相同或更高的线程执行的机会,而sleep()则不会;

2)sleep()方法让当前线程进入阻塞状态,yield()方法让当前线程进入就绪状态;

3)sleep()方法抛出InterruptedException异常,yield()方法无任何异常抛出;

4)sleep()方法比yield()方法有更好的移植性。

public class SleepTest {
    public static void main(String[] args) throws InterruptedException {
        for(int i = 0; i < 10; i++){
            System.out.println("当前线程:" + new Date());
            Thread.sleep(1000);
        }
    }
}

线程优先级

通过设置线程的优先级可以使得线程获得更多的机会,默认情况下,线程的优先级与创建它的父线程优先级相同,也可以利用Thread类提供的setPriority(int newPriority)、getPriority()方法设置和获取优先级;线程优先级参数可以自行设置,也可以使用Thread类提供的静态常量:

MAX_PRIORITY(值为10)、MIN_PRIORITY(值为1)、NORM_PRIORITY(值为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() + ",其优先级是:" + getPriority() + ",循环变量的值为:" + i);
        }
    }

    public static void main(String[] args){
        Thread.currentThread().setPriority(6);
        for(int i = 0; i < 30; i++){
            if(i == 10){
                PriorityTest low = new PriorityTest("低级");
                low.start();
                System.out.println("创建之初的优先级:" + low.getPriority());
                low.setPriority(Thread.MIN_PRIORITY);
            }
            if(i == 20){
                PriorityTest high = new PriorityTest("高级");
                high.start();
                System.out.println("创建之初的优先级:" + high.getPriority());
                high.setPriority(Thread.MAX_PRIORITY);
            }
        }
    }
}

线程同步

线程同步的知识总结用一个例子来讲述什么是同步代码块、同步方法、同步锁、以及死锁问题。
在程序中启动两个或多个线程的时候,会出现多个线程同时访问相同资源的情况,这会因为并发问题导致无法预料的结果;以下将用两个人同时在同一个银行账户中取钱来说明。
首先,我们先创建一个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 this.accountNo;
    }

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

    public double getBalance() {
        return this.balance;
    }

    public void setBalance(double balance){
        this.balance = balance;
    }

    @Override
    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;
    }

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

接下来,通过继承Thread类来创建一个取钱的线程类:

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;
    }
    @Override
    public void run(){
       if( account.getBalance() >= drawAmount){
            System.out.println(getName() + "取钱成功!吐出钞票:" +
             drawAmount);
            account.setBalance(account.getBalance() - drawAmount);
            System.out.println("\t余额为:" + account.getBalance());
        } else {
            System.out.println(getName() + "取钱失败!余额不足!");
        }
    }
}

准备工作已经做完,下面我们在DrawTest类中创建两个线程来模拟两个人取钱的动作:

public class DrawTest {
    public static void main(String[] args){
        Account acct = new Account("1234567",1000);
        new DrawThread("甲",acct, 800.00).start();
        new DrawThread("乙",acct, 800.00).start();
    }
}

执行代码看到如下结果:

乙取钱成功!吐出钞票:800.0
甲取钱成功!吐出钞票:800.0
    余额为:200.0
     余额为:-600.0

很明显,这样的结果是不符合逻辑的。因此,对于这种情况,我们希望代码在执行run()方法中的代码的时候只允许一个线程来执行。换句话来说,就是排队一个一个的来。

同步代码块

对于上述问题,java提供了同步代码块来解决这个问题,代码块的语法格式如下:

synchronized(obj){
// 需要同步的代码
}

obj是一个同步监视器,线程开始执行同步代码块之前,必须获得对同步监视器的锁定。下面我们来改写代码(以下只展示部分代码):

public class DrawThread extends Thread{
   ...
    @Override
    public void run(){
        synchronized(acount){
           if( account.getBalance() >= drawAmount){
                System.out.println(getName() + "取钱成功!吐出钞票:" +
                 drawAmount);
                account.setBalance(account.getBalance() - drawAmount);
                System.out.println("\t余额为:" + account.getBalance());
            } else {
                System.out.println(getName() + "取钱失败!余额不足!");
            } 
        }
    }
}

执行代码看到如下结果:

甲取钱成功!吐出钞票:800.0
    余额为:200.0
乙取钱失败!余额不足!

同步方法

java还提供了同步方法,方式很简单,就是用synchronized关键字来修饰某个方法。我们在Account类中写一个draw()方法,然后将DrawThread类的run()内同步的代码写入draw()方法中,代码如下:

public class Account {
    ...
    /* 账户余额不允许修改,所以balance只提供getter()方法*/

    public double draw(double drawAmount){
        if( account.getBalance() >= drawAmount){
            System.out.println(Thread.currentThread().getName() +
             "取钱成功!吐出钞票:" + drawAmount);
            balance -= drawAmount;
            System.out.println("\t余额为:" + balance);
        } else {
            System.out.println(Thread.currentThread().getName() +
             "取钱失败!余额不足!");
        }
    }
    ...
}

然后将DrawThread类中run()方法改为如下:

public void run(){
    account.draw(drawAmount);
}

同步锁

java 5开始,提供了一种通过显示定义同步锁对象来实现同步,同步锁由Lock对象充当,Lock比synchronized更加的灵活。

import java.util.concurrent.locks.ReentrantLock;

public class Account {
    // 创建一个锁对象
    private final ReentrantLock lock = new ReentrantLock();
    ... //省略部分代码

    public void draw(double drawAmount){
        lock.lock();  // 加锁
        try{
            if( balance >= drawAmount){
                System.out.println(Thread.currentThread().getName() + 
                "取钱成功!吐出钞票:" + drawAmount);
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                balance -= drawAmount;
                System.out.println("\t余额为:" + balance);
            } else {
                System.out.println(Thread.currentThread().getName() + 
                "取钱失败!余额不足!");
            }
        }finally {
            lock.unlock();  // 解锁
        }
    }
    ...
}

死锁

死锁是由于两个线程都在等待对方释放锁而造成的一直处理等待状体的情况,这种情况是在多线程开发中应该避免的问题。

class A{
    synchronized void foo(B b){
        System.out.println("当前线程名:" +
         Thread.currentThread().getName() + " 进入了A实例的foo()方法");
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("当前线程名:" + 
        Thread.currentThread().getName() + " 企图调用B实例的last()方法");
        b.last();
    }

    synchronized void last(){
        System.out.println("进入了A类的last()方法内部");
    }
}

class B{
    synchronized void bar(A a){
        System.out.println("当前线程名:" + 
        Thread.currentThread().getName() + " 进入了B实例的foo()方法");
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("当前线程名:" + 
        Thread.currentThread().getName() + " 企图调用A实例的last()方法");
        a.last();
    }

    synchronized void last(){
        System.out.println("进入了B类的last()方法内部");
    }
}

public class DeadLock implements Runnable {
    private A a = new A();
    private B b = new B();

    private void init(){
        Thread.currentThread().setName("主线程");
        a.foo(b);
        System.out.println("进入了主线程之后");
    }

    @Override
    public void run() {
        Thread.currentThread().setName("副线程");
        b.bar(a);
        System.out.println("进入了副线程之后");
    }

    public static void main(String[] args){
        DeadLock dl = new DeadLock();
        new Thread(dl).start();
        dl.init();

    }
}

线程间的通信

未完待续…

参考资料

1、菜鸟教程

2、tutorialspoint

3、javaTpoint

4、《java从入门到精通》 第五版 清华大学出版社

5、《java疯狂讲义》第4版 李刚 著 电子工业出版社

-------------本文结束感谢您的阅读-------------
Mr.wj wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!