java多线程

Java多线程

1.1 同步和异步

同步:当有多个任务需要执行的时候,这多个任务按照一定的顺序,从前往后开始执行
异步:当有多个任务需要执行的时候,在某一个时间段内,有多个任务在同时执行

1.2 线程与进程

进程:是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
线程:是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。
虽然系统是把资源分给进程,但是CPU很特殊,是被分配到线程的,所以线程是CPU分配的基本单位。
1271254-20190706220449848-1411965352[1].png
二者关系:
一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。


程序计数器:是一块内存区域,用来记录线程当前要执行的指令地址 。
:用于存储该线程的局部变量,这些局部变量是该线程私有的,除此之外还用来存放线程的调用栈祯。
:是一个进程中最大的一块内存,堆是被进程中的所有线程共享的。
方法区:则用来存放 NM 加载的类、常量及静态变量等信息,也是线程共享的 。
 
二者区别:
进程:有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响。
线程:是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉。
 
1) 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
2) 线程的划分尺度小于进程,使得多线程程序的并发性高。
3) 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
4) 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
5) 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别

1.3 并发与并行

并发:是指同一个时间段内多个任务同时都在执行,并且都没有执行结束。并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行 。
并行:是说在单位时间内多个任务同时在执行 。
在多线程编程实践中,线程的个数往往多于CPU的个数,所以一般都称多线程并发编程而不是多线程并行编程。


并发过程中常见的问题:
1、线程安全问题:
1271254-20190706221204713-1988100538[1].png
多个线程同时操作共享变量1时,会出现线程1更新共享变量1的值,但是其他线程获取到的是共享变量没有被更新之前的值。就会导致数据不准确问题。


2、共享内存不可见性问题
1271254-20190706221250040-1762612343[1].png
Java 内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存,线程读写变量时操作的是自己工作内存中的变量 。(如上图所示)
1271254-20190706221646777-1600663580[1].png
上图中所示是一个双核 CPU 系统架构,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辅运算。CPU的每个核都有自己的一级缓存,在有些架构里面还有一个所有CPU都共享的二级缓存。 那么Java内存模型里面的工作内存,就对应这里的 Ll或者 L2 缓存或者 CPU 的寄存器
1、线程A首先获取共享变量X的值,由于两级Cache都没有命中,所以加载主内存中X的值,假如为0。然后把X=0的值缓存到两级缓存,线程A修改X的值为1,然后将其写入两级Cache,并且刷新到主内存。线程A操作完毕后,线程A所在的CPU的两级Cache内和主内存里面的X的值都是l。
2、线程B获取X的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回X=1;到这里一切都是正常的,因为这时候主内存中也是X=l。然后线程B修改X的值为2,并将其存放到线程2所在的一级Cache和共享二级Cache中,最后更新主内存中X的值为2,到这里一切都是好的。
3、线程A这次又需要修改X的值,获取时一级缓存命中,并且X=l这里问题就出现了,明明线程B已经把X的值修改为2,为何线程A获取的还是l呢?这就是共享变量的内存不可见问题,也就是线程B写入的值对线程A不可见。
 
synchronized 的内存语义:
这个内存语义就可以解决共享变量内存可见性问题。进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存。会造成上下文切换的开销,独占锁,降低并发性
 
Volatile的理解:
该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时-,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。volatile的内存语义和synchronized有相似之处,具体来说就是,当线程写入了volatile变量值时就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存),读取volatile变量值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)。不能保证原子性

1.4 线程的生命周期


