关于多线程的学习(上)

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

线程的概述

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

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

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时三条线程都通过了判断,再依次进行输出和自减操作。

线程安全:

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

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

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

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

- 同步代码块
格式:// 模拟票数 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——活锁。
活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。处于活锁的实体是在不断的改变状态,活锁有可能自行解开。

版权声明:玥玥 发表于 2021-04-13 3:44:49。
转载请注明:关于多线程的学习(上) | 女黑客导航