Java多线程技术

1、多线程的概述

1.1、进程与线程

  进程是操作系统进行资源分配和调度的一个独立单位,它是一个内存中运行的应用程序的载体,每个进程都有一个独立的内存空间。进程一般由程序,数据集合和进程控制块三部分组成,具有动态性、并发性、独立性和异步性这四个特征。

  线程指的是进程中的一条条执行路径,一个进程中可以包含一个或多个线程,这些线程共用一个内存空间(也就是该进程所占的内存空间),且线程之间可以相互切换,并发执行。而多线程就是字面上的意思:一个进程中拥有多个可以并发执行的线程。

1.2、线程的调度

  线程实现并发执行是存在特定的方式的,而这些方式就是线程的调度机制,线程的调度分为两种:

  • 分时调度
  • 抢占式调度

  分时调度指的是:所有线程都平均分配占用CPU的时间,即是一种轮流使用CPU的调度方式。

  抢占式调度指的是:CPU会抛出一段空闲的时间片,之后让每个需要执行的线程去抢占这个时间片,抢占成功便可执行。当然这种调度方式存在线程优先级高的线程抢占时间片成功的概率会更高,当然优先级相同也还是随机抢占。

  值得注意的是对于CPU核心而言,在某一时刻,只能执行一个线程。只因为在抢占式调度模式下多个线程在快速切换,而这个快速切换的时间段对于我们来说是毫秒甚至是微秒、纳秒级别的,因此我们就会觉得它们是在同一时间执行的。其实,多线程程序并不能提高程序的运行速度,但能提高程序的运行效率,让CPU的使用效率更高。

1.3、多线程的同步和异步

  在计算机领域,同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去;异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。

  简单理解就是:同步就是会排着队来执行,前面的不执行完,后面的就不能执行,异步就是会同时执行,不受影响。因此,同步就会导致执行的效率低,但执行时数据的安全性高,异步就会导致执行的效率高,但执行时数据的安全性低。

  效率好理解,安全性如何理解:在同步执行时,数据更改的执行对象只有一个,所以不会出现数据错乱的问题,安全性就高,在异步执行时,就会发生前一个对象刚拿到的数据是这个值,还没执行更改,后一个对象就抢占时间片后把这个值改变了,那对于前一个对象来说它所拿到的数据就会发生错乱,这就导致了数据的安全性低。

1.4、并发与并行的概念

  并发是指:两个或多个事件会在同一时间段内发生,所有的并发处理都有排队等候,唤醒和执行这三个步骤;而并行指的是:两个或多个事件在同一时刻同时发生。

  两者区别在于,并发执行的多个程序是在同一个CPU运行的前提下的,而并行执行的多个程序是由多个CPU运行的,才可以做到同一时刻同时执行。因为一个CPU在某一时刻只能处理一件事。

2、多线程的实现

  实现多线程可以通过以下三种方式来实现:

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口

  这样描述看起来比较难以理解,那下面我就来具体解释这三种实现方法。

2.1、继承Thread类

2.1.1、Thread类的介绍

  该类的父类为Object类,同时实现了Runable接口,该类常用的四种构造方法:

  • Thread()
  • Thread​(String name)
  • Thread​(Runnable target)
  • Thread​(Runnable target, String name)

  第一种为无参构造函数,不需要传任何参数。第二种是传入一个String类型参数,表示你想给线程起的名字。第三种是传入一个任务对象,该对象所表示的类实现了Runnable接口。第四种是传入一个任务对象和你想给这个线程起的名字。

  除了构造方法以外,该类也提供了一些实现线程操作的方法:

  1、run方法 2、sleep​方法 3、start方法 4、setName​方法 5、getName方法 

  6、getState方法 7、currentThread方法 8、interrupt方法 9、isAlive方法 

  10、isDaemon方法 11、isInterrupted方法 12、setDaemon​方法

  (1)run方法的作用是:该方法的方法体是该线程所要执行的任务,也就是说线程执行功能的代码编写在该方法中。

  (2)sleep​方法的作用是:是当前执行的线程休眠,休眠的时间长短由自己定,所以使用该方法时要传入一个long类型的参数,该参数的值就是代表你想要线程休眠的时间长短,注意:该时间的单位是毫秒(1000毫秒=1秒)。

  (3)start方法的作用是:调用该方法后让线程启动。

  (4)setName​方法的作用是:给线程设置一个名字,所以使用该方法时,需要传入一个String类型的参数,该参数就是你给线程设置的名字

  (5)getName方法的作用是:获得该线程的名字。

  (6)getState方法的作用是:获得该线程的状态,使用该方法后会返回(BLOCKED 、NEW、RUNNABLE、TERMINATED、TIMED_WAITING、WAITING )六种状态之一。

  注解:BLOCKED:线程的线程状态被阻塞等待监视器锁定。NEW:尚未启动的线程的线程状态。RUNNABLE:可运行线程的线程状态。TERMINATED:终止线程的线程状态。TIMED_WAITING:具有指定等待时间的等待线程的线程状态。 WAITING :等待线程的线程状态。

  (7)currentThread方法作用是:获得线程的引用对象,所以使用后返回值的类型为Thread类型。

  (8)interrupt方法的作用是:进行线程中断操作,使用方法后会返回一个boolean类型的值,true代表中断成功,false代表中断失败(线程已经结束,不需要中断)

  (9)isAlive方法的作用是:判断线程是否存活,使用后会返回一个boolean类型的值,true代表线程存活,false代表线程死亡

  (10)isDaemon方法的作用是:判断线程是否为守护线程,使用后会返回一个boolean类型的值,true代表是守护线程,false代表不是守护线程。

  (11)isInterrupted方法的作用是:判断线程是否中断,使用后会返回一个boolean类型的值,true代表线程被中断,false代表线程没被中断

  (12)setDaemon​方法的作用是:将用户线程转变为守护线程,使用该方法是需传入一个boolean类型的参数,传入的参数的值为true,代表转变为守护线程,值为false,代表不变成守护线程。(守护线程的概念是:在所有用户线程结束或死亡后,守护线程自动死亡)

  Thread类就介绍到这,下面介绍如何继承Thread类实现多线程操作。

