【Java多线程】Java多线程基础知识

Java多线程基础知识

多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”。

多线程的优点:

  • 让多部分代码同时执行。
  • 带来良好的用户体验。
  • 充分利用CPU的资源。
  • 简化编程模型。

多线程的缺点: 导致程序运行效率降低。


进程和线程的基本认识

进程:

  • 进程是一个正在执行的程序,一个程序可以同时执行多个任务(线程)。
  • 进程独占内存空间,同时保持各自的运行状态,相互之间不会干扰。
  • 进程是并发执行程序过程资源分配和管理的基本单位(资源分配的最小单位)。
  • 每个进程都有自己独立的地址空间,每启动一个进程,系统就会分配地址空间。

线程:

  • 通常,每一个任务称作一个线程,线程有时候会被成为轻量级的进程。
  • 它是程序执行的最小单位,一个进程可以拥有多个线程,多个线程之间共享进程的地址空间以及一些进程级别的其他资源,但是各个线程拥有自己的栈空间。

进程和线程的区别和联系?

  • 1、进程是资源分配的最小单位,线程是程序执行(CPU调度)的最小单位。
  • 2、进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
  • 3、线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
  • 4、但是多进程程序更健壮,多线程程序只要有一个线程死掉,那么对于其共享资源的其他线程也会产生影响,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

进程和线程的使用场景?

因为进程是资源分配的基本单位,线程是程序执行的最小单元。所有我们应该从这两点以及进程与线程之间的健壮性来考虑。

  • 1、在程序中,如果需要频繁创建和销毁的使用线程。因为进程创建和销毁开销很大(需要不停的分配资源),但是线程频繁的调用只是改变CPU的执行,开销小。
  • 2、如果需要程序更加的稳定安全时,可以选择进程。如果追求速度,就选择线程。

并发和并行的区别

  • 并发指的是多个线程操作一个资源,不是同时执行,而是交替执行,单核CPU,只不过因为CPU的时间片很短,速度太快,看起来同时执行。
  • 并行才是真正的同时执行,多核CPU,每一个线程都可以使用一个单独的CPU资源来运行。

相关概念:

  • QPS:每秒能够响应的请求数。
  • 平均响应时间:并发数 / 平均响应时间 = QPS。
  • 并发用户数:系统可以承载的最大用户量。
  • 吞吐量:单位时间内能够处理的请求数。

互联网系统架构中,如何提高系统的并发能力?

垂直扩展:

提升单机的处理能力。

  • 1、增强单机的硬件性能:增加CPU的核数、内存升级、磁盘扩容。
  • 2、提升系统的架构能力:使用Cache来提高效率。

水平扩展:

集群、分布式都是水平的扩展方案。

  • 集群:多个人做同一事(例如一个餐厅中同时多顾几个厨师同时炒菜)。
  • 分布式:一个复杂的事情,拆分成几个简单的步骤,分别找不同的人去完成(1.洗菜 2.切菜 3.炒菜)。

具体操作为:

  • 1、站点层扩容:通过Nginx反向代理,实现高并发的系统,将服务部署在多个服务器上。
  • 2、服务层扩容:通过RPC框架实现远程调用:Dubbo,Spring Clodud,将业务逻辑分拆成不同的RPC Client,每个Clident完成各自的不同的业务,如果并发量比较大,则可以新增加RPC Client。
  • 3、数据层扩容:一台数据库拆分成多态,分库分表,主从复制,读写分离。

创建线程

我们以一边看电视、一边吃饭为例、看看多线程情况下如何编写程序?

继承Thread类

继承Thread类,重写run()方法(以下是当前线程的执行逻辑)。

class WatchTV extends Thread {
    @Override
    public void run() {
        System.out.println("Watch TV");
    }
}

class Eat extends Thread {
    @Override
    public void run() {
        System.out.println("eating");
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Thread watchTV = new WatchTV();
        Thread eat = new Eat();
        watchTV.start(); //watchTV();
        eat.start(); //eat();
    }
}

实现Runnable接口

实现Runnable接口,重写run()方法(以下是当前线程的执行逻辑)。

class WatchTV implements Runnable {
    @Override
    public void run() {
        System.out.println("Watch TV");
    }
}

class Eat extends implements Runnable {
    @Override
    public void run() {
        System.out.println("eating");
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Thread watchTV = new Thread(new WatchTV());
        Thread eat = new Thread(new Eat());
        watchTV.start(); //watchTV();
        eat.start(); //eat();
    }
}

匿名内部类

public class TestDemo {
    public static void main(String[] args) {
        new Thread("WatchTV") {
            @Override
            public void run() {
                System.out.println("Watch TV");
            }
        }.start();
        
        new Thread("Eat") {
            @Override
            public void run() {
                System.out.println("eating");
            }
        }.start();
    }
}

实现Callable接口

实现Callable接口,重写call()方法。

//a
class CallableDemo implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= 10000; i++) {
            sum += i;
        }
        return sum;
    }
}

