Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Java多线程详解

Java多线程详解

作者头像
wangweijun
发布于 2020-01-19 13:12:54
发布于 2020-01-19 13:12:54
78500
代码可运行
举报
文章被收录于专栏:wangweijunwangweijun
运行总次数:0
代码可运行

今天我们聊一聊多线程,谈到多线程,很多人就开始难受,这是一个一听就头疼的话题,但是,我希望你在看完这篇文章后能对多线程有一个深入的了解。

案例

那么,首先我就举一个电影院卖票的例子来模拟多线程。 复仇者联盟4上映的那段时间电影院那可是门庭若市啊,那么我们假设现在有一个电影院正在上映复仇者联盟4,共有100张票,而它有三个售票窗口,我们来模拟一下这个电影院的售票情况。 首先创建SellTicket类继承Thread:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class SellTicket extends Thread {
	@Override
	public void run() {
		// 定义100张票
		int tickets = 100;

		while (true) {
			if (tickets > 0) {
				System.out.println(getName() + "正在出售" + (tickets--) + "张票");
			}
		}
	}
}

然后编写测试代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class SellTicketDemo {
	public static void main(String[] args) {
		//创建三个售票窗口
		SellTicket st1 = new SellTicket();
		SellTicket st2 = new SellTicket();
		SellTicket st3 = new SellTicket();
		
		st1.setName("窗口1");
		st2.setName("窗口2");
		st3.setName("窗口3");
		
		st1.start();
		st2.start();
		st3.start();
	}
}

现在我们运行程序,控制台输出信息如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
...
窗口1正在出售第100张票
窗口3正在出售第100张票
窗口2正在出售第100张票
窗口3正在出售第99张票
窗口1正在出售第99张票
窗口3正在出售第98张票
窗口2正在出售第99张票
窗口3正在出售第97张票
窗口1正在出售第98张票
窗口3正在出售第96张票
窗口2正在出售第98张票
...

那么问题出现了,每张票都被卖了三次,很显然这是不符合事实的。那么问题就出现在这个tickets变量的定义位置上,如果将tickets变量定义在了run()方法内,很显然三个线程就都具有了100张票,那么现在来改进一下我们的程序:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class SellTicket extends Thread {

	// 定义100张票
	private int tickets = 100;

	@Override
	public void run() {

		while (true) {
			if (tickets > 0) {
				System.out.println(getName() + "正在出售第" + (tickets--) + "张票");
			}
		}
	}
}

这次我们将tickets定义为成员变量,其它代码不作修改,然后重新运行程序:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
...
窗口1正在出售第100张票
窗口3正在出售第100张票
窗口2正在出售第100张票
窗口3正在出售第99张票
窗口1正在出售第99张票
窗口3正在出售第98张票
窗口2正在出售第99张票
窗口3正在出售第97张票
窗口1正在出售第98张票
窗口3正在出售第96张票
窗口2正在出售第98张票
窗口3正在出售第95张票
...

很显然,这次又出现了问题,三个窗口仍然卖出了同一张票,那么这是为什么呢?原因很简单,tickets虽然作为了成员变量,但是我们创建了三个线程,这样每个线程就都会拥有一个tickets变量,所以刚才的问题其实并没有得到解决。那么为了解决这个问题,也为了使逻辑更加合理,我们应该采用实现Runnable接口的方式来模拟这一过程。 创建SellTicket类实现Runnable接口:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class SellTicket implements Runnable {

	// 定义100张票
	private int tickets = 100;

	@Override
	public void run() {

		while (true) {
			if (tickets > 0) {
				System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
			}
		}
	}
}

编写测试代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class SellTicketDemo {
	public static void main(String[] args) {
		// 创建三个售票窗口
		SellTicket st = new SellTicket();

		Thread t1 = new Thread(st,"窗口1");
		Thread t2 = new Thread(st,"窗口2");
		Thread t3 = new Thread(st,"窗口3");

		t1.start();
		t2.start();
		t3.start();
	}
}

运行程序:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
...
窗口2正在出售第99张票
窗口1正在出售第100张票
窗口3正在出售第98张票
窗口2正在出售第97张票
窗口3正在出售第95张票
窗口1正在出售第96张票
窗口3正在出售第93张票
窗口2正在出售第94张票
窗口3正在出售第91张票
...