2.1.2、实现代码与效果

  以下是java代码的实现:

public class Test {public static void main(String[] args) {Thread thread = new MyThread();thread.start();for(int i=0;i<4;i++){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Main"+i);}}static class MyThread extends Thread{@Overridepublic void run() {for(int i=0;i<4;i++){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("MyThread"+i);}}}
}

  以下是控制台的输出效果:

  由代码可以看出,我创建了一个内部类MyThread并继承了Thread类,而且在类中还重写了run方法,就由我上述所说:run方法中的方法体就是线程执行的任务,start方法是启动线程让其执行任务,我还在输出语句前加入了sleep方法,让线程执行一次任务就睡眠1秒钟,这样会让多线程的表现更突出。大家可以看到效果图,main方法和线程的run方法的执行效果是交替出现的,这就是多线程执行的效果了:不是将一个方法内的执行内容执行完了,才去执行另一个方法,而是在线程之间会争抢时间片来执行,哪个线程抢到了就执行哪个线程的任务。

  注意:如果在你自己编写的代码中不加入sleep方法的话,它的执行就不会有很明显交替执行的效果,但还是在实现多线程的,不要怀疑。

2.2、实现Runnable接口

2.2.1、Runnable接口的介绍

  上面在介绍Thread类时,也提到了Runnable接口,Thread类能进行多线程操作的原因就是实现了Runnable接口。Runnable作为接口被定义出来,就是因为该接口中有一个抽象方法run方法,没错就是Tread类所实现的run方法。只要实现了Runnable接口,并重写了run方法的类,就是个任务执行类,就能让线程执行,从而实现多线程。

2.2.2、实现代码与效果

  以下是java代码的实现:

public class Text1 {public static void main(String[] args) {MyThread t = new MyThread();new Thread(t).start();for(int i=0;i<4;i++){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Main"+i);}}static class MyThread implements Runnable{@Overridepublic void run() {for(int i=0;i<4;i++){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("MyThread"+i);}}}
}

  以下是控制台的输出效果:

  大家可以对比一下继承Thread类的代码和实现Runnable接口的代码,虽然看起来好像没什么差别,但你这样认为就是大错特错了,仔细对比两种方式中对线程类的启动方式:

  继承Thread类的线程类MyThread的启动方式是:先创建一个MyThread类的对象,再由该对象调用start方法;

         Thread thread = new MyThread();thread.start();

  而实现Runnable接口的线程类MyThread的启动方法就有所差别:先创建一个MyThread类的对象,然后将这个对象传给新创建的Thread类,然后Thread类在调用start方法。

         MyThread t = new MyThread();new Thread(t).start();

  很多小伙伴就纳闷了,为啥会是这样呢。大家还记得我上面在Thread类中提到的构造方法吗,其中有一种构造方法传入的参数是实现了Runnable接口的类对象,没错这种构造方法就是提供给实现了Runnable接口的类有启动线程任务的方法,毕竟Runnable接口中就只有一种方法:run方法,实现了Runnable接口的类的对象是不能调用start方法启动线程的。现在是不是豁然开朗了,这两种方式是有小小的不同的,千万别搞错了。

  这两种方式在具体的项目编程时会更多使用的是实现Runnable接口的方式,因为这种方式相较于继承Thread类的方式来说存在几点优势:

  (1)通过创建任务,然后再让线程分配的方式,更适合多个线程执行同一个任务的情况。

