本博客java云同桌学习系列,旨在记录本人学习java的过程,并与大家分享,对于想学习java的同学,我希望这个系列能够鼓励大家一同与我学习java,成为“云同桌”。
每月预计保持更新数量三章起,每章都会从整体框架入手,介绍章节所涉及的重要知识点及相关练习题,并会设置推荐学习时间,每篇博客涉及到的点都会在开篇目录进行总览。(博客中所有高亮部分(即双等号部分)表示是面试题进阶考点,或博主在求职过程中遇到的真题考点)
在正式学习之前,我们必须要弄清楚几组名词的含义
线程与进程
抽象类比: 进程:一个车间 线程:一个车间的一个工人
进程:程序执行过程中分配和管理资源的基本单位,是一个动态概念,拥有独立的内存空间 线程:cpu任务调度和执行的基本单位,是进程的一个执行路径,与其他线程共享一个内存空间,栈空间独立
同步和异步 同步:排队执行,只有前一个执行完毕才能轮到下一个执行,安全,但效率低 异步:同时执行,多个任务同时对一个资源进行操作,无需等待,不安全,但效率高
并发与并行 并发:不同任务在同一时间段内交替执行 并行:不用任务在同一时间内同时执行
线程阻塞和线程中断 线程阻塞:线程在等待某个耗时操作的完成,完成后才能继续线程接下来的任务 线程中断:在java中,线程中断是线程的一个标志,线程中断表示该线程的中断标志设为了true,会产生InterruptedException,由程序员捕获到异常后自行选择处理方式
那么,何为多线程技术,即实现多个线程同时或交替运行,是开发高并发系统的基础
开发中,较为常用的是方式2:实现Runnable接口,相比于方式1,具有以下优势
1.通过创建任务给线程分配的方式,适用于多个线程执行同一任务的情况 2.对于自建的类,可以避免单继承带来的局限性 3.任务与线程相分离,提高程序健壮性 4.在后续的线程池技术中,接受实现Runnable接口方式的任务,而不接受继承Thread类的线程方式3比较适用于主线程需要子线程处理一些数据然后将数据返回至主线程的情况,不是很常用。
1.自己写一个类,实现Callable接口,并重写里面的call()方法,传入的泛型即你需要子线程返回的数据类型 2.新建一个Futuretask对象,将Callable实现类对象作为构造参数 3.以Futuretask对象建立线程并开启 4.执行Futuretask对象的get()方法,使主线程一直等待call()方法执行完毕取得返回值 public class Test { public static void main(String[] args) throws ExecutionException, InterruptedException { MyCallable myCallable = new MyCallable(); //2.以Callable实现类对象作为构造参数,创建Runnable子类Futuretask类对象 FutureTask futureTask = new FutureTask(myCallable); //3.以Futuretask对象建立线程并开启 new Thread(futureTask).start(); //4.运行Futuretask对象的get()方法,是主线程一直等待直到子线程的call()方法执行完毕并返回值 System.out.println(futureTask.get());//100 } //1.写一个Callable接口的实现类,泛型传入需要返回的数据类型 public static class MyCallable implements Callable<Integer>{ @Override public Integer call() throws Exception { return 100; } } }Callable与Runnable相比
Runnable没有返回值;Callable可以返回执行结果
Callable接口的call()允许抛出异常;Runnable的run()不能抛出
接口的定义不同
//Callable接口 public interface Callable<V> { V call() throws Exception; } //Runnable接口 public interface Runnable { public abstract void run(); }java.lang.Thread
介绍:负责线程控制的类
构造方法:
Thread()Thread(Runnable target)Thread(Runnable target, String name)Thread(String name)此构造方法的重载的含义基本一致,如果指定了线程任务,则执行Runnable实现类的run()方法,若没有指定线程任务,则执行Thread子类的run()方法,线程名称未指定则为默认
常用方法:
(static)currentThread() 返回当前正在执行的线程对象getName() 返回此线程的名称 public static void main(String[] args) throws IOException { Thread thread = new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } },"这是个线程"); thread.start(); System.out.println(Thread.currentThread().getName()); /*输出: main 这是个线程 */ }(static)sleep(long millis) 使当前线程休眠millis毫秒
start() 开启当前线程(JVM调用此线程的run()方法)
setName(String name) 设置当前的线程名称
setDaemon(boolean on) 将此线程设置成为守护线程
在java中,线程中断可以理解为线程的一个标志位属性,通过其他线程调用该线程对象的interrupt()方法实现,表示该线程是否被其他线程设置为了中断状态。对于中断状态的处理,需要程序员捕获到因中断状态变为true而允许产生的InterruptedException后,自行决定在释放资源后终止线程。
产生中断并进行处理的步骤:
1.调用线程对象的interrupt()将该线程中断标志设为true 2.在该线程任务run()中,满足异常生成条件,生成InterruptedException异常 3.在线程任务run()中,捕获异常,自定义此次中断的处理方式(如需终止线程,直接return关闭run()方法)以下是产生InterruptedException异常的情况(只有中断标志为true,以下产生条件才有效)
java.lang.Object#wait() java.lang.Object#wait(long) java.lang.Object#wait(long, int) java.lang.Thread#sleep(long) java.lang.Thread#interrupt() java.lang.Thread#interrupted()
线程中断应用实例:
public static void main(String[] args) throws IOException { Thread thread = new Thread(new Runnable() { @Override public void run() { try { System.out.println("开启线程"); //产生中断第二步:满足异常生成条件,生成异常 Thread.sleep(1000); }catch (InterruptedException e){ //产生中断第三步:自定义中断处理方式 System.out.println("线程中断"); //直接return结束run方法相当于终止线程 return; } } }); thread.start(); //产生中断第一步:中断标志设为true thread.interrupt(); /*输出: 开启线程 线程中断 */ }线程可以简单分为守护线程和用户线程 用户线程:类比为:主人,直接建立的正常线程,可以执行所有任务 守护线程:类比为:保姆,运行在后台。为用户线程提供一个额外的服务,生命周期依赖于用户线程,用户线程结束后,守护线程结束。
守护线程的实现: 1.需要在线程开启即start()前调用线程对象的setDaemon(true)将线程守护标志设为true; 2.守护线程中建立的新线程也是守护线程 3.守护线程中不可以执行持久化任务,例如文件的读写,因为守护线程随时可以停止,容易造成资源未正常释放
public class Test { public static void main(String[] args) throws IOException, InterruptedException { Thread thread = new Thread(new Runnable() { @Override public void run() { //子线程是否是守护线程 System.out.println("" +Thread.currentThread().isDaemon()); } }); //子线程设置为守护线程 thread.setDaemon(true); //开启子线程 thread.start(); //休息一会,防止守护线程没来得及输出就因主线程结束而结束 Thread.sleep(1000); //主线程是否是守护线程 System.out.println("" +Thread.currentThread().isDaemon()); /*输出 true false */ } }在之前的学习中,我们经常说一个类,是或不是线程安全的,这取决于这个类在线程执行时是同步还是异步,同步则线程安全,异步则线程不安全。
举个例子:如果需要线程一进入了一个if语句,在语句里正在执行其他语句更改数据,此时,线程二也进入了这个if语句,也在执行其他语句更改数据,本来按照逻辑,线程一更改完数据后,线程二是不可能进入这个if语句的,这时可能就会产生异常的数据。
实例:(创建三个线程,允许都对同一数据count同一时间段进行操作)
//线程不安全 public class Test { public static void main(String[] args) { Task task = new Task(); //创建多个线程执行这个任务 new Thread(task).start(); new Thread(task).start(); new Thread(task).start(); } //创建一个任务 public static class Task implements Runnable{ int count = 10; @Override public void run() { while(true){ if(count > 0){ System.out.println(Thread.currentThread().getName()+"准备"); try { 故意延长操作时间,使效果容易被观察到 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println(Thread.currentThread().getName()+"操作为:"+count); }else{ break; } } } } } /*输出: Thread-2准备 Thread-1准备 Thread-0准备 Thread-0操作为:9 Thread-0准备 Thread-1操作为:8 Thread-1准备 Thread-2操作为:7 Thread-2准备 Thread-2操作为:6 Thread-1操作为:5 Thread-0操作为:4 Thread-1准备 Thread-2准备 Thread-0准备 Thread-2操作为:3 Thread-0操作为:2 Thread-0准备 Thread-1操作为:3 Thread-2准备 Thread-1准备 Thread-0操作为:1 Thread-2操作为:0 Thread-1操作为:-1 Process finished with exit code 0 */观看输出结果可以发现,本来限制条件是count>0,但现在出现了异常数据-1,原因是当count为1时,三个线程先后都在run()方法里,导致执行count–,出现了结果-1
线程安全:在多线程访问同一数据时,确保运行结果与预期一致,而不会产生异常数据
利用synchronized关键字的同步代码块的应用方式,代表此代码块,只能被线程同步访问锁对象,任何对象都可以被打上同步锁的标记,在进入同步代码块时,必须需要先抢占锁对象。没有取得锁对象的线程只能等待下一次锁对象释放。一般的,我们把当前并发访问的共同资源作为同步锁对象.
格式:
synchronized(同步锁对象) { 需要同步操作的代码 }实例:(给操作count上锁,同一时间,只允许一个线程进入并操作count)
//线程安全:同步代码块 public class Test { public static void main(String[] args) { Task task = new Task(); //创建多个线程执行这个任务 new Thread(task).start(); new Thread(task).start(); new Thread(task).start(); } //创建一个任务 public static class Task implements Runnable{ int count = 10; //锁对象 Object object = new Object(); @Override public void run() { while(true){ synchronized (object){ if(count > 0){ System.out.println(Thread.currentThread().getName()+"准备"); try { //故意延长操作时间,使效果容易被观察到 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println(Thread.currentThread().getName()+"操作为:"+count); }else{ break; } } } } } } /*输出: Thread-0准备 Thread-0操作为:9 Thread-0准备 Thread-0操作为:8 Thread-1准备 Thread-1操作为:7 Thread-1准备 Thread-1操作为:6 Thread-1准备 Thread-1操作为:5 Thread-2准备 Thread-2操作为:4 Thread-1准备 Thread-1操作为:3 Thread-1准备 Thread-1操作为:2 Thread-1准备 Thread-1操作为:1 Thread-0准备 Thread-0操作为:0 Process finished with exit code 0 */注意锁对象和所操作的数据只能放到各线程共用的代码区,例,不可放到run()方法里,每个线程都有一个自己的run()方法,那样无法上锁
synchronize也可以对静态方法和实例方法进行同步,代码当该方法被一个线程抢占后,其他线程只能等待该线程释放该方法后再进行抢占执行
格式:
synchronized public void doWork(){ 需要同步操作的代码 }实例:(还是之前的背景,这次改为在run()方法里循环调用同步方法)
//线程安全:同步方法 public class Test { public static void main(String[] args) { Task task = new Task(); //创建多个线程执行这个任务 new Thread(task).start(); new Thread(task).start(); new Thread(task).start(); } //创建一个任务 public static class Task implements Runnable{ int count = 10; @Override public void run() { while(true) { if (!task()) { break; } } } //同步方法 synchronized public boolean task(){ //非静态方法默认锁对象是this //静态方法默认锁对象是类名.class if(count > 0){ System.out.println(Thread.currentThread().getName()+"准备"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println(Thread.currentThread().getName()+"操作为:"+count); return true; }else{ return false; } } } } /*输出: Thread-0准备 Thread-0操作为:9 Thread-0准备 Thread-0操作为:8 Thread-0准备 Thread-0操作为:7 Thread-0准备 Thread-0操作为:6 Thread-0准备 Thread-0操作为:5 Thread-0准备 Thread-0操作为:4 Thread-0准备 Thread-0操作为:3 Thread-2准备 Thread-2操作为:2 Thread-2准备 Thread-2操作为:1 Thread-1准备 Thread-1操作为:0 Process finished with exit code 0 */上述所介绍的两种方式,都属于隐式锁,相比这种比较隐晦。而显式锁,很简单,就是创建Lock子类ReentrantLock的一个对象,然后在同步代码前后手动上锁解锁。以面向对象的思想来看,更推荐显示锁。
格式:
Lock l = new ReentrantLock(); l.lock(); 需要同步的代码 l.unlock();实例:(在任务类里创建Lock类锁对象,在同步前手动上锁,同步后手动解锁)
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; //线程安全三:显式锁 public class Test { public static void main(String[] args) { Task task = new Task(); //创建多个线程执行这个任务 new Thread(task).start(); new Thread(task).start(); new Thread(task).start(); } //创建一个任务 public static class Task implements Runnable{ int count = 10; //创建显式锁对象 Lock l = new ReentrantLock(); @Override public void run() { while(true){ //上锁 l.lock(); if(count > 0){ System.out.println(Thread.currentThread().getName()+"准备"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println(Thread.currentThread().getName()+"操作为:"+count); }else{ break; } //解锁 l.unlock(); } } } } /*输出: Thread-0准备 Thread-0操作为:9 Thread-0准备 Thread-0操作为:8 Thread-1准备 Thread-1操作为:7 Thread-1准备 Thread-1操作为:6 Thread-2准备 Thread-2操作为:5 Thread-2准备 Thread-2操作为:4 Thread-0准备 Thread-0操作为:3 Thread-1准备 Thread-1操作为:2 Thread-2准备 Thread-2操作为:1 Thread-0准备 Thread-0操作为:0 */公平锁:可以类比为排队,线程在等待锁释放的过程中,先到先得,按顺序将锁赋予一下线程 非公平锁:可以理解为抢绣球,线程在等待锁释放的过程中,没有先到先得的理念,只要锁释放了,所有等待线程都有机会获得锁,一起争抢,抢到即获得锁。
上述三种上锁的方式,都是非公平锁
实现公平锁,可以通过ReentrantLock类的另一个构造方法
ReentrantLock(boolean fair) 创建对象时传递一个公平锁的参数,若为true,则为公平锁,false则为非公平锁隐式锁:sync是JVM层面的锁 显示锁:是API层面的锁
使用方式隐式锁:不需要程序员手动上锁和解锁,直接对某一块整体上锁解锁 显示锁:需要程序员自己去划分同步代码然后确定上锁和解锁的时机
公平方面隐式锁:只能默认设置为非公平锁 显示锁:可以通过构造参数选择是公平锁还是非公平锁
线程死锁可以简单理解为:A线程占有D资源,还需要C资源才能结束,B线程占有C资源,还需要D资源才能结束,然后两个线程就陷入了无限期的等待中,互不妥协。
现实生活中的例子也很多。例如:公司要求应聘者有工作经验才能入职,应聘者说只有入职我才能有工作经验/狗头~
死锁的四个必要条件:
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用。 2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。 3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。 4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。最常用的避免死锁的方式是:
尽量不要将锁互相嵌套,造成互相等待的情况,
在资源使用完毕后,最好立即释放锁。
如果事先知道需要加的锁,最好按照顺序加锁,等待前一个锁大概解除后再进行加锁
多线程通信的机制:生产者和消费者模型问题,
1.消费者线程先休眠,生产者线程工作; 2.工作完毕后,将数据给到消费者进行消费; 3.消费者线程被唤醒,生产者线程休眠; 4.然后消费完毕后;消费者再次休眠,生产者线程被唤醒进行生产。。。生产数据————生产者休眠,消费者唤醒————消费数据————消费者休眠,生产者唤醒————生产数据
实现方式: 根据目前我们所学的知识,可以使用Object类的wait()和notify()方法实现
java.lang.Object
返回值方法名描述voidwait()使当前线程进入等待voidwait(long timeoutMillis)使当前线程进入等待,等待时间为timeoutMills,时间结束自动被唤醒voidnotify()唤醒此对象正在等待的随机的一个单个线程voidnotifyAll()唤醒此对象正在等待的所有线程某个对象拥有生产者消费者两个线程,如果生产或消费数据过后,通常由各自的线程执行以下这段代码:
this.notifyAll(); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); }生产者消费者模型的优点:
1. 降低耦合度,生产者和消费者不直接进行通信 2. 可以解决生产和消费速度不一致的问题从Thread类中的内部类Thread.state中可以看到,线程共有6种状态
状态描述BLOCKED线程的线程状态被阻塞等待监视器锁定。NEW尚未启动的线程的线程状态。RUNNABLE可运行线程的线程状态。TERMINATED终止线程的线程状态。TIMED_WAITING具有指定等待时间的等待线程的线程状态。WAITING等待线程的线程状态。状态之间的关系详情见下图: 中间从上到下是一条正常的主线,线程实例化后进入NEW状态,开启线程后进入RUNNABLE运行状态,运行结束终止线程TERMINATED状态,而其中的非主线状态都是在RUNNABLE运行状态时发生改变最后又回到运行状态,改变条件如图。
因为在实际应用中,创建消耗线程可能往往会比线程正常工作所耗费的资源和时间更多,所以为了降低资源消耗,提高响应速度,便于管理线程,引入了线程池的技术。
线程池,可以理解为存储线程的集合。在没有任务的时候,集合中会保留一段时间已创建的线程,长时间空闲的线程会被销毁;有任务的时候,会把任务交给线程池中空闲的线程去执行,任务如果足够多的话,还会对线程池进行扩容,提高并发量。
下面将会列出常用的四种线程池,他们都是通过Executors的不同静态方法创建的实例
//缓冲线程池 ExecutorService service = Executors.newCachedThreadPool(); //定长线程池 ExecutorService service = Executors.newFixedThreadPool(2); //单线程线程池 ExecutorService service = Executors.newSingleThreadExecutor(); //周期性任务定长线程池 ScheduledExecutorService service = Executors.newScheduledThreadPool(2);这种线程池是最基础的线程池
特点:长度无限制
执行流程:
1. 判断线程池是否存在空闲线程 2. 存在则使用 3. 不存在,则创建线程 并放入线程池, 然后使用实例:
public class Test { public static void main(String[] args) { //建立缓冲线程池 ExecutorService executorService = Executors.newCachedThreadPool(); //建立任务对象 MyRunnable myRunnable = new MyRunnable(); executorService.execute(myRunnable); executorService.execute(myRunnable); executorService.execute(myRunnable); /*输出: pool-1-thread-2正在执行 pool-1-thread-1正在执行 pool-1-thread-3正在执行 */ } public static class MyRunnable implements Runnable{ @Override public void run() { //看一下当前是线程池中哪个线程在执行任务 System.out.println(Thread.currentThread().getName()+"正在执行"); } } }这种线程池不可以扩容,在创建时便指定了固定长度
特点:长度固定,不可扩容
执行流程:
1. 判断线程池是否存在空闲线程 2. 存在则使用 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程实例:
public class Test { public static void main(String[] args) { //建立定长线程池,指定最大线程数为2 ExecutorService executorService = Executors.newFixedThreadPool(2); //建立任务对象 MyRunnable myRunnable = new MyRunnable(); executorService.execute(myRunnable); executorService.execute(myRunnable); executorService.execute(myRunnable); executorService.execute(myRunnable); /*输出: pool-1-thread-2正在执行 pool-1-thread-2正在执行 pool-1-thread-1正在执行 pool-1-thread-2正在执行 */ } public static class MyRunnable implements Runnable{ @Override public void run() { //看一下当前是线程池中哪个线程在执行任务 System.out.println(Thread.currentThread().getName()+"正在执行"); } } } ### 单线程线程池 单线程线程池只有一个线程在池,且不能扩容,效果等同于定长为1的定长线程池特点:单线程
执行流程:
1. 判断线程池 的那个线程 是否空闲 2. 空闲则使用 3. 不空闲,则等待 池中的单个线程空闲后 使用实例:
public class Test { public static void main(String[] args) { //建立单线程线程池 ExecutorService executorService = Executors.newSingleThreadExecutor(); //建立任务对象 MyRunnable myRunnable = new MyRunnable(); executorService.execute(myRunnable); executorService.execute(myRunnable); executorService.execute(myRunnable); executorService.execute(myRunnable); /*输出: pool-1-thread-1正在执行 pool-1-thread-1正在执行 pool-1-thread-1正在执行 pool-1-thread-1正在执行 */ } public static class MyRunnable implements Runnable{ @Override public void run() { //看一下当前是线程池中哪个线程在执行任务 System.out.println(Thread.currentThread().getName()+"正在执行"); } } }可以定时执行或周期定时执行任务的特殊线程池
特点:可以设置定时执行和定时周期执行
执行流程:
1.判断时间是否到达触发时间 2.判断线程池是否存在空闲线程 3.存在则使用 4.不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用 5.不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程实例:
public class Test { public static void main(String[] args) { //建立周期定时定长线程池 ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2); //建立任务对象 MyRunnable myRunnable = new MyRunnable(); //定时执行(5秒后执行)参数分别为:任务对象,开始时间,时间单位 scheduledExecutorService.schedule(myRunnable,5, TimeUnit.SECONDS); //定时周期执行(5秒后执行,每1秒再执行一遍)参数分别为:任务对象,开始时间,周期间隔,时间单位 scheduledExecutorService.scheduleAtFixedRate(myRunnable,5,1,TimeUnit.SECONDS); } public static class MyRunnable implements Runnable{ @Override public void run() { //看一下当前是线程池中哪个线程在执行任务 System.out.println(Thread.currentThread().getName()+"正在执行"); } } }为了降低代码冗余性,java提高了内部类的相关写法,但内部类一大坨看着也不够清晰明了;为了使代码更加清晰明了,java提高了匿名内部类的简写方式——Lambda表达式
如果有人进行过安卓开发,应该会非常熟悉,博主在进行Android开发时,使用Android Studio开发,IDE经常会自动将按钮的触发内部类的写法自动简写成Lambda表达式的方式。
其实假如我们编写一个匿名内部类,关注的重点不就是传递的参数和方法里执行的代码嘛,所以,Lambda只需要写形参和内部代码,就可以替代匿名内部类
格式:
() -> 内部代码实例:(两种方式等价)
public class Test { public static void main(String[] args) { //建立单线程线程池 ExecutorService executorService = Executors.newSingleThreadExecutor(); //非简写 executorService.execute(new Runnable() { @Override public void run() { //看一下当前是线程池中哪个线程在执行任务 System.out.println(Thread.currentThread().getName()+"正在执行"); } }); //Lambda表达式简写 executorService.execute(() -> System.out.println(Thread.currentThread().getName()+"正在执行")); } }国庆假期加班不易,都学习到这里了,不妨点个赞加个关注呗~