Java:并发不易,先学会用

从事Java编程已经11年了,绝对是个老兵;但对于Java并发编程,只能算是个新兵蛋子。我说这话估计要遭到某些高手的冷嘲热讽,但我并不感到惧怕。java

由于我知道,每一年都会有不少不少的新人要加入Java编程的大军,他们对“并发”编程中遇到的问题也会有感到无助的时候。而我,很是乐意与他们一道,对使用Java线程进行并发程序开发的基础知识进行新一轮的学习。程序员

0一、咱们为何要学习并发?

个人脑壳没有被如来佛祖开过光,因此喜欢一件事接着一件事的想,作不到“一脑两用”。但有些大佬就不同,好比说诸葛亮,就可以一边想着琴谱一边谈着弹着琴,还能夹带着盘算出司马懿退兵后的打算。编程

诸葛大佬就有着超强的“并发”能力啊。换作是我,面对司马懿的千万大军,不只弹不了琴,弄很差还被吓得屁滚尿流。缓存

每一个人都只有一个脑子,就像电脑只有一个CPU同样。但一个脑子并不意味着不能“一脑两用”,关键就在于脑子有没有“并发”的能力。服务器

脑子要是有了并发能力,那真的是厉害到飞起啊,想一想司马懿被气定神闲的诸葛大佬吓跑的样子就知道了。网络

对于程序来讲,若是具备并发的能力,效率就可以大幅度地提高。你必定注册过很多网站,收到过很多验证码,若是网站的服务器端在发送验证码的时候,没有专门起一个线程来处理(并发),假如网络很差发生阻塞的话,那服务器端岂不是要从天亮等到天黑才知道你有没有收到验证码?若是就你一个用户也就算了,但假若有一百个用户呢?这一百个用户难道也要在那傻傻地等着,那真要等到花都谢了。多线程

可想而知,并发编程是多么的重要!何况,懂不懂Java虚拟机和会不会并发编程,几乎是断定一个Java开发人员是否是高手的不三法则。因此要想挣得多,还得会并发啊并发

0二、并发第一步,建立一个线程

一般,启动一个程序,就至关于起了一个进程。每一个电脑都会运行不少程序,因此你会在进程管理器中看到不少进程。你会说,这不废话吗?ide

不不不,在我刚学习编程的很长一段时间内,我都想固然地觉得这些进程就是线程;但后来我知道不是那么回事儿。一个进程里,可能会有不少线程在运行,也可能只有一个。函数

main函数其实就是一个主线程。咱们能够在这个主线程当中建立不少其余的线程。来看下面这段代码。

public class Wanger {
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			Thread t = new Thread(new Runnable() {
				
				@Override
				public void run() {
					System.out.println("我叫" + Thread.currentThread().getName() + ",我超喜欢沉默王二的写做风格");
				}
			});
			t.start();
		}
	}
}
复制代码

建立线程最经常使用的方式就是声明一个实现了Runnable接口的匿名内部类;而后将它做为建立Thread对象的参数;再而后调用Thread对象的start()方法进行启动。运行的结果以下。

我叫Thread-1,我超喜欢沉默王二的写做风格
我叫Thread-3,我超喜欢沉默王二的写做风格
我叫Thread-2,我超喜欢沉默王二的写做风格
我叫Thread-0,我超喜欢沉默王二的写做风格
我叫Thread-5,我超喜欢沉默王二的写做风格
我叫Thread-4,我超喜欢沉默王二的写做风格
我叫Thread-6,我超喜欢沉默王二的写做风格
我叫Thread-7,我超喜欢沉默王二的写做风格
我叫Thread-8,我超喜欢沉默王二的写做风格
我叫Thread-9,我超喜欢沉默王二的写做风格
复制代码

从运行的结果中能够看得出来,线程的执行顺序不是从0到9的,而是有必定的随机性。这是由于Java的并发是抢占式的,线程0虽然建立得最先,但它的“争宠”能力却通常,上位得比较艰辛

0三、并发第二步,建立线程池

java.util.concurrent.Executors类提供了一系列工厂方法用于建立线程池,可把多个线程放在一块儿进行更高效地管理。示例以下。

public class Wanger {
	public static void main(String[] args) {
		ExecutorService executorService = Executors.newCachedThreadPool();

		for (int i = 0; i < 10; i++) {
			Runnable r = new Runnable() {

				@Override
				public void run() {
					System.out.println("我叫" + Thread.currentThread().getName() + ",我超喜欢沉默王二的写做风格");
				}
			};
			executorService.execute(r);
		}
		executorService.shutdown();
	}
}
复制代码

运行的结果以下。

我叫pool-1-thread-2,我超喜欢沉默王二的写做风格
我叫pool-1-thread-4,我超喜欢沉默王二的写做风格
我叫pool-1-thread-5,我超喜欢沉默王二的写做风格
我叫pool-1-thread-3,我超喜欢沉默王二的写做风格
我叫pool-1-thread-4,我超喜欢沉默王二的写做风格
我叫pool-1-thread-1,我超喜欢沉默王二的写做风格
我叫pool-1-thread-7,我超喜欢沉默王二的写做风格
我叫pool-1-thread-6,我超喜欢沉默王二的写做风格
我叫pool-1-thread-5,我超喜欢沉默王二的写做风格
我叫pool-1-thread-6,我超喜欢沉默王二的写做风格
复制代码

