Java中的多线程
多线程
1. 基本概念
程序:为了完成指定任务,使用某种语言编写的一组指令的集合。(是一组静态代码,还没有加载进内存)
进程:正在运行中的程序。是资源分配的单位,系统在程序运行时会为每个进程分配不同的内存区域。每个进程拥有独立的方法区和堆空间
线程:是程序内部的一条执行路径,是调度和执行的单位,每一个线程都拥有独立的栈和程序计数器(PC)。多个线程共享一个进程中的方法区和堆
*如果一个进程可以在同一时间并行执行多个线程,则称为该进程为多线程。
单核和多核cpu:
单核cpu通过频繁的切换时间片来做到多线程的效果。(类似于收费站只有一个人(单核)通过对多个车道(线程)的车收费)。多核相当于多人...
在Java程序中最少有三个线程:main主线程、垃圾回收(gc)线程、异常处理线程。
并行与并发:
并行:多个cpu同时执行多个任务。比如多个人同时做不同的事情。
并发:一个cpu(采用时间片)同时执行多个任务。比如秒杀、多个人“同时”(通过快速切换cpu达到类似同时的效果)做同一件事。
多线程的优点:
- 提高应用程序的响应。在图形化界面中可以提高用户的体验
- 提高计算机系统cpu的利用率
- 改善程序结构。将既长又复杂的进程分为多个线程独立运行,便于理解和操作。
何时需要多线程:
- 程序需要同时执行两个或者多个任务。(比如Java程序中,主线程在执行的同时,gc线程也在执行)
- 程序需要实现一些需要等待时的任务。比如输入、文件读写等。
- 需要一些后台运行的程序等。
2.线程的创建和使用
2.1 创建多线程的方式一:继承Thread类
Thread类的特性:
- 每个线程都是通过某个特定的Thread对象的run()方法来完成操作的。经常把run()方法的主体称为线程体。
- 通过该Thread对象的start()方法来启动线程,而非直接调用run()方法。
通过继承Thread类创建线程的步骤:
- 创建一个继承于Thread类的子类
- 重写Thread类中的run()方法
- 创建Thread类的子类的对象
- 通过该对象调用start()方法
代码如下:
package com.summer.java; /** * 创建多线程的方式一:继承于Thread类 * 步骤: * 1.创建一个继承于Thread类的子类 * 2.重写Thread类中的run()方法 * 3.创建Thread类的子类的对象 * 4.通过该对象调用start()方法 * @Author 安宁侯 * @create 2021-03-04 22:43 */ class NumberThread1 extends Thread{//1.创建一个继承与thread类的子类 @Override public void run() {//2.重写run()方法 for (int i = 0; i < 100; i++) { System.out.println(getName()+":"+i);//打印100以内的数 } } } public class ExtendsThread { public static void main(String[] args) { //3.创建thread类的子类的对象 NumberThread1 thread1 = new NumberThread1(); //创建多个线程时需要重新创建线程 NumberThread1 thread2= new NumberThread1(); NumberThread1 thread3= new NumberThread1(); thread1.setName("线程1"); thread2.setName("线程2"); thread3.setName("线程3"); //4.通过该对象调用start()方法。 thread1.start(); thread2.start(); thread3.start(); } }
start()类的作用:
- 启动当前线程。
- 调用当前线程的run()方法。
2.2 创建多线程的方式二:实现Runnable接口
通过实现Runnable接口创建多线程的步骤:
- 创建一个实现了runnable接口的类
- 该实现类去实现runnable接口中的run()方法
- 创建该实现类的对象
- 以该对象作为参数,传给Thread类中的构造器,创建Thread类的对象
- 通过Thread类的对象调用start()方法
代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 package com.summer.java; 2 3 /** 4 * 创建多线程的方式二:实现runnable接口 5 * 通过实现Runnable接口创建多线程的步骤: 6 * 7 * 1.创建一个实现了runnable接口的类 8 * 2.该实现类去实现runnable接口中的run()方法 9 * 3.创建该实现类的对象 10 * 4.以该对象作为参数,传给Thread类中的构造器,创建Thread类的对象 11 * 5.通过Thread类的对象调用start()方法 12 * 13 * 14 * @Author 安宁侯 15 * @create 2021-03-04 23:50 16 */ 17 class NumberThread2 implements Runnable{//1.创建一个实现了runnable接口的类 18 @Override 19 public void run() { 20 for (int i = 0; i < 100; i++) {//2.该实现类去实现runnable接口中的run()方法 21 System.out.println(Thread.currentThread().getName()+":"+i); 22 } 23 } 24 } 25 public class ImplementRunnable { 26 public static void main(String[] args) { 27 //3.创建该实现类的对象 28 NumberThread2 numberThread2 = new NumberThread2(); 29 //4.以该对象作为参数,传给Thread类中的构造器,创建Thread类的对象 30 Thread thread1 = new Thread(numberThread2); 31 Thread thread2 = new Thread(numberThread2); 32 Thread thread3 = new Thread(numberThread2); 33 34 thread1.setName("线程1"); 35 thread2.setName("线程2"); 36 thread3.setName("线程3"); 37 //5.通过Thread类的对象调用start()方法 38 thread1.start(); 39 thread2.start(); 40 thread3.start(); 41 } 42 }
API中两种创建多线程方式的比较:
- 实现的方式没有单继承的局限性
- 实现的方式更适合用来处理多个线程有共享数据的情况
说明:就两种方法来说,在开发中,优先选择实现runnable接口的方法来创建多线程
二者的联系:都实现了runnable接口。两种方法都需要重写run()方法,将线程要执行的逻辑声明在run()方法体中
2.3 创建多线程的方式三:实现callable接口
步骤:
- 创建一个实现了Callable接口的类。
- 重写Callable接口中的call()方法。
- 创建实现类Callable接口类的对象。
- 创建Future类的对象,实现了Callable接口的类的对象作为参数传递给Future的构造器。
- 创建Thread类的对象,Future类的对象作为参数传递给Thread类的构造器。
- Thread类的对象调用start()方法。
- 使用future类的对象调用get()方法可以获取call()方法的返回值(不感兴趣可不写)。
代码如下:
package com.summer.java; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /** * @Author 安宁侯 * @create 2021-03-06 16:11 */ class MyCallable implements Callable{ @Override public Object call() throws Exception { int sum=0; for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName()+"打印:"+i); sum+=i; } return sum; } } public class ImplementsCallable { public static void main(String[] args) { MyCallable callable = new MyCallable(); FutureTask task = new FutureTask(callable); Thread thread = new Thread(task); thread.start(); try { System.out.println("sum= "+task.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
如何理解实现callable接口实现多线程比实现Runnable接口实现多线程强大?
- call()可以有返回值。
- call()方法可以抛出异常被外面的操作捕获。
- callable支持泛型。
2.4 创建多线程的方式四:线程池
步骤:
- 提供指定线程数量的线程池
- 执行指定的线程的操作,需要提供实现了runnable接口或者callable接口的类的对现象
- 关闭线程池
代码如下:
package com.summer.java; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 创建多线程的方式四:使用线程池 * @Author 安宁侯 * @create 2021-03-06 16:34 */ class MyNumber1 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 MyNumber2 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){ ExecutorService pool = Executors.newFixedThreadPool(10); MyNumber1 myNumber = new MyNumber1(); pool.execute(myNumber);//适用于实现runnable接口 pool.execute(new MyNumber2()); //pool.submit(new Callable());//适用于实现callable接口 pool.shutdown(); } }
使用线程池的好处:
- 提高了响应速度(减少了创建新线程的时间)
- 降低了资源的消耗(重复利用线程池中的线程,不需要每次都创建)
- 便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大的线程数
keepAliveTime:线程没有任务时最多保持多长时间停止
如何实现线程的管理?
public class ThreadPool { public static void main(String[] args) { //1.创建指定数目的线程池,此时的service是一个接口, // 它的实现类为class java.util.concurrent.ThreadPoolExecutor******** ExecutorService service = Executors.newFixedThreadPool(10); //设置线程池的属性,可以通过ThreadPoolExecutor这个类来实现属性的设置, // 在设置之前需要对接口service进行强转 ThreadPoolExecutor service1= (ThreadPoolExecutor) service; //System.out.println(service.getClass()+"********");//class java.util.concurrent.ThreadPoolExecutor******** service1.setCorePoolSize(15); //2.调用对应的方法实现线程需要完后的内容 service.execute(new NumberThread());//适用于使用runnable接口创建的线程 service.submit(new NumberThread1());//适用于使用callable接口创建的线程 //3.关闭线程池 service.shutdown(); } }
2.5 Thread类的常用方法
-
start():启动当前线程,调用run方法
-
run():通常需要重写该方法,将进程中需要实现的功能写在此方法中
-
currentThread():静态方法,返回执行当前代码的线程
-
getName():返回线程名称
-
setName():设置线程名称
-
yield():释放当前cpu的执行权(可以继续抢占)
-
join():在线程a中调用线程b的join方法,此时线程a进入阻塞状态,直到线程b执行结束之后,a才结束阻塞状态
-
stop():该方法已经过时。强制结束当前进程
-
sleep():静态方法。让当前线程阻塞指定的毫秒数
-
isAlive():判断当前线程是否存活。
-
2.6 线程的调度
Java中线程的调度方法
-
- 同优先级的线程组成先进先出队列(先来先服务),使用时间片策略
- 对于高优先级,使用优先调度的抢占式策略
线程的优先级等级
-
- MAX_PRIORITY:10 最高优先级大小
- MIN_PRIORITY:1 最低优先级大小
- NORM_PRIORITY:5 默认优先级大小
- MAX_PRIORITY:10 最高优先级大小
涉及的方法
-
- getPriority():返回线程的优先级
- setPriority(int newPriority):设置线程的优先级
*说明:高优先级的线程要抢占低优先级线程的cpu执行权,但这只是从概率上来讲,并不意味着一定可以抢到。
3.线程的生命周期
3.1 线程的生命周期
JDK中用Thread.State定义了线程的几种状态。
-
- 新建:当一个Thread类或者子类的对象被声明并且创建时,新生的线程对象处于新建状态。
- 就绪:新建的线程对象被start()后,将进入线程队列等待cpu时间片,此时他已经具备了运行的条件,只是没有分配到cpu资源。
- 运行:当就绪的线程被调度并分配到cpu资源时,就进入运行状态,run()方法定义了线程的操作和功能。
- 阻塞:在某种特殊情况下,被认为挂起或者进行输入输出操作时,让出cpu并且临时中止自己的执行,此时线程就进入阻塞状态。
- 死亡:线程完成了它的全部操作或者被提前强行终止或者出现异常情况导致结束。
五种状态的转换:
新建->就绪:调用start()方法。
就绪->运行:获取到cpu的执行权。
运行->就绪:失去了cpu的执行权。
运行->死亡:执行完run()方法;调用线程的stop()方法;出现异常而且没有进行处理。
运行->阻塞:调用sleep()方法;a线程中调用b的join()方法;等待同步锁;调用wait()方法。
阻塞->就绪:sleep()时间到;join()结束;获取到同步锁;notify()/notifyAll()。
线程的最终状态为死亡。
4.线程的同步(安全问题)
4.1 线程安全问题例题:多窗口卖票
代码如下:
package com.summer.java; /** * @Author 安宁侯 * @create 2021-03-06 12:56 */ class MyTickets implements Runnable{ private int tickets=100; @Override public void run() { while (true){ if (tickets>0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"卖票,票号为:"+tickets); tickets--; } else { System.out.println("当前无票,请选择其他车次"); break; } } } } public class SaleTickets { public static void main(String[] args) { MyTickets tickets = new MyTickets(); Thread thread1 = new Thread(tickets); Thread thread2 = new Thread(tickets); Thread thread3 = new Thread(tickets); thread1.setName("窗口1"); thread2.setName("窗口2"); thread3.setName("窗口3"); thread1.start(); thread2.start(); thread3.start(); } }
输出样例:
... 窗口2卖票,票号为:4 窗口3卖票,票号为:3 窗口1卖票,票号为:3 窗口1卖票,票号为:1 窗口3卖票,票号为:1 当前无票,请选择其他车次 窗口2卖票,票号为:1 当前无票,请选择其他车次 当前无票,请选择其他车次
根据输出样例可以看出,不同的窗口卖出了同一张车票,出现了重票的问题,这样会造成很严重的后果。
原因:当线程一(窗口一)进入车票判断时,车票大于0,然后进行卖票操作,但是在同一时刻,线程二(窗口二)也开始进行车票判断(此时线程一还没有进行票数减一的操作),同样判断出车票是大于0的,所以出现了两个窗口卖出同一张票的结果。
那么,如何解决类似的问题呢?
当一个线程操作共享数据时,其他线程不能参与进来,只有当该线程操作完了共享数据,其他线程才可以进行操作。即使当前线程出现了阻塞,其他线程也不能操作
如何做到呢?在Java中使用同步机制来解决线程安全问题。
4.2 解决线程安全问题方式一:同步代码块
synchronized(同步监视器){ //需要被同步的代码,即需要操作共享数据的代码 }
同步监视器:俗称为锁。任何一个类的对象都可以充当锁。但是必须要保证多个线程共用同一把锁
共享数据:被多个线程共同操作的数据
代码如下:
class MyTickets implements Runnable{ private int tickets=100; Object object=new Object();//定义一个对象 @Override public void run() { while (true){ synchronized(object) {//此时object通常使用this if (tickets > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + tickets); tickets--; } else { System.out.println("当前无票,请选择其他车次"); break; } } } } }
注意:在使用继承Thread创建多线程的方式中,如果要使用同步代码块解决线程安全问题时,需要确保作为锁的对象为静态的。
从上面的代码可以看出,每一次使用同步代码块的方式解决线程安全问题时,需要重新创建对象作为锁,比较麻烦。
所以在实现Runnable接口的方式中,通常使用当前的类对象作为锁,也就是this。
在继承Thread方式中,通常使用当前类作为锁(上面提到过,锁应该是一个对象,那么由此可见类其实也是对象,此处知识涉及到反射,详情可见另一篇文章,Java中的反射机制),在上述代码中可以将object替换为MyTickets.class。
4.3 解决线程安全问题方式二:同步方法
如果操作共享数据的代码刚好声明在一个方法中,不妨将此方法定义为同步的。
例子:在Java的单例模式中,懒汉模式是非线程安全的,此时可以使用同步方法来解决。
代码如下
package com.summer.java; /** * 使用同步机制将懒汉式改写为线程安全的 * @Author 安宁侯 * @create 2021-03-06 14:06 */ public class BankTest { } class Bank{ private Bank(){} private static Bank instance=null; private static synchronized Bank getInstance(){ if (instance==null){ instance=new Bank(); } return instance; } }
关于同步方法的总结:
- 同步方法中仍然涉及到同步监视器,只是不需要我们显式的声明。
- 在非静态的同步方法中,同步监视器为:this。
- 在静态的方法中,同步监视器为:当前类本身。
4.3 线程的死锁问题
死锁的概念:
不同线程分别占用对方的同步资源不放弃,都在等待对方释放自己所需要的同步资源,就形成了线程的死锁。
出现死锁问题后,不会出现异常,不会出现提示,只是所有的线程都将进入阻塞状态,无法继续进行。
例题:
package com.summer.java; /** * @Author 安宁侯 * @create 2021-03-06 14:29 */ public class DeadLock { public static void main(String[] args) { StringBuffer s1=new StringBuffer(); StringBuffer s2=new StringBuffer(); new Thread(){ @Override public void run() { synchronized (s1){ try { Thread.sleep(100);//增加死锁出现的概率 } catch (InterruptedException e) { e.printStackTrace(); } s1.append("a"); s2.append("1"); synchronized (s2){ s1.append("b"); s2.append("2"); System.out.println(s1); System.out.println(s2); } } } }.start(); new Thread(){ @Override public void run() { synchronized (s2){ try { Thread.sleep(100);//增加死锁出现的概率 } catch (InterruptedException e) { e.printStackTrace(); } s1.append("c"); s2.append("3"); synchronized (s1){ s1.append("d"); s2.append("4"); System.out.println(s1); System.out.println(s2); } } } }.start(); } }
解决方法:
- 专门的算法、原则。
- 尽量减少同步资源的定义。
- 尽量避免嵌套同步。
4.4 解决线程安全问题方式三:Lock锁(JDK5.0新增)
步骤:
- 实例化ReentrantLock
- reentrantlock的对象调用lock()方法
- reentrantlock的对象调用unlock()方法
代码如下:
package com.summer.java; import java.util.concurrent.locks.ReentrantLock; /** * 解决线程安全方式三:lock锁,jdk5.0新增 * @Author 安宁侯 * @create 2021-03-06 14:40 */ class MyTickets1 implements Runnable{ private int tickets=100; //1.实例化ReentrantLock private ReentrantLock lock=new ReentrantLock(); @Override public void run() { while (true){ try { //2.调用锁定方法lock() lock.lock(); if (tickets > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + tickets); tickets--; } else { System.out.println("当前无票,请选择其他车次"); break; } } finally { //3.调用解锁方法unlock() lock.unlock(); } } } } public class LockTest { public static void main(String[] args) { MyTickets1 tickets = new MyTickets1(); Thread thread1 = new Thread(tickets); Thread thread2 = new Thread(tickets); Thread thread3 = new Thread(tickets); thread1.setName("窗口1"); thread2.setName("窗口2"); thread3.setName("窗口3"); thread1.start(); thread2.start(); thread3.start(); } }
synchronized 与 lock两种方法的区别
相同点:都可以解决线程安全问题。
不同点:synchronized机制在执行完相应的代码后会自动地释放同步监视器;lock方法需要手动的启动(lock())、关闭监视器(unlock())。
Java中释放锁的操作:
- 当前线程的同步方法、同步代码块执行结束
- 当前线程在同步方法、同步代码块中遇到了break、return终止了代码的运行。
- 当前线程在同步方法、同步代码块中遇到了未处理的异常终止了代码的运行。
- 当前线程在同步方法、同步代码块中线程对象执行了wait()方法,当前线程阻塞并且释放了锁。
Java中不会释放锁的操作:
- 当前线程在执行同步代码块、同步方法时,程序调用了Thread.sleep()、Thread.yield()暂停了当前线程的执行
- 当前线程在执行同步代码块时,其他线程线程调用了该线程的suspend()方法将该线程暂时挂起,该线程不会释放锁(同步监视器)
注意:尽量避免使用suspend()和resume()来控制线程,这两个方法已经过时。
5.线程的通信
5.1 线程通信的例题:两个线程交替打印1~100之内的数
代码如下:
package com.summer.java; /** * @Author 安宁侯 * @create 2021-03-06 15:03 * 线程通信的例题:两个线程交替打印1~100之间的数 */ class NumberPrint implements Runnable{ private int num=1; @Override public void run() { while (true){ synchronized (this) { notify(); if (num<=100){ System.out.println(Thread.currentThread().getName()+"打印出的数为:"+num); num++; try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else break; } } } } public class CommunicationTest { public static void main(String[] args) { NumberPrint print = new NumberPrint(); Thread t1 = new Thread(print); Thread t2 = new Thread(print); t1.setName("线程1"); t2.setName("线程2"); t1.start(); t2.start(); } }
逻辑分析:当线程一进入到while时,先notify(),此时没有线程需要唤醒,继续往下执行,开始打印,到wait()时,线程一阻塞并且释放同步锁。然后线程二开始执行,到notify()时,唤醒线程一,但是此时锁在线程二的手里,线程一就绪,线程二开始打印,到wait()时阻塞并且释放锁,然后线程一开始执行...
5.2 线程通信中涉及到的方法
- wait():一旦执行到此方法,线程将进入阻塞状态,并且释放同步监视器
- notify():一旦执行到此方法,将会唤醒一个被wait()的线程,如果有多个线程被wait(),将会唤醒执行权比较高的线程。
- notifyAll():一旦执行到此方法,将会唤醒所有被wait()的线程。
说明:
- 线程通信的三种方法,在使用的时候必须在同步代码块或者同步方法中,lock在进行线程通信时使用的是另外的方法。
- 这三个方法被调用时的调用者必须是同步方法或者同步代码块中的同步监视器。
- 这三个方法是被定义在Java中的object类中
5.3 sleep()和wait()的区别
- sleep是声明在Thread类中,wait是声明在Object类中
- wait只能在同步方法或者代码块中使用,sleep可以在任何需要的场景使用。
- 二者如果都在同步方法或者同步代码块中使用时,wait可以主动释放锁,sleep不能主动释放锁,必须等到睡眠的时常到。
相同点:一旦执行方法,都会使当前线程进入到阻塞状态
5.4 线程通信的例题二:生产者与消费者的问题
问题描述:生产者(productor)将生产的产品交给店员(clerk),消费者(customer)从店员处取走产品。店员手中只能存放一定数量的(比如20个)产品,如果生产者想要生产更多的产品,此时1店员会让生产者停一下,等有空位的时候继续生产,如果店员手里没有产品,消费者想要取走产品的话,店员会让消费者等一下,等有了产品时再去取走。
分析:
- 是否是多线程问题:是,生产者线程,消费者线程
- 是否拥有共享数据:是,产品
- 如何解决线程安全问题:同步机制
- 是否涉及到线程通信:是(如果产品大于20,生产者wait。产品小于0,消费者wait)
代码如下:
package com.summer.java; /** * @Author 安宁侯 * @create 2021-03-06 15:47 */ class Clerk{ private int products; public Clerk(int products) { this.products = products; } public synchronized void product() { if (products<20){ products++; System.out.println(Thread.currentThread().getName()+"开始生产第"+products+"个产品"); notify(); }else { System.out.println("暂无空位,请等待....."); try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } public synchronized void consum() { if (products>0){ System.out.println(Thread.currentThread().getName()+"开始消费第"+products+"个产品"); products--; notify(); }else { System.out.println("暂无产品,请等待....."); try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Productor implements Runnable{ private Clerk clerk; public Productor(Clerk clerk) { this.clerk = clerk; } @Override public void run() { System.out.println("生产者开始生产产品...."); while (true) { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } clerk.product(); } } } class Consumer implements Runnable{ private Clerk clerk; public Consumer(Clerk clerk) { this.clerk = clerk; } @Override public void run() { System.out.println("消费者开始消费产品...."); while (true){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } clerk.consum(); } } } public class ProductTest { public static void main(String[] args) { Clerk clerk=new Clerk(0); Productor productor = new Productor(clerk); Consumer consumer = new Consumer(clerk); Thread t1 = new Thread(productor); Thread t2 = new Thread(consumer); t1.setName("生产者"); t2.setName("消费者"); t1.start(); t2.start(); } }