感觉好像没问题了,然而在实际生活中, 售票网络是不可能实时传输的,总是存在延时的情况,所以,在出售一张票以后,需要一点时间的延迟。那么我们修改一下程序:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class SellTicket implements Runnable {

	// 定义100张票
	private int tickets = 100;

	@Override
	public void run() {

		while (true) {
			if (tickets > 0) {
				//延迟
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
			}
		}
	}
}

在卖票之前延迟了100毫秒,其它代码不作修改,然后运行程序:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
...
窗口3正在出售第4张票
窗口1正在出售第3张票
窗口3正在出售第2张票
窗口2正在出售第2张票
窗口1正在出售第1张票
窗口2正在出售第0张票
窗口3正在出售第-1张票
...

会发现出现了卖同一张票和负数票的情况,显然这段程序的问题很大。我们说CPU的一次执行必须是一个原子性的操作,原子性就是最简单基本的操作,很显然tickets–并不是一个原子性的操作。那么当某几个线程同时输出ticket值的时候,就出现了卖同一张票的情况;然而当某一个线程在延迟100毫秒的过程中,因为该线程并没有执行到tickets–的步骤,所以其它线程此时也通过了if判断,就出现了卖负数票的情况。

如何解决线程安全问题

要想解决问题,我们首先得知道哪些原因会导致线程安全问题,通过上面的分析,总结如下:

  • 是否为多线程环境
  • 是否有共享数据
  • 是否有多条语句操作共享数据

那我们回头看看案例,会发现这三条原因我们全占了,那么出现问题也就不足为奇了。既然找出了问题所在,我们就试着去解决它。 既然多线程环境和共享数据我们无法操纵,但是我们能够使多条语句操作共享数据不成立。这就引出了今天的主题,“同步机制”。 格式:synchronized(对象){ 需要同步的代码; } 那么括号里的对象是什么呢?我们创建一个对象给它试试。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class SellTicket implements Runnable {

	// 定义100张票
	private int tickets = 100;

	@Override
	public void run() {

		while (true) {
			synchronized (new Object()) {
				if (tickets > 0) {
					// 延迟
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
				}
			}
		}
	}
}

现在运行程序:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
...
窗口2正在出售第5张票
窗口1正在出售第4张票
窗口3正在出售第3张票
窗口2正在出售第2张票
窗口1正在出售第1张票
窗口3正在出售第0张票
窗口2正在出售第-1张票
...

然后问题然是出现了,我们需要注意这个对象,同步机制解决线程安全问题的根本就在这个对象上,我们称其为锁,那么锁住代码的锁只能是同一把,然而上面的事例明显创建了三把锁。 我们再次修改代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class SellTicket implements Runnable {

	// 定义100张票
	private int tickets = 100;
	//创建锁对象
	private Object obj = new Object();

	@Override
	public void run() {

		while (true) {
			synchronized (obj) {
				if (tickets > 0) {
					// 延迟
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
				}
			}
		}
	}
}

现在运行程序:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
窗口1正在出售第13张票
窗口1正在出售第12张票
窗口1正在出售第11张票
窗口1正在出售第10张票
窗口1正在出售第9张票
窗口1正在出售第8张票
窗口1正在出售第7张票
窗口3正在出售第6张票
窗口3正在出售第5张票
窗口3正在出售第4张票
窗口3正在出售第3张票
窗口3正在出售第2张票
窗口2正在出售第1张票

这样关于线程安全的问题就迎刃而解了。 那么同步机制的原理就是当某个线程开始执行并执行到同步的代码之后,就会通过锁对象将该段代码进行一个封锁,当该线程执行完同步代码后就释放锁,然后在代码被锁住的情况下其它线程即使抢占了执行权仍然无法继续执行,它只能等待锁释放才能继续执行。 那么总结一下同步的特点: 前提:

  • 多个线程

解决问题的时候要注意:

  • 多个线程使用的是用一个锁对象

同步的好处:

  • 解决了多线程的安全问题

同步的弊端:

  • 当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗资源的,无形中会降低程序的运行效率。

我们继续深入研究一下同步机制。 我们刚才使用的是Object对象作为锁,这说明任意对象都可以作为同步锁。而如果我们将代码做一些修改:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class SellTicket implements Runnable {

	// 定义100张票
	private int tickets = 100;
	// 创建锁对象
	private Object obj = new Object();
	private int x = 0;

	@Override
	public void run() {

		while (true) {
			if (x % 2 == 0) {
				synchronized (obj) {
					if (tickets > 0) {
						try {
							Thread.sleep(100);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票 ");
					}
				}
			} else {
				sellTicket();
			}
			x++;
		}
	}

	private synchronized void sellTicket() {
		if (tickets > 0) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票 ");
		}
	}
}

