当前位置:主页 > 查看内容

关于多线程的学习(上)

发布时间:2021-06-22 00:00| 位朋友查看

简介:关于多线程的初步学习上 线程的概述 进程在内存中正在执行的程序。一个应用程序如果想被执行需要跑在内存中。 线程是进程的一个 执行单元 用来负责进程中程序(代码)的执行。在进程中可以有一条线程也可以有多条线程。如果只有一条线程称之为单线程程序。如果……

关于多线程的初步学习(上)

线程的概述

进程:在内存中正在执行的程序。一个应用程序如果想被执行,需要跑在内存中。

线程:是进程的一个执行单元,用来负责进程中程序(代码)的执行。在进程中可以有一条线程,也可以有多条线程。如果只有一条线程,称之为单线程程序。如果有多条线程,称之为多线程程序。一个进程至少需要一条线程。

java中线程使用抢占式调度:线程具有优先级,优先级高的抢占到线程CPU资源概率会更大。如果线程的优先级相同,那么会随机选择一个线程执行。优先级分为1~10,理论上数字越大,优先级越高,但实际上相邻的优先级之间差距非常不明显。可以使用**setPriority()**来设置优先级。

创建线程的某些方法:

  • 继承Thread类
    定义一个类继承Thread。
    重写run方法。
    创建子类对象,就是创建线程对象。
    调用start方法,开启线程并让线程执行,同时还会告诉jvm去调用run方法

  • 定义类实现Runnable接口
    覆盖接口中的run方法。
    创建Thread类的对象 将Runnable接口的子类。
    对象作为参数传递给Thread类的构造函数。
    调用Thread类的start方法开启线程。

既然提到线程,那肯定会提到线程安全问题,那什么是线程安全呢?如果不考虑线程安全,那会出现什么情况呢?我们用一个售票的例子来进行模拟:

// 模拟票数
public class Ticket implements Runnable{
	int ticket = 100;
	@Override
	public void run() {
		// TODO Auto-generated method stub
		while (true){
			// 让当前线程休眠 单位是毫秒
			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			// 说明票卖光了
			if (ticket <= 0){
				break;
			}
			System.out.println(Thread.currentThread().getName() + "正在卖" + ticket--);
		}
	}
}

测试代码:

public static void main(String[] args) {
		// 创建票对象
		Ticket ticket = new Ticket();
		// 通过Thread来模拟窗口
		Thread t1 = new Thread(ticket,"窗口1");
		Thread t2 = new Thread(ticket,"窗口2");
		Thread t3 = new Thread(ticket,"窗口3");
		
		// 开启线程进行卖票
		t1.start();
		t2.start();
		t3.start();
	}

如果不考虑线程安全问题的话,那么会出现以下情况:

  • 出现了重复的票
  • 出现了错误的票 0、-1

原因是当某个线程进入某句代码后,可能执行了一部分资源就被其他线程给抢了过去,从而导致该线程的数据进行了一般,可能刚输出完还没写入内存,这时被另一条线程抢走,此时数据还未进行修改,所以就出现了重复数据,而0,-1这种就是数量为1时刚通过上面的判断,但还未进行输出,这时资源被其他线程抢走,从而导致在数量为1时三条线程都通过了判断,再依次进行输出和自减操作。

线程安全:

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

而售票案例明显出了问题,就是存在线程安全隐患。

解决思路:保证核心代码同一时刻只能有一条线程在执行。

那么如和保证线程安全呢?一般我们使用这几种方法

- 同步代码块
格式:synchronized (锁对象) {

可能会产生线程安全问题的代码

}
同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。
方法区的内容是被线程共享的,所以"abc" Math.class 都可以当做同步代码块的锁资源 。
同步代码块会影响多线程的效率,所以只加核心可能出现隐患的代码。

- 同步方法
我们可以使用synchronized 修饰方法,可以保证方法中同一时刻只能有一条线程在执行。

同步方法的锁资源是this。

- 静态同步方法:
被synchronized和static修饰的方法,可以保证方法中同一时刻只能有一条线程在 执行。

静态同步方法的锁资源是 类名.class。

通过上面几种方法,我们可以修改一下我们的代码

// 模拟票数
public class Ticket implements Runnable{
	int ticket = 100;
	// 当做同步代码块的锁资源
	Object obj = new Object();
	@Override
	public void run() {
		// TODO Auto-generated method stub
			
		while (true){
			// 让当前线程休眠 单位是毫秒
			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
//			// 同步代码块
			synchronized (obj) {
				// 说明票卖光了
				if (ticket <= 0){
					break;
				}
				
				System.out.println(Thread.currentThread().getName() + "正在卖" + ticket--);
			}
		}
	}
}

总结:

同步:只允许一个线程执行,此时线程是安全的;
异步:允许多线程同时执行,此时就有可能出现线程安全隐患。

线程安全隐患出现的条件:

  1. 存在多个线程。
  2. 拥有共享的资源。
  3. 对资源进行非原子性操作。

解决线程安全的方法:

1.同步代码块;
锁资源是任何共享的对象。

2.同步方法;
锁资源是this。

3.静态同步方法;
锁资源是类名.class。

补充:

什么是原子操作呢”?
可以理解为类似于数据库的事务,毕竟事务也有原子性,这个(组)操作在执行时不会被其他原因打断,要么执行成功,要么全部失败,不存在一半成功一半失败的可能。

那么原子操作都有哪些呢?
(1)除long和double之外的基本类型的赋值操作
(2)所有引用reference的赋值操作
(3)java.concurrent.Atomic.* 包中所有类的一切操作
注:count++不是原子操作,因为它可以拆分为三个原子操作。

那为什么long和double的赋值不属于原子操作呢?

在网上看到某篇文章,大致意思如下:
1.对一个没有使用volatile修饰的long或double类型的赋值会被拆分成两次写,每次写该类型的32-bit数据,再使用volatile修饰后的long和double类型的读写操作是原子性的。
2.对其引用类型(Long/Double)的读写操作是属于原子操作,尽管他们的实现可能被分为两次32-bit或者一个64-bit。

死锁deadlock:
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
出现死锁的原因是有锁的嵌套,类似于一个线程A套着B,另线程一个B套着A,此时两个线程都在进行,A和B都被锁住,等着另一方释放资源,结果谁也不释放,造成死锁。

活锁
除死锁外还有一个更恐怖的bug——活锁。
活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。处于活锁的实体是在不断的改变状态,活锁有可能自行解开。

;原文链接:https://blog.csdn.net/qq_42649356/article/details/115183467
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文


随机推荐