public class TestDemo {
    public static void main(String[] args) {
        /**
         * 通过Callable和FutureTask创建线程
         * a.创建Callable接口的实现类,同时实现call方法
         * b.创建Callable实现类的对象,使用FutureTask包装当前Callable对象,
         * FutureTask对象封装Callable对象中call方法的返回
         * c.使用FutureTask对象作为Thread的参数创建并且启动线程
         * d.调用FutureTask对象的get()来获取子线程执行的结果
         */
        //b
        Callable<Integer> callableDemo = new CallableDemo();
        FutureTask<Integer> task = new FutureTask<>(callableDemo);
        //c
        Thread thread = new Thread(task);
        thread.start();

        try {
            //d
            Integer result = task.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

Callable接口和Runnable的区别

  • 1、Callable接口实现的是call方法,Runnable接口实现的是run方法。
  • 2、Callable的任务执行后有返回值,Runnable没有返回值。
  • 3、call()方法会抛出异常,run方法不能抛出异常。

start()方法和run()方法的区别

  • start()方法本身是用来启动一个线程,并将其添加到一个线程组里面,这时线程获取CPU资源之后就会执行所定义的run方法的逻辑。
  • 而run()方法本身只是一个普通的方法,并不会启动新的线程。

【Java多线程】Java多线程基础知识_第1张图片


守护线程

API描述为:The java virtual machine exits when the only threads running are all daemon threads。 即:当JVM总没有一个非守护线程时,JVM进程会退出。

守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默完成一些系统性的服务,比如垃圾回收线程,JIT线程就可以理解为守护线程。

与守护线程相对的是用户线程(非守护线程),用户线程可以认为是系统的工作线程,它会完成这个程序要完成的业务员操作。

如果用户线程全部结束,则意味着这个程序无事可做。守护线程要守护的对象已经不存在了,那么整个应用程序就应该结束。因此,当一个Java应用内只有守护线程时,Java虚拟机自然退出。即:守护线程能够自动结束生命周期,而非守护则不具备这一特点。

以GC垃圾回收器为例,GC在从我们main()的主线程开始,就在扫描是否有垃圾,有垃圾就回收掉,一直到main()结束,此时JVM结束,所有线程结束。那要是GC不是守护线程时正常线程。首先我们是不知道一个main()主线程是什么时候结束的,所以GC我就得写个死循环,让它一直扫描是否有垃圾吧。那我们想一下,如果此时main()结束了,GC结束么?不结束啊,因为是死循环啊。所以GC这种从头到尾一直提供服务的线程,我们将它置为守护线程。从调用开始运作,一直到主线程结束,自己自动结束。

我们可以通过Thread.setDaemon设置守护线程。不过需要注意的是守护线程必须在start之前设置,否则会报错。

例如:我们可以创建一个子线程,并让他持续进行睡眠,如下:

Thread thread = new Thread() {
    @Override
    public void run() {
        try {
            while (true) {
                TimeUnit.MILLISECONDS.sleep(1);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
};

此时该线程为用户线程,即非守护线程,那么当主线程结束时,该程序并不会自动结束。

【Java多线程】Java多线程基础知识_第2张图片

此时,如果想当主线程结束时,子线程自动结束,那么就可以将子线程设为守护线程,完整代码如下:

public class TestDemo {
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                try {
                    while (true) {
                        TimeUnit.MILLISECONDS.sleep(1);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        //将子线程变为守护线程
        thread.setDaemon(true);//在线程启动之前去调用
        thread.start();
        try {
            TimeUnit.MILLISECONDS.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Main Thread Finished");
    }
}

【Java多线程】Java多线程基础知识_第3张图片


线程的生命周期

我们通过Thread源码发现,Java的线程生命周期有6种状态。

  • NEW(新建状态)new Thread()创建线程对象。
  • RUNNABLE(就绪状态):线程对象此时调用start()方法,此时在JVM进程中创建了一个线程,线程并不是一经创建就直接得到执行,需要等待操作系统的其他资源,比如:处理器。
  • BLOCKED(阻塞状态):等到一个监视器锁进入到同步代码块或者同步方法中,代码块/方法某一个时刻只允许一个线程去执行,其他线程只能等待,这种情况下等待的线程会从RUNNABLE状态转换到BLOCKED状态;例如Object.wait()方法。
  • WAITING(等待状态):调用Object.wait()/join()/LockSupport.park()等方法,此时线程从RUNNABLE状态转换到WAITING状态
  • TIMED_WAITING(睡眠状态):调用带超时参数的Thread.sleep(long millis)/Object.wait(long timeout)/join(long millis)/LockSupport.parkNanos()/LockSupport.parkUntil等方法都会使得当前线程进入到TIMED_WAITING状态
  • TERMINATED(终止状态): 是线程的最终状态。
    • 线程正常运行结束。
    • 线程运行出错。
    • JVM crash

线程之间的状态转换

现在我们就画一副图来理解一下线程生命周期的6种状态。

【Java多线程】Java多线程基础知识_第4张图片

  • Java线程的6种状态及切换(透彻讲解)

线程常用方法

start()

用来启动一个线程 将其添加一个线程组当中 此时线程就会处于Runnable就绪状态。

public class TestDemo {
    public static void main(String[] args) {
        new Thread(){
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                    System.out.println("子线程运行结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }
}

sleep()

sleep方法使得当前线程指定毫秒级的休眠,暂停执行,不会放弃monitor锁的使用权。

  • jdk1.5之后,引入枚举类型TimeUnit,对sleep方法对其进行了封装,省去了时间单位换算的步骤。
TimeUnit.HOURS.sleep(3); //睡眠3个小时
TimeUnit.MINUTES.sleep(27); //睡眠27分钟
TimeUnit.SECONDS.sleep(8); //睡眠8秒种
public class TestDemo {
    public static void main(String[] args) {
        new Thread(){
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                    System.out.println("子线程运行结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();

        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程运行结束");
    }
}

yield()

属于启发式的方法。线程A.yield()会提醒调度器线程A愿意放弃本次的cpu资源,如果cpu资源不紧张,处理器有可能会忽略这种提示。

public class TestDemo {
    public static void main(String[] args) {
        new Thread(){
            @Override
            public void run() {
                while (true){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.yield();

        try {
            Thread.sleep(1000);
            System.out.println("Main Thread Finished");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

yield()和sleep()的区别?

  • sleep() 方法给其他线程运行机会时不考虑线程的优先级;yield() 方法只会给相同优先级或更高优先级的线程运行的机会。
  • 线程执行 sleep() 方法后进入阻塞状态;线程执行 yield() 方法转入就绪状态,可能马上又得得到执行。
  • sleep() 方法声明抛出 InterruptedException;yield() 方法没有声明抛出异常。
  • sleep() 方法需要指定时间参数;yield() 方法出让 CPU 的执行权时间由 JVM 控制。

join()

含义:thread B中调用threadA.join(),此时thread B进入到等待状态,直到当前threadA结束自己的生命周期或者达到join方法的超时时间。

线程A自己调用A.join()方法没有任何用处。

//t1先输出0~9, t2再输出0~9,主线程最后输出0~9
public class TestDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                for(int i=0; i<10; i++){
                    System.out.println("thread: "+Thread.currentThread().getName()+", "+i);
                }
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        
        Thread t2 = new Thread("t2"){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("thread: " + Thread.currentThread().getName() + ", " + i);
                }
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {

                }
            }
        };
        
        try {
            t1.start();
            t1.join();
            t2.start();
            t2.join();
            for(int i=0; i<10; i++){
                System.out.println("thread: "+Thread.currentThread().getName()+", "+i);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

实现线程中断的方法

interrupt() :将java线程当中的中断状态位置为true

  • 当thread A调用sleep()/join()/wait方法后都会使得当前线程进入阻塞状态,而如果另外一个线程调用被阻塞线程的interrupt方法(例如:在thread B :中调用:thread A对象.interrupt()方法)会打断当前的这种阻塞状态,并抛出一个InterruptedException的异常,这样的方法称之为可中断方法。
  • 需要注意的是,调用interrupt()方法并不是结束当前被阻塞线程的生命周期,只是打断了当前线程的阻塞状态。
public class TestDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(){
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    System.out.println("I am be interrupted");
                }
            }
        };
        thread.start();
        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.interrupt();
    }
}

isInterrupted() :判断中断状态位是否位true

interrupted() :判断中断状态位是否为true

  • 区别在于interrupted方法调用之后会擦除掉线程的interrupt标识。
public class TestDemo {
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                while (true) {
                    System.out.println(Thread.interrupted());
                }
            }
        };
        thread.setDaemon(true);
        thread.start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.interrupt();
//        System.out.println(thread.isInterrupted());
//        thread.interrupt();
//        System.out.println(thread.isInterrupted());
    }
}

wait/notify/notifyAll

  • 首先这三个方法都是存在于Object类中的方法。

  • wait方法调用在synchronized同步代码块或方法当中,使得当前线程进入阻塞状态。

  • notify/notifyAll方法能够唤醒当前线程的阻塞状态。


线程的优先级

每个线程都有优先级,优先级默认为5,可设置的范围为1~10。并且Java线程优先级继承于创建它的线程。

//线程的优先级的相关方法:
setPriority(int grade); //设置当前线程的优先级
getPriority(); //获取当前线程的优先级
  • 设置当前线程的优先级,源码如下:
//设置当前线程的优先级
public final void setPriority(int newPriority) {
    ThreadGroup g;
    checkAccess();
    if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
        throw new IllegalArgumentException();
    }
    if((g = getThreadGroup()) != null) {
        if (newPriority > g.getMaxPriority()) {
            newPriority = g.getMaxPriority();
        }
        setPriority0(priority = newPriority);
    }
}
  • 其中,ThreadGroup用于对一批线程进行统一管理。

需要注意的是,优先级只会增加抢到CPU执行权的概率,而并不是优先级大的执行完再执行优先级小的。

你可能感兴趣的:(Java多线程)