Java并发核心:解决共享资源竞争

并发编程使我们将程序划分为多个分离的、独立运行的任务。 通过使用多线程机制,这些独立运行的任务(也被称为子任务)中的每一个都由一个执行线程来驱动。 一个线程就是在进程中的一个单一的顺序控制流,因此单个进程可以拥有多个并发执行的任务。

实现并发最直接的方式是在操作系统级别使用进程; 进程是运行在它自己的地址空间内的自包容的程序; 而实现并发变成最大的困难是如何协调不同线程驱动的任务之间对这些资源的访问,以使得这些资源不会同时被多个任务访问

并发的多面性

使用并发编程时需要解决的问题有多个,而实现并发的方式也有多种,并且在这两者之间没有明显的映射关系。

用并发解决的问题基本上可分为“速度”和“设计可管理性”两种。

阻塞的定义:

程序中的某个任务因为该程序控制范围之外的某些条件(通常是I/O)而导致不能继续运行,那么我们就说这个任务阻塞了

两种线程的调度模式:

抢占式调度:

抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。

协同式调度:

协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。

线程让步:

给线程调度器一个暗示: 你的工作已经差不多了,可以让出CPU给别的线程了。 这个暗示通过yield()方法来作出(不过这是暗示,没有任何机制会保证它将会被采纳)。

核心问题:解决共享资源竞争

对于并发工作,你需要某种方式来防止两个任务访问相同的资源,至少在关键时候不会出现这种情况 也就是Brain的同步规则: 如果你正在写一个变量,他可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程改写过的变量,那么你必须使用同步,并且,读写线程都必须使用相同的监视器锁同步。一般采用下面两种解决办法:

1.同步控制

java 一般用synchronized关键字的形式来防止资源冲突 ; 当任务执行到被synchronized关键字保护的代码片段的时候,它将检查锁是否可用,然后获取锁,执行代码,释放锁

临界区
synchronized可以用于域,也可以用于对象,被用于对象的锁是对花括号内的代码进行同步控制,synchronized(syncobject){

}
,这也被称为同步代码块;在进入此段代码前,必须先获得synObject对象的锁。通过这种方式分离出来的代码被称为临界区。通过使用同步代码块,而不是对整个方法进行同步控制,可以使多个任务同时访问对象的时间性能得到显著的提高。
基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案。也就意味着在给定时刻只允许一个任务访问资源。 通常这是通过在代码前面加上一条锁语句来实现的。 因为锁语句产生了一种互相排斥的效果,所以这称为互斥量。

/**
 * 
 */
package threads;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

class Car{
	private boolean waxOn = false;
	public synchronized void Wax(){
		waxOn = true;
		notifyAll();
	}
	
	public synchronized void Polishing() {
		waxOn = false;
		notifyAll();
	}
	
	public synchronized void waitWax() throws InterruptedException {
		while(waxOn == false)               //可能有多个任务处于同一个原因在等待一个锁,而第一个唤醒这个锁的可能会改变这个
			wait();                         //在这个任务从其wait状态唤醒时,有可能别的任务做出来改变,从而使得这个任务不能执行
	}
	
	public synchronized void waitPolishing() throws InterruptedException {
		while(waxOn == true)                 //最关键的任务就是检查其所感兴趣的特定条件,并在条件不满足的情况下返回到wait中
			wait();
	}
}

class WaxOn implements Runnable{
	private Car car;
	public WaxOn(Car c) {
		car = c;
	}
	public void run() {
		try {
			while(!Thread.interrupted()) {
			    System.out.println("Waxing!");
				TimeUnit.MILLISECONDS.sleep(200);
			    car.Wax();
			    car.waitPolishing();
			}
		}catch(InterruptedException e) {
			System.out.println("Exiting via interrupt");
		}
		System.out.println("Wax Over!");
	}
}

class WaxOff implements Runnable{
	private Car car;
	public WaxOff(Car c) {
		car = c;
	}
	
	public void run() {
		try {
			while(!Thread.interrupted()) {
				car.waitWax();
				System.out.println("Polishing!");
				TimeUnit.MILLISECONDS.sleep(200);
				car.Polishing();
			}
		}catch(InterruptedException e) {
		     System.out.println("Exiting via InterruptedException!");
		}
		System.out.println("Polishing over!");
	}
}
public class WatiAndNotify {
      public static void main(String[] args) throws Exception{
		Car car = new Car();
		ExecutorService exec = Executors.newCachedThreadPool();
		exec.execute(new WaxOn(car));
		exec.execute(new WaxOff(car));
		TimeUnit.SECONDS.sleep(2);
		exec.shutdownNow();
	}            
}

运行结果:

Polishing!
Waxing!
Polishing!
Waxing!
Polishing!
Waxing!
Polishing!
Waxing!
Polishing!
Exiting via interrupt
Wax Over!
Exiting via InterruptedException!
Polishing over!

2.线程本地存储

线程本地存储是一种自动化机制,可以为使用相同变量的不同线程都创建不同的存储。 创建和管理本地线程都由ThreadLocal类来实现。

/**
 * 
 */
package threads;

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

class Accessor implements Runnable{
	
	private final int id;
	public Accessor(int idn) {
		id = idn;
	}
	public void run() {
		while(!Thread.currentThread().isInterrupted()) {
			ThreadLocalVariableHodler.increment();
			System.out.println(this);
			Thread.yield();
		}
	}
	public String toString() {
		return "#" + id +": " + ThreadLocalVariableHodler.get(); 
	}
}
public class ThreadLocalVariableHodler {
    private static ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
    	private Random rand = new Random(47);
    	protected synchronized Integer initialValue() {
    		return rand.nextInt(10000);
    	}
    };
	public static void increment() {
    	value.set(value.get()+1);
    }
    public static int get() {
    	return value.get();
    }
    
    public static void main(String[] args) throws Exception {
		ExecutorService exec = Executors.newCachedThreadPool();
		for(int i=0;i<5;i++) {
			exec.execute(new Accessor(i));
		}
		Thread.sleep(3);
		exec.shutdownNow();
	}
}


