zoukankan      html  css  js  c++  java
  • java中的多线程

    1.多线程
    1.1.多线程介绍
      学习多线程之前,我们先要了解几个关于多线程有关的概念。

      进程:正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能,进程是系统进行资源分配和调度的一个独立单位。进程是正在运行的程序,进程负责给程序分配内存空间,而每一个进程都是由程序代码组成的,这些代码在进程中执行的流程就是线程。

      线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

      简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程,但至少有一个线程。什么是多线程呢?即就是一个程序中有多个线程在同时执行。

    1.2.多线程运行原理
      大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个任务。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。感觉这些软件好像在同时运行着。

      其实这些软件在某一时刻,只会运行一个进程。这是为什么呢?这是由于CPU(中央处理器)在做着高速的切换而导致的。对于CPU而言,它在某个时间点上,只能执行一个程序,即就是说只能运行一个进程,CPU不断地在这些进程之间切换。只是我们自己感觉不到。为什么我们会感觉不到呢?这是因为CPU的执行速度相对我们的感觉实在太快了,虽然CPU在多个进程之间轮换执行,但我们自己感到好像多个进程在同时执行。

      多线程真的能提高效率吗?其实并不是这样的,因为我们知道,CPU会在多个进程之间做着切换,如果我们开启的程序过多,CPU切换到每一个进程的时间也会变长,我们也会感觉机器运行变慢。所以合理的使用多线程可以提高效率,但是大量使用,并不能给我们带来效率上的提高。

    1.3.主线程
      回想我们以前学习中写过的代码,当我们在dos命令行中输入java空格类名回车后,启动JVM,并且加载对应的class文件。虚拟机并会从main方法开始执行我们的程序代码,一直把main方法的代码执行结束。如果在执行过程遇到循环时间比较长的代码,那么在循环之后的其他代码是不会被执行的。如下代码演示:

    class Demo
    {
    String name;
    Demo(String name)
    {
    this.name = name;
    }
    void show()
    {
    for (int i=1;i<=20 ;i++ )
    {
    System.out.println("name="+name+",i="+i);
    }
    }
    }
    class ThreadDemo
    {
    public static void main(String[] args)
    {
    Demo d = new Demo("小强");
    Demo d2 = new Demo("旺财");
    d.show();
    d2.show();
    System.out.println("Hello World!");
    }
    }
    复制代码
      若在上述代码中show方法中的循环执行次数很多,这时书写在d.show();下面的代码是不会执行的,并且在dos窗口会看到不停的输出name=小强,i=值,这样的语句。为什么会这样呢?

      原因是:jvm启动后,必然有一个执行路径(线程)从main方法开始的。一直执行到main方法结束。这个线程在java中称之为主线程。当主线程在这个程序中执行时,如果遇到了循环而导致程序在指定位置停留时间过长,无法执行下面的程序。

      可不可以实现一个主线程负责执行其中一个循环,由另一个线程负责其他代码的执行。实现多部分代码同时执行。这就是多线程技术可以解决的问题。

    1.4.如何创建线程
    1.4.1.创建线程方式一:继承Thread类
      该如何创建线程呢?通过API中的英文Thread的搜索,查到Thread类。通过阅读Thread类中的描述。创建新执行线程有两种方法。一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。接下来可以分配并启动该子类的实例。

      创建线程的步骤:

      1. 定义一个类继承Thread。

      2. 重写run方法。

      3. 创建子类对象,就是创建线程对象。

      4. 调用start方法,开启线程并让线程执行,同时还会告诉jvm去调用run方法。

    class Demo extends Thread //继承Thread
    {
    String name;
    Demo(String name)
    {
    this.name = name;
    }
    //复写其中的run方法
    public void run()
    {
    for (int i=1;i<=20 ;i++ )
    {
    System.out.println("name="+name+",i="+i);
    }
    }
    }
    class ThreadDemo
    {
    public static void main(String[] args)
    {
    //创建两个线程任务
    Demo d = new Demo("小强");
    Demo d2 = new Demo("旺财");
    //d.run(); 这里仍然是主线程在调用run方法,并没有开启两个线程
    //d2.run();
    d2.start();//开启一个线程
    d.run();//主线程在调用run方法
    }
    }
    复制代码
      打印部分结果:由于多线程操作,输出数据会有所不同

        name=旺财,i=1

        name=小强,i=1

        name=旺财,i=2

        name=小强,i=2

        name=小强,i=3

        name=旺财,i=3

        name=旺财,i=4

        name=旺财,i=5

        name=旺财,i=6

        name=旺财,i=7

        ..........

      思考:线程对象调用 run方法和调用start方法区别?

     线程对象调用run方法不开启线程。仅是对象调用方法。线程对象调用start开启线程,并让jvm调用run方法在开启的线程中执行。

    1.4.2.继承Thread类原理
      为什么要继承Thread类,并调用其的start方法才能开启线程呢?

      继承Thread类:因为Thread类描述线程事物,具备线程应该有功能。

      那为什么不直接创建Thread类的对象呢?

    1 Thread t1 = new Thread();
    2 t1.start();//这样做没有错,但是该start调用的是Thread类中的run方法,而这个run方法没有做什么事情,更重要的是这个run方法中并没有定义我们需要让线程执行的代码。
    复制代码
      创建线程的目的是什么?

      是为了建立单独的执行路径,让多部分代码实现同时执行。也就是说线程创建并执行需要给定的代码(线程的任务)。对于之前所讲的主线程,它的任务定义在main函数中。自定义线程需要执行的任务都定义在run方法中。Thread类中的run方法内部的任务并不是我们所需要,只有重写这个run方法,既然Thread类已经定义了线程任务的位置,只要在位置中定义任务代码即可。所以进行了重写run方法动作。

    1.5.多线程的内存图解
      多线程执行时,到底在内存中是如何运行的呢?

      以上个程序为例,进行图解说明:

      多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。

      当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。

    1.6.获取线程名称
      开启的线程都会有自己的独立运行栈内存,那么这些运行的线程的名字是什么呢?该如何获取呢?既然是线程的名字,按照面向对象的特点,是哪个对象的属性和谁的功能,那么我们就去找那个对象就可以了。查阅Thread类的API文档发现有个方法是获取当前正在运行的线程对象。还有个方法是获取当前线程对象的名称。既然找到了,我们就可以试试。

       Thread.currentThread()获取当前线程对象

       Thread.currentThread().getName();获取当前线程对象的名称

    class Demo extends Thread //继承Thread
    {
    String name;
    Demo(String name)
    {
    this.name = name;
    }
    //复写其中的run方法
    public void run()
    {
    for (int i=1;i<=20 ;i++ )
    {
    System.out.println("name="+name+","+Thread.currentThread().getName()+",i="+i);
    }
    }
    }
    class ThreadDemo
    {
    public static void main(String[] args)
    {
    //创建两个线程任务
    Demo d = new Demo("小强");
    Demo d2 = new Demo("旺财");
    d2.start();//开启一个线程
    d.run();//主线程在调用run方法
    }
    }
    复制代码
    原来主线程的名称: main

    自定义的线程: Thread-0 线程多个时,数字顺延。Thread-1......

    进行多线程编程时不要忘记了Java程序运行时从主线程开始,main方法的方法体就是主线程的线程执行体。

    1.7.创建线程的第二种方式
      掌握了如何创建线程对象,以及开启线程后,记得在查阅API时,还说了有第二种开启线程的方式,那么第二种是什么呢?

    1.7.1.实现Runnable接口
      继续查看API发现,创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后可以分配该类的实例,在创建 Thread 时作为一个参数来传递并启动。

      怎么还要实现Runnable接口,Runable是啥玩意呢?继续API搜索。

      查看Runnable接口说明文档:Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为 run 的无参数方法。

      总结:

      创建线程的第二种方式:实现Runnable接口。

       1、定义类实现Runnable接口。

       2、覆盖接口中的run方法。。

       3、创建Thread类的对象

       4、将Runnable接口的子类对象作为参数传递给Thread类的构造函数。

       5、调用Thread类的start方法开启线程。

    代码演示:
    class Demo implements Runnable
    {
    private String name;
    Demo(String name)
    {
    this.name = name;
    }
    //覆盖了接口Runnable中的run方法。
    public void run()
    {
    for(int i=1; i<=20; i++)
    { System.out.println("name="+name+"..."+Thread.currentThread().getName()+"..."+i);
    }
    }
    }
    class ThreadDemo2
    {
    public static void main(String[] args)
    {
    //创建Runnable子类的对象。注意它并不是线程对象。
    Demo d = new Demo("Demo");
    //创建Thread类的对象,将Runnable接口的子类对象作为参数传递给Thread类的构造函数。
    Thread t1 = new Thread(d);
    Thread t2 = new Thread(d);
    //将线程启动。
    t1.start();
    t2.start();
    System.out.println(Thread.currentThread().getName()+"----->");
    System.out.println("Hello World!");
    }
    }
    复制代码
      输出结果:

    1.7.2.实现Runnable的原理
      为什么需要定一个类去实现Runnable接口呢?继承Thread类和实现Runnable接口有啥区别呢?

      实现Runnable接口,避免了继承Thread类的单继承局限性。覆盖Runnable接口中的run方法,将线程任务代码定义到run方法中。创建Thread类的对象,只有创建Thread类的对象才可以创建线程。线程任务已被封装到Runnable接口的run方法中,而这个run方法所属于Runnable接口的子类对象,所以将这个子类对象作为参数传递给Thread的构造函数,这样,线程对象创建时就可以明确要运行的线程的任务。

    1.7.3.实现Runnable的好处
      第二种方式实现Runnable接口避免了单继承的局限性,所以较为常用。实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦。

    1.8.线程状态图
      查阅API关于IllegalThreadStateException这个异常说明信息发现,这个异常的描述信息为:指示线程没有处于请求操作所要求的适当状态时抛出的异常。这里面说适当的状态,啥意思呢?难道是说线程还有状态吗?

      1、新建(new):线程对象被创建后就进入了新建状态。如:Thread thread = new Thread();  

      2、就绪状态(Runnable):也被称为“可执行状态”。线程对象被创建后,其他线程调用了该对象的start()方法,从而启动该线程。如:thread.start(); 处于就绪状态的线程随时可能被CPU调度执行。

      3、运行状态(Running):线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。

      4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权限,暂时停止运行。直到线程进入就绪状态,才有机会进入运行状态。阻塞的三种情况:

    等待阻塞:通过调用线程的wait()方法,让线程等待某工作的完成。
    同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态。
    其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或超时、或者I/O处理完毕时,线程重新转入就绪状态。
      5、死亡状态(Dead):线程执行完了或因异常退出了run()方法,该线程结束生命周期。

    1.8.1.sleep,wait,yield,join的区别
    sleep()方法

    在指定时间内让当前正在执行的线程暂停执行,但不会释放“锁标志”。不推荐使用。 sleep()使当前线程进入阻塞状态,在指定时间内不会执行。

    wait()方法

    在其他线程调用对象的notify或notifyAll方法前,导致当前线程等待。线程会释放掉它所占有的“锁标志”,从而使别的线程有机会抢占该锁。 当前线程必须拥有当前对象锁。如果当前线程不是此锁的拥有者,会抛出IllegalMonitorStateException异常。 唤醒当前对象锁的等待线程使用notify或notifyAll方法,也必须拥有相同的对象锁,否则也会抛出IllegalMonitorStateException异常。 waite()和notify()必须在synchronized函数或synchronized block中进行调用。如果在non-synchronized函数或non-synchronized block中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。

    yield方法

    暂停当前正在执行的线程对象。 yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。 yield()只能使同优先级或更高优先级的线程有执行的机会。 调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。

    join方法

    等待该线程终止。 等待调用join方法的线程结束,再继续执行。如:t.join();//主要用于等待t线程运行结束,若无此句,main则会执行完毕,导致结果不可预测

    1.9.线程的安全问题
      带女朋友看电影,需要买票,电影院要卖票,模拟电影院的买票操作

      假设我们想要的电影是 “功夫熊猫3”,本次电影的座位共100个(本厂电影只能卖100张票)

      模拟电影院的售票窗口,实现多个窗口同时卖 “功夫熊猫3”这场电影票(多个窗口一起卖这100张票)

      需要窗口:采用线程对象

      需要票:Runnable接口子类来模拟

    public class ThreadDemo {
    public static void main(String[] args) {
    //创建票对象
    Ticket ticket = new Ticket();

    //创建3个窗口
    Thread t1 = new Thread(ticket, "窗口1");
    Thread t2 = new Thread(ticket, "窗口2");
    Thread t3 = new Thread(ticket, "窗口3");

    t1.start();
    t2.start();
    t3.start();
    }
    }

    public class Ticket implements Runnable {
    //共100票
    int ticket = 100;

    @Override
    public void run() {
    //模拟卖票
    while(true){
    //t1,t2,t3
    if (ticket > 0) {
    //模拟选坐的操作
    try {
    Thread.sleep(1);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
    }
    }
    }
    }
    复制代码
      总结:上面程序出新了问题
       票出现了重复的票

       错误的票 0

    1.10.同步的锁
    同步代码块: 在代码块声明上 加上synchronized
    synchronized (锁对象) {
    可能会产生线程安全问题的代码
    }
    复制代码
      同步代码块中的锁对象可以是任意的对象,多个线程对象使用的是同一个锁对象

      把Ticket.java进行了代码修改

    public class Ticket implements Runnable {
    //共100票
    int ticket = 100;

    //定义所对象
    Object lock = new Object();
    @Override
    public void run() {
    //模拟卖票
    while(true){
    //同步代码块
    synchronized (lock){
    if (ticket > 0) {
    //模拟选坐的操作
    try {
    Thread.sleep(10);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
    }
    }
    }
    }
    }
    复制代码
      当使用了同步代码块后,上述的线程的安全问题,解决了。

    同步方法:在方法声明上加上synchronized
    public synchronized void method(){

    可能会产生线程安全问题的代码

    }
    复制代码
      同步方法中的锁对象是 this

    //同步方法,锁对象this
    public synchronized void method(){
    //this.name = name;
    if (ticket > 0) {
    //模拟选坐的操作
    try {
    Thread.sleep(10);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
    }
    }
    复制代码
    静态同步方法: 在方法声明上加上synchronized
    public static synchronized void method(){
    可能会产生线程安全问题的代码
    }
    复制代码
    1.11.死锁
      同步锁的另一个弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:死锁。这种情况能避免就避免掉。

    synchronzied(A锁){
    synchronized(B锁){

    }
    }
    复制代码
    /*
    * 定义锁对象
    */
    public class MyLock {
    public static final Object lockA = new Object();
    public static final Object lockB = new Object();
    }

    /*
    * 线程任务类
    */
    public class ThreadTask implements Runnable {

    int x = new Random().nextInt(1);//0,1

    //指定线程要执行的任务代码
    @Override
    public void run() {
    while(true){
    if (x%2 ==0) {
    //情况一
    synchronized (MyLock.lockA) {
    System.out.println("if-LockA");
    synchronized (MyLock.lockB) {
    System.out.println("if-LockB");
    System.out.println("if大口吃肉");
    }
    }
    } else {
    //情况二
    synchronized (MyLock.lockB) {
    System.out.println("else-LockB");
    synchronized (MyLock.lockA) {
    System.out.println("else-LockA");
    System.out.println("else大口吃肉");
    }
    }
    }
    x++;
    }
    }
    }

    public class ThreadDemo {
    public static void main(String[] args) {
    //创建线程任务类对象
    ThreadTask task = new ThreadTask();

    //创建两个线程
    Thread t1 = new Thread(task);
    Thread t2 = new Thread(task);

    //启动线程
    t1.start();
    t2.start();
    }
    }
    复制代码
    1.12.Lock接口
      查阅API,发现Lock接口,比同步更厉害,有更多操作;

       lock():获取锁

       unlock():释放锁;

      提供了一个更加面对对象的锁,在该锁中提供了更多的显示的锁操作。使用Lock接口,以及其中的lock()方法和unlock()方法替代同步。

      如下代码演示:

    public class Ticket implements Runnable {
    //共100票
    int ticket = 100;

    //创建Lock锁对象
    Lock ck = new ReentrantLock();

    @Override
    public void run() {
    //模拟卖票
    while(true){
    //synchronized (lock){
    ck.lock();
    if (ticket > 0) {
    //模拟选坐的操作
    try {
    Thread.sleep(10);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
    }
    ck.unlock();
    //}
    }
    }
    }
    复制代码
    1.13.线程的匿名内部类的使用
    方式1
    new Thread() {
    public void run() {
    for (int x = 0; x < 40; x++) {
    System.out.println(Thread.currentThread().getName()
    + "...X...." + x);
    }
    }
    }.start();

    方式2
    Runnable r = new Runnable() {
    public void run() {
    for (int x = 0; x < 40; x++) {
    System.out.println(Thread.currentThread().getName()
    + "...Y...." + x);
    }
    }
    };
    new Thread(r).start();
    复制代码
    2.多线程文件上传
      实现服务器端可以同时接收多个客户端上传的文件。 我们要修改服务器端代码

    /*
    * 文件上传 服务器端
    *
    */
    public class TCPServer {
    public static void main(String[] args) throws IOException {
    //1,创建服务器,等待客户端连接
    ServerSocket serverSocket = new ServerSocket(6666);

    //实现多个客户端连接服务器的操作
    while(true){
    final Socket clientSocket = serverSocket.accept();
    //启动线程,完成与当前客户端的数据交互过程
    new Thread(){
    public void run() {
    try{
    //显示哪个客户端Socket连接上了服务器
    InetAddress ipObject = clientSocket.getInetAddress();//得到IP地址对象
    String ip = ipObject.getHostAddress(); //得到IP地址字符串
    System.out.println("小样,抓到你了,连接我!!" + "IP:" + ip);

    //7,获取Socket的输入流
    InputStream in = clientSocket.getInputStream();
    //8,创建目的地的字节输出流 D:\upload\192.168.74.58(1).jpg
    BufferedOutputStream fileOut = new BufferedOutputStream(new FileOutputStream("D:\upload\"+ip+"("+System.currentTimeMillis()+").jpg"));
    //9,把Socket输入流中的数据,写入目的地的字节输出流中
    byte[] buffer = new byte[1024];
    int len = -1;
    while((len = in.read(buffer)) != -1){
    //写入目的地的字节输出流中
    fileOut.write(buffer, 0, len);
    }

    //-----------------反馈信息---------------------
    //10,获取Socket的输出流, 作用:写反馈信息给客户端
    OutputStream out = clientSocket.getOutputStream();
    //11,写反馈信息给客户端
    out.write("图片上传成功".getBytes());

    out.close();
    fileOut.close();
    in.close();
    clientSocket.close();
    } catch(IOException e){
    e.printStackTrace();
    }
    };
    }.start();
    }

    //serverSocket.close();
    }
    }
    复制代码
    3.总结
    创建线程的方式
      方式1,继承Thread线程类

      步骤

        1, 自定义类继承Thread类

        2, 在自定义类中重写Thread类的run方法

        3, 创建自定义类对象(线程对象)

        4, 调用start方法,启动线程,通过JVM,调用线程中的run方法

       方式2,实现Runnable接口

       步骤

        1, 创建线程任务类 实现Runnable接口

        2, 在线程任务类中 重写接口中的run方法

        3, 创建线程任务类对象

        4, 创建线程对象,把线程任务类对象作为Thread类构造方法的参数使用

        5, 调用start方法,启动线程,通过JVM,调用线程任务类中的run方法

    同步锁
      多个线程想保证线程安全,必须要使用同一个锁对象

    A.同步代码块

    synchronized (锁对象){
    可能产生线程安全问题的代码
    }
    复制代码
    同步代码块的锁对象可以是任意的对象

      

    B.同步方法

    public synchronized void method()
    可能产生线程安全问题的代码
    }
    复制代码
    同步方法中的锁对象是 this

    C.静态同步方法

    public synchronized static void method()
    可能产生线程安全问题的代码
    }
    复制代码
    静态同步方法中的锁对象是 类名.class
    ---------------------
    作者:weixin_34179968
    来源:CSDN
    原文:https://blog.csdn.net/weixin_34179968/article/details/88125116
    版权声明:本文为博主原创文章,转载请附上博文链接!

  • 相关阅读:
    SpringCloud Gateway使用实例
    Nacos服务注册与发现
    HashMap源码分析——put方法
    Volatile关键字——内存可见性
    Java的JIT编译器
    why spring?
    mysql 锁
    sql server 表变量和临时表
    mysql 存储过程
    mysql 截取字符串
  • 原文地址:https://www.cnblogs.com/zhangtan/p/10496217.html
Copyright © 2011-2022 走看看