并发与线程安全(哈工大软件构造)

  在在本学期第七章,也是最后一章的学习中,我们学习了关于并发编程以及并发编程下维护线程安全的内容。下面在此介绍一些学习过程中较难理解的概念。

并发

  首先我们需要了解什么是并发。
  并发意味着一个时间段中有多个程序都处于已启动运行到运行完毕之间,但在某个确定的时刻,有且仅有一个程序在处理器上运行。并发是效率不断提高的必然性,随着技术的发展,摩尔定律逐渐失效,只有通过并发才能更高一步地提高计算机的效率。
  并发中涉及到线程的概念,初学时有许多人会将线程进程混淆,在此我们做出区分:

  1. 进程(Process):指的是运行中的程序,是一个程序在运行状态下的一个实例。一个进程可以通过多个线程实现。一般情况下进程之间不进行数据共享,每个进程有其私自的内存空间。
  2. 线程(Thread):线程是进程的一个特定执行路径,每个进程可以在有多条执行路径,即通过多个线程执行。线程之间往往会共享一些内存空间,但每个线程有其独立的堆栈,其中保存线程本身的局部变量。

  线程对于内存空间的占有可以通过下图理解:
线程对内存的占有
  显然,不同的线程之间需要进行数据交换,以维持程序的正常运行,现有两种较为常用的信息交换模型,分别为共享内存消息传递。前者直接通过共享内存进行数据交换,后者则通过不同线程之间发送消息到彼此的消息队列,进行信息交换。
  进程与线程的关系还可以对比这篇博客了解:什么是进程?什么是线程?进程和线程之间的区别是什么?

在Java中创建和运行线程

  一般我们有三种方式创建线程,除了课程中提及的两种之外,还可以实现Lambda表达式进行创建,如下:

  1. 继承Thread类:
public class myThread extends Thread {
	// 每个线程都需要重写run方法
	@Override
	public void run(){
		System.out.println("This is my thread!");
	}
	
	public static void main(String args[]){
		//启动线程是调用start方法,而非run方法。start中会自动调用run。
		(new myThread()).start(); 
	}
}

  这种创建线程的方法较为繁琐,需要专门为线程创建一个线程类,一般很少用这种方式。

  1. 实现Runnable接口:
public class myRunnable implements Runnable {
	public void run(){
		System.out.println("This is my thread!");
	}

	public static void main(String args[]) {
		(new Thread(new myRunnable)).start(); 
	}	
}

  这样与之前的类方法一样,需要创建新的类,一般情况下,我们可以采用匿名类启动线程,如下:

public class myThreadTest {
	public static void main(String args[]){
		new Thread(new Runnable(){
			public void run(){
				System.out.println("This is my thread!");
			}
		}).start();
	}
}
  1. 使用Lambda表达式
public class myThreadTest {
	public static void main(String args[]){
		new Thread(() -> System.out.println("Hello from a thread!")).start();
	}
}

线程之间的交错和竞争

  在多线程的情况下,由于在某个特定的时刻一个核上有且仅能有一个线程运转,所以操作系统会为线程划分时间分片,在决定线程的工作流程。若线程之间互不干扰,这样的划分可能并不会影响结果,但如之前所说的,一个进程中可能有多个线程,由于不同的线程之间共享有一定的内存,并且编译器出与优化的目的,许多底层指令的执行顺序可能并不是我们编写出的代码的顺序,所以多线程进程中,往往会因为不同线程中代码执行的交错和对资源的竞争,这对于可变类型的变量十分不友好,不知顺序的修改会导致系统出现莫名其妙的bug。这就要求程序员在编写代码时关注代码之间的并行状况,控制不同的线程对于内存的访问顺序,保证代码合理进行。
  在Java中,提供了一些控制线程进行的函数,在此就其中的几个做一介绍:

  1. thread.sleep(time):sleep方法可以使线程 thread 休眠 time 时间(单位为毫秒),在此期间线程不会进行工作,需要注意的是,sleep方法不会使线程丧失对的所有权(锁的概念在之后会有介绍)。并且,sleep是一个实例方法,会使调用该方法的线程 thread 休眠。
  2. thread.join():这也是一个实例方法,会使调用该方法的线程 thread 持续执行直到结束,而不会切换到其他线程。
  3. thread.interrupt():这属于实例方法,其效果为调用该方法的线程向指定线程 thread 发出一个中断信号,请求 thread 暂停运行。需要注意的是,这只是“有礼貌”的请求,并不会强制该线程停止,这个方法会设置 thread 中的一个标识程序状态的 boolean 变量为 true,当线程 thread 执行一些低级可阻塞的方法,如 sleep ,join 时,线程会抛出 InterruptedException 异常,程序捕获该异常后终止。 若线程之后没有执行这类低级可阻塞方法,线程则不会终止。
  4. thread.isInterrupted():这个方法较为简单,是一个实例方法,即检测之前说到的线程内部的标识终止状态的 boolean 变量,并返回其当前状态。
  5. Thread.interrupted():同上,这也是一个检测终止状态变量的方法,但这个方法会在检测后将该变量重新赋值为 false,并且这个方法是一个类方法,用于检测执行该方法的线程的状态。

