【JavaSE】多线程与并发编程(总结)


一、多线程概述

参考:https://www.cnblogs.com/zsql/p/11144688.html

进程与线程

进程与线程的区别

  1. 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位

  2. 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线

  3. 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间 (包括代码段,数据集,堆等) 及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见

  4. 调度和切换:线程上下文切换比进程上下文切换要快得多

在这里插入图片描述
线程(栈+PC+TLS)

  • 栈:用于存储该线程的局部变量,这些局部变量是该线程私有的,除此之外还用来存放线程的调用栈祯。
    我们通常都是说调用堆栈,其实这里的堆是没有含义的,调用堆栈就是调用栈的意思。
    那么我们的栈里面有什么呢?
    我们从主线程的入口main函数,会不断的进行函数调用,
    每次调用的时候,会把所有的参数和返回地址压入到栈中。

  • PC:是一块内存区域,用来记录线程当前要执行的指令地址 。
    Program Counter 程序计数器,操作系统真正运行的是一个个的线程,
    而我们的进程只是它的一个容器。PC就是指向当前的指令,而这个指令是放在内存中。
    每个线程都有一串自己的指针,去指向自己当前所在内存的指针。
    计算机绝大部分是存储程序性的,说的就是我们的数据和程序是存储在同一片内存里的
    这个内存中既有我们的数据变量又有我们的程序。所以我们的PC指针就是指向我们的内存的。

  • 缓冲区溢出
    例如我们经常听到一个漏洞:缓冲区溢出
    这是什么意思呢?
    例如:我们有个地方要输入用户名,本来是用来存数据的地方。
    然后黑客把数据输入的特别长。这个长度超出了我们给数据存储的内存区,这时候跑到了
    我们给程序分配的一部分内存中。黑客就可以通过这种办法将他所要运行的代码
    写入到用户名框中,来植入进来。我们的解决方法就是,用用户名的长度来限制不要超过
    用户名的缓冲区的大小来解决。

  • TLS:
    全称:thread local storage
    之前我们看到每个进程都有自己独立的内存,这时候我们想,我们的线程有没有一块独立的内存呢?答案是有的,就是TLS。
    可以用来存储我们线程所独有的数据。
    可以看到:线程才是我们操作系统所真正去运行的,而进程呢,则是像容器一样他把需要的一些东西放在了一起,而把不需要的东西做了一层隔离,进行隔离开来。

并行与并发

并发:是指同一个时间段内多个任务同时都在执行,并且都没有执行结束。并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行 。

并行同一时刻上多个任务同时在执行 。

在多线程编程实践中,线程的个数往往多于CPU的个数,所以一般都称多线程并发编程而不是多线程并行编程。

线程安全问题

多个线程同时操作共享变量1时,会出现线程1更新共享变量1的值,但是其他线程获取到的是共享变量没有被更新之前的值。就会导致数据不准确问题。

在这里插入图片描述

共享内存不可见性问题

  1. Java 内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存,线程读写变量时操作的是自己工作内存中的变量 。(如下图所示)

在这里插入图片描述

在这里插入图片描述

上图中所示是一个双核 CPU 系统架构,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辅运算。CPU的每个核都有自己的一级缓存,在有些架构里面还有一个所有CPU都共享的二级缓存。 那么Java内存模型里面的工作内存,就对应这里的 Ll或者 L2 缓存或者 CPU 的寄存器

  1. 线程A首先获取共享变量X的值,由于两级Cache都没有命中,所以加载主内存中X的值,假如为0。然后把X=0的值缓存到两级缓存,线程A修改X的值为1,然后将其写入两级Cache,并且刷新到主内存。线程A操作完毕后,线程A所在的CPU的两级Cache内和主内存里面的X的值都是1。

  2. 线程B获取X的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回X=1;到这里一切都是正常的,因为这时候主内存中也是X=l。然后线程B修改X的值为2,并将其存放到线程2所在的一级Cache和共享二级Cache中,最后更新主内存中X的值为2,到这里一切都是好的。

  3. 线程A这次又需要修改X的值,获取时一级缓存命中,并且X=l这里问题就出现了,明明线程B已经把X的值修改为2,为何线程A获取的还是l呢?这就是共享变量的内存不可见问题,也就是线程B写入的值对线程A不可见。

