Java教程

阿毛的Java基础学习笔记 --- (8)多线程

本文主要是介绍阿毛的Java基础学习笔记 --- (8)多线程,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

第8章 多綫程

目录

    • 第8章 多綫程
      • 8.1 綫程概述
        • 8.1.1 進程
        • 8.1.2 线程
      • 8.2 綫程的創建
        • 8.2.1 继承Thread类创建多线程
        • 8.2.2 实现Runnable接口创建多线程
      • 8.3 綫程的生命周期及狀態轉換
      • 8.4 綫程的調度
        • 8.4.1 线程的优先级
        • 8.4.2 线程休眠
        • 8.4.3 线程让步
        • 8.4.4 线程插队
      • 8.5 多綫程同步
        • 8.5.1 线程安全问题
        • 8.5.2 同步代码块
        • 8.5.3 同步方法
        • 8.5.4 死锁问题

8.1 綫程概述

計算機能夠同時完成多項任務,這就是多綫程技術,計算機的CPU即使是單核,也可以同時運行多個任務,因爲操作系統執行多個任務時就是讓CPU對多個任務輪流交替執行

Java是支持多线程的语言之一,它内置了对多线程技术的支持,可以使程序同时执行多个执行片段

8.1.1 進程

進程概述:在一个操作系统中,每个独立执行的程序都可称之为一个进程,也就是“正在运行的程序”

在多任务操作系统中,表面上是支持进程并发执行的,但实际上这些进程并不是同时运行的。在计算机中,所有的应用程序都是由CPU执行的,对于一个CPU而言,在某个时间点只能运行一个程序,也就是说只能执行一个进程。操作系统会为每一个进程分配一段有限的CPU使用时间,CPU在这段时间中执行某个进程,然后会在下一段时间切换到另一个进程中去执行。由于CPU运行速度很快,能在极短的时间内在不同的进程之间进行切换,所以给人以同时执行多个程序的感觉

8.1.2 线程

每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元同时运行,这些执行单元可以看作程序执行的一条条线索,被称为线程

操作系统中的每一个进程中都至少存在一个线程,进程与线程是包含于被包含的关系

Java程序启动时,就会产生一个进程,该进程中会默认创建一个线程,在这个线程上会运行main()方法中的代码

在这里插入图片描述

代码按照调用顺序依次往下进行,没有出现两段程序代码交替运行的效果,这样的程序就是单线程程序,如果希望程序实现多段程序代码交替运行的效果,则需要创建多个线程,即多线程程序

多线程:所谓的多线程是指一个进程在执行过程中可以产生多个单线程,这些单线程程序在运行时是相互独立的,它们可以并发执行,多线程与进程相似,看似是多个线程同时执行,其实也是由CPU轮流执行的

进程与线程虽然是包含关系,但是多任务即可以由多线程实现,也可以由单进程的多线程实现,还可以混合多线程、多进程,具体采用哪种方式,还要考虑到进程和线程的特点

多进程与多线程

多进程的优点:与多线程相比,多进程的稳定性更高,因为在多进程的情况下进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃都会直接导致整个进程崩溃

多进程的缺点:

  • 创建进程的开销比创建线程的大,尤其在Windows系统上
  • 进程间通信比线程间通信缓慢,因为线程间通信就是读写同一个变量,速度很快

8.2 綫程的創建

Java中提供了两种多线程实现方式

  • 一种是继承java.lang包下的Thread类,覆写Thread类的run()方法,在run()方法中实现运行在线程上的代码
  • 另一种是实现java.lang.Runnable接口,同样是在run()方法中实现运行在线程上的代码

在学习多线程之前先来熟悉单线程程序

1 public class Example01 {
2	public static void main(String[] args) {
3		MyThread myThread = new MyThread(); // 创建MyThread实例对象
4		myThread.run();                     // 调用MyThread类的run()方法
5		while (true) {                          // 该循环是一个死循环,打印输出语句
6			System.out.println("Main方法在运行");
7		}
8	}
9 }
10 class MyThread {
11	public void run() {
12		while (true) {	// 该循环是一个死循环,打印输出语句
13			System.out.println("MyThread类的run()方法在运行");
14		}
15	}
16 }