此时运行程序的话,卖出同一张票的情况就又出现了,我们说同步的锁对象只能是同一个,那么同步方法的锁对象是什么呢? 同步方法的锁对象就是this,所以如果将括号内的锁对象替换为this,该程序就并不会出现问题了。 而静态方法的锁对象就是类的字节码文件对象(.class)。

死锁问题

那么在同步中有一个致命的问题,死锁问题。 死锁问题是指两个或两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待的现象。 死锁问题中比较经典的问题就是哲学家吃饭问题。在哲学家吃饭问题中,每个哲学家都有可能拿起了左手边的筷子而永远在等右边的筷子,事实上,他永远也等不到。 在程序中,不恰当的嵌套也有可能导致死锁问题,我们看一个例子: 创建MyLock类:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class MyLock {
	//创建两个锁对象
	public static final Object objA = new Object();
	public static final Object objB = new Object();
}

然后创建DieLock类:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class DieLock extends Thread {

	private boolean flag;

	public DieLock(boolean flag) {
		this.flag = flag;
	}

	@Override
	public void run() {
		if (flag) {
			synchronized (MyLock.objA) {
				System.out.println("if objA");
				synchronized (MyLock.objB) {
					System.out.println("if objB");
				}
			}
		}else {
			synchronized (MyLock.objB) {
				System.out.println("else objB");
				synchronized (MyLock.objA) {
					System.out.println("else objA");
				}
			}
		}
	}
}

接着编写测试代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class DieLockDemo {
	public static void main(String[] args) {
		DieLock dl1 = new DieLock(true);
		DieLock dl2 = new DieLock(false);
		
		dl1.start();
		dl2.start();
	}
}

多次运行之后,死锁现象出现了。

原因是当某个线程执行if判断时使用了锁A,当该线程想继续执行时,第二条线程执行else使用了锁B,此时第一条线程需第二条线程执行完释放锁B,而第二条线程因为也在等待第一条线程释放锁A从而无法释放锁B,进而造成了死锁。

如何避免死锁

在有些情况下死锁是可以避免的。三种用于避免死锁的技术:

加锁顺序(线程按照一定的顺序加锁) 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁) 死锁检测 加锁顺序 当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。 加锁时限 另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行(译者注:加锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑)。 死锁检测 死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。

每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。 当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自己持有着。这是它就知道发生了死锁。

我们可以通过破坏死锁产生的4个必要条件来 预防死锁,由于资源互斥是资源使用的固有特性是无法改变的。

