抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

简介

重要性及开发历史

在Java语言里面最大的特点是支持多线程的开发(也是为数不多支持多线程的编程语言),所以在整个的java技术的学习里面,如果你不能够对多线程的概念有一个全面并且细致的了解,则在日后进行些项日设计的过程之中尤其是并发访问设计的过程之中就会出现严重的技术缺漏。

如果要想理解线程,那么首先就需要了解一下进程的概念,在传统的DOS系统的时代,其本身有一个特征:如果你电脑上出现了病毒,那么所有的程序将无法执行,因为传统的DOS采用的是单进程处理,而单进程处理的最大特点:在同一个时间段上只允许一个程序在执行

那么后来到了 Windows时代就开启了多进程的设计,于是就表示在一个时间段上可以同时运行多个程序,并且这些程序将进行资源的轮流抢占,所以在同一个时间段上会有多个程序依次执行,但是在同一个时间点上只会有一个进程执行,而后来到了多核的CPU,由于可以处理的CPU多了,那么即便有再多的进程出现,也可以比单核CPU处理的速度有所提升。

线程是在进程基础之上划分的更小的程序单元,线程是在进程基础上创建并且使用的,所以线程依赖进程的支持,但是线程的启动速度要比进程快许多,所以当使用多线程进行并发处理的时候其执行的性能要高于进程。

Java是多线程的编程语言,所以Java在进行并发访问处理时可以得到更高的性能。

如果要想在Java之中实现多线程的定义,那么就需要有一个专门的线程主体类进行线程的执行任务的定义,而这个主体类的 定义是有要求的,必须实现特定的接口或者继承特定的父类オ可以完成。

作用

多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。

这里定义和线程相关的另一个术语 - 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。

多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。

创建一个线程

Java 提供了三种创建线程的方法:

  • 通过 Runnable 接口实现;
  • 通过继承 Thread 类本身;
  • 通过 Callable 和 Future 创建线程。

通过继承 Thread 类本身

创建线程的主体类

Java里面提供有一个java.lang.Thread的程序类,那么一个类只要继承了此类就表示这个类为线程的主体类;但是并不是说这个类就可以直接实现多线程处理了,因为还需要覆写 Thread类中提供的一个 run()public void run())方法,而这个方法就属于线程的主方法。