程序一直打印“MyThread类的run()方法在运行”,这是因为该程序是一个单线程程序,第4行代码调用MyThread类的run()方法时,遇到第12~14行代码定义的死循环中,循环会一直进行。因此,MyThread类的打印语句将被无限执行,而main()方法中的打印语句无法得到执行

8.2.1 继承Thread类创建多线程

如果希望上面的程序中的两个while循环都能够执行,就需要实现多线程

java提供了一个线程类Thread类,通过继承这个类,并重写run方法便可实现多线程,Thread类也提供了一个start方法用于启动新线程,线程启动后虚拟机会自动调用run方法

实例

1 public class Example02 {
2	public static void main(String[] args) {
3		MyThread myThread = new MyThread(); // 创建线程MyThread的线程对象
4		myThread.start(); // 开启线程
5		while (true) { // 通过死循环语句打印输出
6			System.out.println("main()方法在运行");
7		}
8	}
9 }
10 class MyThread extends Thread {
11	public void run() {
12		while (true) { // 通过死循环语句打印输出
13			System.out.println("MyThread类的run()方法在运行");
14		}
15	}
16 }

从运行结果,可以看到两个循环中的语句都有输出,说明该文件实现了多线程

多线程与多线程的区别

在这里插入图片描述

单线程的程序在运行时,会按照代码的调用顺序执行,而在多线程中,main()方法和MyThread类的run()方法却可以同时运行,互不影响,这正是单线程和多线程的区别

8.2.2 实现Runnable接口创建多线程

通过继承Thread类可以实现多线程,但是这种方式有一定的局限性。因为Java只支持单继承,一个类一旦继承了某个父类就无法再继承Thread类,比如学生类Student继承了Person类,就无法通过继承Thread类创建线程

为了克服这种弊端,Thread类提供了另外一个构造方法Thread(Runnable target),其中Runnable是一个接口,它只有一个run方法,当通过Thread(Runnable target)

创建线程对象时,只需为该方法传递一个实现了Runnable接口的实例化对象,这样创建的线程将调用实现了Runnable接口的类中的run()方法作为运行代码,而不需要调用Thread类中的run()方法

实例

1 public class Example03 {
2	public static void main(String[] args) {
3		MyThread myThread = new MyThread(); // 创建MyThread的实例对象
4		Thread thread = new Thread(myThread); // 创建线程对象
5		thread.start();          // 开启线程,执行线程中的run()方法
6		while (true) {
7			System.out.println("main()方法在运行");
8		}
9	}
10 }
11 class MyThread implements Runnable {
12	public void run() {        // 线程的代码段,当调用start()方法时,线程从此处开始执行
13		while (true) {
14			System.out.println("MyThread类的run()方法在运行");
15		}
16	}
17 }

通过应用场景分析

  • 继承Tread类创建多线程:

假设售票厅有四个窗口可发售某日某次列车的100张车票,这时,100张车票可以看做共享资源,四个售票窗口需要创建四个线程。为了更直观显示窗口的售票情况,可以通过Thread的currentThread()方法得到当前的线程的实例对象,然后调用getName()方法可以获取到线程的名称

1 public class Example04 {
2	public static void main(String[] args) {
3		new TicketWindow().start(); // 创建第一个线程对象TicketWindow并开启
4		new TicketWindow().start(); // 创建第二个线程对象TicketWindow并开启
5		new TicketWindow().start(); // 创建第三个线程对象TicketWindow并开启
6		new TicketWindow().start(); // 创建第四个线程对象TicketWindow并开启
7	}
8 }
9 class TicketWindow extends Thread {
10	private int tickets = 100;
11	public void run() {
12		while (true) { // 通过死循环语句打印语句
13			if (tickets > 0) {
14				Thread th = Thread.currentThread(); // 获取当前线程
15				String th_name = th.getName(); // 获取当前线程的名字
16				System.out.println(th_name + " 正在发售第 " + tickets-- + " 张票 ");
17			}
18		}
19	}
20 }