破坏“不可剥夺”条件:一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到 系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。 破坏”请求与保持条件“:第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源。第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源。 破坏“循环等待”条件:采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Java基础知识(十一)--多线程
多线程(同步方法) 使用synchronized关键字修饰一个方法,该方法中所有的代码都是同步的 class Printer { public static void print1() { synchronized(Printer.class){ //锁对象可以是任意对象,但是被锁的代码需要保证是同一把锁,不能用匿名对象 System.out.print("程"); System.out.print("序"); System.out.print("员"); System
用户7386338
2021/06/22
2220
线程安全问题
1、为什么出现线程安全问题? 首先想为什么出现问题?(也是我们判断是否有问题的标准) 是否是多线程环境 是否有共享数据 是否有多条语句操作共享数据 public class SellTicket
星哥玩云
2022/09/14
4120
Java从入门到精通十二(java线程)
按照操作系统的理解,进程是操作系统分配资源的基本单位。 线程是调度资源的基本单位。
兰舟千帆
2022/07/16
7850
Java从入门到精通十二(java线程)
【愚公系列】2022年01月 Java教学课程 60-线程同步
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
愚公搬代码
2022/01/15
1300
【愚公系列】2022年01月 Java教学课程 60-线程同步
三种方式模拟两个线程抢票【最全版】
在多线程编程中,资源竞争是一个常见的问题。资源竞争发生在多个线程试图同时访问或修改共享资源时,可能导致数据不一致或其他并发问题。在模拟两个线程抢票的场景中,我们需要考虑如何公平地分配票,并确保每个线程都有机会成功获取票。
绿毛龟
2024/02/02
3160
三种方式模拟两个线程抢票【最全版】
十五、多线程【黑马JavaSE笔记】
假如计算机只有一个CPU,那么CPU在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的
啵啵鱼
2022/11/23
2940
十五、多线程【黑马JavaSE笔记】
【Java多线程-6】synchronized同步锁
前文描述了Java多线程编程,多线程的方式提高了系统资源利用和程序效率,但多个线程同时处理共享的数据时,就将面临线程安全的问题。
云深i不知处
2020/09/16
9060
Java学习笔记之多线程 生产者 消费者
        所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
Jetpropelledsnake21
2022/03/07
6240
Java学习笔记之多线程 生产者 消费者
Java基础-23(02)总结多线程,线程实现Runnable接口,线程名字获取和设置,线程控制,线程安全,同步线程
(7)线程的生命周期(参照 线程生命周期图解.bmp) A:新建 B:就绪 C:运行 D:阻塞 E:死亡 (8)电影院卖票程序的实现 A:继承Thread类 package cn.itcast_06;(1) public class SellTicket extends Thread { // 定义100张票 // private int tickets = 100; // 为了让多个线程对象共享这100张票,我们其实应该用静态修饰 private static int tickets
Java帮帮
2018/03/16
9530
同步解决线程安全问题的三种实现
同步解决线程安全问题的三种实现 /* * 同步可以解决安全问题的根本原因就在那个对象上。 * * A:同步代码块的格式及其锁对象问题? * 格式: * sy
黑泽君
2018/10/11
4070
java基础知识01
正所谓万丈高楼平地起,有了扎实的基础才能进阶更深奥的课程,才能让你后面的走得更轻松,学Java亦是如此!所以千万不能忽略基础的重要性,下面一起来温习一下那些容易忽略、容易混淆以及比较重要的Java基础。
贪挽懒月
2018/12/04
6200
Java多线程(全知识点)
概述:本文为Java多线程的基础知识点的第一部分,主要包括,通过继承Thread来实现进程,线程调度,线程控制,run(),start(),join(),sleep(),setDaemon()方法的使用,获取线程名字currentThread(),线程同步,非静态锁,静态方法的锁,Lock锁,生产者与消费者问题,卖票问题。
GeekLiHua
2025/01/21
1610
Java多线程(全知识点)
Java多线程基础
主线程挂了但是子线程还在继续执行,这并不会导致应用程序的结束。说明: 当main线程启动一个子线程 Thread-0, 主线程不会阻塞, 会继续执行(不会等执行完毕后再往下执行),这时 主线程和子线程是交替执行。
timerring
2023/05/07
3100
Java多线程基础
java基础thread——java5之后的多线程(浅尝辄止)
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。
100000860378
2018/09/13
3840
java基础thread——java5之后的多线程(浅尝辄止)
【Java多线程】的学习总结
多线程其实就是进程中一个独立的控制单元或者说是执行路径,线程控制着进程的执行,【重点】一个进程中,至少有一个线程存在。
张拭心 shixinzhang
2022/11/30
5830
JDK5中Lock锁的使用
(1)JDK5中Lock锁的使用   虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock接口。 即:JDK5以后的针对线程的锁定操作和释放操作。   Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。
黑泽君
2018/10/11
5430
Java之多线程-------入门
是指从软件或者硬件上实现多个线程并发执行的技术。 具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。
楠羽
2022/11/18
3790
Java之多线程-------入门
如何判断一个程序是否会有线程安全问题?
如何判断一个程序是否会有线程安全问题? /* * 如何解决线程安全问题呢? * * 要想解决问题,就要知道哪些原因会导致出问题:(而且这些原因也是以后我们判断一个程序是否会有线程安全问题的依据
黑泽君
2018/10/11
2.2K0
java线程详解(史上最全)
根据本人多年从业以及学习经验,录制了一套最新的Java精讲视频教程,如果你现在也在学习Java,在入门学习Java的过程当中缺乏系统的学习教程,你可以加QQ群654631948领取下学习资料,面试题,开发工具等,群里有资深java老师做答疑,每天也会有基础部分及架构的直播课,也可以加我的微信renlliang2013做深入沟通,只要是真心想学习Java的人都欢迎。
全栈程序员站长
2022/09/08
2750
java线程详解(史上最全)
java基础第十六篇之多线程
1:线程的概念 进程(任务):一个正在运行的程序 进程的调度:CPU来决定什么时候该运行哪个进程 (时间片轮流法) 线程在一个应用程序中,同时,有多个不同的执行路径,是进程中的实际运作单位。 好处是提高程序效率。
海仔
2019/08/05
2960
相关推荐
Java基础知识(十一)--多线程
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验