java基础---多线程
一、基本概念
- 程序: 是完成特定任务、用某种语言编写的一组指令的集合,是一段静态的代码,是静态对象
- 进程: 正在运行的一段程序,是资源分配的最小单元,系统在运行时会为每个进程分配不同的内存区域,有自身的产生、运行和消亡过程
- 线程: 是一个程序内部的一条执行路径,是调度和执行的最小单元,每个线程拥有独立的运行栈和程序计数器,线程切换的开销小
- 一个进程中的多个线程共享相同到内存单元、内存地址空间,他们从同一个堆中分配对象,访问相同的变量和对象,使得线程间通信更简便高效,但多个线程操作共享的系统资源会带来安全隐患
- 并行: 多个CPU同时执行多个任务
- 并发:一个CPU采用时间片同时执行多个任务
- 一个java应用程序java.exe至少有3个线程:main主线程,垃圾回收线程和异常处理线程
- 多线程的优点:
- 提高应用程序的响应,对图形化界面更有意义,增强用户体验
- 提高计算机CPU利用率
- 改善程序结构,分成多个线程独立运行,利与理解和修改
- 多线程的使用场景: 程序需要执行多个任务,需要实现一些等待的任务,需要一些后台运行的程序
二、线程的创建
1. 继承于Thread类
- 创建一个继承于Thread类的子类
- 重写Thread类的run() --> 将此线程执行的操作声明在run()中
- 创建Thread类的子类的对象
- 通过此对象调用start()
//1. 创建一个继承于Thread类的子类 class MyThread extends Thread { //2. 重写Thread类的run() @Override public void run() { for (int i = 0; i < 100; i++) { if(i % 2 == 0){ System.out.println(Thread.currentThread().getName() + ":" + i); } } } } public class ThreadTest { public static void main(String[] args) { //3. 创建Thread类的子类的对象 MyThread t1 = new MyThread(); //4.通过此对象调用start():①启动当前线程 ② 调用当前线程的run() t1.start(); //不能通过直接调用run()的方式启动线程,直接调用就不是创建子线程进行调用 // t1.run(); //start只能启动一次,否则会报IllegalThreadStateException // t1.start(); //我们需要重新创建一个线程的对象 MyThread t2 = new MyThread(); t2.start(); //如下操作仍然是在main线程中执行的。 for (int i = 0; i < 100; i++) { if(i % 2 == 0){ System.out.println(Thread.currentThread().getName() + ":" + i + "***********main()************"); } } } }
2. 实现Runnable接口
- 创建一个实现了Runnable接口的类
- 实现类去实现Runnable中的抽象方法:run()创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中
- 创建Thread类的对象
- 通过Thread类的对象调用start()
//1. 创建一个实现了Runnable接口的类 class MThread implements Runnable{ //2. 实现类去实现Runnable中的抽象方法:run() @Override public void run() { for (int i = 0; i < 100; i++) { if(i % 2 == 0){ System.out.println(Thread.currentThread().getName() + ":" + i); } } } } public class ThreadTest1 { public static void main(String[] args) { //3. 创建实现类的对象 MThread mThread = new MThread(); //4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象 Thread t1 = new Thread(mThread); t1.setName("线程1"); //5. 通过Thread类的对象调用start():① 启动线程 ②调用当前线程的run()-->调用了Runnable类型的target的run() t1.start(); //再启动一个线程,遍历100以内的偶数 Thread t2 = new Thread(mThread); t2.setName("线程2"); t2.start(); } }
源码:
//thread类里的属性和方法 private Runnable target; public Thread(Runnable target) { this(null, target, "Thread-" + nextThreadNum(), 0); } @Override public void run() { if (target != null) { target.run(); } }
- 开发中优先选择实现runnable接口的方式创建线程,可以多实现,适合处理多个线程有共享的数据
- 二者的联系:thread类也实现了runable接口,都需要重写run方法
3. 实现callable接口
- 相比run()方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,比如获取返回结果
- Future接口可以对具体Runnable、 Callable任务的执行结果进行取消、查询是否完成、获取结果等。
- FutrueTask是Futrue接口的唯一的实现类
- FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
//1.创建一个实现Callable的实现类 class NumThread implements Callable{ //2.实现call方法,将此线程需要执行的操作声明在call()中 @Override public Object call() throws Exception { int sum = 0; for (int i = 1; i <= 100; i++) { if(i % 2 == 0){ System.out.println(i); sum += i; } } return sum; } } public class ThreadNew { public static void main(String[] args) { //3.创建Callable接口实现类的对象 NumThread numThread = new NumThread(); //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象 FutureTask futureTask = new FutureTask(numThread); //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start() new Thread(futureTask).start(); try { //6.获取Callable中call方法的返回值 //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。 Object sum = futureTask.get(); System.out.println("总和为:" + sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
4. 线程池创建
- 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
- 思路: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具
- 好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
- JDK 5.0起提供了线程池相关API: ExecutorService 和 Executors
- ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
- <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行Callable
- void shutdown() :关闭连接池
- ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
- Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
class NumberThread implements Runnable{ @Override public void run() { for(int i = 0;i <= 100;i++){ if(i % 2 == 0){ System.out.println(Thread.currentThread().getName() + ": " + i); } } } } class NumberThread1 implements Runnable{ @Override public void run() { for(int i = 0;i <= 100;i++){ if(i % 2 != 0){ System.out.println(Thread.currentThread().getName() + ": " + i); } } } } public class ThreadPool { public static void main(String[] args) { //1. 提供指定线程数量的线程池 ExecutorService service = Executors.newFixedThreadPool(10); ThreadPoolExecutor service1 = (ThreadPoolExecutor) service; //设置线程池的属性 // System.out.println(service.getClass()); // service1.setCorePoolSize(15); // service1.setKeepAliveTime(); //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象 service.execute(new NumberThread());//适合适用于Runnable service.execute(new NumberThread1());//适合适用于Runnable // service.submit(Callable callable);//适合使用于Callable //3.关闭连接池 service.shutdown(); } }
二、常用的thread类方法
| 方法声明 | 功能介绍 |
| Thread() | 使用无参的方式构造对象,默认线程名为thread+i |
| Thread(String name) | 根据参数指定的名称来构造对象 |
| Thread(Runnable target) | 根据参数指定的引用来构造对象,其中Runnable是个接口类型 |
| Thread(Runnable target,String name) |
根据参数指定引用和名称来构造对象 |
| void run() | 若使用Runnable引用构造了线程对象,调用该方法时最终调用接口中的版本 若没有使用Runnable引用构造线程对象,调用该方法时则啥也不做 |
| void start() | 用于启动线程,Java虚拟机会自动调用该线程的run方法 |
| static currentThread() | 静态方法,返回当前执行的线程 |
| getName() | 获取当前线程的名字 |
| setName() | 设置当前线程的名字 |
| static void yield() | 释放当前CPU的执行权 |
| void join() | 在线程a中调用线程b的join方法,线程a阻塞,直到线程b执行完才会结束阻塞状态 |
| void join(long millis) |
等待参数指定的毫秒数 |
| void stop() | 已过时,强制结束当前线程 |
| static void sleep() | 让当前线程睡眠指定的时间毫秒数,在指定的毫秒内当前线程是阻塞状态 |
| boolean isAlive() | 判断当前线程是否还存活 |
三、线程的调度
- 抢占式,通过高优先级的线程抢占CPU
- 线程的优先级等级
- MAX_PRIORITY:10
- MIN _PRIORITY:1
- NORM_PRIORITY:5 -->默认优先级
- 获取和设置当前线程的优先级
- getPriority():获取线程的优先级
- setPriority(int p):设置线程的优先级
- 说明:高优先级的线程要抢占低优先级线程cpu的执行权。从概率上讲,高优先级的线程高概率的情况下 优先执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行
四、线程的生命周期
java语言使用Thread类及其子类的对象来表示线程, 在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪: 处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
- 运行: 当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
- 阻塞: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
- 死亡: 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

五、线程的同步
多线程容易出现安全问题:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行会导致共享数据的错误
对于多条操作共享数据的语句,每次应只让一个线程全部执行完毕,在执行过程中,其他线程不可以参与执行
在Java中,我们通过同步机制,来解决线程的安全问题。
- 同步代码块
- 操作共享数据的代码是需要被同步的代码
- 共享数据是多个线程共同操作的变量
- 同步监视器(锁)可以是任何一个类的对象,多个线程必须共用一把锁
- 在实现runnable接口创建多线程的方式中,可以用this充当同步监视器
- 在继承thread创建多线程的方式中,可以用反射机制(当前类.class)充当同步监视器
synchronized(同步监视器){ //需要被同步的代码 }
class Window1 implements Runnable{ private int ticket = 100; // Object obj = new Object(); @Override public void run() { // Object obj = new Object(); while(true){ synchronized (this){//此时的this:唯一的Window1的对象 if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket); ticket--; } else { break; } } } } } public class WindowTest1 { public static void main(String[] args) { Window1 w = new Window1(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } }
class Window2 extends Thread{ private static int ticket = 100; private static Object obj = new Object(); @Override public void run() { while(true){ //正确的 // synchronized (obj){ synchronized (Window2.class){//Class clazz = Window2.class,Window2.class只会加载一次 //错误的方式:this代表着t1,t2,t3三个对象 // synchronized (this){ if(ticket > 0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName() + ":卖票,票号为:" + ticket); ticket--; }else{ break; } } } } } public class WindowTest2 { public static void main(String[] args) { Window2 t1 = new Window2(); Window2 t2 = new Window2(); Window2 t3 = new Window2(); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } }
- 同步方法
- 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
- 非静态的同步方法,同步监视器是:this
- 静态的同步方法,同步监视器是:当前类本身
public synchronized void show (){ … }
class Window3 implements Runnable { private int ticket = 100; @Override public void run() { while (true) { show(); } } private synchronized void show(){//同步监视器:this //synchronized (this){ if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket); ticket--; } //} } } public class WindowTest3 { public static void main(String[] args) { Window3 w = new Window3(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } }
class Window4 extends Thread { private static int ticket = 100; @Override public void run() { while (true) { show(); } } private static synchronized void show(){//同步监视器:Window4.class //private synchronized void show(){ //同步监视器:t1,t2,t3。此种解决方式是错误的 if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket); ticket--; } } } public class WindowTest4 { public static void main(String[] args) { Window4 t1 = new Window4(); Window4 t2 = new Window4(); Window4 t3 = new Window4(); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } }
- 显示锁Lock(锁)
- 从JDK 5.0开始, Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义, 在实现线程安全的控制中,比较常用的是ReentrantLock, 可以显式加锁、释放锁。
import java.util.concurrent.locks.ReentrantLock; class Window implements Runnable{ int ticket = 100; private final ReentrantLock lock = new ReentrantLock(); public void run(){ while(true){ try{ lock.lock(); if(ticket > 0){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(ticket--); }else{ break; } }finally{ lock.unlock(); } } } } public class ThreadLock { public static void main(String[] args) { Window t = new Window(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t1.start(); t2.start(); } }
- synchronized 与 Lock 的对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁), synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁, synchronized有代码块锁和方法锁
- 使用Lock锁, JVM将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类),优先使用lock
六、死锁
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
- 解决方法
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
线程一执行的代码: public void run(){ synchronized(a){ //持有对象锁a,等待对象锁b synchronized(b){ 编写锁定的代码; } } } 线程二执行的代码: public void run(){ synchronized(b){ //持有对象锁b,等待对象锁a synchronized(a){ 编写锁定的代码; } } }
七、线程的通信
- wait() 与 notify() 和 notifyAll()
- wait():令当前线程挂起并放弃CPU、 同步资源并等待, 使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行
- notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
- notifyAll ():唤醒正在排队等待资源的所有线程结束等待
- 这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常。
- 这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,这三个方法只能在Object类中声明。
//两个线程打印 1-100。线程1, 线程2 交替打印 class Communication implements Runnable { int i = 1; public void run() { while (true) { synchronized (this) { notify(); if (i <= 100) { System.out.println(Thread.currentThread().getName() + ":" + i++); } else break; try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
- wait() 方法:在当前线程中调用方法: 对象名.wait()
- 使当前线程进入等待(某对象)状态 ,直到另一线程对该对象发出 notify(或notifyAll) 为止。
- 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
- 调用此方法后,当前线程将释放对象监控权 ,然后进入等待
- 在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行。
- notify()/notifyAll()在当前线程中调用方法: 对象名.notify()
- 功能:唤醒等待该对象监控权的一个/所有线程。
- 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁),且必须是同一个同步监视器对象
class Number implements Runnable{ private int number = 1; private Object obj = new Object(); @Override public void run() { while(true){ synchronized (obj) { obj.notify(); if(number <= 100){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":" + number); number++; try { //使得调用如下wait()方法的线程进入阻塞状态,此进程wait的同时,其他进程已经可以参与执行了,相当于把这个锁释放了 obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else{ break; } } } } } public class CommunicationTest { public static void main(String[] args) { Number number = new Number(); Thread t1 = new Thread(number); Thread t2 = new Thread(number); t1.setName("线程1"); t2.setName("线程2"); t1.start(); t2.start(); } }
八、生产-消费问题
- 问题描述:生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
- 需要解决问题:生产者比消费者快时,消费者会漏掉一些数据没有取到;消费者比生产者快时,消费者会取相同的数据。
class Clerk{ private int productCount = 0; //生产产品 public synchronized void produceProduct() { if(productCount < 20){ productCount++; System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品"); notify(); }else{ //等待 try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } //消费产品 public synchronized void consumeProduct() { if(productCount > 0){ System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个产品"); productCount--; notify(); }else{ //等待 try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Producer extends Thread{//生产者 private Clerk clerk; public Producer(Clerk clerk) { this.clerk = clerk; } @Override public void run() { System.out.println(getName() + ":开始生产产品....."); while(true){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } clerk.produceProduct(); } } } class Consumer extends Thread{//消费者 private Clerk clerk; public Consumer(Clerk clerk) { this.clerk = clerk; } @Override public void run() { System.out.println(getName() + ":开始消费产品....."); while(true){ try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } clerk.consumeProduct(); } } } public class ProductTest { public static void main(String[] args) { Clerk clerk = new Clerk(); Producer p1 = new Producer(clerk); p1.setName("生产者1"); Consumer c1 = new Consumer(clerk); c1.setName("消费者1"); Consumer c2 = new Consumer(clerk); c2.setName("消费者2"); p1.start(); c1.start(); c2.start(); } }
浙公网安备 33010602011771号