当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新生(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直"霸占"着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换


1. 新建状态,当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值
2. 就绪状态,当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行
3. 运行状态,如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态
4. 阻塞状态,当处于运行状态的线程失去所占用资源之后,便进入阻塞状态
5.死亡状态,一个线程对象需要被销毁


在线程的生命周期当中,线程的各种状态的转换过程

新建和就绪状态

当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。
当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。
注意:启动线程使用start()方法,而不是run()方法。永远不要调用线程对象的run()方法。调用start0方法来启动线程,系统会把该run()方法当成线程执行体来处理;但如果直按调用线程对象的run()方法,则run()方法立即就会被执行,而且在run()方法返回之前其他线程无法并发执行。也就是说,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体。需要指出的是,调用了线程的run()方法之后,该线程已经不再处于新建状态,不要再次调用线程对象的start()方法。只能对处于新建状态的线程调用start()方法,否则将引发IllegaIThreadStateExccption异常。
调用线程对象的start()方法之后,该线程立即进入就绪状态——就绪状态相当于"等待执行",但该线程并未真正进入运行状态。如果希望调用子线程的start()方法后子线程立即开始执行,程序可以使用Thread.sleep(1) 来让当前运行的线程(主线程)睡眠1毫秒,1毫秒就够了,因为在这1毫秒内CPU不会空闲,它会去执行另一个处于就绪状态的线程,这样就可以让子线程立即开始执行。

运行和阻塞状态

线程调度

如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态,如果计算机只有一个CPU。那么在任何时刻只有一个线程处于运行状态,当然在一个多处理器的机器上,将会有多个线程并行执行;当线程数大于处理器数时,依然会存在多个线程在同一个CPU上轮换的现象。
当一个线程开始运行后,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就执行结束了)。线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务;当该时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。在选择下一个线程时,系统会考虑线程的优先级
所有现代的桌面和服务器操作系统都采用抢占式调度策略,但一些小型设备如手机则可能采用协作式调度策略,在这样的系统中,只有当一个线程调用了它的sleep()或yield()方法后才会放弃所占用的资源——也就是必须由该线程主动放弃所占用的资源。

线程阻塞

当发生如下情况时,线程将会进入阻塞状态
**① **线程调用sleep()方法主动放弃所占用的处理器资源
**② **线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。关于同步监视器的知识、后面将存更深入的介绍
**④ **线程在等待某个通知(notify)
**⑤ **程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法
当前正在执行的线程被阻塞之后,其他线程就可以获得执行的机会。被阻塞的线程会在合适的时候重新进入就绪状态,注意是就绪状态而不是运行状态。也就是说,被阻塞线程的阻塞解除后,必须重新等待线程调度器再次调度它。

解除阻塞

针对上面几种情况,当发生如下特定的情况时可以解除上面的阻塞,让该线程重新进入就绪状态
**① **调用sleep()方法的线程经过了指定时间。
**② **线程调用的阻塞式IO方法已经返回。
**③ **线程成功地获得了试图取得的同步监视器。
④ 线程正在等待某个通知时,其他线程发出了个通知。
⑤ 处于挂起状态的线程被调甩了resdme()恢复方法。
图 2.1 线程状态转换图
image.png
从图2.1中可以看出,线程从阻塞状态只能进入就绪状态,无法直接进入运行状态。而就绪和运行状态之间的转换通常
不受程序控制
,而是由
系统线程调度
所决定。当处于就绪状态的线程获得处理器资源时,该线程进入运行状态;当处于运行状态的线程失去处理器资源时,该线程进入就绪状态。但有一个方法例外,调用yield()方法可以让运行状态的线程转入就绪状态。关于yield()方法后面有更详细的介纽。

线程死亡

死亡状态

线程会以如下3种方式结束,结束后就处于死亡状态
**① **run()或call()方法执行完成,线程正常结束。
**② **线程抛出一个未捕获的Exception或Error。
**③ **直接调用该线程stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用。

程序设计

主线程结束时,其他线程不受任何影响,并不会随之结束。一旦子线程启动起来后,它就拥有和主线程相同的地位,它不会受主线程的影响。为了测试某个线程是否已经死亡,可以调用线程对象的isAlivc()方法,当线程处于就绪、运行、阻塞了种状态时,该方法将返回true;当线程处于新建、死亡状态时,该方法将返回false。

不要试图对一个已经死亡的线程调用start()方法使它重新启动,死亡就是死亡,该线程将不可再次作为线程执行。

