【Java基础】Java多线程编程:Thread、Runnable与Callable、线程的同步、异步与死锁、生产者与消费者案例

《第一行代码:Java》第9章、多线程 读书笔记

第9章、多线程

9.1 线程与进程

这里简单介绍一下线程与进程的基本概念。想深入了解建议学习一下操作系统。

  • 进程:是程序的一次动态执行的过程,他经历了从代码加载、执行到执行完毕的一个完整过程。

    进程的五状态模型:
    在这里插入图片描述

  • 线程:和进程一样都是实现并发的一个基本单位,线程是在进程的基础上进一步的划分。比如在使用QQ同时进行视频对话、文字聊天和文件传输,则QQ这个程序的执行就是一个进程,而这个进程又划分为视频对话、文字聊天和文件传输3个线程。

  • 进程与线程:线程依附于进程,线程是程序执行流的最小单位,进程是资源分配的基本单位,所以一旦进程消失,线程也会消失。
    在这里插入图片描述

  • 并发与并行:两者在宏观上都是同时进行的意思,但并发在微观上并不是“同时”的,而是指多个任务交替使用CPU,同一时刻还是只有一个任务在跑,并行是多个任务同时执行。(对于单核CPU)

9.2 多线程实现

在Java中,如果想要实现多线程的程序,就必须依靠一个线程的主体类(就好比主类的概念一样,表示的是一个线程的主类)。但这个线程的主体类在定义时也需要有一些特殊的要求,即此类需要继承Thread类或实现Runnable(Callable)接口来完成定义。

继承Thread类

java.lang.Thread是一个负责线程操作的类,任何类只要继承Thread类就可以成为一个线程的主类。既然是主类就必须要有它的使用方法,而线程启动的主方法需要覆写Thread类中的run()方法实现。

  • 线程主体类的定义格式如下:

    class 类名称 extends Thread {		// 继承Thread类
        属性;							// 类中定义属性
        方法;							// 类中定义方法
        public void run(){			 // 覆写Thread类中的fun()方法,此方法是线程的主体
            线程主体方法;
        }
    }
    
  • 定义一个线程操作类:

    class MyThread extends Thread {				// 这就是一个多线程的操作类
    	private String name ;					// 定义类中的属性
    	public MyThread(String name) {			// 定义构造方法
    		this.name = name ;
    	}
    	@Override
    	public void run() {						// 覆写run()方法,作为线程的主操作方法
    		for (int x = 0 ; x < 200 ; x ++) {
    			System.out.println(this.name + " --> " + x);
    		}
    	}
    }
    

    如果直接调用run()方法,并不能启动多线程,启动多线程的唯一方法是调用Thread类中的start()方法:public void start() ;调用此方法后执行的方法体是run()方法中定义的代码。

  • 使用start()方法启动多线程:

    public class TestDemo {								// 主类
    	public static void main(String[] args) {
    		MyThread mt1 = new MyThread("线程A") ;	// 实例化多线程类对象
    		MyThread mt2 = new MyThread("线程B") ;	// 实例化多线程类对象
    		MyThread mt3 = new MyThread("线程C") ;	// 实例化多线程类对象
    		mt1.start();										// 启动多线程
    		mt2.start();										// 启动多线程
    		mt3.start();										// 启动多线程
    	}
    }
    /*
    程序执行结果:
    	程序A --> 0
    	程序B --> 0
    	程序C --> 0
    	程序C --> 1
    	程序A --> 1
    	...
    */
    
  • 为什么多线程的启动不直接调用tun()方法而是调用start()方法?

    因为start()方法不仅要启动多线程的执行代码,还要根据不同的操作系统进行资源的分配,即还需要进行其他操作。

实现Runnable接口