输出结果:
#0: 6694
#3: 962
#2: 1862
#4: 556
#1: 9259
#3: 963
#2: 1863
#2: 1864
#2: 1865
#2: 1866
#2: 1867
#2: 1868
#2: 1869
...

ThreadLocal对象通常当作静态域存储。 在创建ThreadLocal时,你只能通过get()和set()方法来访问该对象的内容,其中get()方法返回与线程相关联的副本,而set()方法会将数据插入到其线程存储的对象中,并返回存储中原有的对象。

volatile关键字

也许还有人会说可以用volatile关键字来解决,因为有这么一句话:如果一个域可能会被多个任务同时访问,或者这些任务中至少有一个是写入任务,那么你就应该把这个域设置为volatile的。但是volatile关键字是不能保证线程安全的,该关键字只能使修饰的变量获得原子性(简单的赋值与返回操作),看个例子就知道了

package test;

public class volatileTest {
	public static volatile int race = 0;
	
	public static void increase() {
		race++;
	}
	private static final int THREADS_COUNT = 20;
	public static void main(String[] args) {
	   Thread[] threads = new Thread[THREADS_COUNT];
	   for(int i=0;i<THREADS_COUNT;i++) {
		   threads[i] = new Thread(new Runnable() {
			   public void run() {
				   for(int i =0; i<1000; i++) {
					   increase();
				   }
			   }
		   });
		   threads[i].start();
	   }
	   while(Thread.activeCount() >1) {
		   Thread.yield();
	   }
	   System.out.println(race);
 	}
}

race++这条代码编译成字节码之后是有四条字节码指令的,

 getstatic  // Field race:I
 iconst_1    
 iadd
 putstatic  //Field race: I

当getstatic指令把race的值取到操作栈顶时,volatile关键字保证了race的值在此时是正确的,但是在执行iconset_1、iadd这些指令时,其他线程肯能已经把race的值加大了,而操作栈顶的数据就变成了过期的数据,所以putstatic指令执行后就可能把较小的race值同步到主内存之中。
volatile关键字主要功能就是两点

  1. 保证此变量对所有线程的可见性,指一条线程修改了这个变量的值,新值对于其他线程是可见的,但并不是多线程安全的。
  2. 禁止指令重排序优化。

Volatile如何保证内存可见性
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到内存。
当读一个valatile时,JMM会把该线程对应的本地内存置为无效。线程将会从主内存中读取共享变量。

线程中断

有时我们必须中断一个被阻塞的任务,这时我们需要调用Thread类的Interrupt()方法, 这个方法可以设置线程的中断状态。
不能中断正在试图获取synchronized锁或者正在执行I/O操作的线程。

当一个线程已经被阻塞,或者试图执行一个阻塞操作,那么设置这个线程的中断状态将抛出InterruptException。 当抛出该异常或者该任务调用Thread.interrupted()时,中断状态将被复位;Thread.interrupted提供了离开run()循环而不抛出异常的第二种方式。

检查中断

当我们在线程上调用interrupt()方法时,中断发生的唯一时刻是在任务要进入到阻塞操作中,或者已经在阻塞操作的内部(如我们所见,除了不可中断的I/O操作或被阻塞的synchronized方法之外,在其余的例外情况下,似乎并没有什么作用);
但是如果我们只能通过在阻塞上抛出异常来退出,那么我们就无法总是可以离开run()循环;因此我们就需要用到第二种方法来退出,就是上面提到的interrupted()方法,该方法可以检查中断状态,这不仅可以告诉我们intrrupt()是否被调用过。而且还可以清除中断状态。 清除中断状态可以确保不并发结构不会就某个任务被中断这个问题而通知我们两次,我们可以经由单一的InterruptedException或单一的成功的Thread.interrupted()测试来得到通知。

在任务之间使用管道进行输入/输出

提供线程功能的类库以“管道”的形式对线程间的输入/输出进行了支持。 Java类库中对应的输入/输出类就是 pipedWriter类(允许任意管道间的写)和PipedReader类(允许多个不同线程对同一个管道读取,并且是可中断的)。

死锁的产生:

某个任务在等待另一个任务,而后者又在等待别的任务,这样一直下去,直到这个任务链上的某个任务又在等待第一个任务释放锁。
要发生死锁必须同时满足四个条件:

  1. 互斥条件。任务使用的资源至少有一个不是共享的
  2. 至少有一个任务它持有一个资源且正在等待一个当前被别的任务持有的资源
  3. 资源不能被任务抢占,任务必须把资源释放当作普通事件。
  4. 必须有循环等待,这时,一个任务等待其他任务所持有的资源,后者由在等待另一个任务所持有的资源。

免锁容器的定义:

容器是所有编程中基础工具,这其中自然也包括并发编程。 出于这个原因,像Vector和HashTable这种早期容器就包含有许多synchronized方法,当它们用于非多线程的程序时,便会导致不可接受的开销。所以后面添加了新的容器,通过使用更灵巧的技术来消除加锁,从而提高线程安全的性能。

这些免锁容器背后的策略是:对容器的修改可以与读取操作同时发生,只要用户只能看到完成修改的结果即可。 修改是在容器数据结构的某个部分的一个单独的副本(有时是整个数据结构的副本)上执行的,并且这个副本在修改过程中是不可视的;只有当修改完成时,被修改的结构才会自动地与主数据结构进行交换。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值