下面程序尝试对处于死亡状态的线程再次调用start()。

  1. package test;
  2. public class StartDead extends Thread {
  3. private int i;

  4. // 重写run方法,run方法的方法体就是线程执行体
  5. public void run() {
  6. for (; i < 100; i++) {
  7. System.out.println(getName() + “” + i);
  8. }
  9. }

  10. public static void main(String[] args) {
  11. // 创建线程对象
  12. StartDead sd = new StartDead();
  13. for (int i = 0; i < 300; i++) {
  14. // 调用Thread的currentThread方法获取当前线程
  15. System.out.println(Thread.currentThread().getName() + “” + i);
  16. if (i == 20) {
  17. // 启动线程
  18. sd.start();
  19. // 判断启动后线程的isAlive()值,输出true
  20. System.out.println(sd.isAlive());
  21. }
  22. // 只有当线程处于新建、死亡两种状态时isAlive()方法返回false。
  23. // 当i > 20,则该线程肯定已经启动过了,如果sd.isAlive()为假时,
  24. // 那只能是死亡状态了。
  25. if (i > 20 && !sd.isAlive())

  26. {
  27. // 试图再次启动该线程
  28. sd.start();
  29. }
  30. }
  31. }
  32. }

运行结果
main 0
main 1
main 2
main 3
main 4
main 5
main 6
main 7
main 8
main 9
main 10
main 11
main 12
main 13
main 14
main 15
main 16
main 17
main 18
main 19
main 20
true
main 21
…………
Thread-0 0
Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
Thread-0 5
Thread-0 6
Thread-0 7
Thread-0 8
………………
Thread-0 92
Thread-0 93
Thread-0 94
Thread-0 95
Thread-0 96
Thread-0 97
Thread-0 98
Thread-0 99
main 25
Exception

main→主线程
Thread-0→线程1
Exception→异常

上面程序中的粗体字代码试图在线程已死亡的情况下再次调用start()方法来启动该线程。运行上面程序,将引发IllegaIThreadStateException异常,这表明处于死亡状态的线程无法再次运行了。

1.5 线程的开辟方式

package com.qianfeng.thread;

public class ThreadCreate {

	public static void main(String[] args) {
		// 线程的实例化
		// 1.继承Thread类,做一个线程子类(自定义的线程类)
//		MyThread mt = new MyThread();
		
		// 注意:
		// 需要调用start方法,使线程启动
		// start方法会开启一个新的线程,来执行run中的逻辑
		// 如果直接调用run方法,则线程不会进入到就绪状态
//		mt.start();
		System.out.println("主线程中的逻辑执行结束了");
		
		// 2.  通过Runnable接口
		Runnable r = new Runnable() {
			
			@Override
			public void run() {
				for(int i=0; i<10;i++) {
					System.out.println("子线程2中的逻辑"+i);
				}
		}	
		
	};
		Thread t2 =new Thread(r);
		t2.start();

	}
}
//这是一个自定义的线程类
class MyThread extends Thread{
	
	/*
	 * 需要重写run方法
	 * 将需要并发执行的任务写到run方法中
	 * */

	@Override
	public void run() {
		for(int i=0; i<10;i++) {
			System.out.println("子线程中的逻辑"+i);
		}
	}
	
}

1.6 线程常用的方法


线程的命名

package com.qianfeng.thread;

public class ThreadMethod {

	public static void main(String[] args) {
		//线程的命名
		// 1.实例化一个线程对象的同时,对线程进行命名
//		Thread t =new Thread();
//		t.setName("custom");
//		System.out.println(t.getName());
		
		// 2.实例化一个线程对象的同时,通过构造方法对线程进行命名
//		Thread t =new Thread(()->{}, "custom");
//		System.out.println(t.getName());
		
		// 3. 使用自定义的线程类,在实例化线程对象的同时,进行名称的赋值
		// 需要给线程类添加对应的构造方法
		MyThread2 t = new MyThread2("custom");
		System.out.println(t.getName());
		


	}

}
class MyThread2 extends Thread{
	public MyThread2() {}
	public MyThread2(String name) {
		this.setName(name);
		//super(name)
	}
	
}

线程的休眠

package com.qianfeng.thread;