使用Thread类确实可以方便的进行多线程的实现,但其最大缺点就是单继承问题。为此,在Java中也可以利用Runnable接口来实现多线程。

  • Runnable接口定义如下:

    @FunctionalInterface
    public interface Runnable {
        public void run();
    }
    
  • 实现Runnable接口:

    class MyThread implements Runnable { 				// 定义线程主体类
    	private String name; 							// 定义类中的属性
    	public MyThread(String name) { 					// 定义构造方法
    		this.name = name;
    	}
    	@Override
    	public void run() { 							// 覆写run()方法
    		for (int x = 0; x < 200; x++) {
    			System.out.println(this.name + " --> " + x);
    		}
    	}
    }
    

    问题来了,由于Runnable接口中没有start()方法,那么应该怎么启动多线程呢?

    其实在Thread类中有一个可以接收一个Runnable接口对象的有参构造方法:public Thread(Runnable target)。那么我们就可以用Thread类来接收一个Runnable接口对象,这样就能调用Thread类中的start()方法了。

  • 利用Thread类接收Runnable接口对象以启动多线程:

    public class TestDemo { 
    	public static void main(String[] args) {
    		MyThread mt1 = new MyThread("线程A") ;		// 实例化多线程类对象
    		MyThread mt2 = new MyThread("线程B") ;		// 实例化多线程类对象
    		MyThread mt3 = new MyThread("线程C") ;		// 实例化多线程类对象
    		new Thread(mt1).start();						// 利用Thread启动多线程
    		new Thread(mt2).start();						// 利用Thread启动多线程
    		new Thread(mt3).start();						// 利用Thread启动多线程
    	}
    }
    
  • 使用Lambda表达式实现多线程:

    public class TestDemo {
        public static void main(String[] args) {
            String[] names = {"线程A", "线程B", "线程C"};
            for (String name : names){
                new Thread(() -> {
                	for (int i = 0; i < 200; i++) {
                    	System.out.println(name + " --> " + i);
                	}
            	}).start();
            }
        }
    }
    
多线程两种实现方式的区别

对于Java的实际开发角度来说,由于Runnable接口避免了单继承的局限,所以一般是采用Runnable接口实现多线程。

  • 为解释Thread类与Runnable接口的区别,下面可以观察Thread类的定义:

    public class Thread extends Object implements Runnable
    

    可以看出其实Thread类也是Runnable接口的子类,所以Thread类与Runnable接口的关系类似于代理设计模式,如图9-2所示。
    在这里插入图片描述

  • 使用Runnable接口可以更好的实现数据共享(但不是说Thread类不能实现数据共享)

    • 使用继承Thread类实现买票系统:

      package com.yootk.demo;
      class MyThread extends Thread { 					// 线程的主体类
      	private int ticket = 10; 						// 一共10张票
      	@Override
          public void run() { 							// 线程的主方法
      		for (int x = 0; x < 50; x++) {				// 循环50次
      			if (this.ticket > 0) {
      				System.out.println("卖票,ticket = " + this.ticket --);
      			}
      		}
      	}
      }
      public class TestDemo {
      	public static void main(String[] args) throws Exception {
      		MyThread mt1 = new MyThread() ;		// 创建线程对象
      		MyThread mt2 = new MyThread() ;		// 创建线程对象
      		MyThread mt3 = new MyThread() ;		// 创建线程对象
      		mt1.start() ;								// 启动线程
      		mt2.start() ;								// 启动线程
      		mt3.start() ;								// 启动线程
      	}
      }
      /*
      程序执行结果:
      	卖票,ticket = 10
      	卖票,ticket = 10
      	卖票,ticket = 9
      	卖票,ticket = 10
      	卖票,ticket = 8
      	...
      */
      

      本程序定义了3个线程对象,希望3个线程对象同时卖10张票,而最终结果是卖出了30张票。所以这是的内存关系为:
      在这里插入图片描述

    • 利用Runnable接口实现多线程:

      package com.yootk.demo;
      class MyThread implements Runnable { 		// 线程的主体类
      	private int ticket = 5; 						// 一共5张票
      	@Override
      	public void run() { 							// 线程的主方法
      		for (int x = 0; x < 50; x++) {			// 循环50次
      			if (this.ticket > 0) {
      				System.out.println("卖票,ticket = " + this.ticket --);
      			}
      		}
      	}
      }
      public class TestDemo {
      	public static void main(String[] args) throws Exception {
      		MyThread mt = new MyThread();		// 创建线程对象
      		new Thread(mt).start() ;				// 启动线程
      		new Thread(mt).start() ;				// 启动线程
      		new Thread(mt).start() ;				// 启动线程
      	}
      }
      

      本程序使用Runnable实现对线程,同时启动了3个线程对象,但这3个线程对象都占着一个Runnable接口对象的引用,所以实现了数据共享的操作。本程序的内存关系图如图9-4所示:
      在这里插入图片描述

利用Callable接口实现多线程

