深入理解java并发编程基础篇(一)-------并发编程相关概念

3,081 阅读5分钟

一、前言

  拖了很久的并发编程,今天会开始第一篇,主要分为俩大部分进行学习:分为基础篇以及进阶篇,下面就开始基础篇的学习。

二、并发编程的相关概念

2.1.同步(Sync)与异步(Async)

2.2.1 同步(Sync)

  所谓同步,就是发出一个功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作。
  根据这个定义,Java中所有方法都是同步调用,应为必须要等到结果后才会继续执行。 我们在说同步、异步的时候,一般而言是特指那些需要其他端协作或者需要一定时间完成的任务。简单来说,同步就是必须一件一件事做,等前一件做完了才能做下一件事。

2.2.2 异步(Async)

  异步与同步相对,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。 当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制。

如下图示:

2.2.并发(Concurrency)和并行(Parallelism)

  并发和并行是两个非常容易被混淆的概念。他们都可以表示两个或者多个任务一起执行,但是侧重点有所不同。并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的,而并行是真正意义上的“同时执行”,如下图:

  实际上,如果系统内只有一个CPU,而使用多进程或者多线程任务,那么真实环境中这些任务不可能是真实并行的,毕竟一个CPU一次只能执行一条指令,在这种情况下多进程或者多线程就是并发的,而不是并行的(操作系统会不停地切换多任务)。真实的并行也只可能出现在拥有多个CPU的系统中(比如多核CPU)。

2.3.临界区

  临界区表示公共资源或是共享数据,可以被多个线程使用。但是每次只能有一个线程使用它,一旦临界区的资源被占用,其他线程就必须等到资源释放后才能继续使用该资源。在Java程序开发中,对于这样的资源一般都需要做同步的操作,例如下面的这段代码,用的就是synchronized关键字来对临界区资源进行同步:

package com.MyMineBug.demoRun.test;

/**
 * 
 * @author 18360
 *
 */
public class SyncTest implements Runnable {

	// 临界区资源
	public static SyncTest instance = new SyncTest();

	@Override
	public void run() {
		synchronized (instance) {

		}
	}
	
	public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new SyncTest());
        Thread t2 = new Thread(new SyncTest());
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }

}

2.4. 阻塞(Blocking)和非阻塞(Non-Blocking)

  阻塞和非阻塞通常用来形容很多线程间的相互影响。比如一个线程占用了共享资源,那么其他所有需要这个资源的线程就必须在这个临界区中等待。等待会导致线程挂起,这种情况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其他线程阻塞在这个临界区上的线程都不能工作。

  非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行,所有的线程都会尝试不断向前执行

2.5.死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock)

2.5.1 死锁(Deadlock)

  死锁一般是指两个或者两个以上的线程互相持有对方所需的资源,并且永远在等待对方释放的一种阻塞状态。例如有两个线程A和B同时共享临界区的资源C,当A占用C时,B处于阻塞状态,然而A的释放需要用到B的资源,这样一来,就变成了A一直在等待B,B也一直在等待A,互相之间永远在等待对方释放的状态。

以下是死锁的简单例子:

package com.MyMineBug.demoRun.test;

public class Demo1 {

	public static String obj1 = "obj1";
	public static String obj2 = "obj2";

	public static void main(String[] args) {
		Thread a = new Thread(new Lock1());
		Thread b = new Thread(new Lock2());
		a.start();
		b.start();
	}

}

	class Lock1 implements Runnable {
	
		@Override
		public void run() {
			try {
				System.out.println("Lock1 running");
				while (true) {
					synchronized (Demo1.obj1) {
						System.out.println("Lock1 lock obj1");
						Thread.sleep(3000);// 获取obj1后先等一会儿,让Lock2有足够的时间锁住obj2
						synchronized (Demo1.obj2) {
							System.out.println("Lock1 lock obj2");
						}
					}
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
	
		}
	
	}
	
	class Lock2 implements Runnable {
	
		@Override
		public void run() {
			try {
				System.out.println("Lock2 running");
				while (true) {
					synchronized (Demo1.obj2) {
						System.out.println("Lock2 lock obj2");
						Thread.sleep(3000);
						synchronized (Demo1.obj1) {
							System.out.println("Lock2 lock obj1");
						}
					}
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
	
		}
	
	}


运行结果如下:

一般来说,死锁的发生是由于程序的设计不合理导致,而且死锁很难解决,最好的方式就是预防.

2.5.2 饥饿(Starvation)

  饥饿是指某一个或者多个线程因为种种原因无法获得所需的资源,导致一直无法执行。比如它的线程优先级太低,而高优先级的线程不断抢占它所需的资源,导致低优先级资源无法工作。

2.5.3 活锁(Livelock)

  活锁的情况是线程一种非常有趣的情况,在生活中我们可能会碰到这样的情况,那就是出门的时候可能会遇到有人要进门,你打算让他先进门,他又打算让你先出门,结果,两个人都互相退后了,然后你打算先出门时对方也向前一步,来来回回就一直卡在门口。当然,这种事情正常人很快就能解决,但如果是线程碰到就没那么幸运了。

  如果两个线程占用着公共的资源,并且秉承着 “谦让” 的原则,主动把资源让给他人使用,你让我也让,这样就造成资源在两个线程间不断跳动但线程之间都拿不到资源的情况,这样的情况就是活锁了。

  如果觉得还不错,请点个赞!!!

  Share Technology And Love Life