从运行结果可以看出,每张票都被打印了四次。出现这样现象的原因是四个线程没有共享100张票,而是各自出售了100张票。在程序中创建了四个TicketWindow对象,就等于创建了四个售票程序,每个程序中都有100张票,每个线程在独立地处理各自的资源。需要注意的是,上述程序中每个线程都有自己的名字,主线程默认的名字是“main”,用户创建的第一个线程的名字默认为“Thread-0”,第二个线程的名字默认为“Thread-1”,以此类推。如果希望指定线程的名称,可以通过调用setName(String name)方法为线程设置名称

由于现实中铁路系统的票资源是共享的,因此上面的运行结果显然不合理

为了保证资源共享,在程序中只能创建一个售票对象,然后开启多个线程去运行同一个售票对象的售票方法。简单来说就是四个线程运行同一个售票程序,这时就需要用到多线程的第二种实现方式

  • 通过继承Runnable接口创建多线程:
1 public class Example05 {
2	public static void main(String[] args) {
3		TicketWindow tw = new TicketWindow(); // 创建TicketWindow实例对象tw
4		new Thread(tw, "窗口1").start(); // 创建线程对象并命名为窗口1,开启线程
5		new Thread(tw, "窗口2").start(); // 创建线程对象并命名为窗口2,开启线程
6		new Thread(tw, "窗口3").start(); // 创建线程对象并命名为窗口3,开启线程
7		new Thread(tw, "窗口4").start(); // 创建线程对象并命名为窗口4,开启线程
8	}
9 }
10 class TicketWindow implements Runnable {
11	private int tickets = 100;
12	public void run() {
13		while (true) {
14			if (tickets > 0) {
15				Thread th = Thread.currentThread(); // 获取当前线程
16				String th_name = th.getName(); // 获取当前线程的名字
17				System.out.println(th_name + " 正在发售第 " + tickets-- + " 张票 ");
18			}
19		}
20	}
21 }

创建了一个TicketWindow对象并实现了Runnable接口,然后在mian方法中创建了四个线程,在每个线程上都去调用这个TicketWindow对象中的run()方法,这样就可以确保四个线程访问的是同一个tickets变量,共享100张车票

(这就是所谓线程间通信访问的是同一个变量)

实现Runnable接口相对于继承Thread接口1来说具有的优势

  • 适合多个相同程序代码的线程去处理同一个资源的情况,把线程同程序代码、数据有效的分离,很好的体现了面向对象语言的设计思想
  • 可以避免java的单继承带来的局限性,在开发中时常会碰到一个子类继承于一个父类,由于一个子类不能同时有两个父类,所以不能通过继承Thread类的方式创建多线程,只能采用实现Runnable接口的方式

多学一点

JDK8简化了多线程的创建方法,在创建线程时指定线程要调用的方法

Thread t = new Thread(()->{

	//main方法代码

});

8.3 綫程的生命周期及狀態轉換

生命周期简介:在Java中,任何对象都有生命周期,线程也不例外,它也有自己的生命周期。当Thread对象创建完成时,线程的生命周期便开始了。当run()方法中代码正常执行完毕或者线程抛出一个未捕获的异常(Exception)或者错误(Error)时,线程的生命周期便会结束

线程整个生命周期可以分为五个阶段,分别是新建状态(New)、就绪状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)和死亡状态(Terminated)

在这里插入图片描述

线程的不同状态表明了线程当前正在进行的活动

在上图中,单箭头表示状态只能单向的转换(例如·只能从新建状态转换到就绪状态),双箭头表示两种状态可以互相转换(例如就绪状态和运行状态之间)

