1. 程序、进程、线程的区别
1.1 程序
程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
1.2 进程
进程(process)是程序的一次执行过程,或是正在运行的一个程序,是系统运行程序的基本单位。因此进程是动态的,系统运行一个程序即是一个进程从创建、运行到消亡的过程。如:运行中的QQ、运行中的MP3播放器。
进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
1.3 线程
线程(thread)是进程划分成更小的运行单位,一个进程在其执行过程中可以产生多个线程。
与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
1.4 进程和线程的关系
从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。
线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护,而进程正相反。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
程序计数器为什么是私有的?
程序计数器主要有下面两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java代码时程序计数器记录的才是下一条指令的地址。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
虚拟机栈和本地方法栈为什么是私有的?
虚拟机栈: 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
一句话简单了解堆和方法区
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
2. 并发与并行的区别?
并行: 多个CPU同时执行多个任务,比如,多个人同时做不同的事;
并发: 一个CPU(采用时间片)同时执行多个任务,比如,秒杀、多个人做同一件事。
3. 为什么要使用多线程呢?
先从总体上来说:
从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
再深入到计算机底层来探讨:
单核时代: 在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到 100%了。
多核时代: 多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。
4. 使用多线程可能带来什么问题?
并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。
5. Threa类
5.1 Thread类的特性:
每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程题。
通过该Thread对象的start()方法来调用这个线程。
5.2 构造器:
- Thread():创建新的Thread对象
- Thread(String threadname):创建线程并指定线程实例名
- Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接 口中的run方法
- Thread(Runnable target, String name):创建新的Thread对象
5.3 Thread类有关的方法:
- start(): 启动当前线程;调用当前线程的run()
- run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
- currentThread(): 静态方法,返回执行当前代码的线程
- getName(): 获取当前线程的名字
- setName(): 设置当前线程的名字
- yield(): 释放当前cpu的执行权
- join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
- stop(): 已过时。当执行此方法时,强制结束当前线程。
- sleep(long millitime): 让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。
- isAlive(): 判断当前线程是否存活
6. 创建线程的四种方式
6.1 继承Thread类
public class ThreadTest{
public static void main(String[] args) {
//3.创建Thread类的子类的对象
MyThread myThread1 = new MyThread();
myThread1.setName("线程1");
/*如果不执行start,直接执行run的话,只有一个线程*/
//4.通过此对象调用start
myThread1.start();
}
}
//1.创建一个继承于Thread的子类
class MyThread extends Thread {
//2.重写Thread类的run()
public void run(){
//求100以内的质数
label: for (int i = 2; i <= 100; i++) {
for (int j = 2; j <= Math.sqrt(i); j++) {
if(i % j == 0)
continue label;
}
System.out.println(i);
}
}
}
6.2 实现Runnable接口
//1.创建了一个runnable接口的类
class MThread implements Runnable{
//2.实现run()
@Override
public void run() {
/*
* 找1000以内的水仙花数 三位数 其各个位数上的和等于其本身
* 153 = 1*1*1 + 3*3*3 + 5*5*5
* */
for (int i = 100; i <= 999; i++) {
int a = i / 100;
int b = (i / 10) % 10;
int c = i % 10;
if(i == a*a*a + b*b*b +c*c*c)
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class ThreadTest2 {
public static void main(String[] args) {
//3.创建实现类的对象
MThread mThread = new MThread();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread thread = new Thread(mThread);
thread.setName("线程1");
//5.通过Thread的对象调用start()
thread.start();
}
}
线程的调度
1. Java的调度方法:同优先级线程组成先进先出队列(先到先服务),使用时间片策略。对高优先级,使用优先调度的抢占式策略。
2. 线程的优先级:
- MAX_PRIORITY: 10
- MIN_PRIORITY: 1
- NORM_PRIORITY: 5 (默认优先级)
3. 如何获取和设置当前线程的优先级
- getPriority():获取线程的优先级
- setPriority():设置线程的优先级
4. 说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。
6.3 实现Callable接口
//1.创建一个实现Callable的实现类
class MThreas1 implements Callable {
//2.实现call()方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
/*
* 找出1000以内的完数--并且返回其个数
* 完数:一个数如果恰好等于它的因子之和,这个数就成为完数。(因子:除去这个数本身的约数)
* 例: 6 = 1 + 2 + 3
* */
int factor = 0;
int sum = 0;
for (int i = 1; i <= 1000; i++) {
for (int j = 1; j < i; j++) {
if(i % j == 0){
factor += j;
}
}
if(i == factor){
System.out.println(i);
sum ++;
}
factor = 0;
}
return sum;
}
}
public class ThreadTest3 {
public static void main(String[] args) {
//3.创建callable接口实现类的对象
MThreas1 mThreas1 = new MThreas1();
//4.将此callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(mThreas1);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start
new Thread(futureTask).start();
try {
//6.获取callable中call方法的返回值
Object o = futureTask.get();
System.out.println("一共有"+o+"个完数");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
6.4 使用线程池
6.4.1 为什么要使用线程池
-
经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响比较大。线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。
-
线程池的好处:
1)降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
2)提高响应速度。 减少了创建新线程的时间。
3)提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
6.4.2 创建线程池
JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors。
-
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor 。
1)void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行 Runnable
2) Future submit(Callable task):执行任务,有返回值,一般又来执行 Callable
3)void shutdown() :关闭连接池 -
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
1)Executors.newCachedThreadPool():该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。2)Executors.newFixedThreadPool(n):该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
3)Executors.newSingleThreadExecutor() :方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
4)Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运 行命令或者定期地执行
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1. 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
// ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//设置线程池的属性
// System.out.println(service.getClass());
// service1.setCorePoolSize(15);
// service1.setKeepAliveTime();
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合适用于Runnable
service.execute(new NumberThread1());//适合适用于Runnable
// service.submit(Callable callable);//适合使用于Callable
//3.关闭连接池
service.shutdown();
}
}
-
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors 返回线程池对象的弊端如下:
1)FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM。
2)CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
实现Runnable接口和Callable接口的区别?
如果想让线程池执行任务的话需要实现Runnable接口或Callable接口。 Runnable接口或Callable接口实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。两者的区别在于 Runnable 接口不会返回结果, Callable 接口可以返回结果。
备注: 工具类Executors可以实现Runnable对象和Callable对象之间的相互转换。(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule))。
执行execute()方法和submit()方法的区别是什么呢?
- execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
- submit() 方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
7. 线程的生命周期和状态
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建(NEW): 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态;
- 就绪(READY):处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件;
- 运行(RUNNABLE):当就绪的线程被调度并获得处理器资源时,便进入运行状态,run()方法定义了线程的操作和功能;
- 阻塞(BLOCKED):在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态;
- 死亡(TERMINATED):线程完成了它的全部工作或线程被提前强制性地终止(terminate)。
什么是上下文切换?
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换会这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
8. 线程的同步
8.1 解决线程安全问题方式一:Synchronized关键字
情景描述:模拟火车站售票程序,开启三个窗口售票。
8.1.1 同步代码块
- 共享数据:多个线程共同操作的变量。比如,此题中的ticket就是共享数据。
- 同步监视器(俗称:锁):任何一个类的对象,都可以充当锁。
- 同步代码块实现Runnable的线程安全问题
(用this来充当同步监视器,且共享数据不用是static的)
class Windows1 implements Runnable{
private int ticket = 50;
@Override
public void run() {
while (true){
synchronized (this){
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖第"+ticket+"张票");
ticket --;
}else
break;
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
Windows1 windows1 = new Windows1();
Thread t1 = new Thread(windows1);
Thread t2 = new Thread(windows1);
Thread t3 = new Thread(windows1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
- 同步代码块解决继承Thread类方式的线程安全问题
(用类来充当同步监视器,且共享数据是static的)
class Window2 extends Thread{
private static int ticket = 50;
public void run(){
while (true){
synchronized (Window2.class){
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"卖第"+ticket+"张票");
ticket --;
}else
break;
}
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
Window2 t1 = new Window2();
Window2 t2 = new Window2();
Window2 t3 = new Window2();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
8.1.2 同步方法
- 如果操作共享数据的代码完整的声明在一个方法中,我们可以把此方法声明为同步的。
- 同步方法仍涉及到同步监视器,只是不需要我们显示的声明。非静态的同步方法(实现Runnable),同步监视器是this,静态方法(继承Thread类)的同步监视器是类本身。
class Window3 implements Runnable{
private int ticket = 50;
@Override
public void run() {
while (true){
show();
}
}
private synchronized void show(){ //同步监视器:this
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖第"+ticket+"张票");
ticket --;
}
}
}
public class WindowTest3 {
public static void main(String[] args) {
Window3 windows3 = new Window3();
Thread t1 = new Thread(windows3);
Thread t2 = new Thread(windows3);
Thread t3 = new Thread(windows3);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window4 extends Thread{
private static int ticket = 50;
public void run(){
while (true){
show();
}
}
private static synchronized void show(){
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+"卖第"+ticket+"张票");
ticket --;
}
}
}
public class WindowTest4 {
public static void main(String[] args) {
Window4 t1 = new Window4();
Window4 t2 = new Window4();
Window4 t3 = new Window4();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
8.2 解决线程安全问题方式二:Lock锁。
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
class Window implements Runnable{
private int ticket = 50;
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try {
lock.lock(); //2.调用锁定方法lock
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖第"+ticket+"张票");
ticket --;
}else
break;
} finally {
lock.unlock(); //3.调用解锁方法:unlock()
}
}
}
}
public class LookTest {
public static void main(String[] args) {
Window window = new Window();
Thread t1 = new Thread(window);
Thread t2 = new Thread(window);
Thread t3 = new Thread(window);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
9. 线程的通信
9.1 wait()与notify()和notifyAll()
- wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
- notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
- notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
- 说明:
- wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
- wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException异常
- wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
sleep() 方法和 wait() 方法的异同?
- 相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。(暂停线程的执行)
- 不同点:
1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁(同步监视器)。
4)wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
5)wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
9.2 练习:两个线程交叉打印1~10的数。
方式一:
public class Test {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
class Number implements Runnable{
private int number = 1;
@Override
public void run() {
while (true){
synchronized (this){
this.notify();
if(number <= 10){
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println(Thread.currentThread().getName()+":"+number);
number ++;
try {
this.wait(); //告知线程进入阻塞状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}else
break;
}
}
}
}
方式二:
public class Test {
private static Object lock = new Object();
private static Boolean flag = true;
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 10; i += 2) {
synchronized (lock){
if(!flag){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":"+i);
flag = false;
lock.notify();
}
}
}
},"线程1").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 2; i <= 10; i += 2) {
synchronized (lock){
if(flag){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+":"+i);
flag = true;
lock.notify();
}
}
}
},"线程2").start();
}
}
Output:
线程1:1
线程2:2
线程1:3
线程2:4
线程1:5
线程2:6
线程1:7
线程2:8
线程1:9
线程2:10
9.3 小练习:三个线程交叉打印a、b、c各10遍。
9.3.1 synchronize+wait()+notifyAll()实现
public class Test2 {
private static int state = 1;
private static Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(new Thread1());
t1.setName("线程1");
t1.start();
Thread t2 = new Thread(new Thread2());
t2.setName("线程2");
t2.start();
Thread t3 = new Thread(new Thread3());
t3.setName("线程3");
t3.start();
}
static class Thread1 implements Runnable{
public void run(){
int i = 0;
while (i < 10){
synchronized (lock){
if(state == 1){
System.out.println(Thread.currentThread().getName()+":A,第"+i+"遍");
i ++;
state = 2;
lock.notifyAll();//哪个对象调用的方法记得加上!!!!(如果是this也可省略)
}else {
try {
lock.wait(); //哪个对象调用的方法记得加上!!!!
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
static class Thread2 implements Runnable{
public void run(){
int i = 0;
while (i < 10){
synchronized (lock){
if(state == 2){
System.out.println(Thread.currentThread().getName()+":B,第"+i+"遍");
i ++;
state = 3;
lock.notifyAll();
}else {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
static class Thread3 implements Runnable{
public void run(){
int i = 0;
while (i < 10){
synchronized (lock){
if(state == 3){
System.out.println(Thread.currentThread().getName()+":C,第"+i+"遍");
i ++;
state = 1;
lock.notifyAll();
}else {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
Output:
线程1:A,第0遍
线程2:B,第0遍
线程3:C,第0遍
线程1:A,第1遍
线程2:B,第1遍
线程3:C,第1遍
.
.
.
线程1:A,第9遍
线程2:B,第9遍
线程3:C,第9遍
9.3.2 Lock+state状态实现
public class Test3 {
private static int state = 1;
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(new Thread4());
Thread t2 = new Thread(new Thread5());
Thread t3 = new Thread(new Thread6());
t1.setName("线程1");
t2.setName("线程2");
t3.setName("线程3");
t1.start();
t2.start();
t3.start();
}
static class Thread4 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10;) {
try {
lock.lock();
if(state == 1){
System.out.println(Thread.currentThread().getName()+":A,第"+i+"遍");
state = 2;
i ++;
}
}finally {
lock.unlock();
}
}
}
}
static class Thread5 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10;) {
try {
lock.lock();
if(state == 2){
System.out.println(Thread.currentThread().getName()+":B,第"+i+"遍");
state = 3;
i ++;
}
}finally {
lock.unlock();
}
}
}
}
static class Thread6 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10;) {
try {
lock.lock();
if(state == 3){
System.out.println(Thread.currentThread().getName()+":C,第"+i+"遍");
state = 1;
i ++;
}
}finally {
lock.unlock();
}
}
}
}
}
9.4 生产者与消费者问题
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
9.4.1 synchronized
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
p1.setName("生产者1");
Consumer c1 = new Consumer(clerk);
c1.setName("消费者1");
// Consumer c2 = new Consumer(clerk);
// c2.setName("消费者2");
p1.start();
c1.start();
// c2.start();
}
}
class Clerk{
private int productCount = 0;
//生产产品
public synchronized void produceProduct() {
if(productCount < 20){
productCount++;
System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");
notify();
}else{
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费产品
public synchronized void consumeProduct() {
if(productCount > 0){
System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个产品");
productCount--;
notify();
}else{
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread{//生产者
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始生产产品.....");
while(true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct();
}
}
}
class Consumer extends Thread{//消费者
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始消费产品.....");
while(true){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}
9.4.2 lock
public class ProductTest2 {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer producer = new Producer(clerk);
Consumer consumer = new Consumer(clerk);
new Thread(producer,"生产者A").start();
new Thread(consumer,"消费者B").start();
}
}
class Clerk{
private int product = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
//进货
public void get(){
lock.lock();
try {
if(product >= 20){
System.out.println("产品已满!");
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"生产第"+ ++product+"个产品");
condition.signalAll();
}finally {
lock.unlock();
}
}
//卖货
public void sale(){
lock.lock();
try {
if(product <= 0){
System.out.println("缺货!");
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"消费第"+ product-- +"个产品");
condition.signalAll();
}finally {
lock.unlock();
}
}
}
//生产者
class Producer implements Runnable{
private Clerk clerk;
public Producer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.get();
}
}
}
//消费者
class Consumer implements Runnable{
private Clerk clerk;
public Consumer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(15);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.sale();
}
}
}
10. 线程的死锁问题
10.1 认识线程死锁
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况
public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
Output:
Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1
线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过 Thread.sleep(1000);让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。
产生死锁必须具备以下四个条件:
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
10.2 如何避免线程死锁?
我们只要破坏产生死锁的四个条件中的其中一个就可以了。
破坏互斥条件
这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
破坏请求与保持条件
一次性申请所有的资源。
破坏不剥夺条件
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
我们对线程 2 的代码修改成下面这样就不会产生死锁了。
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 2").start();
Output:
Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2
Process finished with exit code 0
我们分析一下上面的代码为什么避免了死锁的发生?
线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。