第三十二章 多线程、单例模式
提纲
- 32.1 多线程
- 32.1.1 什么是并行和并发
- 32.1.2 什么是多进程
- 31.1.3 什么是多线程
- 31.1.4 实现线程的两种方式
- 31.1.5 线程同步
- 31.1.6 线程的生命周期
- 32.2 单例模式
- 32.2.1 什么是单例模式
- 32.2.2 为什么使用单例模式
- 32.2.3 单例模式的特点
- 32.2.4 单例模式的写法
- 32.2.5 总结
32.1 多线程
-
32.1.1 什么是并行和并发
- 并行:指在某一个时间段内同时运行多个程序。
- 并发:指在某一个时间点同时运行多个程序。
-
32.1.2 什么是多进程:以Windows系统为例,Windows操作系统是多任务操作系统,它以进程为单位。系统可以分配给每个进程一段有限的使用CPU的时间(也可以称为CPU时间片),CPU在这段时间中执行某个进程,然后下一个时间片又跳至另一个进程中去执行。由于CPU转换较快,所以使得每个进程好像是同时执行一样。所以,Windows系统中,进程是并行的,即在某一个时间点只能执行一个进程。
-
31.1.3 什么是多线程:一个线程是进程中的执行流程,一个进程可以同时包含多个线程,多个线程共享一个进程的资源。每个线程也可以得到一小段程序的执行时间,但是多线程也是并行而不是并发的,所以一个时间点也只能运行一个线程。
-
31.1.4 实现线程的两种方式:继承java.lang.Thread类与实现java.lang.Runnable接口。
- 继承java.lang.Thread类
- 构造方法:
- Thread():分配新的 Thread 对象。
- Thread(String name):创建一个名为name的线程对象。
- Thread(Runnable target):通过Runnable接口分配新的 Thread 对象。
- Thread(Runnable target, String name):通过Runnable接口创建一个名为name的线程对象。
- 常用方法:
- run():如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。返回值:void。
- start():使该线程开始执行;Java 虚拟机调用该线程的 run 方法。返回值:void。
- sleep(long millis):在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。返回值:static void。
- join():等待该线程终止。返回值:void。
- join(long millis):等待该线程终止的时间最长为 millis 毫秒。返回值:void。
- interrupt():中断线程。返回值:void。
- interrupted() :测试当前线程是否已经中断。返回值:static boolean。
- currentThread() :返回对当前正在执行的线程对象的引用。返回值:static Thread。
- getId():返回该线程的标识符。返回值:long。
- getName():返回该线程的名称。返回值:String。
- getPriority():返回线程的优先级。返回值:int。
- setName(String name):改变线程名称,使之与参数 name 相同。返回值:void。
- setPriority(int newPriority):更改线程的优先级。返回值:void。
- notify():唤醒在此对象监视器上等待的单个线程。返回值:void。
- notifyAll(): 唤醒在此对象监视器上等待的所有线程。返回值:void。
- wait():在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。返回值:void。
- wait(long timeout):在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待。返回值:void。
- 字段摘要
- MAX_PRIORITY:线程可以具有的最高优先级。变量类型:static int。
- MIN_PRIORITY:线程可以具有的最低优先级。变量类型:static int。
- NORM_PRIORITY:分配给线程的默认优先级。变量类型:static int。
- 使用步骤举例
-
实现步骤:
- 继承Thread 类
- 重写run方法 在run方法写上自己想执行的代码 因为线程执行的代码都在run方法里面
- 创建线程类对象,
- 这里是(MyThread mt = new MyThread();)
- 然后将mt交给Thread(Thread tr = new Thread(mt);)
- 启动线程
-
实例:
//创建线程 public class MyThread extends Thread{ public void run() { Thread th = Thread.currentThread();//获取当前线程 //Thread.currentThread().getName()获取当前线程的名字 System.out.println("当前线程的名字:"+th.getName()); th.setName("MyThread的线程");//设置当前线程的名称 System.out.println("设置线程名字后:"+th.getName()); System.out.println("线程的标识符:"+th.getId()); System.out.println("线程的优先级:"+th.getPriority()); System.out.println("最高优先级:"+Thread.MAX_PRIORITY); System.out.println("最低优先级:"+Thread.MIN_PRIORITY); System.out.println("默认优先级:"+Thread.NORM_PRIORITY); for (int i = 0; i < 10; i++) { try { Thread.sleep(500);//线程等待,参数传毫秒数 System.out.print(i+" "); } catch (InterruptedException e) { e.printStackTrace(); } } } } //测试线程 public static void main(String[] args) { MyThread mt = new MyThread(); mt.start();//启动线程 } //执行结果 当前线程的名字:Thread-0 设置线程名字后:MyThread的线程 线程的标识符:11 线程的优先级:5 最高优先级:10 最低优先级:1 默认优先级:5 0 1 2 3 4 5 6 7 8 9
-
- 构造方法:
- 实现java.lang.Runnable接口
-
里面只有一个方法:run():使用实现接口 Runnable 的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用对象的 run 方法。
-
使用步骤:
- 实现Runnable接口
- 重写run方法
- 创建线程类对象,这里是(MyRunnable mr = new MyRunnable();)
- 创建Thread类对象,然后将(这里是mr)参数传入。(Thread tr = new Thread(mr);)
- 启动线程
-
实例
//重新编写继承Thread类 public class MyThread extends Thread{ //实现线程 分两种 //线程在一个时间点只能运行一个 //第一种(步骤): //1、继承Thread 类 //2、重写run方法 在run方法写上自己想执行的代码 因为线程执行的代码都在run方法里面 //3、创建线程类对象,这里是(MyThread mt = new MyThread();) //4、启动线程 //第二种: //1、实现Runnable接口 //2、重写run方法 //3、a.创建线程类对象,这里是(MyRunnable mr = new MyRunnable();) // b.创建Thread类对象,然后将(这里是mr)参数传入。(Thread tr = new Thread(mr);) //4、启动线程 public void run() { Thread th = Thread.currentThread();//获取当前线程 //Thread.currentThread().getName()获取当前线程的名字 System.out.println("当前线程的名字:"+th.getName()); th.setName("MyThread的线程");//设置当前线程的名称 System.out.println(th.getName() + ":" + "设置线程名字后:"+th.getName()); System.out.println(th.getName() + ":" + "线程的标识符:"+th.getId()); System.out.println(th.getName() + ":" + "线程的优先级:"+th.getPriority()); System.out.println(th.getName() + ":" + "最高优先级:"+Thread.MAX_PRIORITY); System.out.println(th.getName() + ":" + "最低优先级:"+Thread.MIN_PRIORITY); System.out.println(th.getName() + ":" + "默认优先级:"+Thread.NORM_PRIORITY); for (int i = 0; i < 10; i++) { try { Thread.sleep(1000);//线程等待,参数传毫秒数 System.out.println(th.getName() + ":" + i); } catch (InterruptedException e) { e.printStackTrace(); } } } } //编写实现Runnable接口类 public class MyRunnable implements Runnable { public void run() { Thread th = Thread.currentThread();// 获取当前线程 // Thread.currentThread().getName()获取当前线程的名字 System.out.println("当前线程的名字:" + th.getName()); th.setName("MyRunnable的线程");// 设置当前线程的名称 System.out.println(th.getName() + ":" + "设置线程名字后:" + th.getName()); System.out.println(th.getName() + ":" + "线程的标识符:" + th.getId()); System.out.println(th.getName() + ":" + "线程的优先级:" + th.getPriority()); System.out.println(th.getName() + ":" + "最高优先级:" + Thread.MAX_PRIORITY); System.out.println(th.getName() + ":" + "最低优先级:" + Thread.MIN_PRIORITY); System.out.println(th.getName() + ":" + "默认优先级:" + Thread.NORM_PRIORITY); for (int i = 10; i < 20; i++) { try { Thread.sleep(1000); System.out.println(th.getName() + ":" + i); } catch (Exception e) { e.printStackTrace(); } } } } //执行结果 当前线程的名字:Thread-0 MyThread的线程:设置线程名字后:MyThread的线程 MyThread的线程:线程的标识符:11 MyThread的线程:线程的优先级:5 MyThread的线程:最高优先级:10 MyThread的线程:最低优先级:1 MyThread的线程:默认优先级:5 当前线程的名字:Thread-1 MyRunnable的线程:设置线程名字后:MyRunnable的线程 MyRunnable的线程:线程的标识符:12 MyRunnable的线程:线程的优先级:5 MyRunnable的线程:最高优先级:10 MyRunnable的线程:最低优先级:1 MyRunnable的线程:默认优先级:5 MyThread的线程:0 MyRunnable的线程:10 MyThread的线程:1 MyRunnable的线程:11 MyThread的线程:2 MyRunnable的线程:12 MyThread的线程:3 MyRunnable的线程:13 MyThread的线程:4 MyRunnable的线程:14 MyThread的线程:5 MyRunnable的线程:15 MyThread的线程:6 MyRunnable的线程:16 MyThread的线程:7 MyRunnable的线程:17 MyRunnable的线程:18 MyThread的线程:8 MyThread的线程:9 MyRunnable的线程:19
-
- 继承java.lang.Thread类
-
31.1.5 线程同步:synchronized。同步关键字,修饰方法或者代码块,保证这个代码块或者方法每次只有一个线程在执行。
-
同步方法举例:
//取钱类 public class Account { static double money = 1000000; /** * 取钱 * @param qMoney 每次取多少钱 */ public synchronized void getMoney(double qMoney){ money -= qMoney; } } //取钱的线程 public class AccountThread extends Thread{ private Account account; public AccountThread(Account account) { this.account = account; } public void run() { //取50000次 for (int i = 0; i < 50000; i++) { account.getMoney(10); } } } //测试类 public class AccountTest { public static void main(String[] args) { Account account = new Account(); AccountThread at1 = new AccountThread(account); AccountThread at2 = new AccountThread(account); at1.start(); at2.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("剩下的钱:"+Account.money); } }
-
同步代码块举例:
-
举例1:
public class ThreadSafeTest extends Thread{ public void run() { //其中this代表创建的本线程对象 synchronized (this) { for (int i = 0; i < 2; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":"+i); } } } public static void main(String[] args) { ThreadSafeTest tst = new ThreadSafeTest(); Thread t1 = new Thread(tst, "Thread1"); Thread t2 = new Thread(tst, "Thread2"); t1.start(); t2.start(); } } //执行结果 Thread1:0 Thread1:1 Thread2:0 Thread2:1
-
结论:执行结果中,可能是Thread1先执行,也可能是Thread2先执行,所以线程之间也是在抢CPU的资源。谁先抢到谁先执行。
- 举例2:如果将调用方式修改一下public static void main(String[] args) { ThreadSafeTest tst1 = new ThreadSafeTest(); ThreadSafeTest tst2 = new ThreadSafeTest(); Thread t1 = new Thread(tst1, "Thread1"); Thread t2 = new Thread(tst2, "Thread2"); t1.start(); t2.start(); } //执行结果 Thread2:0 Thread1:0 Thread1:1 Thread2:1 结论:如果创建了两个ThreadSafeTest则会按照两个不同的线程进行同步,即例中的this是不同的。所以会同时运行。
-
-
31.1.6 线程的生命周期
- 线程的生命周期图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kx80lOb0-1571042519388)(课上图片/线程的生命周期.png)] - 操作线程的常用方法:
-
线程的休眠:sleep(long millis)方法。millis为毫秒数,当调用这个方法后会使线程中的代码经过millis毫秒之后继续执行后续代码。
-
线程等待:wait()方法。即先将线程暂时挂起。然后让别的线程执行。
-
线程唤醒:notify()方法。即将挂起的线程随机唤醒一个,成为就绪状态,继续执行。
//例:使用线程实现功能:在程序运行后,要求达到: //1.如果会议室为未满,则继续让人进去。 //2.如果会议室满了之后,让人从会议室走出。3.无限重复1和2的过程。 /** * 人类 */ public class Person { static int pCount = 0;//会议室当前人数,会议室能坐20人 /** * 人入场 */ public synchronized void in(){ if (pCount < 20) { pCount += 1;//每次进来一个人 System.out.println("会议室未坐满,继续入场:"+pCount); } else { System.out.println("会议室人数已满,开会了N个小时,准备离场"); try { this.notify();//【随机】通知另一个线程 this.wait();//挂起,线程等待 } catch (InterruptedException e) { e.printStackTrace(); } } } /** * 人离场 */ public synchronized void leave(){//synchronized同步的关键字 if (pCount > 0) { pCount -= 1; System.out.println("会已开完,开始离场:"+pCount); } else { System.out.println("人员已全部离场,准备开始进场"); try { this.notify(); this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } /** * 入场的线程 */ public class InThread extends Thread{ private Person person; public InThread(Person person) { this.person = person; } public void run() { while(true){ try { Thread.sleep(1000); person.in();//入场方法 } catch (InterruptedException e) { e.printStackTrace(); } } } } /** * 离场的线程 */ public class LeaveThread extends Thread{ private Person person; public LeaveThread(Person person) { this.person = person; } public void run() { while(true){ try { person.leave();//为了让离场线程先运行 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } /** * 测试类测试线程 */ public class PersonTest { public static void main(String[] args) { Person person = new Person(); InThread it = new InThread(person); it.start(); LeaveThread lt = new LeaveThread(person); lt.start(); } }
-
线程的加入:join()方法:当某个线程使用join()方法加入到另一个线程时,另一个线程会等待该线程执行完毕后再继续执行。
//创建线程ThreadA public class ThreadA extends Thread{ private ThreadB threadB; public ThreadA(ThreadB threadB) { this.threadB = threadB; } public void run() { for (int i = 0; i < 5; i++) { try { Thread.sleep(1000); System.out.println(Thread.currentThread().getName()+":"+i); if (i == 2) {//i=2时,加入B线程 threadB.start(); threadB.join();//在打印一次后加入ThreadB线程 } } catch (InterruptedException e) { e.printStackTrace(); } } } } //创建线程ThreadB public class ThreadB extends Thread{ public void run() { Thread.currentThread().setName("ThreadB"); for (int i = 'A'; i < 'F'; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":"+(char)i); } } } //测试 public class TestAB { public static void main(String[] args) { Thread threadA = new Thread(new ThreadA(new ThreadB()), "threadA"); threadA.start(); } }
-
线程的中断:interrupt()方法
public class InterruptedThread extends Thread { public void run() { for (int i = 0; i < 5; i++) { try { System.out.println(Thread.currentThread().getName()+":"+i); Thread.sleep(1000); } catch (InterruptedException e) { System.out.println("当前线程被中断"); break; } } } public static void main(String[] args) { InterruptedThread it = new InterruptedThread(); Thread thread = new Thread(it, "it"); thread.start(); thread.interrupt(); } } //执行结果 it:0 当前线程被中断
-
线程的优先级(注意:优先级越大,最先的几率越大!):
//线程 public class PriorityThread implements Runnable{ @Override public void run() { try { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName()+":"+i); Thread.sleep(1000); } } catch (InterruptedException e) { e.printStackTrace(); } } } //测试类 public class PriorityTest { public static void setPriority(String threadName, int priority, Thread t){ t.setPriority(priority); t.setName(threadName); t.start(); } public static void main(String[] args) { Thread threadA = new Thread(new PriorityThread()); Thread threadB = new Thread(new PriorityThread()); Thread threadC = new Thread(new PriorityThread()); Thread threadD = new Thread(new PriorityThread()); PriorityTest.setPriority("threadA", Thread.MAX_PRIORITY, threadA); PriorityTest.setPriority("threadB", Thread.MIN_PRIORITY, threadB); PriorityTest.setPriority("threadC", Thread.NORM_PRIORITY, threadC); PriorityTest.setPriority("threadD", 3, threadD); } }
-
- 线程的生命周期图
32.2 单例模式
- 32.2.1 什么是单例模式:Java中单例(Singleton)模式是一种广泛使用的设计模式。单例模式的主要作用是保证在Java程序中,某个类只有一个实例存在。一些管理器和控制器常被设计成单例模式。
- 32.2.2 为什么使用单例模式: 单例模式有很多好处,它能够避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间;能够避免由于操作多个实例导致的逻辑错误。如果一个对象有可能贯穿整个应用程序,而且起到了全局统一管理控制的作用,那么单例模式也许是一个值得考虑的选择。
- 32.2.3 单例模式的特点:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
- 32.2.4 单例模式的写法:单例模式有很多种写法,大部分写法都或多或少有一些不足。下面将分别对这几种写法进行介绍:
- 饿汉模式(常用)
-
代码:
public class Singleton{ private static Singleton instance = new Singleton(); private Singleton(){ } public static Singleton newInstance(){ return instance; } }
-
优缺点:
- 优点:
- 从代码中我们看到,类的构造函数定义为private的,保证其他类不能实例化此类,然后提供了一个静态实例并返回给调用者。
- 饿汉模式是最简单的一种实现方式。
- 只在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题。
- 缺点:即使这个单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费了。
- 适用场合:这种实现方式适合单例占用内存比较小,在初始化时就会被用到的情况。但是,如果单例占用的内存比较大,或单例只是在某个特定场景下才会用到,使用饿汉模式就不合适了。
- 优点:
-
- 懒汉模式
- 代码:
- 普通模式(线程不安全):
-
代码:
public class Singleton{ private static Singleton instance = null; private Singleton(){} public static Singleton newInstance(){ if(null == instance){ instance = new Singleton(); } return instance; } }
-
优缺点:
- 优点:懒汉模式中单例是在需要的时候才去创建的,如果单例已经创建,再次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象。
- 缺点:但是这里的懒汉模式并没有考虑线程安全问题,在多个线程可能会并发调用它的getInstance()方法,将导致创建多个实例。
- 使用场合:如果某个单例使用的次数少,并且创建单例消耗的资源较多,那么就需要实现单例的按需创建,这个时候使用懒汉模式就是一个不错的选择。
-
- synchronized关键字同步模式(线程安全):
-
代码:
public class Singleton{ private static Singleton instance = null; private Singleton(){} public static synchronized Singleton newInstance(){ if(null == instance){ instance = new Singleton(); } return instance; } }
-
优缺点:
- 优点:加锁的懒汉模式看起来即解决了线程并发问题,又实现了延迟加载。
- 缺点:synchronized修饰的同步方法比一般方法要慢很多,如果多次调用getInstance(),累积的性能损耗就比较大了。
-
- 普通模式(线程不安全):
- 代码:
- 双重校验锁模式:
- 未禁止指令重排序优化模式:
-
代码:
public class Singleton { private static Singleton instance = null; private Singleton(){} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) {//2 instance = new Singleton(); } } } return instance; } }
-
优缺点:
- 优点:
- 可以看到上面在同步代码块外多了一层instance为空的判断。
- 由于单例对象只需要创建一次,如果后面再次调用getInstance()只需要直接返回单例对象。因此,大部分情况下,调用getInstance()都不会执行到同步代码块,从而提高了程序性能。
- 不过还需要考虑一种情况,假如两个线程A、B,A执行了if (instance == null)语句,它会认为单例对象没有创建,此时线程切到B也执行了同样的语句,B也认为单例对象没有创建,然后两个线程依次执行同步代码块,并分别创建了一个单例对象。为了解决这个问题,还需要在同步代码块中增加if (instance == null)语句,也就是上面看到的代码2。
- 缺点:这个问题的关键就在于由于指令重排优化的存在,导致初始化Singleton和将对象地址赋给instance字段的顺序是不确定的。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错。(所谓指令重排优化是指在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。JVM中并没有规定编译器优化相关的内容,也就是说JVM可以自由的进行指令重排序的优化。)
- 优点:
-
- 禁止指令重排序优化模式:(常用)
-
代码:
public class Singleton { private static volatile Singleton instance = null; private Singleton(){} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
-
优缺点:
- 优点:保证了instance变量被赋值的时候对象已经是初始化过的,从而避免了上面说到的问题。
-
- 未禁止指令重排序优化模式:
- 静态内部类模式:
-
代码:
public class Singleton{ private static class SingletonHolder{ public static Singleton instance = new Singleton(); } private Singleton(){} public static Singleton newInstance(){ return SingletonHolder.instance; } }
-
优缺点:
- 优点:只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。
-
- 饿汉模式(常用)
- 32.2.5 总结:四种Java中实现单例的方法,其中前两种都不够完美,双重校验锁和静态内部类的方式可以解决大部分问题,平时工作中使用的最多的也是这两种方式。枚举方式虽然很完美的解决了各种问题,但是这种写法多少让人感觉有些生疏。个人的建议是,在没有特殊需求的情况下,使用第三种和第四种方式实现单例模式。