线程(thread)是一个程序内部的一条执行流程。如果只有一条执行流程,那么这个程序就是单线程的程序,例如只有main方法的一个程序。
多线程是从软硬件上实现多条执行流程的技术,在各种通信、购物等系统上都有多线程计算的应用。
- 多线程的创建
通常来说我们创建多线程有三大方法:继承Thread类、实现Runnable接口、实现Callable接口。
- 继承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();//启动线程 //主线程的内容写后面
- 实现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();
- 实现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方法可以获取任务执行的结果。
- Thread常用API
获取线程名称getName()、设置名称setName()、获取当前线程对象currentThread()。
Thread类的线程休眠方法 public static void sleep(long time):Thread.sleep(5);//毫秒
- 线程安全
多个线程同时操作同一个共享资源的时候可能会出现安全问题。
多线程同时访问并修改共享资源:例如,有一个公共的账户有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); } }
因为在取钱的时候,首先判断是否有足够的余额,两个线程同时进来,就可能导致判断都通过,继而后面的操作都通过,使得两个人都取到了钱。
- 线程同步
上面的例子中,多个线程同时执行,发现账户都是可以取钱的,而实际上不可能发生这样的事情。这就是为了解决线程安全问题,采用了线程同步,让多个线程实现先后依次访问共享资源。
线程同步的核心思想:加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。(操作系统中的进程互斥,对某个系统资源,一个进程正在使用它,另外一个想用它的进程就必须等待,而不能同时使用 。)
方法一:同步代码块,对出现问题的核心代码使用synchronized进行加锁,每次只能一个线程占锁进入访问。
方法二:同步方法,把出现线程安全问题的核心方法给上锁。
方法三:Lock锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。Lock是接口不能直接实例化,采用它的实现类ReentrantLock来构建Lock锁对象。lock()、unlock()分别获得锁和释放锁。