  (2)可以避免单继承的局限性,因为接口可以多实现。

  (3)任务和线程是分离的,提高了程序的健壮性

  (4)线程池技术支持实现Runnable接口的任务,不支持Thread类型的线程。

  

2.3、实现Callable接口

  实现Callable接口的方式相较于前面两种实现方式来说运用的更少,但这种方式也是可以实现多线程操作的,这一点大家要记住。

2.3.1、Callable接口的介绍

  该接口和Runnable有相同的地方,但又有不同地方,不同体现在这几个方面:

  (1)两个接口中的方法不一样:Runnable接口中是run方法,而Callable接口中的是call方法

  (2)方法的返回值不一样:run方法没有返回值,call方法有返回值,返回值的类型可以自己定义,也还可能会抛出异常。

  (3)执行多线程的效果不一样:实现Runnable接口后执行的多线程之间是互不影响的,而实现Callable接口后执行的多线程之间既可以互不影响,也可以做到主线程等副线程执行完再执行,这就和第二点的返回值有关了。有小伙伴就很疑惑了,既然要等待,为啥还去实现这个接口呢,直接顺序执行不就可以了,但如果主线程等了几100个副线程呢,这就很明显了,所以实现Callable接口的这个效果是有意义的。

  (4)两种方式所要进行线程运行的操作是不一太样的。

  说完这两个接口的对比,现在介绍Callable接口特有的机制:FutureTark类,该类提供了一个get方法来获取Callable接口中的call方法的返回值,该方法的实现就可以让主线程去等副线程执行完后取得返回值后再去执行主线程。

  理论不如实现代码效果来的更清楚,那下面我就来实现Callable接口的方法来实现多线程操作。

2.3.2、实现代码与效果

  以下是java代码的实现:

public class Text2 {public static void main(String[] args) throws ExecutionException, InterruptedException {MyThread thread = new MyThread();FutureTask<Integer> task = new FutureTask<>(thread);new Thread(task).start();Integer integer = task.get();System.out.println("返回值为"+integer);for(int i=0;i<4;i++){Thread.sleep(1000);System.out.println("Main"+i);}}static class MyThread implements Callable<Integer>{@Overridepublic Integer call() throws Exception {for (int i=0;i<4;i++){Thread.sleep(1000);System.out.println("MyThread"+i);}return 20;}}
}

  以下是控制台的输出效果:

  看到代码对比和效果图对比是不是就很清楚,Callable接口中的方法是call方法,且有返回值,返回值类型是自己定义的Integer类型,实现线程启动的方式也有区别:

  实现Runnable接口的启动线程方式:

         MyThread t = new MyThread();new Thread(t).start();

  实现Callable接口的启动线程方式:

     MyThread thread = new MyThread();FutureTask<Integer> task = new FutureTask<>(thread);new Thread(task).start();

  效果图也展示出两种方式的不同之处,当然你把下面这两行代码注释掉,实现的效果图又会和实现Runnable接口的方式一样,你们可以试试看。

     Integer integer = task.get();System.out.println("返回值为"+integer);

  三种实现多线程的方式我就总结到这,有图有理论,希望可以帮到大家,下面进入下个阶段:多线程可以实现的方法和操作。

3、多线程可以实现的方法和操作

3.1、设置和获取线程的名称

  当你的线程数量较多且分不清哪个线程执行那个任务时,你就可以给线程设置各自的名字,用于分辨,当然设置了名字后,你也是可以获取每个线程的名字的。

   设置线程名字的方法:在创建新的Thread类时,直接传入线程的名称,上面中介绍Thread类的构造方法我有提及,还有一种就是用线程的对象去调用setName方法;获取线程名字的操作就一种:用线程的对象调用getName方法来获取名字。我就拿实现Runnable接口的代码修改一下,来演示给你们看看。

  以下是java代码的实现:

public class Text3 {public static void main(String[] args) {MyThread t = new MyThread();new Thread(t,"FriendA").start();MyThread t2 = new MyThread();Thread thread = new Thread(t2);thread.setName("FriendB");thread.start();System.out.println(Thread.currentThread().getName());}static class MyThread implements Runnable{@Overridepublic void run() {System.out.println(Thread.currentThread().getName());}}
}

  以下是控制台的输出效果:

  大家可以看到我用了两种方式去设置线程的名字,效果都是一样的,当然如果你不设置名字,也不是说你使用getName方法获取的就是空值,而是系统会给个默认的名字,就像main方法一样有个默认的名字main。可能细心地小伙伴会发现我用了一个其他的方法:currentThread方法,该方法我上面在Thread类的介绍中提到过,作用是:获取线程的引用对象,这是个很实用的方法。

3.2、线程休眠