public class ThreadMethod {

public static void main(String[] args) {
	threadSleep();
}

private static void threadSleep() {
	MyThread2 t2 = new MyThread2();
	t2.start();
}

class MyThread2 extends Thread{
	public MyThread2() {}
	public MyThread2(String name) {
		this.setName(name);
		//super(name)
	}
	@Override
	public void run() {
		for (int i =0; i<10;i++) {
			System.out.println(i);
			try {
				Thread.sleep(1000);//单位毫秒
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
	
}

线程的优先级
https://jingyan.baidu.com/article/fc07f9894bd93312fee51976.html


线程的礼让
Thread类中提供了一种礼让方法,使用yield()方法表示,它只是给当前正处于运行状态下的线程一个提醒,告知它可以将资源


礼让给其他线程,但这仅仅是一种暗示,没有任何一种机制保证当前线程会将资源礼让。


yield()方法使具有同样优先级的线程有进入可执行状态的机会,当当前线程放弃执行权时会再度回到就绪状态。


对于支持多任务的操作系统来说,不需要调用yeild()方法,因为操作系统会为线程自动分配CPU时间片来执行。

1.7 临界资源问题

1.产生原因
临界资源:被多个线程同时访问的资源
如果有多个线程同时访问同一份资源,这个资源对应的值有可能会出现值不准确的情况【临界资源产生的原因:在多个线程访问同一份资源的时候,如果一个线程在取值的过程中,时间片又被其他线程抢走了,临界资源问题就产生了】


2.解决方案
当多个线程同时访问同一份资源的时候,如果其中的一个线程抢到了时间片,如果给这个资源“上一把锁“,这个时候其他剩余的线程只能在锁外面进行等待


3.锁
对象锁:任意的对象都可以充当一把锁
类锁:把任意一个类当做锁,格式:类名.class


4.使用锁来解决临界资源问题
1>同步代码块
语法:
synchronized(锁) {
//需要访问的临界资源
}
说明:
a.程序执行到了这个代码段中,就用锁锁住了临界资源,这个时候,其他的线程将不能执行代码段中的代码,只能在锁外面进行等待
b.当执行完代码段中的代码时,会自动解锁,然后剩下的线程就可以开始争抢cpu时间片
c.一定要保证不同的线程看到的是一把锁,否则解决临界资源问题没有任何意义
同步代码块和对象锁的使用
同步代码块和类锁的使用


2>同步方法
语法:
sychronized 访问权限修饰符 返回值类型 函数名(参数列表) {
//需要访问的临界资源
}
说明:
a.如果一个线程走到这个方法内部,就会用锁来锁住临界资源,其他的线程将不能进入到这个方法的内部
b.隐式锁,如果这个方法是静态方法,锁是类锁【当前类】,如果是一个非静态方法,则是一个对象锁【this】




3>显式锁【同步锁】
通过使用ReentrantLock这个类来进行锁的操作,实现了Lock接口
使用ReentrantLock来进行显式的加锁和解锁
lock():加锁
unlock():解锁

1.8 同步代码段

package com.qianfeng.sourceconflict;

public class SychronizedDemo {

	public static void main(String[] args) {
		//实例化4个售票员,用4个线程来模拟
		Runnable r=()->{
			while (TicketCenter.restCount>0) {
				//对象锁 ""
				//类锁 SychronizedDemo.class
				synchronized (SychronizedDemo.class) {
					if (TicketCenter.restCount<=0) {
						return;
					}
					System.out.println(Thread.currentThread().getName()+"卖出一张"+ -- TicketCenter.restCount+"张");
				}
			}
		};
		

		Thread t1 = new Thread(r,"thread - 1");
		Thread t2 = new Thread(r,"thread - 2");
		Thread t3 = new Thread(r,"thread - 3");
		Thread t4 = new Thread(r,"thread - 4");
		
		t1.start();
		t2.start();
		t3.start();
		t4.start();
		

	}

}
class TicketCenter {
	//描述剩余票的数量
	public static int restCount = 100;
}

1.9 同步方法

package com.qianfeng.sourceconflict;

public class SychronizedFunction {

	public static void main(String[] args) {
		// 同步方法:用关键字Sychronized修饰的方法就是同步方法
		//实例化4个售票员,用4个线程来模拟
				Runnable r=()->{
					while (TicketCenter.restCount>0) {
						soldTicket();
					}
				};
				
				Thread t1 = new Thread(r,"thread - 1");
				Thread t2 = new Thread(r,"thread - 2");
				Thread t3 = new Thread(r,"thread - 3");
				Thread t4 = new Thread(r,"thread - 4");
				
				t1.start();
				t2.start();
				t3.start();
				t4.start();
			
	}
//	同步方法:
//	静态方法:同步锁就是 类锁 当前类.class
//	非静态方法:同步锁就是 this
	private synchronized static void soldTicket() {
			if (TicketCenter.restCount<=0) {
				return;
			}
			System.out.println(Thread.currentThread().getName()+"卖出一张"+ -- TicketCenter.restCount+"张");	
	}
}

1.10 显示锁

package com.qianfeng.sourceconflict;

import java.util.concurrent.locks.ReentrantLock;

public class LockDemo {

	public static void main(String[] args) {	
		ReentrantLock lock = new ReentrantLock();
		//实例化4个售票员,用4个线程来模拟
				Runnable r=()->{
					while (TicketCenter.restCount>0) {
//						对临界资源上锁
						lock.lock();
							if (TicketCenter.restCount<=0) {
								return;
							}
							System.out.println(Thread.currentThread().getName()+"卖出一张"+ -- TicketCenter.restCount+"张");
//						对临界资源解锁
							lock.unlock();
					}
				};
				Thread t1 = new Thread(r,"thread - 1");
				Thread t2 = new Thread(r,"thread - 2");
				Thread t3 = new Thread(r,"thread - 3");
				Thread t4 = new Thread(r,"thread - 4");
			
				t1.start();
				t2.start();
				t3.start();
				t4.start();
			}	
}

1.11 死锁

死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

package com.qianfeng.sourceconflict;

public class DeadLock {
	public static void main(String[] args) {
		Runnable runnable1 = ()->{
			synchronized ("A") {
				System.out.println("A线程持有了A锁,等待B锁");
			
				synchronized ("B") {
					System.out.println("A线程持有了A锁和B锁");	
				}	
			}
		};
		
		Runnable runnable2 = ()->{
			synchronized ("B") {
				System.out.println("B线程持有了B锁,等待A锁");
				
				synchronized ("A") {
					System.out.println("B线程持有A,B锁");
					
				}
			}
			
		};
		Thread t1 = new Thread(runnable1);
		Thread t2 = new Thread(runnable2);
		t1.start();
		t2.start();
	}
}

解决死锁

package com.qianfeng.sourceconflict;

public class DemoLock2 {
	public static void main(String[] args) {
		
		
		//wait:等待,是object类中的一个方法,当前的线程释放自己的锁标记,并且让出CPU资源。使得当前的线程进入等待队列
		//notify:通知,是object类中的一个方法,唤醒等待队列中的一个线程,使这个线程进入锁池
		//notifyAll:通知,是object类中的一个方法,唤醒等待队列中的所有线程,使这些线程进入锁池
		Runnable runnable1 = ()->{
			synchronized ("A") {
				System.out.println("A线程持有了A锁,等待B锁");
				//释放已经持有的A锁标记,并进入等待队列
				try {
					"A".wait();
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			
				synchronized ("B") {
					System.out.println("A线程持有了A锁和B锁");	
				}	
			}
		};
		
		Runnable runnable2 = ()->{
			synchronized ("B") {
				System.out.println("B线程持有了B锁,等待A锁");
				
				synchronized ("A") {
					System.out.println("B线程持有A,B锁");
					
					"A".notifyAll();
					
				}
			}
			
		};
		Thread t1 = new Thread(runnable1);
		Thread t2 = new Thread(runnable2);
		t1.start();
		t2.start();
	}
}

1.11 多线程中的单例

懒汉式的多线程单例解决方式

package com.qianfeng.sourceconflict;

public class singletonTest {
	public static void main(String[] args) {
		
		Runnable runnable=()->{
			Boss.getBoss();
		};
		
		for (int i = 0;i<100;i++) {
			new Thread(runnable).start();
		}
	}

}

class Boss{
	private Boss() {
		System.out.println("一个Boss对象被实例化了");
	}
	private static Boss Instance = null;
	
	public static synchronized Boss getBoss() {
		 
			if (Instance == null) {
				Instance = new Boss();
			}
			return Instance;
		
		
	}
	
	
}

1.12 生产设消费者设计模式


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值