synchronized 的内存语义:

这个内存语义就可以解决共享变量内存可见性问题。进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存。会造成上下文切换的开销,独占锁,降低并发性

Volatile的理解:

该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时-,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。volatile的内存语义和synchronized有相似之处,具体来说就是,当线程写入了volatile变量值时就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存),读取volatile变量值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)。不能保证原子性

二、实现多线程

方式1:继承Thread类

需求:我们要实现多线程的程序。 如何实现呢?

由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。
而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。


Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。 但是呢? Java可以去调用C / C++写好的程序来实现多线程程序。

由C/C++去调用系统功能创建进程,然后由Java去调用这祥的API, 然后提供一些类供我们使用。我们就可以实现多线程程序了。


那么Java提供的类是什么呢?

  • Thread类
    步骤
  • A : 自定义类MyThread继承Thread
  • B : 重写run () 方法
    • 为什么是run ()方法呢?
  • C :创建对象
  • D : 启动线程

注意:单独调用 run() 方法其实和调用一个类的普通方法是没有区别的,要想启动线程实际上应该调用的是 start() 方法,这是为什么呢?


start() 和 run() 的区别?

  • start() :
    使该线程开始执行;Java 虚拟机调用该线程的 run() 方法。
    用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体中的代码执行完毕而直接继续执行后续的代码。通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里的run()方法 称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。

  • run() :
    如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回
    Thread 的子类应该重写该方法。
    run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。

总结:
在这里插入图片描述
在这里插入图片描述
两个小问题

  • 为什么要重写 run()方法?
    • 因为run()是用来封装被线程执行的代码
  • run() 方法和 start() 方法的区别?
    • run():封装线程执行的代码,直接调用,相当于普通方法的调用
    • start():启动线程;然后由JVM调用此线程的run()方法

创建自定义线程代码:

//定义一个线程类
public class MyThread extends Thread {
  @Override
  public void run() {
    for(int i=0; i<100; i++) {
      System.out.println(i);
   }
 }
}

//调用方法执行线程
public class MyThreadDemo {
  public static void main(String[] args) {
    MyThread my1 = new MyThread();
    MyThread my2 = new MyThread();
//    my1.run();
//    my2.run();
    //void start() 导致此线程开始执行; Java虚拟机调用此线程的run方法
    my1.start();

匿名内部类的方式创建一个线程

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("多线程");
    }
}).start();

获取和设置线程对象名称

在这里插入图片描述

//void setName(String name):将此线程的名称更改为等于参数 name
    my1.setName("高铁");
    my2.setName("飞机");
    
    
    
    //Thread(String name)
    MyThread my1 = new MyThread("高铁");
    MyThread my2 = new MyThread("飞机");
    
    
    System.out.println(Thread.currentThread().getName());
    
    

线程优先级
在这里插入图片描述
线程控制

在这里插入图片描述

在这里插入图片描述
等待线程死亡

public class ThreadTest {
    public static void main(String[] args)  {
        ThreadDemo th1=new ThreadDemo();
        ThreadDemo th2=new ThreadDemo();
        ThreadDemo th3=new ThreadDemo();

        th1.setName("康熙");
        th2.setName("四阿哥");
        th3.setName("八阿哥");

        th1.start();
        try {
            th1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        //下面的两个线程会等待康熙线程死亡以后才开始执行
        th2.start();
        th2.start();
    }
}

守护线程

th1.setName("刘备");
th2.setName("张飞");
th3.setName("关羽");

th2.setDaemon(true);
th3.setDaemon(true);

th2.start();
th3.start();
th1.start();
//th2、th3 在th1死亡时,也会跟着死亡

礼让线程

/*

 * public static void yield():暂停当前正在执行的线程对象,并执行其他线程。

 * 让多个线程的执行更和谐,但是不能靠它保证一人一次。

 */
public class ThreadYield extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 100; i++){
            System.out.println(getName() + ":" + i);

