线程(thread)是一个程序内部的一条执行流程。如果只有一条执行流程,那么这个程序就是单线程的程序,例如只有main方法的一个程序。

多线程是从软硬件上实现多条执行流程的技术,在各种通信、购物等系统上都有多线程计算的应用。

  1. 多线程的创建

通常来说我们创建多线程有三大方法:继承Thread类、实现Runnable接口、实现Callable接口

  1. 继承Thread类

我们可以写一个子类MyThread继承线程类Thread,重写run()方法。然后创建MyThread类的对象,调用线程对象的start()方法启动线程。

这种方法实现简单,但是线程类已经继承Thread,无法继承其他类,不利于扩展。

class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 500; i++) {
            System.out.println("子线程 "+i);
        }
    }
}
        Thread t=new MyThread();//子线程对象
        t.start();//启动线程
        //主线程的内容写后面
  1. 实现Runnable接口

我们写一个线程任务类MyRunnable实现Runnable接口,重写run()方法。

class myRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println("子线程 "+i);
        }
    }
}

然后创建MyRunnable任务对象,把MyRunnable任务对象交给Thread处理。调用线程对象的start()方法启动线程。这里出现了Thread的构造器,Thread(Runnable target)、

public Thread(Runnable target ,String name )指定线程名称

        Runnable work= new myRunnable();//创建MyRunnable任务对象
        Thread t=new Thread(work);
        t.start();
//主线程的内容写后面

线程任务类只是实现接口,可以继续继承类和实现接口,但是多一层对象包装,线程执行结果不可以直接返回。

//简化: 
       new Thread(() -> {
            for (int i = 0; i < 200; i++) {
                System.out.println("Lambda 子线程 "+i);
            }
        }).start();
  1. 实现Callable接口 ——可以得到线程执行的结果

前2种线程创建方式都存在一个问题,重写的run方法均不能直接返回结果。

步骤1、得到任务对象

定义类实现Callable接口,重写call方法,封装要做的事情。

class myCallable implements Callable<String>{
    private int n;
    public myCallable(int n){
        this.n=n;
    }
    @Override
    public String call() throws Exception {
        int sum=0;
        for (int i = 0; i < n; i++) {
            System.out.println("任务进行中: "+i+"/"+n);
            sum+=i;
        }
        System.out.println("任务完成: "+n);
        return "子线程计算1+...+n的值:"+sum;
    }
}

用FutureTask把Callable对象封装成线程任务对象。(FutureTask实现了Runnable的对象,可以给Thread,可以使用get方法获得返回值

        Callable<String> call=new myCallable(190);
        FutureTask<String> futureTask=new FutureTask<>(call);
//2
        Thread t=new Thread(futureTask);
        t.start();
//3
        try {//如果线程任务没有完成会在这里等待
            System.out.println(futureTask.get());
        } catch (Exception e) {
            e.printStackTrace();
        }

步骤2、把线程任务对象交给Thread处理,调用Thread的start方法启动线程

步骤3、FutureTask的get方法可以获取任务执行的结果。

  1. Thread常用API

获取线程名称getName()、设置名称setName()、获取当前线程对象currentThread()。

Thread类的线程休眠方法 public static void sleep(long time):Thread.sleep(5);//毫秒

  1. 线程安全

多个线程同时操作同一个共享资源的时候可能会出现安全问题。

多线程同时访问并修改共享资源:例如,有一个公共的账户有1万元。如果A和B同学同时来取1万元,由于AB是两个线程操作,可能存在都取出情况。

    public static void main(String[] args) {
        account acc=new account("happy",10000);
        new getMoneyThread(acc,"张三").start();
        new getMoneyThread(acc,"李四").start();
    }
//账户类
class account{
    private String ID;
    private double money;
    public account(){}
    public account(String id,double money){
        this.ID=id;
        this.money=money;
    }

    public void getMoney(double money){
        String name=Thread.currentThread().getName();
        //取钱
        if(this.money>=money){
            System.out.println(name+"取出"+money);
            this.money-=money;
            System.out.println(ID+"剩余"+this.money);
        }
        else
            System.out.println(name+"取钱"+money+" 失败"+"剩余"+this.money);
    }
    public String getID() {
        return ID;
    }
    public double getMoney() {
        return money;
    }
}
//取钱的线程类
class getMoneyThread extends Thread{
    private account a;
    public getMoneyThread(account a,String name){
        super(name);
        this.a=a;
    }
    @Override
    public void run() {
        a.getMoney(10000);
    }
}

因为在取钱的时候,首先判断是否有足够的余额,两个线程同时进来,就可能导致判断都通过,继而后面的操作都通过,使得两个人都取到了钱。

  1. 线程同步

上面的例子中,多个线程同时执行,发现账户都是可以取钱的,而实际上不可能发生这样的事情。这就是为了解决线程安全问题,采用了线程同步,让多个线程实现先后依次访问共享资源。

线程同步的核心思想:加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。(操作系统中的进程互斥,对某个系统资源,一个进程正在使用它,另外一个想用它的进程就必须等待,而不能同时使用 。

方法一:同步代码块,对出现问题的核心代码使用synchronized进行加锁,每次只能一个线程占锁进入访问。

方法二:同步方法,把出现线程安全问题的核心方法给上锁。

方法三:Lock锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。Lock是接口不能直接实例化,采用它的实现类ReentrantLock来构建Lock锁对象。lock()、unlock()分别获得锁和释放锁。

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