线程生命周期中的五种状态

  1. 新建状态(New):创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,没有表现出任何线程的动态特征
  2. 就绪状态(Runnable):当线程对象调用了start()方法后,该线程就进入就绪状态。处于就绪状态的线程位于线程队列中,此时它只是具备了运行的条件,能否获得CPU的使用权并开始运行,还需要等待系统的调度(等待CPU为其分配时间)
  3. 运行状态(Running):如果处于就绪状态的线程获得了CPU的使用权,并开始执行run()方法中的线程执行体,则该线程处于运行状态。一个线程启动后,它可能不会一直处于运行状态,当运行状态的线程使用完系统分配的时间后,系统就会剥夺该线程占用的CPU资源,让其他线程获得执行的机会。需要注意的是,只有处于就绪状态的线程才可能转换到运行状态
  4. 阻塞状态(Blocked):一个正在执行的线程在某些特殊情况下,如被人为挂起或执行耗时的输入/输出操作时,会让出CPU的使用权并暂时中止自己的执行,进入阻塞状态。线程进入阻塞状态后,就不能进入排队队列。只有当引起阻塞的原因被消除后,线程才可以转入就绪状态
  5. 死亡状态(Terminated):当线程调用stop()方法或run()方法正常执行完毕后,或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他状态

线程由运行状态传唤成阻塞状态的原因,以及如何从阻塞状态转换成就绪状态

  • 当线程试图获取某个对象的同步锁时,如果该锁被其他线程所持有,则当前线程会进入阻塞状态,如果想从阻塞状态进入就绪状态必须得获取到其他线程所持有的锁
  • 当线程调用了一个阻塞式的IO方法时,该线程就会进入阻塞状态,如果想进入就绪状态就必须要等到这个阻塞的IO方法返回
  • 当线程调用了某个对象的wait()方法时,也会使线程进入阻塞状态,如果想进入就绪状态就需要使用notify()方法唤醒该线程
  • 当线程调用了Thread的sleep(long millis)方法时,也会使线程进入阻塞状态,在这种情况下,只需等到线程睡眠的时间到了以后,线程就会自动进入就绪状态
  • 当在一个线程中调用了另一个线程的join()方法时,会使当前线程进入阻塞状态,在这种情况下,需要等到新加入的线程运行结束后才会结束阻塞状态,进入就绪状态

注意:线程从阻塞状态只能进入就绪状态,而不能直接进入运行状态,也就是说结束阻塞的线程需要重新进入可运行池中,等待系统的调度

8.4 綫程的調度

线程调度的两种模式:分时调度模式、抢占式调度模式

  • 分时调度模式:指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU的时间片
  • 抢占式调度模式:指让可运行池中优先级高的线程优先占用CPU,而对于优先级相同的线程,随机选择一个线程使其占用CPU,当它失去了CPU的使用权后,再随机选择其他线程获取CPU使用权

Java虚拟机默认采用抢占式调度模型,通常情况下程序员不需要去关心它,但在某些特定的需求下需要改变这种模式,由程序自己来控制CPU的调度

8.4.1 线程的优先级

线程优先级简介:在应用程序中,如果要对线程进行调度,最直接的方式就是设置线程的优先级。优先级越高的线程获得CPU执行的机会越大,而优先级越低的线程获得CPU执行的机会越小。线程的优先级用1~10之间的整数来表示,数字越大优先级越高

除了可以直接使用数字表示线程的优先级,还可以使用Thread类中提供的三个静态常量表示线程的优先级

Thread****类的静态常量功能描述
static int MAX_PRIORITY表示线程的最高优先级,值为10
static int MIN_PRIORITY表示线程的最低优先级,值为1
static int NORM_PRIORITY表示线程的普通优先级,值为5

改变线程优先级:main线程具有普通优先级。然而线程优先级不是固定不变的,可以通过Thread类的setPriority(int newPriority)方法进行设置,setPriority()方法中的参数newPriority接收的是1~10之间的整数或者Thread类的三个静态常量