  见名知意,该操作就是让线程休眠(停止)指定的时间,使用的方法就是:sleep方法,使用该方法是传入的参数就是你想要线程休眠的时间,单位是毫秒。具体就不演示了,上面三种实现方式的代码中有关于sleep方法使用的代码,效果图展示也不明显,小伙伴们可以自己去尝试敲出来,看看效果。具体使用的时候会有异常,你可以进行抛出,也可以try-catch捕捉起来,两种方式都可以。

  实现的代码就这一行,具体运行的小伙伴往上看就有。

     Thread.sleep(1000);

3.3、线程的中断和判定是否中断

3.3.1、线程中断操作

  要让线程进行中断操作使用的是interrupt方法,注意不能使用stop方法,因为使用stop方法会产生大量的bug(不能及时的释放资源,导致资源的占用),该方法已经被java官方标记成了过时的方法(就是不能使用了意思)。那interrupt方法具体如何实现线程的中断操作的,给大家解释原理:使用interrupt方法并不能直接将线程中断掉,因为该方法只是给线程打上了一个中断异常的标志,线程中的任务中如果有这些方法:sleep方法、 wait方法、interrupted方法和interrupt方法时会检测到异常中断从而抛出异常,所以在try-catch代码块中可以进行中断操作:如果你在catch代码块中直接编写一个return,那run方法就会直接退出,达到线程中断的操作,如果在catch不编写一个return,那线程还是会继续执行。

  以下是java代码的实现:

public class Text4 {public static void main(String[] args) {Thread t = new Thread(new MyThread());t.start();for(int i=0;i<5;i++){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":该线程执行");}t.interrupt();}static class MyThread implements Runnable{@Overridepublic void run() {for(int i=0;i<10;i++){try {Thread.sleep(1000);System.out.println(Thread.currentThread().getName()+":该线程执行");} catch (InterruptedException e) {System.out.println(Thread.currentThread().getName()+":该线程中断");return;}}}}
}

  以下是控制台的输出效果:

  大家可以看到我在run方法中的try-catch代码块中写了这两行代码,return语句执行后就直接结束了run方法,当然小伙伴可以讲return语句注释掉,看看效果。

             System.out.println(Thread.currentThread().getName()+":该线程中断");return;

  效果应该是这样的,会显示线程中断这句话,但线程还会继续执行,如效果图:

3.3.2、判断线程是否中断

  判断线程是否中断使用isInterrupted方法,该方法会返回一个boolean类型的值,如果值为true表示该线程中断了,如果为false表示该线程没有中断或中断失败。多的不说,直接代码和效果图展示,不过代码,我只给实现代码,运行代码就是上面中断操作的代码,将下面的代码放在使用了Interrupted方法的代码的后面就可以了。

  以下是java代码的实现:

         boolean b = t.isInterrupted();if(b){System.out.println(t.getName()+"已中断");}

  以下是控制台的输出效果:

3.4、设置守护线程和判断是否为守护线程

3.4.1、守护线程的设置

  守护线程的概念是:在所有用户线程结束或死亡后,守护线程自动死亡,用户线程就是除了设置了守护线程的其他所有线程。设置守护线程的方法是:setDaemon​方法 使用时要传入一个boolean类型的参数,参数的值为true表示设置为守护线程,参数的值为false表示为不设置成守护线程,注意:setDaemon​方法要编写在在start方法之前。

  以下是java代码的实现:

     public class Text5 {public static void main(String[] args) {Thread t = new Thread(new MyThread());t.setDaemon(true);t.start();for(int i=0;i<5;i++){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"线程执行");}System.out.println(Thread.currentThread().getName()+"线程停止");}static class MyThread implements Runnable{@Overridepublic void run() {for (int i=0;i<10;i++){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"线程执行");}}}
}

  以下是控制台的输出效果:

  大家可以对照代码和效果图发现,我在run方法中设置的循环次数是10次,在没有设置守护线程的情况下run方法是还会执行的,但设置了守护线程后,main方法停止,run方法也自动停止,这就守护线程的效果了,大家可以试试将setDaemon​方法的语句注释掉,看看运行效果,我在这就不演示了。

3.4.2、判断是否为守护线程

  判断是否为守护线程使用isDaemon方法,该方法使用后会返回一个boolean类型的值,值为true代表是守护线程,值为false代表不是守护线程。多的不说,直接代码和效果图展示,不过代码,我只给实现代码,运行代码就是上面设置守护线程的代码,将下面的代码放在使用了setDaemon​方法的代码的后面就可以了。

  以下是java代码的实现:

     boolean b = t.isDaemon();if(b){System.out.println(t.getName()+"是守护线程");}

  以下是控制台的输出效果:

4、多线程的安全问题

4.1、异步多线程的不安全性

  多线程如果不做处理的话,这些线程是异步执行的,就像上面我演示的这些案例,它们都是异步执行的,这样就会导致线程出现安全性的问题:数据错乱。下面我就举一个例子来演示线程不安全的情况。