ExecutorsnewCachedThreadPool()方法用于建立一个可缓存的线程池,调用该线程池的方法execute()能够重用之前的线程,只要该线程可用;好比说,pool-1-thread-4pool-1-thread-5pool-1-thread-6就获得了重用的机会。我能想到的最佳形象代言人就是女皇武则天。

若是没有可用的线程,就会建立一个新线程并添加到池中。固然了,那些60秒内尚未被使用的线程也会从缓存中移除。

另外,ExecutorsnewFiexedThreadPool(int num)方法用于建立固定数目线程的线程池;newSingleThreadExecutor()方法用于建立单线程化的线程池(你能想到它应该使用的场合吗?)。

可是,故事要转折了。阿里巴巴的Java开发手册(可在「沉默王二」公众号的后台回复关键字「Java」获取)中明确地指出,不容许使用Executors来建立线程池。

不能使用Executors建立线程池,那么该怎么建立线程池呢?

直接调用ThreadPoolExecutor的构造函数来建立线程池呗。其实Executors就是这么作的,只不过没有对BlockQueue指定容量。咱们须要作的就是在建立的时候指定容量。代码示例以下。

ExecutorService executor = new ThreadPoolExecutor(10, 10,
        60L, TimeUnit.SECONDS,
        new ArrayBlockingQueue(10));
复制代码

0四、并发第三步,解决共享资源竞争的问题

有一次,我陪家人在商场里面逛街,出电梯的时候有一个傻叉非要抢着进电梯。女儿的小推车就压到了那傻叉的脚上,他居然不依不饶地指着个人鼻子叫嚣。我直接一拳就打在他的鼻子上,随后咱们就纠缠在了一块儿。

这件事情说明了什么问题呢?第一,遇到不讲文明不知道“先出后进”(LIFO)规则的傻叉真的很麻烦;第二,竞争共享资源的时候,弄很差要拳脚相向。

在Java中,解决共享资源竞争问题的首个解决方案就是使用关键字synchronized。当线程执行被synchronized保护的代码片断的时候,会对这段代码进行上锁,其余调用这段代码的线程会被阻塞,直到锁被释放。

下面这段代码使用ThreadPoolExecutor建立了一个线程池,池里面的每一个线程会对共享资源count进行+1操做。如今,闭上眼想想,当1000个线程执行结束后,count的值会是多少呢?

public class Wanger {
	public static int count = 0;
	
	public static int getCount() {
		return count;
	}
	
	public static void addCount() {
		 count++;
	}
	
	public static void main(String[] args) {
		ExecutorService executorService = new ThreadPoolExecutor(10, 1000,
		        60L, TimeUnit.SECONDS,
		        new ArrayBlockingQueue<Runnable>(10));


		for (int i = 0; i < 1000; i++) {
			Runnable r = new Runnable() {

				@Override
				public void run() {
					Wanger.addCount();
				}
			};
			executorService.execute(r);
		}
		executorService.shutdown();
		System.out.println(Wanger.count);
	}
}
复制代码

事实上,共享资源count的值颇有多是99六、998,但不多会是1000。为何呢?

由于一个线程正在写这个变量的时候,另一个线程可能正在读这个变量,或者正在写这个变量。这个变量就变成了一个“不肯定状态”的数据。这个变量必须被保护起来

一般的作法就是在改变这个变量的addCount()方法上加上synchronized关键字——保证线程在访问这个变量的时候有序地进行排队。

示例以下:

public synchronized static void addCount() {
	 count++;
}
复制代码

还有另外的一种经常使用方法——读写锁。分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,由Java虚拟机控制。若是代码容许不少线程同时读,但不能同时写,就上读锁;若是代码不容许同时读,而且只能有一个线程在写,就上写锁。

读写锁的接口是ReadWriteLock,具体实现类是 ReentrantReadWriteLocksynchronized属于互斥锁,任什么时候候只容许一个线程的读写操做,其余线程必须等待;而ReadWriteLock容许多个线程得到读锁,但只容许一个线程得到写锁,效率相对较高一些。

咱们先使用枚举建立一个读写锁的单例。代码以下:

public enum Locker {

	INSTANCE;

	private static final ReadWriteLock lock = new ReentrantReadWriteLock();

	public Lock writeLock() {
		return lock.writeLock();
	}

}
复制代码

再在addCount()方法中对count++;上锁。示例以下。

public static void addCount() {
	// 上锁
	Lock writeLock = Locker.INSTANCE.writeLock();
	writeLock.lock();
	count++;
	// 释放锁
	writeLock.unlock();
}
复制代码

使用读写锁的时候,切记最后要释放锁。

0五、最后

并发编程难学吗?说实话,真的不太容易。来看一下王宝令老师总结的思惟导图就能知道。

但你也知道,“冰冻三尺非一日之寒”,学习是一件按部就班的事情。只要你学会了怎么建立一个线程,学会了怎么建立线程池,学会了怎么解决共享资源竞争的问题,你已经在并发编程的领域里迈出去了一大步。

为本身加个油,好吗?


PS:欢迎关注个人公众号「沉默王二」,一个不止写代码的程序员,还写有趣有益的文字,给不喜欢严肃的你。

相关文章
相关标签/搜索