实例

//定义MaxPriority实现Runnable接口
class MaxPriority implements Runnable{
    public void run(){
        for(int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"正在输出"+i);
        }
}
class MinPriority implements Runnable{
    public void run(){
        for(int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"正在输出"+i);
        }
    }
}
public static void main(String[] args){
    //创建两个线程
    Thread minPriority = new Thread(new MinPriority(),"优先级较低的线程");
    Thread maxPriority = new Thread(new MaxPriority(),"优先级较高的线程");
    //设置两个线程的优先级
    minPriority.setPriority(Thread.MIN_PRIORITY);
    maxPriority.setPriority(Thread.MAX_PRIORITY);
    //开启两个线程
    maxPriority.start();
    minPriority.start();
}

注意:虽然Java中提供了10个线程优先级,但是这些优先级需要操作系统的支持,不同的操作系统对优先级的支持是不一样的,不会和Java中线程优先级一一对应,因此,在设计多线程应用程序时,其功能的实现一定不能依赖于线程的优先级,而只能把线程优先级作为一种提高程序效率的手段

8.4.2 线程休眠

线程休眠简介:如果希望人为地控制线程,使正在执行的线程暂停,将CPU让给别的线程,这时可以使用静态方法sleep(long millis),该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。当前线程调用sleep(long millis)方法后,在指定时间(单位毫秒)内该线程是不会执行的,这样其他的线程就可以得到执行的机会了

sleep(long millis)方法声明会抛出InterruptedException异常,因此在调用该方法时应该捕获异常,或者声明抛出该异常

实例

package Eight;

public class test04 {
	//定义SleepThread类实现Runnable接口
	public static class SleepThread implements Runnable{
		@Override
		public void run() {
			// TODO Auto-generated method stub
			for(int i=1;i<=10;i++) {
				if(i==3) {
					try {
						Thread.sleep(2000);
					}catch(InterruptedException e) {
						e.printStackTrace();
					}
				}
				System.out.println("SleepThread线程正在输出"+i);
				try {
					Thread.sleep(500);
				}catch(Exception e) {
					e.printStackTrace();
				}
			}
		}
		
	}
	public static void main(String[]args) throws InterruptedException {
		//创建线程
		new Thread(new SleepThread()).start();
		for(int i=1;i<=10;i++) {
			if(i==5) {
				Thread.sleep(2000);
			}
			System.out.println("主线程正在输出"+i);
			Thread.sleep(500);
		}
	}
}

sleep()是静态方法,只能控制当前正在运行的线程休眠,而不能控制其他线程休眠。当休眠时间结束后,线程就会返回到就绪状态,而不是立即开始运行

8.4.3 线程让步

线程让步简介:所谓的线程让步是指正在执行的线程,在某些情况下将CPU资源让给其他线程执行,线程让步可以通过yield()方法来实现,该方法和sleep()方法有点相似,都可以让当前正在运行的线程暂停,区别在于yield()方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。当某个线程调用yield()方法之后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会

实例

1 // 定义YieldThread类继承Thread类
2 class YieldThread extends Thread {
3     // 定义一个有参的构造方法
4	public YieldThread(String name) { 
5		super(name); // 调用父类的构造方法
6	}
7	public void run() {
8		for (int i = 0; i < 6; i++) {
9			System.out.println(Thread.currentThread().getName() + "---" + i);
10			if (i == 3) {
11				System.out.print("线程让步:");
12				Thread.yield(); // 线程运行到此,作出让步
13			}
14		}
15	}
16 }
17 public class Example08 {
18	public static void main(String[] args) {
19         // 创建两个线程
20		Thread t1 = new YieldThread("线程A");
21		Thread t2 = new YieldThread("线程B");
22         // 开启两个线程
23		t1.start();
24		t2.start();
25	}
26 }

8.4.4 线程插队

线程插队简介:在Thread类中也提供了一个join()方法来实现这个“功能”。当在某个线程中调用其他线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后它才会继续运行