/ThreadDemo.java
class Mythread extends Thread { //线程的主体类 private String title; public Mythread(String title) { this.title = title; } @Override public void run() { //线程的主体方法 //IntStream.range(0, 19).mapToObj(x -> this.title + "运行,x=" + x).forEach(System.out::println); for (int x = 0; x < 19; x++) { System.out.println(this.title + "运行,x=" + x); } } }

调用方法

多线程要执行的功能都应该在run()方法中进行定义。需要说明的是:在正常情况下如果要想使用一个类中的方法,那么肯定要产生实例化对象,而后去调用类中提供的方法,但是run()方法是不能够被直接调用的,因为这里面牵扯到一个操作系统的资源调度问题,所以要想启动多线程必须使用 start()方法完成(public void start())。

/ThreadDemo.java
public class ThreadDemo { public static void main(String[] args) { - new MyThread("线程A").run(); - new MyThread("线程B").run(); - new MyThread("线程C").run(); + new MyThread("线程A").start(); + new MyThread("线程B").start(); + new MyThread("线程C").start(); } }
为什么多线程必须使用 Thread类中的star()方法启动

通过以上的调用你可以发现,虽然调用了是 start()方法,但是最终执行的是run()方法,并且所有的线程对象都是交替执行的。

为什么多线程的启动不直接使用run()方法而必须使用 Thread类中的star()方法呢?

如果要想清楚这个问题,可以直接观察源代码。

/SourceCode
public synchronized void start() { if (threadStatus != 0) //判断线程的状态。 throw new IllegalThreadStateException(); //抛出了一个异常 group.add(this); boolean started = false; try { start0(); //在start()方法里面调用了 start0()方法 started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { } } } private native void start0(); //只定义了方法名称,但是没有实现

发现在start()方法里面会抛出一个"Illegal Thread State Exception"异常类对象,但是整个的程序并没有使用 throws或者是明确的try..catch处理,因为该异常一定是 RuntimeException的子类,每一个线程类的对象只允许启动一次,如如果重复启动则就抛出此异常。

例如:下面的代码就会抛出异常。

/ThreadDemo.java
public class ThreadDemo { public static void main(String[] args) { MyThread mt = new MyThread("线程A"); mt.start(); mt.start(); } }
异常
Exception in thread "main" java.lang.IllegalThreadStateExcepton

在Java程序执行的过程之中考虑到对于不同层次开发者的需求,所以其支持有本地的操作系统函数调用,而这项技术就被称为JNI( Java Native Inteface)技术,但是Java开发过程之中并不推荐这样使用,利用这项技术可以使用一些操作系统提供的底层函数进行一些特殊的处理,而在Thread类里面提供的 start00就表示需要将此方法依赖于不同的操作系统实现。

任何情况下,只要定义了多线程,多线程的启动永远只有一种方案: Thread类中的 start方法

✦通过 Runnable 接口实现

虽然可以通过Thread类的继承来实现多线程的定义,但是在Java程序里面对于继承永远都是存在有单继承局限的,所以在Java里面又提供有第二种多线程的主体定义结构形式:实现 java.lang.Runnable接口,此接口定义如下:

/SourceCode
@FunctionalInterface //从JDK1.8引入了Lambda表达式之后就变为了函数式接口 public interface Runnable { public void run(); }

创建线程的主体类

范例:通过 Runnable实现多线程的主体类

/ThreadDemo.java
class MyThread implements Runnable { //线程的主体类 private String title; public MyThread(String title) { this.title = title; } @Override public void run() { //线程的主体方法 //IntStream.range(0, 6).mapToObj(x -> this.title + "运行,x=" + x).forEach(System.out::println); for (int x = 0; x < 6; x++) { System.out.println(this.title + "运行,x=" + x); } } }

但是此时由于不再继承Thread父类了,那么对于此时的MyThread类中也就不再支持有start()这个继承的方法,可是如果不使用Thread.start()方法是无法进行多线程启动的,那么这个时候需要观察Thread类所提供的构造方法:

  • 构造方法:public Thread(Runnable target);

启动多线程

范例:启动多线程

/ThreadDemo.java
public class ThreadDemo { public static void main(String[] args) { Thread threadA = new Thread(new MyThread("线程A")); Thread threadB = new Thread(new MyThread("线程B")); Thread threadC = new Thread(new MyThread("线程C")); threadA.start(); threadB.start(); threadC.start(); } }

这个时候的多线程实现里面可以发现,由于只是实现了 Runnable接口对象,所以此时线程主体类上就不再有单继承局限了,那么这样的设计才是一个标准型的设计。

可以发现从JDK1.8开始, Runnable接口使用了函数式接口定义,所以也可以直接利用 Lambda表达式进行线程类实现。

范例:利用 Lambda实现多线程定义

/ThreadDemo.java
public class ThreadDemo { public static void main(String[] args) { for (int x = 0; x < 2; x++) { String title = "线程对象_" + x; Runnable run = () -> { for (int y = 0; y < 6; y++) System.out.println(title + "运行,y=" + y); }; new Thread(run).start(); } } }

改进版:

/ThreadDemo.java
public class ThreadDemo { public static void main(String[] args) { for (int x = 0; x < 2; x++) { String title = "线程对象_" + x; new Thread(() -> { for (int y = 0; y < 6; y++) System.out.println(title + "运行,y=" + y); }).start(); } } }

在以后的开发之中对于多线程的实现,优先考虑的就是Runnable接口实现,并且永恒都是通过Thread类对象启动多线程。

✦Thread与 Runnable关系

经过一系列的分析之后可以发现,在多线程的实现过程之中已经有了两种做法:Thread类、Runnable接口,如果从代码的结构本身来讲肯定使用Runnable是最方便的,因为其可以避免单继承的局限,同时也可以更好的进行功能的扩充。

但是从结构上也需要来观察ThreadRunnable的联系,Thread类的定义:

/SourceCode
public class Thread extends Object implements Runnable {}

发现现在Thread类也是Runnable接口的子类,那么在之前继承 Thread类的时候实际上覆写的还是Runnable接口的run()方法,于是此时来观察一下程序的类结构。

/ThreadDemo.java
import java.util.stream.IntStream; class MyThread implements Runnable { //线程的主体类 private String title; public MyThread(String title) { this.title = title; } @Override public void run() { //线程的主体方法 //IntStream.range(0, 6).mapToObj(x -> this.title + "运行,x=" + x).forEach(System.out::println); for (int x = 0; x < 6; x++) { System.out.println(this.title + "运行,x=" + x); } } } public class ThreadDemo { public static void main(String[] args) { Thread threadA = new Thread(new MyThread("线程A")); Thread threadB = new Thread(new MyThread("线程B")); Thread threadC = new Thread(new MyThread("线程C")); threadA.start(); threadB.start(); threadC.start(); } }

多线程的设计之中,使用了代理设计模式的结构,用户自定义的线程主体只是负责项目核心功能的实现,而所有的辅助实现全部交由 Thread类来处理。

在进行 Thread启动多线程的时候调用的是start()方法,而后找到的是run()方法,但通过 Thread类的构造方法传递了一Runnable接口对象的时候,那么该接口对象将被Thread类中的 target()属性所保存,在start()方法执行的时候会调用Thread类中的run()方法,而这个run()方法去调用Runnable接口子类被覆写过的run()方法。

多线程开发的本质实质上是在于多个线程可以进行同一资源的抢占,那么Thread主要描述的是线程,而资源的描述是通过Runnable完成的。

范例:利用卖票程序来实现多个线程的资源并发访问

/ThreadDemo.java
import java.util.stream.IntStream; class MyThread implements Runnable { //线程的主体类 private int ticket = 5; @Override public void run() { //线程的主体方法 //IntStream.range(0, 6).mapToObj(x -> this.title + "运行,x=" + x).forEach(System.out::println); for (int x = 0; x < 100; x++) { if (this.ticket > 0) { System.out.println("买票,ticket = " + this.ticket--); } } } } public class ThreadDemo { public static void main(String[] args) { MyThread mt = new MyThread(); new Thread(mt).start(); //第一个线程启动 new Thread(mt).start(); //第二个线程启动 new Thread(mt).start(); //第三个线程启动 } }

通过内存分析图来分析本程序的执行结构:

✧通过Callable接口实现

从最传统的开发来讲如果要进行多线程的实现肯定依靠的就是Runnable,但是Runnable接口有一个缺点:当线程执行完毕之后无法获取一个返回值,所以从JDK1.5之后就提出了个新的线程实现接口:javautil.concurrent.Callable接口,首先来观察这个接口的定义:

SourceCode
@Functionalinterface public interface Callable<V> { public V call() throws Exception; }

可以发现Callable定义的时候可以设置一个泛型,此泛型的类型就是返回数据的类型,这样的好处是可以避免向下转型所带来的安全隐患。

创建线程的主体类

范例:使用Callable实现多线程处理

/ThreadDemo.java
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; class MyThread implements Callable<String> { //线程的主体类 @Override public String call() throws Exception { for (int x = 0; x < 10; x++) { System.out.println("_____ 线程执行 __ x = " + x + "_____"); } return "线程执行完毕。"; } } public class ThreadDemo { public static void main(String[] args) throws Exception { FutureTask<String> task = new FutureTask<>(new MyThread()); new Thread(task).start(); System.out.println("【线程返回数据】" + task.get()); } }

面试题:请解释RunnableCallable的区别?

  • Runnable是在JDK1.0的时候提出的多线程的实现接口,而Callable是在JDK1.5之后提出的
  • java.lang.Runnable接口之中只提供有一个run()方法,并且没有返回值
  • java.util.concurrent.Callable接口提供有call()方法,可以有返回值

线程运行状态

对于多线程的开发而言,编写程序的过程之中总是按照:定义线程主体类,而后通过Thread类进行线程的启动,但是并不意味着你调用了start()方法,线程就已经开始运行了,因为整体的线程处理有自己的一套运行的状态

  • 任何一个线程的对象都应该使用Thread类进行封装,所以线程的启动使用的是start(),但是启动的时候实际上若干个线程都将进入到一种就绪状态,现在并没有执行;
  • 进入到就绪状态之后就需要等待进行资源调度,当某一个线程调度成功之后则进入到运行状态(run()方法),但是所有的线程不可能一致持续执行下去,中间需要产生一些暂停的状态,例如:某个线程执行一段时间之后就需要让出资源,而后这个线程就将进入到阻塞状态,随后重新回归到就绪状态;
  • run()方法执行完毕之后,实际上该线程的主要任务也就结東了,那么此时就可以直接进入到停止状态;

线程常用操作方法

多线程的主要操作方法都在 Thread类中定义了。

✦线程的命名和取得

多线程的运行状态是不确定的,那么在程序的开发之中为了可以获取到一些需要使用到线程就只能够依靠线程的名字来进行操作。所以线程的名字是一个至关重要的概念,这样在 Thread类之中就提供有线程名称的处理:

  • 构造方法:public Thread(Runnable target, String name);
  • 设置名字:public final String setName();
  • 取得名字:public final String getName();

对于线程对象的获得是不可能只是依靠一个this来完成的,因为线程的状态不可控,但是有一点是明确的,所有的线程对象定要执行run()方法,那么这个时候可以考虑获取当前线程,在Thread类里面提供有获取当前线程的方法:

  • 获取当前线程:public static Thread currentThread()

范例:观察线程的命名操作

/ThreadDemo.java
class MyThread implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName()); } } public class ThreadDemo { public static void main(String[] args) throws Exception { MyThread mt = new MyThread(); new Thread(mt, "线程A").start(); //设置了线程名字 new Thread(mt).start(); //未设置 new Thread(mt).start(); //未设置 new Thread(mt).start(); //未设置 new Thread(mt, "线程B").start(); //设置了线程名字 } }

当开发者为线程设置名字的时候就使用设置的名字,而如果没有设置名字,则会自动生成一个不重复的名字,这种自动的属性命名主要是依靠了 static属性完成的,在 Thread类里面定义有如下操作:

SourceCode
private static int threadInitNumber; private static synchronized int nextThreadNum() { return threadInitNumber++; }

范例:观察一个程序

/ThreadDemo.java
class MyThread implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName()); } } public class ThreadDemo { public static void main(String[] args) throws Exception { MyThread mt = new MyThread(); new Thread(mt, "线程对象").start(); //设置了线程名字 mt.run(); //对象直接调用run()方法 } }

通过此时的代码可以发现当使用了mt.run()直接在主方法之中调用线程类对象中的run()方法所获得的线程对象的名字为“main”,所以可以得出一个结论:主方法也是一个线程。

所有的线程都是在进程上的划分,那么进程在哪里?

  • 每当使用java命令执行程序的时候就表示启动了一个JVM的进程,一台电脑可以同时启动若干个JVM进程,所以每一个JVM进程都会有各自的线程。

在任何的开发之中,主线程可以创建若干个子线程,创建子线程的目的是可以将一些复杂逻辑或者比较耗时的逻辑交由子线程处理;

范例:子线程处理

/ThreadDemo.java
public class ThreadDemo { public static void main(String[] args) throws Exception { System.out.println("执行操作任务1"); System.out.println("执行操作任务2"); - int temp = 0; - for (int x = 0; x < Integer.MAX_VALUE; x++) { - temp += x; - } + new Thread(() -> { + int temp = 0; + for (int x = 0; x < Integer.MAX_VALUE; x++) { + temp += x; + } }); System.out.println("执行操作任务3"); System.out.println("执行操作任务4"); } }

主线程负责处理整体流程,而子线程负责处理耗时操作。

✦线程休眠

如果说现在希望某一个线程可以暂缓执行一次,那么就可以使用休眠的处理,在 Thread类之中定义的休眠方法如下:

  • 休眠:public static void sleep(long millis) throws InterruptedException
  • 休眠:public static void sleep(long millis, int nanos) throws InterruptedException

在进行休眠的时候有可能会产生中断异常InterruptedException,中断异常属于Exception的子类,所以证明该异常必须进行处理。

范例:观察休眠处理

/ThreadDemo.java
public class ThreadDemo { public static void main(String[] args) { new Thread(() -> { for (int x = 0; x < 10; x++) { try { Thread.sleep(100); //暂缓执行 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " _ x = " + x); } }, "线程对象").start(); } }

休眠的主要特点是可以自动实现线程的唤醒,以继续进行后续的处理。但是需要注意的是,如果现在有多个线程对象,那么休眠也是有先后顺序的。

范例:产生多个线程对象进行休眠处理

/ThreadDemo.java
public class ThreadDemo { public static void main(String[] args) { for (int m = 0; m < 5; m++) { new Thread(() -> { for (int x = 0; x < 10; x++) { try { Thread.sleep(100); //暂缓执行 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " _ x = " + x); } }, "线程对象 - " + m).start(); } } }

代码清晰化

/ThreadDemo.java
public class ThreadDemo { public static void main(String[] args) { Runnable run = () -> { for (int x = 0; x < 10; x++) { try { Thread.sleep(100); //暂缓执行 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " _ x = " + x); } }; for (int m = 0; m < 5; m++) { new Thread(run, "线程对象 - " + m).start(); } } }

此时将产生五个线程对象,并且这五个线程对象执行的方法体是相同的。

线程中断

在之前发现线程的休眠里面提供有一个中断异常,实际上就证明线程的休眠是可以被打断的,而这种打断肯定是由其它线程完成的在 Thread类里面提供有这种中断执行的处理方法:

  • 判断线程是否被中断:public boolean isInterrupted()
  • 中断线程执行:public void interrupt()

范例:观察线程的中断处理操作

/ThreadDemo.java
public class ThreadDemo { public static void main(String[] args) { Thread thread = new Thread(()->{ System.out.println("*** 72个小时的疯狂我需要睡觉补充精力 ***"); try { Thread.sleep(10000); //预计准备休眠10秒 System.out.println("*** 睡足了,可以出去继续祸害别人了 ***"); } catch (InterruptedException e) { System.out.println("*** 敢打扰我睡觉,老子宰了你 ***"); } }); thread.start(); //开始睡觉 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } if (!thread.isInterrupted()) { //该线程中断了吗? System.out.println("*** 我偷偷打扰一下你的睡眠 ***"); thread.interrupt(); //中断执行 } } }

所有正在执行的线程都是可以被中断的,中断线程必须进行异常的处理。

线程强制运行

所谓的线程的强制执行指的是当满足于某些条件之后,某一个线程对象将可以一直独占资源,直到该线程的程序执行结束

范例:观察一个没有强制执行的程序

/ThreadDemo.java
public class ThreadDemo { public static void main(String[] args) { Thread thread = new Thread(() -> { for (int i = 0; i < 101; i++) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "执行 __ number = " + i); } }, "玩耍的线程"); thread.start(); for (int i = 0; i < 101; i++) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("【霸道的main线程】number = " + i); } } }

这个时候主线程和子线程都在交替执行着,但是如果说现在你希望主线程独占执行。那么就可以利用 Thread类中的方法:

  • 强制执行: public final void join() throws InterruptedException;
/ThreadDemo.java
public class ThreadDemo { public static void main(String[] args) { Thread mainThread = Thread.currentThread(); //获得主线程 Thread thread = new Thread(() -> { for (int i = 0; i < 101; i++) { if (i == 3) { //【霸道的线程】来了 try { mainThread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "执行 __ number = " + i); } }, "玩耍的线程"); thread.start(); for (int i = 0; i < 101; i++) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("【霸道的main线程】number = " + i); } } }

在进行线程强制执行的时候一定要获取强制执行线程对象之后才可以执行join()调用。

线程礼让

线程的礼让指的是先将资源让出去让别的线程先执行。线程的礼让可以使用Thread类中提供的方法:

  • 礼让:public static void yield()

范例:使用礼让操作

/ThreadDemo.java
public class ThreadDemo { public static void main(String[] args) { Thread mainThread = Thread.currentThread(); //获得主线程 Thread thread = new Thread(() -> { for (int i = 0; i < 11; i++) { if (i % 2 == 0) { //线程礼让 Thread.yield(); System.out.println('\n' + "*** 线程礼让执行 ***"); } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "执行 __ number = " + i); } }, "线程"); thread.start(); for (int i = 0; i < 11; i++) { if (i % 2 != 0) { //线程礼让 Thread.yield(); System.out.println('\n' + "*** 【main线程】礼让执行 ***"); } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("【main线程】number = " + i); } } }

礼让执行的时候每一次调用yield()方法都只会礼让一次当前的资源。

✦线程优先级

从理论上来讲线程的优先级越高越有可能先执行(越有可能先抢占到资源)。在 Thread类里面针对于优先级的操作提供有如下的两个处理方法:

  • 设置优先级: public final void setPriority(int newPriority)
  • 获取优先级: public final int getPriority()

在进行优先级定义的时候都是通过int型的数字来完成的,而对于此数字的选择在Thread类里面就定义有三个常量:

  • 最高优先级(10):public static final int MAX_PRIORITY;
  • 中等优先级(5):public static final int NORM_PRIORITY;
  • 最低优先级(1):public static final int MIN_PRIORITY;

范例:优先级

/ThreadDemo.java
public class ThreadDemo { public static void main(String[] args) { Runnable run = (() -> { for (int i = 0; i < 10; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "执行"); } }); Thread threadA = new Thread(run, "线程对象A"); Thread threadB = new Thread(run, "线程对象B"); Thread threadC = new Thread(run, "线程对象C"); threadA.setPriority(Thread.MIN_PRIORITY); threadB.setPriority(Thread.MIN_PRIORITY); threadC.setPriority(Thread.MAX_PRIORITY); threadA.start(); threadB.start(); threadC.start(); } }

主方法和默认线程也是一个主线程,那么他们的优先级呢?

/ThreadDemo.java
public class ThreadDemo { public static void main(String[] args) { System.out.println(new Thread().getPriority()); System.out.println(Thread.currentThread().getPriority()); } }

主线程是属于中等优先级,而默认创建的线程也是中等优先级。

线程的同步与死锁

在多线程的处理之中,可以利用Runnable描述多个线程操作的资源,而Thread描述每一个线程对象,于是当多个线程访问同一资源的时候如果处理不当就会产生数据的错误操作。

线程同步

下面编写一个简单的卖票程序,将创建若干个线程对象实现卖票的处理操作

范例:实现卖票操作(问题引出)

/ThreadDemo.java
class MyThread implements Runnable { private int ticket = 10; @Override public void run() { while (true) { if (this.ticket > 0) { System.out.println(Thread.currentThread().getName() + "买票, ticket = " + this.ticket--); } else { System.out.println("**** 票已抢光 ****"); break; } } } } public class ThreadDemo { public static void main(String[] args) { MyThread mt = new MyThread(); new Thread(mt, "票贩子A").start(); new Thread(mt, "票贩子B").start(); new Thread(mt, "票贩子C").start(); } }

此时的程序将创建三个线程对象,并且这三个线程对象将进行5张票的出售。此时的程序在进行卖票处理的时候并没有任何的问题(假象),下面可以模拟一下卖票中的延迟操作。

/ThreadDemo.java
class MyThread implements Runnable { private int ticket = 10; @Override public void run() { while (true) { if (this.ticket > 0) { try { Thread.sleep(100); //模拟网络延迟 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "买票, ticket = " + this.ticket--); } else { System.out.println("**** 票已抢光 ****"); break; } } } } public class ThreadDemo { public static void main(String[] args) { MyThread mt = new MyThread(); new Thread(mt, "票贩子A").start(); new Thread(mt, "票贩子B").start(); new Thread(mt, "票贩子C").start(); } }

经过分析之后己经可以确认同步问题所产生的主要原因了,那么下面就需要进行同步问题的解决,但是解决同步问题的关键是锁,指的是当某一个线程执行操作的时候,其它线程外面等待。

如果要想在程序之中实现这把锁的功能,就可以使用synchronized关键字米实现,利用此关键字可以定义同步方法或同步代码块,在同步代码块的操作里面的代码只允许一个线程执行。

利用同步代码块进行处理:

Logic
synchronized(同步对象) { 同步代码操作 }

一般要进行同步对象处理的时候可以采用当前对象this进行同步。

范例:利用同步代码块解决数据同步访问问题

/ThreadDemo.java
class MyThread implements Runnable { private int ticket = 10; @Override public void run() { while (true) { synchronized (this) { if (this.ticket > 0) { try { Thread.sleep(100); //模拟网络延迟 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "买票, ticket = " + this.ticket--); } else { System.out.println("**** 票已抢光 ****"); break; } } } } } public class ThreadDemo { public static void main(String[] args) { MyThread mt = new MyThread(); new Thread(mt, "票贩子A").start(); new Thread(mt, "票贩子B").start(); new Thread(mt, "票贩子C").start(); } }

加入同步处理之后,程序的整体的性能下降了。同步实际上会造成性能的降低。

✦利用同步方法解决

只需要在方法定义上使用synchronized关键字即可。

/ThreadDemo.java
class MyThread implements Runnable { private int ticket = 10; public synchronized boolean sale() { if (this.ticket > 0) { try { Thread.sleep(100); //模拟网络延迟 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "买票, ticket = " + this.ticket--); return true; } else { System.out.println("**** 票已抢光 ****"); return false; } } @Override public void run() { while (this.sale()) { } } } public class ThreadDemo { public static void main(String[] args) { MyThread mt = new MyThread(); new Thread(mt, "票贩子A").start(); new Thread(mt, "票贩子B").start(); new Thread(mt, "票贩子C").start(); } }

在日后学习Java类库的时候会发现,系统中许多的类上使用的同步处理采用的都是同步方法。

线程死锁

f1aa845dd21c478d993296a593549474_tplv-k3u1fbpfcp-zoom-1

死锁是在进行多线程同步的处理之中有可能产生的一种问题,所谓的死锁指的是若干个线程彼此互相等待的状态。下面通过一个简单的代码来观察一下死锁的表现形式,但是对于此代码不作为重点。

/ThreadDemo.java
class Person1 { public synchronized void say(Person2 Per2) { System.out.println("交钱!"); Per2.get(); } public synchronized void get() { System.out.println("好的"); } } class Person2 { public synchronized void say(Person1 Per1) { System.out.println("不给!!"); Per1.get(); } public synchronized void get() { System.out.println("给你"); } } public class ThreadDemo implements Runnable { private Person1 Per1 = new Person1(); private Person2 Per2 = new Person2(); public ThreadDemo() { new Thread(this).start(); Per1.say(Per2); } public static void main(String[] args) { new ThreadDemo(); } @Override public void run() { Per2.say(Per1); } }

现在死锁造成的主要原因是因为彼此都在互相等待着,等待着对方先让出资源。死锁实际上是一种开发中出现的不确定的状态,有的时候代码如果处理不当则会不定期出现死锁,这是属于正常开发中的调试问题。

若干个线程访问同一资源时一定要进行同步处理,而过多的同步会造成死锁。

实例

生产者与消费者基本程序模型

在多线程的开发过程之中最为著名的案例就是生产者与消费者操作,该操作的主要流程如下

  • 生产者负责信息内容的生产
  • 每当生产者生产完成一项完整的信息之后消费者要从这里面取走信息
  • 如果生产者没有生产者则消费者要等待它生产完成,如果消费者还没有对信息进行消费,则生产者应该等待消费处理完成后再继续生产。

程序基本实现:

可以将生产者与消费者定义为两个独立的线程类对象。

  • 数据一: title = 王建、content = 宇宙大帅哥
  • 数据二: title = 小高、content = 猥琐第一人

既然生产者与消费者是两个独立的线程,那么这两个独立的线程之间就需要有一个数据的保存集中点,那么可以定义一个Messga类实现数据的保存。

/ThreadDemo.java
class Producer implements Runnable { private Message msg; public Producer(Message msg) { this.msg = msg; } @Override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 == 0) { this.msg.setTitle("Person1"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } this.msg.setContent("111"); } else { this.msg.setTitle("Person2"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } this.msg.setContent("222"); } } } } class Message { private String title; private String content; public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } } class Consumer implements Runnable { private Message msg; public Consumer(Message msg) { this.msg = msg; } @Override public void run() { for (int i = 0; i < 100; i++) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this.msg.getTitle() + " - " + this.msg.getContent()); } } } public class ThreadDemo { public static void main(String[] args) { Message msg = new Message(); new Thread(new Producer(msg)).start(); new Thread(new Consumer(msg)).start(); } }

通过整个代码的执行你会发现此时有两个主要问题:

  • 问题一:数据不同步了
  • 问题二:生产一个取走一个,但是发现有了重复生产和重复取出问题

解决同步问题

如果要解决问题,首先解決的就是数据同步的处理问题,如果要想解决数据同步最简单的做法是使用synchronized关键字定义同步代码块或同步方法,于是这个时候对于同步的处理就可以直接在Message类中完成。

/ThreadDemo.java
class Producer implements Runnable { private Message msg; public Producer(Message msg) { this.msg = msg; } @Override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 == 0) { this.msg.set("Person1", "111"); } else { this.msg.set("Person2", "222"); } } } } class Message { private String title; private String content; public synchronized void set(String title, String content) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } this.title = title; this.content = content; } public synchronized String get() { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } return this.title + " - " + this.content; } } class Consumer implements Runnable { private Message msg; public Consumer(Message msg) { this.msg = msg; } @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(this.msg.get()); } } } public class ThreadDemo { public static void main(String[] args) { Message msg = new Message(); new Thread(new Producer(msg)).start(); //启动生产者线程 new Thread(new Consumer(msg)).start(); //启动消费者线程 } }

在进行同步处理的时候肯定需要有一个同步的处理对象,那么此时肯定要将同步操作交由Message类处理是最合适的。这个时候发现数据已经可以正常的保持一致了,但是对于重复操作的问题依然存在。

✦利用Object类解决重复操作

如果说现在要想解决生产者与消费者的问题,那么最好的解决方案就是使用等待与唤醒机制。

主要依靠的是 Object类中提供的方法处理的:

  • 等待机制:
    • 死等:public final void wait() throws InterruptedException
    • 设置等待时间:public final void wait(long timeout) throws InterruptedException
    • 设置等待时间:public final void wait(long timeout, int nanos) throws InterruptedException
  • 唤醒第一个等待线程:public final void notify()
  • 唤醒全部等待线程:public final void notifyAll()

如果此时有若干个等待线程的话,那么notify()表示的是唤醒第一个等待的线程,而其它的线程继续等待,,而notifyAll()表示会唤醒所有等待的线程,那个线程的优先级高就有可能先执行。

对于当前的问题主要的解決应该通过Message类完成处理。

范例:修改Message

/ThreadDemo.java
class Producer implements Runnable { private Message msg; public Producer(Message msg) { this.msg = msg; } @Override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 == 0) { this.msg.set("Person1", "111"); } else { this.msg.set("Person2", "222"); } } } } class Message { private String title; private String content; + private boolean flag = true; //表示生产或消费的形式 + /* + flag=true,允许生产,但是不允许消费 + flag= false,允许消费,不允许生产 + */ public synchronized void set(String title, String content) { + if (!this.flag) { //无法进行生产,应该等待被消费 + try { + super.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } this.title = title; this.content = content; + this.flag = false; //已经生产过了 + super.notify(); //唤酲等待的线程 } public synchronized String get() { + if (this.flag) { //还未生产,需要等待 + try { + super.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } + try { return this.title + " - " + this.content; + } finally { + this.flag = true; + super.notify(); + } } } class Consumer implements Runnable { private Message msg; public Consumer(Message msg) { this.msg = msg; } @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(this.msg.get()); } } } public class ThreadDemo { public static void main(String[] args) { Message msg = new Message(); new Thread(new Producer(msg)).start(); //启动生产者线程 new Thread(new Consumer(msg)).start(); //启动消费者线程 } }

这种处理形式就是在进行多线程开发过程之中最原始的处理方案,整个的等待、同步、唤醒机制都由开发者自行通过原生代码实现控制。

✦多线程深入

优雅的停止线程

在多线程操作之中如果要启动多线程肯定使用的是Thread类中的 start()方法,而如果对于多线程需要进行停止处理,Thread类原本提供有stop()方法,但是对于这些方法从JDK1.2版本开始就已经将其废除了,而且一直到现在也不再建议出现在你的代码之中,而除了 stop()之外还有儿个方法也被禁用了

  • 停止多线程: public void stop()

  • 销毁多线程: public void destroy()

  • 挂起线程: public final void suspend()、暂停执行;

  • 恢复挂起的线程执行: public final void resume()

之所以废除掉这些方法,主要的原因是因为这些方法有可能导致线程的死锁,所以从JDK1.2开始就都不建议使用了,如果这个时候要想实现线程的停止需要通过一种柔和的方式来进行。

范例:实现线程柔和的停止

/ThreadDemo.java
public class ThreadDemo { public static boolean flag = true; public static void main(String[] args) { new Thread(() -> { long num = 0; while (flag) { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在运行!_ num = " + num++); } }, "执行线程").start(); try { Thread.sleep(200); //运行200毫秒 } catch (InterruptedException e) { e.printStackTrace(); } flag = false; } }

万一现在有其它的线程去控制这个flag的内容,那么这个时候对于线程的停止也不是说停就立刻停止的,而是会在执行中判断flag的内容来判断。

后台守护线程

现在假设有一个人并且这个人有一个保镖,那么这个保镖一定是在这个人活着的时候进行守护,如果这个人已经死了,保镖就没用,所以在多线程里面可以进行守护线程的定义,也就是说如果现在主线程的程序或者其它的线程还在执行的时候,那么守护线程将一直存在,并且运行在后台状态。

在 Thread类里面提供有如下的守护线程的操作方法:

  • 设置为守护线程:public final void setDaemon(boolean on)
  • 判断是否为守护线程:public final boolean isDaemon()

范例:使用守护线程。

/ThreadDemo.java
public class ThreadDemo { public static void main(String[] args) { Thread userThread = new Thread(() -> { for (int x = 0; x < 10; x++) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在运行!_ x = " + x); } }, "用户线程"); //完成核心的业务 Thread deamonThread = new Thread(() -> { for (int x = 0; x < Integer.MAX_VALUE; x++) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在运行!_ x = " + x); } }, "守护线程"); deamonThread.setDaemon(true); //设置为守护线程 userThread.start(); deamonThread.start(); } }

可以发现所有的守护线程都是围绕在用户线程的周围,如果程序执行完毕,守护线程也就消失了,在整个的JVM里面最大的守护线程就是GC线程程序执行中GC线程会一直存在,如果程序执行完毕,GC线程也将消失。

volatile关键字

在多线程的定义之中, volatile关键字主要是在属性定义上使用的,表示此属性为直接数据操作,而不进行副本的拷贝处理,这样的话在一些书上就将其错误的理解为同步属性了。

在正常进行变量处理的时候往往会经历如下的几个步骤:

  • 获取变量原有的数据内容副木
  • 利用副本为变量进行数学计算
  • 将计算后的变量,保存到原始空间之中

而如果一个属性上追加了 volatile关键字,表示的就是不使用副本,而是直接操作,相当于节约了:拷贝副本、重新保存的时间

/ThreadDemo.java
class MyThread implements Runnable { private volatile int ticket = 10; @Override public void run() { while (true) { if (this.ticket > 0) { try { Thread.sleep(100); //模拟网络延迟 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "买票, ticket = " + this.ticket--); } else { System.out.println("**** 票已抢光 ****"); break; } } } } public class ThreadDemo { public static void main(String[] args) { MyThread mt = new MyThread(); new Thread(mt, "票贩子A").start(); new Thread(mt, "票贩子B").start(); new Thread(mt, "票贩子C").start(); } }

面试题:请解释 volatile与 synchronized的区别?

  • volatile主要在属性上使用,面、”cd是在代码块与方法上使用的;
  • volatile无法描述同步的处理,它只是一种直接内存的处理,避免了副本的操作,而 synchronized是实现同步的;

多线程综合案例

数字加减

设计4个线程对象,两个线程执行减操作,两个线程执行加操作。

/ThreadDemo.java
class Resource { //定义一个操作的资源 private int num = 0; //这个要进行加减操作的数据 private boolean flag = true; //加减的切换 //flag=true:表示可以进行加法操作,但是无法进行减法操作 //flag=false:表示可以进行碱法操作,但是无法进行加法操作 public synchronized void add() throws InterruptedException { //加法 if (!this.flag) { //现在需要执行的是减法操作,加法操作要等待 super.wait(); } Thread.sleep(100); this.num++; System.out.println("【加法操作 - " + Thread.currentThread().getName() + "】 & num = " + this.num); this.flag = false; super.notifyAll(); //唤酲全部等待线程 } public synchronized void sub() throws InterruptedException { //减法 if (this.flag) { //现在需要执行的是加法操作,减法操作要等待 super.wait(); } Thread.sleep(200); this.num--; System.out.println("【减法操作 - " + Thread.currentThread().getName() + "】 & num = " + this.num); this.flag = true; super.notifyAll(); } } class AddThread implements Runnable { private Resource resource; public AddThread(Resource resource) { this.resource = resource; } @Override public void run() { for (int i = 0; i < 50; i++) { try { this.resource.add(); } catch (InterruptedException e) { e.printStackTrace(); } } } } class SubThread implements Runnable { private Resource resource; public SubThread(Resource resource) { this.resource = resource; } @Override public void run() { for (int i = 0; i < 50; i++) { try { this.resource.sub(); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class ThreadDemo { public static void main(String[] args) { Resource res = new Resource(); SubThread st = new SubThread(res); AddThread at = new AddThread(res); new Thread(at, "加法线程 - A").start(); new Thread(at, "加法线程 - B").start(); new Thread(st, "减法线程 - X").start(); new Thread(st, "减法线程 - Y").start(); } }

这是一个经典的多线程开发操作,这一个程序里面一定要考虑的核心本质在于:加一个、减一个,整体的计算结果应该在0、-1、1 中循环出现才是合理的。

生产电脑

设计一个生产电脑和搬运电脑类,要求生产出一台电脑就搬走一台电脑,如果没有新的电脑生产出来,则搬运工要等待新电脑产出:如果生产出的电脑没有搬走,则要等待电脑搬走之后再生产,并统计出生产的电脑数量。

在本程序中,就是一个标准的生产者与消费者的处理模型,那么下面实现具体的程序代码。

/ThreadDemo.java
public class ThreadDemo { public static void main(String[] args) throws Exception { Resource res = new Resource() ; new Thread(new Producer(res)).start() ; new Thread(new Consumer(res)).start() ; new Thread(st).start() ; new Thread(st).start() ; } } class Producer implements Runnable { private Resource resource ; public Producer(Resource resource) { this.resource = resource ; } @Override public void run() { for (int x = 0; x < 50; x++) { try { this.resource.make() ; } catch (Exception e) { e.printStackTrace() ; } } } } class Consumer implements Runnable { private Resource resource ; public Consumer(Resource resource) { this.resource = resource ; } @Override public void run() { for (int x = 0 ; x < 50 ; x++) { try { this.resource.get() ; } catch (Exception e) { e.printStackTrace() ; } } } } class Resource { private Computer computer ; private boolean flag = true ; //标记 public synchronized void make() throws Exception { if (this.computer != null) { //已经生产过了 super.wait() ; } Thread.sleep(100) ; this.computer = new Computer("MLDN牌电脑", 1.1) ; System.out.println("【生产电脑】"+ this.computer) ; super.notifyAll() ; } public synchronized void get() throws Exception { if (this.computer == null) { //没有生产过 super.wait() ; } Thread.sleep(10) ; System.out.println("【取走电脑】"+ this.computer) ; this.computer = null ; //已经取走了 super.notifyAll() ; } } class Computer { private static int count = 0 ; //表示生产的个数 private String name ; private double price ; public Computer(String name, double price) { this.name = name ; this.price = price ; count++ ; } public String toString() { return "【第" + count + "台电脑】电脑名字:" + this.name + "价值:" + this.price ; } }

竞争抢答

实现一个竞拍抢答程序:要求设置三个抢答者(三个线程),而后同时发出抢答指令,抢答成功者给出成功提示,未抢答成功者给出失败提示。

对于这个多线程操作,由于里面需要牵扯到数据返回问题,那么现在最好使用Callable是比较方便的一种处理形式。

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class ThreadDemo {
    public static void main(String[] args) throws Exception {
        MyThread mt = new MyThread();
        FutureTask<String> taskA = new FutureTask(mt);
        FutureTask<String> taskB = new FutureTask(mt);
        FutureTask<String> taskC = new FutureTask(mt);
        new Thread(taskA, "竞赛者A").start();
        new Thread(taskB, "竞赛者B").start();
        new Thread(taskC, "竞赛者C").start();
        System.out.println(taskA.get());
        System.out.println(taskB.get());
        System.out.println(taskC.get());
    }
}

class MyThread implements Callable<String> {
    private boolean flag = false;          //抢到处理

    @Override
    public String call() throws Exception {
        synchronized (this) {                //数据同步
            if (this.flag == false) {        //抢答成功
                this.flag = true;
                return Thread.currentThread().getName() + "抢答成功!";
            } else {
                return Thread.currentThread().getName() + "抢答失败!";
            }
        }
    }
}

评论