使用Runnable接口可以避免单继承的局限性,但Runnable接口中的run()方法不能返回操作结果。所以为了解决这个问题,从JDK1.5开始,Java对于多线程的实现提供了一个新的接口:java.util.concurrent.Callable

  • Callable接口定义如下:

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

    Callable接口中定义了一个call()方法,而call()方法是可以实现线程操作数据的返回的,而返回值类型由Callable接口上的泛型类型动态决定的。

  • 定义一个线程主体类:

    import java.util.concurrent.Callable;
    class MyThread implements Callable<String> { 			// 多线程主体类
    	private int ticket = 10;							// 卖票
    	@Override
    	public String call() throws Exception {
    		for (int x = 0; x < 100; x++) {
    			if (this.ticket > 0) {						// 还有票可以出售
    				System.out.println("卖票,ticket = " + this.ticket--);
    			}
    		}
    		return "票已卖光!";								// 返回结果
    	}
    }
    

    当线程主体定义完成后,按理说需要调用Thread类中的start()方法类启动多线程,但start()方法无法接收Callable接口对象。所以从JDK1.5开始,Java提供了一个:java.util.concurrent.FutureTask<V>类

  • FutureTask类的定义:

    public class FutureTask<V> extends Object implements RunnableFuture<V>
    

    通过定义可以看出FutureTask类实现了RunnableFuture接口,而RunnableFuture接口又同时实现了Future和Runnable接口。FutureTask类继承关系如图9-5所示:
    在这里插入图片描述

  • FutureTask类的常用方法:

    public FutureTask(Callable<V> callable)							// 构造,接收Callable接口实例对象
    public FutureTask(Runnable runnable, V result)					// 构造,接收Runnable接口实例对象,并指定返回结果类型
    public V get throws InterruptedException, ExecutionException	// 普通,取得线程操作结果,此方法为Future接口中定义的
    

    从上面构造方法可以知道,FutureTask类可以接收Callable接口对象,而FutureTask类实现了Runnable接口,所以FutureTask类对象可向上转型为Runnable接口对象,而Thread类中的start()方法可接收Runnable接口对象,所以start()方法也可接收FutureTask类对象。

  • 所以启动Callable多线程:

    public class TestDemo {
    	public static void main(String[] args) throws Exception {
    		MyThread mt1 = new MyThread();		// 实例化Callable多线程对象
    		MyThread mt2 = new MyThread();		// 实例化Callable多线程对象
    		FutureTask<String> task1 = new FutureTask<String>(mt1) ;	// 实例化FutureTask类对象
    		FutureTask<String> task2 = new FutureTask<String>(mt2) ;	// 实例化FutureTask类对象
    		// FutureTask是Runnable接口子类,所以可以使用Thread类的构造来接收task对象
    		new Thread(task1).start();			// 启动第一个线程 
    		new Thread(task2).start(); 			// 启动第二个线程
    		// 多线程执行完毕后可以取得内容,依靠FutureTask的父接口Future中的get()方法实现
    		System.out.println("A线程的返回结果:" + task1.get());
    		System.out.println("B线程的返回结果:" + task2.get());
    	}
    }
    /*
    程序执行结果:
    	卖票,ticket = 10
    	卖票,ticket = 10
    	卖票,ticket = 9
    	...
    	卖票,ticket = 1
    	卖票,ticket = 2
    	卖票,ticket = 1
    	A线程的返回结果:票已卖光!
    	B线程的返回结果:票已卖光!
    */
    
  • 通过Callable接口与Runnable接口实现的对比,可以发现,Callable接口只是胜在有返回值。所以在不需要多线程的返回值时,还是建议优先使用Runnable接口。

线程的操作状态

先要实现多线程,必须在主线程中创建新的线程对象。任何线程一般都具有五种状态:创建、就绪、运行、阻塞和终止态。线程状态转换图如下图所示:
在这里插入图片描述

  • 创建状态:在程序中用构造方法创建一个线程对象后,新的线程对象便处于新建状态,此时,它已经有相应的内存空间和其他资源,但还处于不可运行状态。新建一个线程对象可采用Thread类或自定义的Runnable接口子类的构造方法来实现,例如:Thread thread = new Thread()。
  • 就绪状态:创建线程对象后,调用该线程的start()方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待CPU服务,这表明它已经具备了运行条件。
  • 运行状态:当就绪状态的线程被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的run()方法。run()方法定义了该线程的操作和功能。
  • 堵塞状态:一个正在执行的线程在某些特殊情况下,如被人为挂起或需要执行耗时的输入输出操作时,将让出CPU 并暂时中止自己的执行,进入堵塞状态。在可执行状态下,如果调用sleep()、suspend()、wait()等方法,线程都将进入堵塞状态。堵塞时,线程不能进入排队队列,只有当引起堵塞的原因被消除后,线程才可以转入就绪状态等待CPU服务
  • 终止状态:线程调用stop()方法时或run()方法执行结束后,就处于终止状态。处于终止状态的线程不具有继续运行的能力。

9.3 多线程常用操作

Java处理支持多线程的定义以外,还提供了许多多线程操作方法,其中大部分方法都在Thread类中定义。

线程的命名与取得