            Thread.yield();//暂停当前正在执行的线程对象
        }
    }
}


public class TreadDemo {
    public static void main(String[] args) {
        ThreadYield th1 = new ThreadYield();
        ThreadYield th2 = new ThreadYield();
        th1.setName("呵呵");
        th2.setName("哈哈");

        th1.start();
        th2.start();
    }
}

JOJO:0
林青霞:0
JOJO:1
林青霞:1
JOJO:2
林青霞:2
JOJO:3
林青霞:3
JOJO:4
林青霞:4
JOJO:5
林青霞:5
JOJO:6
林青霞:6
JOJO:7
林青霞:7
林青霞:8
JOJO:8
林青霞:9
JOJO:9
林青霞:10
JOJO:10
林青霞:11
JOJO:11
林青霞:12
JOJO:12
林青霞:13
林青霞:14
JOJO:13
林青霞:15
JOJO:14
JOJO:15
林青霞:16
JOJO:16
林青霞:17
JOJO:17
JOJO:18
林青霞:18
JOJO:19
林青霞:19

Process finished with exit code 0

中断线程
stop() 中断线程很暴力,后面的程序都不走了

public boolean isInterrupted()

public void interrupt()

public static boolean interrupted()

在这里插入图片描述

线程的生命周期
在这里插入图片描述

方式2:实现Runnable接口

多线程的实现方案有两种

  • 继承 Thread类
  • 实现 Runnable接口
    相比继承 Thread类,实现Runnable接口的好处
  • 避免了 Java单继承的局限性
  • 适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,较好的体现了面向对象的设计思想
 MyRunnable runnable = new MyRunnable();
        Thread th1 = new Thread(runnable,"线程一");
        Thread th2 = new Thread(runnable,"线程二");

        th1.start();
        th2.start();
/**
 * 自定义runnable接口,
 */
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("自定义runnable接口下的run()方法执行了...");
    }
}

方式3:实现Callable接口,线程池

参考文章:https://www.cnblogs.com/dolphin0520/p/3949310.html
https://blog.csdn.net/zsj777/article/details/85089993
https://blog.csdn.net/meng19910117/article/details/81043988

创建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口。
这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。
如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。

我的理解:

  • Callable
    Callable类似于Runnable,只不过Callable中要实现的方法是有返回值结果的,也就线程执行完任务后是会有一个结果返回给用户看的噢。
public interface Runnable {
    public abstract void run();
}
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}
  • ExecutorService
    ExecutorService,是用来把线程跑起来的服务,下面是该接口中声明的一些方法,可见它支持很多重载方式去将一个可以执行的任务跑起来,而且返回值都是 Future 类型,那么Future是干啥的呢?
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
  • Future
      Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
      简言之,Future可以控制和查看任务的执行进度、还可以获取任务的结果,这不就是我们实现Callable想要的嘛~我们就是想获得结果呀!
public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}
  • FutureTask
    因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的实现类FutureTask。一个FutureTask 可以用来包装一个 Callable或是一个runnable对象。因为FurtureTask实现了Runnable方法,所以一个 FutureTask可以提交(submit)给一个Excutor执行(excution).

    如上提供了两个构造函数,一个以Callable为参数,另外一个以Runnable为参数。这些类之间的关联对于任务建模的办法非常灵活,允许你基于FutureTask的Runnable特性(因为它实现了Runnable接口),把任务写成Callable,然后封装进一个由执行者调度并在必要时可以取消的FutureTask。

    FutureTask可以由执行者调度,这一点很关键。它对外提供的方法基本上就是Future和Runnable接口的组合:get()、cancel、isDone()、isCancelled()和run(),而run()方法通常都是由执行者调用,我们基本上不需要直接调用它。