  以下是java代码的实现:

public class Text6 {public static void main(String[] args) {MyThread t = new MyThread();new Thread(t).start();new Thread(t).start();}static class MyThread implements Runnable{int count = 6;@Overridepublic void run() {while (count>0){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}count--;System.out.println(count);             }}}
}

  以下是控制台的输出效果:

  大家可以看到我代码中的while的判定条件是count>0,最后竟然有线程输出了-1,甚至两个线程还输出有相同值,这就是线程异步导致的数据错乱,使得线程不安全,避免线程的不安全的做法就是让线程同步执行(线程排队执行)。下面我就为大家演示如何让线程同步执行的。

4.2、实现同步多线程

  实现线程的同步有三种方式:

  • 实现同步代码快
  • 实现同步方法
  • 设置显示锁

4.2.1、实现同步代码块

  实现的格式:

Object o = new Object();
synchronized (o) {//里面放置你需要运行的代码
}

  上面定义的Object类的对象o的作用是:给同步代码块提供隐式锁(不需要你去实现上锁和解锁的操作),线程可以实现排队执行的操作也是因为这个隐式锁的缘故:当一个线程运行了同步代码块中的代码,隐式锁就会自动给同步代码块上锁,让其他线程不可以执行,当一个线程执行完同步代码块的代码后,隐式锁又会自动解锁,让线程可以继续执行同步代码块的内容,这就达到了让线程排队执行的效果了。

  我就拿上面的不安全演示代码修改了一下,给你们看看效果:

  以下是java代码的实现:

public class Text6 {public static void main(String[] args) {MyThread t = new MyThread();new Thread(t).start();new Thread(t).start();}static class MyThread implements Runnable{int count = 6;Object o = new Object();@Overridepublic void run() {synchronized (o) {while (count > 0) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}count--;System.out.println(count);}}}}
}

  以下是控制台的输出效果:

  大家可以看到就不会出现上面线程不安全时所演示的效果了,没有负数也没有重复值。

4.2.2、实现同步方法

  实现的格式:直接将你所要执行的逻辑代码编写成由synchronized修饰的方法。

  我就拿上面的不安全演示代码修改了一下,给你们看看效果:

  以下是java代码的实现:

public class Text7 {public static void main(String[] args) {MyThread t = new MyThread();new Thread(t).start();new Thread(t).start();}static class MyThread implements Runnable{int count = 6;@Overridepublic void run() {while (true){if(!sub()){break;}}}public synchronized boolean sub(){if(count>0){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}count--;System.out.println(count);return true;}return false;}}
}

  以下是控制台的输出效果:

  大家可以看到也没有出现上面线程不安全时所演示的效果,没有负数也没有重复值。

  注意:实现同步方法也是有隐式锁存在的,该隐式锁有两种:在没有被static修饰的同步方法的隐式锁就是this所表示的任务对象(拿我的代码来说就是t对象),而被static修饰的同步方法的隐式锁就是该方法所在类的类名.class(就拿我的代码来说就是:MyThread.class)。

4.2.3、设置显示锁

  实现的格式:创建一个锁对象,然后自己设置关锁和开锁代码

 //创建锁对象Lock l = new ReentrantLock();//关锁l.lock();//开锁l.unlock();

  我就拿上面的不安全演示代码修改了一下,给你们看看效果:

  以下是java代码的实现:

public class Text8 {public static void main(String[] args) {MyThread t = new MyThread();new Thread(t).start();new Thread(t).start();}static class MyThread implements Runnable{int count = 6;Lock l = new ReentrantLock();@Overridepublic void run() {a:while (true){l.lock();try {if(count>0) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}count--;System.out.println(count);}else {break a;}}finally {l.unlock();}}}}
}

  以下是控制台的输出效果:

  大家可以看到也没有出现上面线程不安全时所演示的效果,没有负数也没有重复值。

  注意:在实现关锁和解锁代码时,要加try-finally代码块,代码格式如下:

 l.lock();try {//在这里编写你的逻辑代码}finally {l.unlock();}

  讲完显示锁和隐式锁之后还要讲解一个概念:公平锁和非公平锁。公平锁就是在锁解开后,先排队的线程先执行,而非公平锁就是在锁解开后所有线程都可以抢占执行的时间片。而我在上面所讲的三种方式所创建的锁都是非公平锁,要创建公平锁,就要在创建显示锁对象时传入一个true,就可以创建出公平锁,代码如下:

Lock l = new ReentrantLock(true);

4.3、线程的死锁

  死锁是线程在同步执行时会发生的情况,为何会发生,是因为有多个同步方法相会调用,这就会导致线程的死锁。

  举个例子:就比如警察和绑匪的情况,警察对绑匪说:“你放了人质,我放了你”,警察说完后等绑匪回复,但是,绑匪听到后并没有放人质,而是又向警察说:“你放了我,我就放了人质”,这时绑匪又要等警察回复。就是在这种两者都在等对方的回复,且又是在等不到对方回复就无法继续接下来行动的情况下,就会产生僵持,也就是线程的死锁。

  不要让线程出现死锁的方法就是:避免同步方法之间相互调用。

5、多线程的通信问题

  多线程之间的通信时会发生线程相互不协调问题:在这个线程执行完,另外一个线程必须接着这个线程执行完后执行的情况下,就会产生不协调问题,因为线程的执行是随机的。但通过Object的wait方法和notifyAll方法或notify方法的使用就可以解决上面的不协调问题。

  wait方法的作用是:让一个线程休眠,让它不能在执行或是在一段时间后在执行。

  notify方法的作用是:唤醒一个线程,让线程恢复可以执行的状态。

  notifyAll方法作用是:唤醒同一个Object对象实现了wait方法的所有线程,让这些线程恢复可以执行的状态。

  经典的多线程通信:生产者与消费者问题上就可以使用wait方法和notifyAll方法或notify方法来解决,在生产者执行完操作后调用wait方法让其休眠同时调用otifyAll方法唤醒消费者让其可以执行操作,且在消费者执行完操作后也要调用wait方法让其休眠同时调用otifyAll方法唤醒生产者让其可以执行操作。

6、线程的六大状态

  • BLOCKED:线程的线程状态被阻塞等待监视器锁定。
  • NEW:尚未启动的线程的线程状态。
  • RUNNABLE:可运行线程的线程状态。
  • TERMINATED:终止线程的线程状态。
  • TIMED_WAITING:具有指定等待时间的等待线程的线程状态。
  • WAITING :等待线程的线程状态。

  六种状态的关系连接图:

  由最开始的new状态进入到到runnable状态,在runnable状态下可能会进入blocked状态、waiting状态或time_waiting状态,进入这三种状态的其中一种都是可以重新进入runnable状态的,最后当就只有runnable状态的情况下,runnable状态会转变为terminated状态,结束这个流程

7、线程池

7.1、概述

  线程池是一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。

  使用线程池的好处有以下几点:

  • 降低资源消耗
  • 提高响应速度
  • 提高线程的可管理性

  接下来就为大家讲解Java中常见的四种线程池类型。

  

7.2、缓存线程池

  第一种是缓存线程池,这种线程池的容量是无限的,可以理解成该线程池会自动扩容。该线程池的执行机制是:先判断该线程池中是否有空闲的线程,如果有就是用空闲的线程执行任务,如果不存在空闲的线程了,那就会自动创建一个新的线程,并放入该线程池中,然后再去使用新建的线程来执行任务。 

  创建缓存线程池的格式:

     ExecutorService service = newCachedThreadPool();

  这种创建方式是IDEA2019的解决方案给出来的格式,这种格式需要进行的导包代码为:

     import static java.util.concurrent.Executors.*;

  我实际的创建缓存线程池的格式会报错,然后IDEA给了我的解决方案就会变成上面的那种格式了,实际我编写的代码不是这样的,你们试试这种会不会报错,不报错就照下面的写,报错就写成上面的那种。代码如下:

         ExecutorService service = Executors.newCachedThreadPool();

  让该线程池执行任务需要线程池对象调用execute方法,并传入任务对象,代码展示:

      service.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"该线程执行");}});

  大家可能会很疑惑,怎么是这种格式。大家仔细看,其实我这种编写格式也是在execute方法中传入了新创建任务的方式,只不过我将这个任务类写成了匿名内部类的形式(在该类只用一次的情况下,可以将该类写成匿名内部类的形式),当然你们新建一个类实现Runnable接口,然后传入该类的对象的方法也是可以的。下面就是具体的代码实现和效果图了。

  以下是java代码的实现:

public class Text9 {public static void main(String[] args) {ExecutorService service = newCachedThreadPool();service.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"该线程执行");}});service.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"该线程执行");}});service.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"该线程执行");}});}
}

  以下是控制台的输出效果:

  可以看到三个任务由三个不同的线程执行,这就是缓存线程池的机制:在没有空闲线程时,线程池会自动创建新线程来执行任务。

7.3、定长线程池

  第二种介绍的是定长线程池,这种线程池的容量是固定的,不可以更改。该线程池的机制是:首先判断该线程池是否有空闲线程,如果有就是用空闲的线程执行任务,如果没有,要分两种情况:(1)该线程池的容量未满时,会自动创建新的线程来执行任务;(2)该线程池的容量已满时,传入的任务就需要排队等有空闲的线程时才会执行。

  创建缓存线程池的格式:

     ExecutorService service = newFixedThreadPool(2);

  当然,这种格式也是解决方案给的,我实际的创建格式代码是:

     ExecutorService service = Executors.newFixedThreadPool(2);

  这样就创建了一个容量为2的定长线程池了。让该线程池执行任务需要线程池对象调用execute方法,并传入任务对象,代码展示:

      service.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"该线程执行");}});

  下面就展示具体的代码和效果图吧。  

  以下是java代码的实现:

public class Text10 {public static void main(String[] args) {ExecutorService service = newFixedThreadPool(2);service.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"该线程执行");}});service.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"该线程执行");}});service.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"该线程执行");}});}
}

  以下是控制台的输出效果:

  大家可以看到效果图中有两个线程1和2,是不会和缓存线程池一样出现第三个线程3的,因为我只设置2容量的线程池,这就是定长线程池的效果了。大家有没有发现我的效果图是没有结束的,和上面的缓存线程池的效果图不一样,不要以为这是个bug,其实并不是,这是由于线程池的机制造成的,虽然任务已经结束,但线程池还会继续运行一段时间才会停止。你们也可以试一试,等一会它会自动结束的。

7.4、单线程线程池

  第三个要介绍的是单线程线程池,顾名思义就是只有一个线程的线程池。该线程池的机制就是:首先判断该线程池中的线程是否空闲,如果空闲就执行任务,如果不空闲将要等线程空闲了才能执行。

  创建缓存线程池的格式:

     ExecutorService service = newSingleThreadExecutor();

  当然,这种格式也是解决方案给的,我实际的创建格式代码是:

     ExecutorService service = Executors.newSingleThreadExecutor();

  让该线程池执行任务需要线程池对象调用execute方法,并传入任务对象,代码展示:

      service.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"该线程执行");}});

  下面就展示具体的代码和效果图吧。  

  以下是java代码的实现:

public class Text11 {public static void main(String[] args) {ExecutorService service = newSingleThreadExecutor();service.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"该线程执行");}});service.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"该线程执行");}});service.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"该线程执行");}});}
}

  以下是控制台的输出效果:

  由效果图可以看出,三个任务都是一个线程执行的,符合单线程线程池的机制。

  

7.5、周期定长线程池

  最后介绍的是周期定长线程池,这种线程池和定长线程池不一样的是:周期定长线程池可以设定任务为定时开始执行(和闹钟一个道理)。该线程池的机制和定长线程池一样:首先判断该线程池是否有空闲线程,如果有就是用空闲的线程执行任务,如果没有,要分两种情况:(1)该线程池的容量未满时,会自动创建新的线程来执行任务;(2)该线程池的容量已满时,传入的任务就需要排队等有空闲的线程时才会执行。

  创建缓存线程池的格式:

     ScheduledExecutorService service = newScheduledThreadPool(2);

  当然,这种格式也是解决方案给的,我实际的创建格式代码是:

     ScheduledExecutorService service = Executors.newScheduledThreadPool(2);

  让该线程池执行任务需要线程池对象调用schedule方法,并传入任务对象,时间和时间单位,代码展示:

     service.schedule(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"该线程执行");}},1, TimeUnit.SECONDS);

  schedule方法和上面的execute方式是有区别的:schedule方法传入的有三个参数:

  (1)任务对象;

  (2)时间长短(int类型参数);

  (3)时间的单位(是 TimeUnit类的字段)

TimeUnit类的字段:

  • DAYS 时间单位代表二十四小时。
  • HOURS 时间单位代表六十分钟。
  • MICROSECONDS 时间单位代表千分之一毫秒。
  • MILLISECONDS 时间单位代表千分之一秒。
  • MINUTES 时间单位代表六十秒。
  • NANOSECONDS 时间单位代表千分之一微秒。
  • SECONDS 时间单位代表一秒。

  下面就展示具体的代码和效果图吧。  

  以下是java代码的实现:

public class Text12 {public static void main(String[] args) {ScheduledExecutorService service = newScheduledThreadPool(2);service.schedule(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"该线程执行");}},1, TimeUnit.SECONDS);service.schedule(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"该线程执行");}},2, TimeUnit.SECONDS);service.schedule(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"该线程执行");}},3, TimeUnit.SECONDS);}
}

  以下是控制台的输出效果:

  该最后运行的效果图是和定长线程池是一个效果,不过在执行过程中的效果就和定长线程池不一样了,每个线程的执行是有时间间隔的,小伙伴可以自己尝试一下。

  关于多线程的知识我就解析到这了。如果小伙伴还想学习更多,请自行百度,谢谢大家的浏览,希望能帮到你们。