线程的名字一般而言会在其启动之前进行定义,且不建议对已经启动的线程更改名称。

  • Thread类中常见的命名操作:

    public Thread(Runnable target, String name)	// 构造,实例化程序对象,接收Runnable接口子类对象,同时设置线程名称
    public static Thread currentThread()		// 静态,取得当前正在运行的线程对象
    public final void setName(String name)		// 普通,设置线程名称
    public final String getName()				// 普通,取得线程名称
    
  • 观察线程的名称:

    package com.yootk.demo;
    class MyThread implements Runnable {
    	@Override
    	public void run() {
            System.out.println(Thread.currentThread().getName());
    	}
    }
    public class TestDemo {
    	public static void main(String[] args) throws Exception {
    		MyThread mt = new MyThread();
    		new Thread(mt, "自己的线程A").start();
    		new Thread(mt).start();
    		new Thread(mt, "自己的线程B").start();
    		new Thread(mt).start();
    		new Thread(mt).start();
    	}
    }
    /*
    程序执行结果:
    	自己的线程A
    	Thread-0
    	自己的线程B
    	Thread-2
    	Thread-1
    */
    

    通过本程序可以发现,实例化Thread类对象时可以自己定义线程名称,否则将使用默认名称。

  • 取得主方法线程名称:

    package com.yootk.demo;
    class MyThread implements Runnable {
    	@Override
    	public void run() {
    		System.out.println(Thread.currentThread().getName());
    	}
    }
    public class TestDemo {
    	public static void main(String[] args) throws Exception {
    		MyThread mt = new MyThread();
    		new Thread(mt, "自己的线程对象").start();
    		mt.run(); 				// 直接调用run()方法,main
    	}
    }
    /*
    程序执行结果:
    	main					// mt.run();产生的结果
    	自己的线程对象				// new Thread(mt, "自己的线程对象").start();产生的结果
    */
    

    通过上面可以知道:主方法本身也属于一个线程

  • 每一个JVM运行就是进程:

    • 每当用户使用Java命令执行一个类时就表示启动了一个JVM进程,而主方法只是这个进程上的一个线程而已

    • 每个JVM进程至少启动一下两个线程:

      • main线程:程序的主要执行,以及启动子程序;
      • gc线程:负责垃圾收集