总结:

  • Callable和Runnable差不多,但是任务执行时有返回值有最后的结果。
  • ExecutorService 是执行者,会帮我们去调用任务噢
  • Future 是接口,可以操控线程任务获取一些东西,
  • FutureTask是Future的实现类,可以实例化,实例化时可以以Callable、Runnable为参数,还可以在执行者带动下跑起任务来,还可以跑起来后观察这个线程任务、还可以从中获得些结果。
public FutureTask(Callable<V> callable) {  
        if (callable == null)  
            throw new NullPointerException();  
        sync = new Sync(callable);  
    }  
    public FutureTask(Runnable runnable, V result) {  
        sync = new Sync(Executors.callable(runnable, result));  
    }  

一个Callable、Future、ExecutorService联合的例子
实现Callable接口

package com.ps.learn.socketio.service;
 
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;
 
/**
 * 自定义一个任务类,实现Callable接口
 */
public  class MyCallableClass implements Callable<String> {
    // 标志位
    private AtomicInteger flag = new AtomicInteger(0);
 
 
 
    public AtomicInteger getFlag() {
        return flag;
    }
 
    public void setFlag(AtomicInteger flag) {
        this.flag = flag;
    }
 
 
 
    public String call() throws Exception {
        if (this.flag.get() == 0) {
            // 如果flag的值为0,则立即返回
            return "flag = 0";
        }
        if (this.flag.get() == 1) {
            // 如果flag的值为1,做一个无限循环
            try {
                while (true) {
                    System.out.println("looping......");
                    //sleep 中断当前线程
                    Thread.sleep(2000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "false";
        } else {
            // falg不为0或者1,则抛出异常
            throw new Exception("Bad flag value!");
        }
    }
}

程序启动类

package com.ps.learn.socketio.service;
 
/**
 * Author:ZhuShangJin
 * Date:2018/12/19
 */
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
 
/**
 * Callable 和 Future接口
 * Callable是类似于Runnable的接口,实现Callable接口的类和实现Runnable的类都是可被其它线程执行的任务。
 * Callable和Runnable有几点不同:
 * (1)Callable规定的方法是call(),而Runnable规定的方法是run().
 * (2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
 * (3)call()方法可抛出异常,而run()方法是不能抛出异常的。
 * (4)运行Callable任务可拿到一个Future对象, Future表示异步计算的结果。
 * 它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。
 * 通过Future对象可了解任务执行情况,可取消任务的执行,还可获取任务执行的结果。
 */
public class CallableAndFuture {
 
 
    public static void main(String[] args) {
        // 定义3个Callable类型的任务
        MyCallableClass task1 = new MyCallableClass();
        task1.setFlag(new AtomicInteger(0));
        MyCallableClass task2 = new MyCallableClass();
        task2.setFlag(new AtomicInteger(1));
        MyCallableClass task3 = new MyCallableClass();
        task3.setFlag(new AtomicInteger(2));
 
        // 创建一个执行任务的服务
        ExecutorService es = Executors.newFixedThreadPool(3);
        try {
            // 提交并执行任务,任务启动时返回了一个Future对象,
            // 如果想得到任务执行的结果或者是异常可对这个Future对象进行操作
            Future future1 = es.submit(task1);
            // 获得第一个任务的结果,如果调用get方法,当前线程会等待任务执行完毕后才往下执行
            System.out.println("task1: " + future1.get());
//
            Future future2 = es.submit(task2);
            // 等待5秒后,再停止第二个任务。因为第二个任务进行的是无限循环
            Thread.sleep(5000);
            System.out.println("task2 cancel: " + future2.cancel(true));
 
            // 获取第三个任务的输出,因为执行第三个任务会引起异常
            // 所以下面的语句将引起异常的抛出
            Future future3 = es.submit(task3);
            System.out.println("task3: " + future3.get());
        } catch (Exception e) {
            //捕获异常
            e.printStackTrace();
        }
        // 停止任务执行服务
        es.shutdownNow();
    }
}

FutureTask执行多任务计算的使用场景
利用FutureTask和ExecutorService,可以用多线程的方式提交计算任务,主线程继续执行其他任务,当主线程需要子线程的计算结果时,在异步获取子线程的执行结果。

public class FutureTest1 {
 
    public static void main(String[] args) {
        Task task = new Task();// 新建异步任务
        FutureTask<Integer> future = new FutureTask<Integer>(task) {
            // 异步任务执行完成,回调
            @Override
            protected void done() {
                try {
                    System.out.println("future.done():" + get());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
        };
        // 创建线程池(使用了预定义的配置)
        ExecutorService executor = Executors.newCachedThreadPool();
        executor.execute(future);
 
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        // 可以取消异步任务
        // future.cancel(true);
 
        try {
            // 阻塞,等待异步任务执行完毕-获取异步任务的返回值
            System.out.println("future.get():" + future.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
 
    // 异步任务
    static class Task implements Callable<Integer> {
        // 返回异步任务的执行结果
        @Override
        public Integer call() throws Exception {
            int i = 0;
            for (; i < 10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + "_"
                            + i);
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return i;
        }
    }
 
}

三、线程安全问题

安全问题出现的条件

  • 是多线程环境
  • 有共享数据
  • 有多条语句操作共享数据

同步的好处和弊端

  • 好处:解决了多线程的数据安全问题
  • 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

怎么实现呢 ?

  • 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
  • Java 提供了同步代码块的方式来解决

synchronized

方式一:synchronized 同步代码块

Object obj=new Object();

synchronized (obj) {
多条语句操作共享数据的代码
}

在这里插入图片描述

在这里插入图片描述

Lock类

方式二:Lock类

  • Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
  • 使用try....finally代码块来包裹
private int ticket=100;
private Lock lock=new ReentrantLock();


@Override
public void run() {

    while (true) {

       try {
           lock.lock();
           if (ticket > 0) {
               try {
                   Thread.sleep(100);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println(Thread.currentThread().getName() + "卖票" + ticket);
               ticket--;
           }
       }finally {
           lock.unlock();
       }

线程安全的集合

回顾:StringBuffer、Vector、Hashtable集合
在这里插入图片描述

四、练习案例

两种匿名内部类方式创建线程

public class TreadDemo02 {
    public static void main(String[] args) {
        // 继承Thread类实现多线程
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i<20;i++){
                    System.out.println(Thread.currentThread().getName() + ": Thread : " + i);
                }
            }
        }.start();

        // 实现Runnable接口实现多线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i<20;i++){
                    System.out.println(Thread.currentThread().getName() + ": Runnable : " + i);
                }
            }
        }){
            //thread子类
        }.start();
    }
}

在这里插入图片描述

简单面试题 01

1:多线程有几种实现方案,分别是哪几种?
两种。

继承Thread类
实现Runnable接口

扩展一种:实现Callable接口。这个得和线程池结合。

2:同步有几种方式,分别是什么?
两种。

同步代码块
同步方法

3:启动一个线程是run()还是start()?它们的区别?
start();

run():封装了被线程执行的代码,直接调用仅仅是普通方法的调用
start():启动线程,并由JVM自动调用run()方法

4:sleep()和wait()方法的区别

sleep():必须指时间;不释放锁。
wait():可以不指定时间,也可以指定时间;释放锁。

5:为什么wait(),notify(),notifyAll()等方法都定义在Object类中

因为这些方法的调用是依赖于锁对象的,而同步代码块的锁对象是任意锁。
而Object代码任意的对象,所以,定义在这里面。

回顾集合和IO的面试题

集合:
1: HashMap和Hashtable的区别。

2:Collection 和 Collections的区别。

3: List, Set, Map是否继承自Collection接口?

4:说出ArrayList,Vector, LinkedList的存储性能和特性?

5:你所知道的集合类都有哪些?主要方法?

IO:
1: java中有几种类型的流?JDK为每种类型的流提供了一些抽象类以供继承,请说出他们分别是哪些类?

2:什么是java序列化,如何实现java序列化?

猜你喜欢

转载自blog.csdn.net/qq_41864648/article/details/106187643