Java多线程技术解析相关推荐

  1. 利用java多线程技术和图像显示技术来完成动画设计。

    利用java多线程技术和图像显示技术来完成动画设计. package p2;import java.applet.Applet; import java.awt.Graphics; import ja ...

  2. Java多线程技术~生产者和消费者问题

    Java多线程技术~生产者和消费者问题 本文是上一篇文章的后续,详情点击该连接 线程通信 应用场景:生产者和消费者问题 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取 ...

  3. JAVA物联所需技术_基于JAVA多线程技术解决物联云端服务雪崩效应的方法与流程...

    本发明涉及互联网技术领域,特别涉及一种基于JAVA多线程技术解决物联云端服务雪崩效应的方法. 背景技术: 目前,物联云系统已经作为普遍的智能电视平台出现在我们面前,而细致分析物联云系统我们可以发现,当 ...

  4. java resume过时方法_学点开发|关于Java多线程用法解析

    在进行学习之前,我们先来了解下,什么是Java多线程: 多线程是实现并发机制的一种有效手段.进程和线程一样,都是实现并发的一个基本单位.为了让大家更清晰读懂关于Java多线程用法,由以下几点入手学,帮 ...

  5. Java多线程技术概述(知识点整理)

    文章目录 多线程技术概述 线程和进程 线程调度 同步与异步 并发与并行 两种创建方式 Thread Runnable 线程常用方法 getName()与setName() sleep() 线程阻塞 线 ...

  6. java多线程全面解析

    文章目录 java多线程 1.线程简介 2.线程实现(三种) 继承Thread类(重点) 步骤: 示例代码: 多线程下载图片: 实现Runnable接口(重点) 步骤: 基础代码: 体现Runnabl ...

  7. Java多线程技术-Volatile关键字解析

    分析volatile关键字可以从这三个方面分析,什么是程序的原子性,什么是程序的可见性,什么是程序的有序性 什么是程序的原子性 以下语句那些是原子操作? public class ThreadCoun ...

  8. JAVA多线程技术-IO密集型与CPU密集型

    任务最终是反应到计算机来执行的,线程在执行过程中和计算机的硬件是有关联的,他们有什么关系呢?这里介绍下线程与IO和CPU的关系. CPU密集型 CPU密集型也叫计算密集型,指的是系统的硬盘.内存性能相 ...

  9. JAVA多线程技术-线程的生命周期

    当线程被创建并启动后,它既不是一启动就进入了执行状态,也不是一直处于执行状态,在线程的生命周期中,它要经过新建(New).就绪(Ready).运行(Running).阻塞(Blocked).和死亡(D ...

最新文章

  1. 了解EOS看这一篇就够了一、团队二、技术三、项目进度四、争议和风险五、展望
  2. lopa分析_HAZOP : 保护层分析之失效使能分析导则
  3. 崛起吧,亲爱的,该背单词了!!!
  4. Android Status(状态栏) 着色
  5. 前端学习(1603):脚手架组件使用
  6. logisim优先编码器怎么用_变频电机为什么要用编码器?又该如何选型?
  7. 干货:用Python玩转数据可视化,炫酷图表是这样做出来的
  8. 异常检测3——AutoEncoder异常检测
  9. rtmp服务器 协议之同步
  10. 在Spring+Hibernate项目中使用原生SQL进行查询和执行SQL处理
  11. 伺服电机转矩常数的标定方法
  12. Limelight完成了对雅虎Edgecast的收购,合并后的公司更名为Edgio,成为全球边缘解决方案的领导者...
  13. ML:图像数据、字符串数据等计算相似度常用的十种方法(余弦相似性、皮尔逊、闵可夫斯基距离/曼哈顿距离/欧氏距离/切比雪夫距离、马氏距离、汉明距离、编辑距离、杰卡德相似系数、相对熵/KL散度、Helli
  14. GITHUB无法打开与下载失败解决方法总结
  15. linux命令在线练习,随手练习Linux命令
  16. 微信团队分享:微信支付代码重构带来的移动端软件架构上的思考
  17. 修改Oracle序列
  18. 高学历就意味着高薪资?低学历转行3D建模,游戏建模成为首选
  19. Golang开发新手常犯的50个错误
  20. 荣耀正式更换了Logo,从此再也没有‘华为荣耀’之说

热门文章

  1. plc通讯的握手信号_PLC通讯及网络技术
  2. IBM 用机器学习寻找外星人,不用再望穿银河秋水
  3. 艾永亮超级产品:正式开工,企业该如何做好一份完整的产品规划
  4. 苹果电脑更改sd卡只读_sd卡反复变成只读解决办法
  5. Java B组蓝桥杯第十届国赛:最优旅行
  6. Java基于JSP的幼儿园管理系统
  7. 华为机试python编程题_牛客网华为机试题之Python解法
  8. 银河麒麟V10虚拟机里用virtualbox安装虚拟机
  9. Pandas数据分析—groupby分组统计
  10. net framework 4.0 4.6.1