线程安全

  如上一小节所说,程序员需要自己控制线程的执行方式,保证线程在任何情况下都能执行正确,能够严格遵守ADT的RI,不违反spec,不依靠于底层的时序。一般有以下四种方式用于保证线程安全:

  1. 限制数据共享:即将可变类型的数据限制在单一线程之内,而不与其他线程共享。这种方式有效但会极大的限制ADT的设计,有时难以做到ADT之间不共享可变的数据。
  2. 使用不可变数据类型和不可变引用:与上一条同理,不可变类型的变量在多线程之间往往是比较安全的。需要注意的是,在之前的ADT涉及博客中提到过一类有益的可变,即我们可以在不变类中修改其表示值而保持用户的抽象值不变,这样也可以称为不可变类型。但这类可变在多线程编程下不再允许,我们设计时需要杜绝这类可变。
  3. 使用线程安全的数据类型:在我们设计ADT时,有时会必须在多个线程间共享可变数据类型,这就要求我们使用更安全的方式。在Java中,往往为数据类型提供了两种形式,如对于数据类型 StringBuilder ,Java还提供了一种线程安全的类型 StringBuffer ,在线程安全的类型下,所有的操作都保证为原子操作,即执行该操作期间不会有其他操作介入,保证线程安全。但线程安全的数据类型往往性能较差,所以在使用时需要我们进行权衡。对于常见的 Collection 类,Java提供了一种装饰器模式,可将其包装为线程安全的类型,具体方法如下:
    线程安全类的修饰
      具体使用方法举例如下:
 Map<Interer,Boolean>  threadSafeList = Collections.synchronizedList(new ArrayList<>()) ;

  但这类线程安全的数据类型,只能保证单个方法的原子性,即执行单个方法的时候,不会插入其他方法的执行,但方法之间的交错引起的线程不安全情况仍然存在,这就需要我们利用的机制进行控制。
4. :即对数据结构加锁,只有拥有锁的线程才能对该数据结构进行操作,其他所有加锁的线程需要等待拥有锁的线程释放锁,再公平竞争这把锁以进行自己的线程。需要注意的是,所有的需要控制的线程都必须用同一把锁进行控制,否则无法达到同步的目的。在Java中,每个 Object 对象都可以作为锁进行控制。通常我们有两种同步机制进行同步,如下:

/*对代码块加锁,或对方法加锁*/

public class ThreadSafe {
	// 方法一:同步代码块,通过将自己(或者其他数据结构)作为synchronized的对象进行加锁
	public ThreadSafe() {
		synchronized(this) {
			doSomething();
		}
	}
	//方法二:通过在方法前关键词进行同步,注意,构造方法不能用此类关键词修饰。
	public synchronized void soSomething() {
		System.out.println("do something!");
	}
}

  在锁的机制下,容易出现一种死锁的情况,如线程A拥有锁1,等待锁2,线程B拥有锁2,等待锁1。这会导致两个线程无限期等待,导致程序失败,这需要我们更加巧妙的进行设计,防止死锁现象的出现。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值