package Eight;

public class test05 {
	//创建EmergencyThread类实现Runnable接口
	public static class EmergencyThread implements Runnable{
		@Override
		public void run() {
			// TODO Auto-generated method stub
			for(int i=1;i<6;i++) {
				System.out.println(Thread.currentThread().getName()+"输入"+i);
				try {
					Thread.sleep(500);
				}catch(InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
	public static void main(String[]args) throws InterruptedException {
		//创建线程
		Thread t = new Thread(new EmergencyThread(),"紧急线程");
		t.start();
		for(int i=1;i<6;i++) {
			System.out.println(Thread.currentThread().getName()+"输入"+i);
			if(i==2) {
				t.join();
			}
			Thread.sleep(500);
		}
	}
}

在上述代码中,在第4行代码中开启了一个线程t,两个线程的循环体中都调用了Thread的sleep(500)方法,以实现两个线程的交替执行。当main线程中的循环变量为2时,调用t线程的join()方法,这时,t线程就会“插队”优先执行。从运行结果可以看出,当main线程输出2以后,线程一就开始执行,直到线程一执行完毕,main线程才继续执行

8.5 多綫程同步

多线程同步简介:多线程的并发执行可以提高程序的效率,但是,当多个线程去访问同一个资源时,也会引发一些安全问题。例如,当统计一个班级的学生数目时,如果有同学进进出出,则很难统计正确。为了解决这样的问题,需要实现多线程的同步,即限制某个资源在同一时刻只能被一个线程访问

8.5.1 线程安全问题

前面讲解的售票案例,极有可能碰到“意外”情况,如一张票被打印多次,或者打印出的票号为0甚至负数。这些“意外”都是由多线程操作共享资源ticket所导致的线程安全问题。接下来对售票案例进行修改,模拟四个窗口出售10张票,并在售票的代码中使用sleep()方法,令每次售票时线程休眠10毫秒

package Eight;

public class test06 {
	//定义SaleThread类实现Runable接口
	public static class SaleThread implements Runnable{
		private int tickets = 10;
		public void run() {
			while(tickets>0) {
				try {
					Thread.sleep(10);
				}catch(InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName()+"---卖出的票:"+tickets--);
			}
		}
	}
	public static void main(String[] args) {
		//创建SaleThread对象
		SaleThread st = new SaleThread();
		//创建并启动四个线程
		new Thread(st,"线程1").start();
		new Thread(st,"线程2").start();
		new Thread(st,"线程3").start();
		new Thread(st,"线程4").start();
	}
}

在运行结果中,最后打印售出的票出现了0和负数,这种现象是不应该出现的,因为售票程序中只有当票号大于0时才会进行售票。运行结果中之所以出现了负数的票号是因为多线程在售票时出现了安全问题

安全问题的来源:在售票程序的while循环中添加了sleep()方法,由于线程有延迟,当票号减为1时,假设线程1此时出售1号票,对票号进行判断后,进入while循环,在售票之前通过sleep()方法让线程休眠,这时线程二会进行售票,由于此时票号仍为1,因此线程二也会进入循环,同理,四个线程都会进入while循环,休眠结束后,四个线程都会进行售票,这样就相当于将票号减了四次,结果中出现了0、-1、-2这样的票号

8.5.2 同步代码块

线程安全问题其实就是由多个线程同时处理共享资源所导致的,要想解决线程安全问题,必须得保证在任何时刻只能有一个线程访问共享资源

为了实现这种限制,Java中提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放在一个使用synchronized关键字修饰的代码块中,这个代码块被称作同步代码块

synchronized(lock){
操作共享资源代码块
}

上面的格式中,lock是一个锁对象,它是同步代码块的关键。当某一个线程执行同步代码块时,其他线程将无法执行当前同步代码块,会发生阻塞,等当前线程执行完同步代码块后,所有的线程开始抢夺线程的执行权,抢到执行权的线程将进入同步代码块,执行其中的代码。循环往复,直到共享资源被处理完为止。这个过程就好比一个公用电话亭,只有前一个人打完电话出来后,后面的人才可以打

同步代码块的锁是自己定义的任意类型的对象

实例

1 //定义Ticket1类继承Runnable接口
2 class Ticket1 implements Runnable {
3	private int tickets = 10; // 定义变量tickets,并赋值10
4	Object lock = new Object(); // 定义任意一个对象,用作同步代码块的锁
5	public void run() {
6		while (true) {
7			synchronized (lock) { // 定义同步代码块
8				try {
9					Thread.sleep(10); // 经过的线程休眠10毫秒
10				} catch (InterruptedException e) {
11					e.printStackTrace();
12				}
13				if (tickets > 0) {
14					System.out.println(Thread.currentThread().getName()
15							+ "---卖出的票" + tickets--);
16				} else { // 如果 tickets小于0,跳出循环
17					break;
18				}
19			}
20		}
21	}
22 }
23public class Example11 {
24	public static void main(String[] args) {
25		Ticket1 ticket = new Ticket1(); // 创建Ticket1对象
26		// 创建并开启四个线程
27		new Thread(ticket, "线程一").start();
28		new Thread(ticket, "线程二").start();
29		new Thread(ticket, "线程三").start();
30		new Thread(ticket, "线程四").start();
31	}
32 }

运行结果中并没有出现线程二和线程三售票的语句,出现这样的现象是很正常的,因为线程在获得锁对象时有一定的随机性,在整个程序的运行期间,线程二和线程三始终未获得锁对象,所以未能显示它们的输出结果

同步代码块中的锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是唯一的。“任意”说的是共享锁对象的类型。锁对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁,每个锁都有自己的标志位,这样线程之间便不能产生同步的效果

8.5.3 同步方法

同步方法简介:同步代码块可以有效解决线程的安全问题,当把共享资源的操作放在synchronized定义的区域内时,便为这些操作加了同步锁。在方法前面同样可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能

synchronized 返回值类型 方法名([参数1,…]){}
被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行该方法(电话亭效应)

实例

1 // 定义Ticket1类实现Runnable接口
2 class Ticket1 implements Runnable {
3	private int tickets = 10;
4	public void run() {
5		while (true) {
6			saleTicket(); // 调用售票方法
7			if (tickets <= 0) { 
8				break;
9			}
10		}
11	}
12 // 定义一个同步方法saleTicket()
13	private synchronized void saleTicket() {
14		if (tickets > 0) {
15			try {
16				Thread.sleep(10); // 经过的线程休眠10毫秒
17			} catch (InterruptedException e) {
18				e.printStackTrace();
19			}
20			System.out.println(Thread.currentThread().getName() + "---卖出的票"
21					+ tickets--);
22		}
23	}
24 }
25 public class Example12 {
26	public static void main(String[] args) {
27		Ticket1 ticket = new Ticket1(); // 创建Ticket1对象
28         // 创建并开启四个线程
29		new Thread(ticket,"线程一").start();
30		new Thread(ticket,"线程二").start();
31		new Thread(ticket,"线程三").start();
32		new Thread(ticket,"线程四").start();
33	}
34 }

同步代码块的锁是自己定义的任意类型的对象,同步方法也有锁,它的锁就是当前调用该方法的对象,也就是this指向的对象。这样做的好处是,同步方法被所有线程所共享,方法所在的对象相对于所有线程来说是唯一的,从而保证了锁的唯一性。当一个线程执行该方法时,其他的线程就不能进入该方法中,直到这个线程执行完该方法为止。从而达到了线程同步的效果

注意Java中静态方法的锁是该方法所在类的class对象,该对象在装载该类时自动创建,该对象可以直接用类名.class的方式获取

同步代码块和同步方法的弊端:同步解决了多个线程同时访问共享数据时的线程安全问题,只要加上同一个锁,在同一时间内只能有一条线程执行。但是线程在执行同步代码时每次都会判断锁的状态,非常消耗资源,效率较低

8.5.4 死锁问题

死锁问题简介:两个线程在运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象称为死锁

实例

package Eight;

public class test07 {
	//创建DeadLock类实现Runnable接口
	public static class DeadLock implements Runnable{
		static Object chopsticks = new Object();
		static Object knifeandfork = new Object();
		private boolean flag;
		//定义有参构造方法
		DeadLock(boolean flag){
			this.flag=flag;
		}
		@Override
		public void run() {
			// TODO Auto-generated method stub
			if(flag) {
				while(true) {
					// chopsticks锁对象上的同步代码块
					synchronized(chopsticks) {
						System.out.println(Thread.currentThread().getName()+"---if---chopsticks");
						// knifeAndFork锁对象上的同步代码
						synchronized(knifeandfork) {
							System.out.println(Thread.currentThread().getName()+ "---if---knifeAndFork"); 

						}
					}
				}
			}else {
				while(true) {
					// knifeAndFork锁对象上的同步代码
					synchronized(knifeandfork) {
						System.out.println(Thread.currentThread().getName()+"---if---knifeandfork");
						// chopsticks锁对象上的同步代码块
						synchronized(chopsticks) {
							System.out.println(Thread.currentThread().getName()+ "---if---chopsticks"); 

						}
					}
				}
			}
		}
	}
		public static void main(String[] args) {
		// 创建两个DeadLockThread对象
			DeadLock d1 = new DeadLock(true);
			DeadLock d2 = new DeadLock(false);
	      // 创建并开启两个线程
			new Thread(d1, "Chinese").start();   // 创建开启线程Chinese
			new Thread(d2, "American").start(); // 创建开启线程American
		}
}

【案例8-1】 龟兔赛跑

众所周知的“龟兔赛跑”故事,兔子因为太过自信,比赛中途休息而导致乌龟赢得了比赛.本案例要求编写一个程序模拟龟兔赛跑,乌龟的速度为1米/1500毫秒,兔子的速度为5米/500毫秒,等兔子跑到第700米时选择休息10000毫秒,结果乌龟赢得了比赛。

【案例8-2】 Svip优先办理服务

在日常工作生活中,无论哪个行业都会设置一些Svip用户,Svip用户具有超级优先权,在办理业务时,Svip用户具有最大的优先级。
本案例要求编写一个模拟Svip优先办理业务的程序,在正常的业务办理中,插入一个Svip用户,优先为Svip用户办理业务。本案例在实现时,可以通过多线程实现。

【案例8-3】 模拟银行存取钱

在银行办理业务时,通常银行会开多个窗口,客户排队等候,窗口办理完业务,会呼叫下一个用户办理业务。本案例要求编写一个程序模拟银行存取钱业务办理。假如有两个用户在存取钱,两个用户分别操作各自的账户,并在控制台打印存取钱的数量以及账户的余额。

【案例8-4】 模拟12306售票

互联网为人们带来了巨大的方便,越来越多的事情都可以在互联网办理,很大程度上节约了成本。例如,12306售票系统,乘客可以在该系统抢购自己想要的车票。本案例要求编写一个模拟12306售票的系统,要求如下:假设需要抢票十次才可以抢到一张票,会员需要300毫秒抢一次,普通用户需要800毫秒抢一次。

【案例8-5】 小朋友就餐

一圆桌前坐着5位小朋友,两个人中间有一只筷子,桌子中央有面条。小朋友边吃边玩,当饿了的时候拿起左右两只筷子吃饭,必须拿到两只筷子才能吃饭。但是,小朋友在吃饭过程中,可能会发生5个小朋友都拿起自己右手边的筷子,这样每个小朋友都因缺少左手边的筷子而没有办法吃饭。本案例要求编写一个程序解决小朋友就餐问题,使每个小朋友都能成功就餐。

这篇关于阿毛的Java基础学习笔记 --- (8)多线程的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!