线程休眠
  • 在Thread类中线程休眠操作方法为:

    public static void sleep(long millis) throws InterruptedException		// 休眠时间单位为:毫秒(ms)
    
  • 观察休眠特点:

    package com.yootk.demo;
    class MyThread implements Runnable {
    	@Override
    	public void run() {
    		for (int x = 0; x < 3; x++) {
    			try {
    				Thread.sleep(1000);				// 每次执行休眠1s
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    			System.out.println(Thread.currentThread().getName() + ",x = " + x);
    		}
    	}
    }
    public class TestDemo {
    	public static void main(String[] args) throws Exception {
    		MyThread mt = new MyThread();
    		new Thread(mt, "自己的线程对象A").start();
    	}
    }
    /*
    程序执行结果:
    	自己的线程对象A,x = 0
    	自己的线程对象A,x = 1
    	自己的线程对象A,x = 2
    */
    
线程优先级

在Java的线程操作中,所有的线程在运行前都会保持就绪状态,此时哪个线程优先级高,哪个线程就可能会被优先执行。

  • 线程优先级操作:

    public static final int MAX_PRIORITY				// 最高优先级,数值为10
    public static final int NORM_PRIORITY				// 中等优先级,数值为5
    public static final int MIN_PRIORITY				// 最低优先级,数值为1
    public final void setPriority(int newPriority)		// 设置线程优先级
    public final int getPriority()						// 取得线程优先级
    
  • 设置线程优先级:

    package com.yootk.demo;
    class MyThread implements Runnable {
    	@Override
    	public void run() {
    		for (int x = 0; x < 20; x++) {
    			try {
    				Thread.sleep(100);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    			System.out.println(Thread.currentThread().getName() + ",x = " + x);
    		}
    	}
    }
    public class TestDemo { 
    	public static void main(String[] args) throws Exception {
    		MyThread mt = new MyThread();
    		Thread t1 = new Thread(mt, "自己的线程对象A");
    		Thread t2 = new Thread(mt, "自己的线程对象B");
    		Thread t3 = new Thread(mt, "自己的线程对象C");
    		t3.setPriority(Thread.MAX_PRIORITY);			// 修改一个线程对象的优先级
    		t1.start();
    		t2.start();
    		t3.start();
    	}
    }
    
  • 主线程优先级:

    public class TestDemo {
    	public static void main(String[] args) throws Exception {
    		System.out.println(Thread.currentThread().getPriority());
    	}
    }
    // 程序执行结果:	5
    

9.4 线程的同步与死锁

如果一个程序采用多线程机制,利用主线程创建出许多子程序(相当于多了许多帮手),一起进行资源的操作,那么效率自然高。
在这里插入图片描述

虽然多线程同时处理资源效率比单线程高得多,但同样也会产生一些问题。

同步问题的引出
  • 观察非同步情况下的操作:

    package com.yootk.demo;
    class MyThread implements Runnable {
    	private int ticket = 5; 						// 一共有5张票
    	@Override
    	public void run() {
    		for (int x = 0; x < 20; x++) {
    			if (this.ticket > 0) {					// 判断当前是否还有剩余票
    				try {
    					Thread.sleep(100);				// 休眠1s,模拟延迟
    				} catch (InterruptedException e) {
    					e.printStackTrace();
                    }
    				System.out.println(Thread.currentThread().getName()
    						+ " 卖票,ticket = " + this.ticket--);
    			}
    		}
    	}
    }
    public class TestDemo { 
    	public static void main(String[] args) throws Exception {
    		MyThread mt = new MyThread();
    		new Thread(mt, "票贩子A").start();			// 启动多线程
    		new Thread(mt, "票贩子B").start();			// 启动多线程
    		new Thread(mt, "票贩子C").start();			// 启动多线程
    		new Thread(mt, "票贩子D").start();			// 启动多线程
    	}
    }
    /*
    程序执行结果:
    	票贩子D 卖票,ticket = 3
        票贩子B 卖票,ticket = 2
        票贩子C 卖票,ticket = 5
        票贩子A 卖票,ticket = 4
        票贩子C 卖票,ticket = 1
        票贩子A 卖票,ticket = -2
        票贩子D 卖票,ticket = 0
        票贩子B 卖票,ticket = -1
    */
    

    由于在判断是否还有票语句if (this.ticket > 0)和票数减一this.ticket–之间加上了延迟,那么就可能出现当只剩最后一张票时,有多个线程通过了判断语句,使得最后结果出现负数。举个例子:一个银行账户被两个线程操作,在此之前该帐户中有100元,此时这两个线程同时判断帐户内还剩100,两个线程同时取出,之后账户余额-100,然后就会出现-100元的情况。

同步操作

如果想解决以上程序的问题,就需要使用同步操作。

  • 同步操作:多个线程同时操作一个可共享的资源变量时,一次只允许一个线程进行,其他线程要完成之后才能继续执行,此时其他正在等待的线程是处于阻塞状态的。
    在这里插入图片描述

    完成这一过程就是要在一个线程对该共享资源变量进行操作时“上锁”,上锁的时候其他线程无法对改资源变量操作,只有那个线程完成对该变量的操作之后,再进行解锁,之后其他线程才能继续操作。

  • Java中要实现线程的同步,可以使用synchronized关键字。synchronized关键字可以通过以下两种方式进行使用:

    • 同步代码块:利用synchronized包装的代码块,但是需要指定同步对象,一般设置为this;
    • 同步方法:利用synchronized定义方法
  • synchronized关键字的作用:

    一个类的实例化对象中的使用synchronized关键字定义的代码块和方法同时只能由一个线程执行;即若一个类中若定义了一个同步代码块和一个同步方法,线程1要执行同步代码块,线程2要执行同步方法,那么他们不能同时执行。

  • 使用同步代码块解决问题:

    package com.yootk.demo;
    class MyThread implements Runnable {
    	private int ticket = 5; 							// 一共有5张票
    	@Override
    	public void run() {
    		for (int x = 0; x < 20; x++) {
    			synchronized(this) {						// 定义同步代码块
    				if (this.ticket > 0) {					// 判断当前是否还有剩余票
    					try {
    						Thread.sleep(1000);				// 休眠1s,模拟延迟
    					} catch (InterruptedException e) {
    						e.printStackTrace();
    					}
    					System.out.println(Thread.currentThread().getName()
    							+ " 卖票,ticket = " + this.ticket--);
    				}
    			}
    		}
    	}
    }
    public class TestDemo { 
    	public static void main(String[] args) throws Exception {
    		MyThread mt = new MyThread();
    		new Thread(mt, "票贩子A").start();			// 启动多线程
    		new Thread(mt, "票贩子B").start();			// 启动多线程
    		new Thread(mt, "票贩子C").start();			// 启动多线程
    		new Thread(mt, "票贩子D").start();			// 启动多线程
    	}
    }
    /*
    程序执行结果:
    	票贩子A 卖票,ticket = 5
        票贩子D 卖票,ticket = 4
        票贩子D 卖票,ticket = 3
        票贩子D 卖票,ticket = 2
        票贩子D 卖票,ticket = 1
    */
    

    本程序将判断是否有票以及买票这两个操作都统一放在了同步代码块中,这样当某一个线程执行该代码块时,其他程序无法进入该代码块中进行操作,从而实现了线程的同步操作。

  • 使用同步方法解决问题:

    package com.yootk.demo;
    class MyThread implements Runnable {
    	private int ticket = 5; 						// 一共有5张票
    	@Override
        public void run() {
    		for (int x = 0; x < 20; x++) {
    			this.sale();							// 卖票操作
    		}
    	}
    	public synchronized void sale() {				// 同步方法
    		if (this.ticket > 0) {						// 判断当前是否还有剩余票
    			try {
    				Thread.sleep(1000);					// 休眠1s,模拟延迟
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    			System.out.println(Thread.currentThread().getName()
    					+ " 卖票,ticket = " + this.ticket--);
    		}
    	}
    }
    public class TestDemo { 
    	public static void main(String[] args) throws Exception {
    		MyThread mt = new MyThread();
    		new Thread(mt, "票贩子A").start();			// 启动多线程
    		new Thread(mt, "票贩子B").start();			// 启动多线程
    		new Thread(mt, "票贩子C").start();			// 启动多线程
    		new Thread(mt, "票贩子D").start();			// 启动多线程
    	}
    }
    /*
    程序执行结果:
        票贩子C 卖票,ticket = 5
        票贩子C 卖票,ticket = 4
        票贩子C 卖票,ticket = 3
        票贩子C 卖票,ticket = 2
        票贩子B 卖票,ticket = 1
    */
    

    一般加入同步后,代码的性能会降低,但数据的安全性高。

  • Java中方法的完整定义格式:

    [public|protected|private] [static] [final] [native] [synchronized] 返回值类型 方法名称(参数列表) [throws 异常类型] { 方法体; }
    
  • 同步性与异步性的区别:

    • 异步性:即很多事情可以一起并排着去做,不同于同步那样,非要等上一件事情做完了才能去做下一件事情。
    • 同步性:有些代码之间有先后的逻辑关系在里面,比如一定要先执行完代码A才能执行代码B,完成这样的要求就是代码的同步性
  • 方法定义中,abstract不能与static、native、synchronized同时声明使用

死锁

同步就是指一个线程要等另一个线程执行完毕之后才会继续执行的一种情形,虽然在一个线程中,使用同步能够保证资源共享操作的正确性,但也会产生其他问题。

假如:线程A,线程B都需要共享资源A和B,但在资源分配时,线程A分到了资源A,线程B分到了资源B。但因为他们都只有其中一个资源,都没法继续执行下去。此时线程A和B都在等待对方的手中的资源,一直会处于阻塞态,这实际上就是死锁了。

详细的锁死概念和过程还是建议去看操作系统的相关知识。

  • 程序死锁操作:

    package com.yootk.demo;
    class A {
    	public synchronized void say(B b) {
    		System.out.println("A先生说:把你的本给我,我给你笔,否则不给!");
            try {
                Thread.sleep(1000);					// 延迟操作
            } catch(InterruptedException e) {
                e.printStackTrace();
            }
    		b.get();
    	}
    	public synchronized void get() {
    		System.out.println("A先生:得到了本,付出了笔,还是什么都干不了!");
    	}
    }
    class B {
    	public synchronized void say(A a) {
    		System.out.println("B先生说:把你的笔给我,我给你本,否则不给!");
            try {
                Thread.sleep(1000);				// 延迟操作
            } catch(InterruptedException e) {
                e.printStackTrace();
            }
    		a.get();
    	}
    	public synchronized void get() {
    		System.out.println("B先生:得到了笔,付出了本,还是什么都干不了!");
    	}
    }
    public class TestDemo implements Runnable {
    	private static A a = new A();				// 定义类对象
    	private static B b = new B();				// 定义类对象
    	public static void main(String[] args) throws Exception {
    		new TestDemo();							// 实例化本类对象
    	}
    	public TestDemo() {							// 构造方法
    		new Thread(this).start();				// 启动线程
    		b.say(a);								// 互相引用
        }
    	@Override
    	public void run() {
    		a.say(b);								// 互相引用
    	}
    }
    /*
    程序执行结果:
    	B先生说:把你的笔给我,我给你本,否则不给!
    	A先生说:把你的本给我,我给你笔,否则不给!
    	(程序会卡在这,不会再往下执行了)
    */
    

    由于同一个类的实例化对象中的synchronized声明的同步代码块和同步方法同时只能由一个线程运行,所以以上程序会产生死锁现象。

9.5 生产者与消费者案例

在开发中线程的运行状态并不固定,所以只能利用线程的名字以及当前执行的线程对象来进行区分。

问题的引出
  • 在生产者和消费者模型中,生产者不断的生成,消费者不断地取走产品。

  • 生产者产出消息后将其放在一个区域中,然后消费者从该区域取出数据,但由于线程运行的不确定性,所以会存在以下两个问题:

    • 假设生产者线程向数据存储空间添加第一段信息,还没等其添加第二段信息,程序就切换到了消费者线程,消费者线程将把该信息的第一段和上一条信息的第二段结合在一起取出了。
    • 生产者放了多次信息后,消费者才开始取出信息;或者消费者取完一个信息后,还没等生产者放入新信息,又重复取出已经取过的信息。
  • 程序基本模型:

    package com.yootk.demo;
    class Message {
        private String title;							// 保存第一段信息
        private String content;							// 保存第二段信息
        public void setTitle(String title) {
            this.title = title;
        }
        public void setContent(String content) {
            this.content = content;
        }
        public String getTitle() {
            return this.title;
        }
        public String getContent() {
            return this.content;
        }
    }
    class Producer implements Runnable {				// 定义生产者
        private final Message msg;
        public Producer(Message msg) {
            this.msg = msg;
        }
        public void run() {
            for (int i = 0; i < 20; i++) {				// 生成20次信息
                this.msg.setTitle("Java" + i);			// 设置第一段信息
                try {
                    Thread.sleep(100);					// 延迟操作
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.msg.setContent(",学习" + i);		  // 设置第二段信息
            }
        }
    }
    class Consumer implements Runnable {				// 定义消费者
        private final Message msg;
        public Consumer(Message msg) {
            this.msg = msg;
        }
        public void run() {
            for (int i = 0; i < 20; i++) {				// 取走20次信息
                try {
                    Thread.sleep(100);					// 延迟操作
                } catch(InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(this.msg.getTitle() + this.msg.getContent());
            }
        }
    }
    public class TestDemo {
        public static void main(String[] args) {
            Message msg = new Message();				// 定义Message对象,用于保存和取出信息
            new Thread(new Producer(msg)).start();		// 启动生产者线程
            new Thread(new Consumer(msg)).start();		// 启动消费者线程
        }
    }
    /*
    程序执行结果:
        Java1,学习0
        Java1,学习1
        Java2,学习1
        Java3,学习2
        Java4,学习3
        Java5,学习4
        Java6,学习6
        Java8,学习7
        Java8,学习7
        ...
    */
    

    通过本程序的运行结果,可以发现两个严重的问题:设置的数据错位;数据会重复取出或重复设置

解决数据错乱问题
  • 数据错乱完全是非同步操作问题,所以只需要使用同步操作处理就可以了。

  • 因为取出和设置是两个不同的操作,所以想要进行同步控制,就需要将其定义在一个类中完成:

    package com.yootk.demo;
    class Message {
        private String title;							// 保存第一段信息
        private String content;							// 保存第二段信息
        public synchronized void set(String title, String content) {	// 定义同步方法
            this.title = title;
            try {
                Thread.sleep(200);
            } catch(InterruptedException e) {
                e.printStackTrace();
            }
            this.content = content;
        }
        public synchronized void get() {								// 定义同步方法
            try {
                Thread.sleep(100);
            } catch(InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.title + this.content);
        }
    }
    
    class Producer implements Runnable {				// 定义生成者
        private final Message msg;
        public Producer(Message msg) {
            this.msg = msg;
        }
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {				// 生成20次数据
                this.msg.set("java" + i, ",学习" + i);  // 设置属性
            }
        }
    }
    
    class Consumer implements Runnable {				// 定义消费者
        private final Message msg;
        public Consumer(Message msg) {
            this.msg = msg;
        }
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {				// 取得20次消息
                this.msg.get();							// 取得信息
            }
        }
    }
    
    public class TestDemo {
        public static void main(String[] args) {
            Message msg = new Message();				// 定义Message对象,用于保存和取出信息
            new Thread(new Producer(msg)).start();		// 启动生产者线程
            new Thread(new Consumer(msg)).start();		// 启动消费者线程
        }
    }
    /*
    程序执行结果:
        java0,学习0
        java0,学习0
        java2,学习2
        java3,学习3
        java5,学习5
        java5,学习5
        java6,学习6
        ...
    */
    

    本程序虽然解决了数据错乱的问题,但可以发现依然存在重复取出与重复设置问题。

解决数据重复问题
  • 可以设置一个标志位,如果该标志位为true则生产者可以生产,而消费者应该等待,等到生产者生产完毕,将标志位改为false,并唤醒等待线程;在false标志下,消费者可以取走数据,而生产者应该等待,等到消费者取走了数据,将标志位改为true,并唤醒等待线程。流程如下:
    在这里插入图片描述

  • 在Java中,有3个有关等待唤醒机制的方法,都在Object类中:

    public final void wait() throws InterruptedException	// 普通,线程等待 
    public final void notify()								// 普通,唤醒第一个等待线程
    public final void notifyAll()							// 普通,唤醒全部等待线程,哪个线程优先级高,哪个线程就有可能先执行。
    
  • 使用唤醒、等待机制解决程序问题:

    package com.yootk.demo;
    class Message {
        private String title;							// 保存第一段信息
        private String content;							// 保存第二段信息
        private boolean flag = true;					// 定义标志位,初始值为true
        public synchronized void set(String title, String content) {	// 定义同步方法
            if (!flag) {								// 已经生产过了,不能生产
                try {
                    super.wait();						// 线程等待,等待被唤醒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            this.title = title;
            try {
                Thread.sleep(200);
            } catch(InterruptedException e) {
                e.printStackTrace();
            }
            this.content = content;
            this.flag = false;							// 完成生产,修改标志位
            super.notify();								// 唤醒等待中的线程
        }
        public synchronized void get() {				// 定义同步方法
            if (flag) {									// 未生产,不能取走
                try {
                    super.wait();						// 线程等待,等待被唤醒
                } catch(InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                Thread.sleep(100);
            } catch(InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.title + this.content);
            this.flag = true;							// 已经取走,可以继续生产
            super.notify();								// 唤醒等待线程
        }
    }
    

    生产者没产生一个信息就要等待消费者把它取走,消费者每取走一个信息就要等待生产者生产下一个,这样就避免了重复生产和重复取走的问题。

  • sleep()和wait()的区别:

    • sleep():是Thread类定义的static方法,表示线程休眠,将执行机会给其他线程,但是监控状态依然保持,休眠时间到就会自动恢复。
    • wait():是Object类定义的方法,表示线程等待,一致等待其他线程执行了notify()或notifyAll()后唤醒。

9.6 线程的生命周期

  • 在Java中,一个线程对象也有自己的生命周期,其过程如图9-15所示:
    在这里插入图片描述

  • 从上图中可以发现,大部分线程周期的方法基本都学过了,只有以下3个:

    • suspend():暂时挂起线程
    • resume():恢复挂起线程
    • stop():停止线程

但是这3个方法不推荐使用,因为他们已经被满满废除掉了,主要原因是这3个方法在操作时可能会导致死锁的问题。

  • 在多线程的开发中可以通过设置标志位的方式停止一个线程的运行。

    package com.yootk.demo;
    class MyThread implements Runnable {
    	private boolean flag = true; 					// 定义标志位属性
    	public void run() {								// 覆写run()方法
    		int i = 0;
    		while (this.flag) {							// 循环输出
    			while (true) {
    				System.out.println(Thread.currentThread().getName() + "运行,i = "
    						+ (i++));					// 输出当前线程名称
    			}
    		}
    	}
    	public void stop() { 								// 编写停止方法
    		this.flag = false;							// 修改标志位
    	}
    }
    public class StopDemo {
    	public static void main(String[] args) {
    		MyThread my = new MyThread();			// 实例化Runnable接口对象
    		Thread t = new Thread(my, "线程");		// 建立线程对象
    		t.start() ;									// 启动线程
    		my.stop() ;									// 线程停止,修改标志位
    	}
    }
    

    通过调用自定义的stop()方法,将线程对象中的标志位flag变量设置为false,这样run()方法就会停止运行,这种停止方法在开发中是比较推荐的。

本章小结

  • 线程(thread)是指程序的运行流程。“多线程”的机制可以同时运行多个程序块,使程序运行的效率更高,也解决了传统程序设计语言无法解决的问题。
  • 如果在类里要激活线程,必须先做好两项准备:此类必须继承Thread 类或者实现 Runnable 接口;线程的处理必须覆写run()方法。
  • 每一个线程在其创建和消亡之前,均会处于创建、就绪、运行、阻塞、终止5种状态之一。
  • Thread类里的sleep()方法可用来控制线程的休眠状态,休眠的时间要视sleep()里的参数而定。
  • 当多个线程对象操纵同一共享资源时,要使用synchronized关键字来进行资源的同步处理。
  • 线程的存在离不开进程。进程如果消失后线程一定会消失,反之如果线程消失了,进程未必会消失。
  • 对于多线程的实现,重点在于Runnable接口与Thread类启动的配合上。
  • Thread.currentThread()可以取得当前线程类对象;
  • 优先级越高的线程对象越有可能先执行。