上一篇文章的传送门:关于一些基础的Java问题的解答(四)
实现多线程的两种方法:Thread与Runable
在Java中实现多线程编程有以下几个方法:
继承Thread类,重写run方法
1 | public class Test { |
实现Runnable接口,作为参数传入Thread构造函数
1 | public class Test { |
使用ExecutorService类
1 | import java.util.concurrent.ExecutorService; |
补充:根据《阿里巴巴Java开发手册》,并不推荐使用Executors类中提供的线程池来开启线程,虽然这会比较方便,但是因为线程池参数不太合理的缘故,容易造成系统OOM
线程同步的方法:sychronized、lock、reentrantLock等
多线程编程时同步一直是一个非常重要的问题,很多时候我们由于同步问题程序失败的概率非常低,导致往往存在我们的代码缺陷,但他们看起来是正确的:
1 | public class Test { |
上面的代码创建了两个线程操作Test类中的静态变量value,调用next方法每次会为value的值加2,理论上来说isEven方法的返回值应该总是true,两个线程的工作会不停止的执行下去。但事实是:
因此在我们进行多线程并发编程时,使用同步技术是非常重要的。
synchronized
Java以提供关键字synchronized的形式,为防止资源冲突提供了内置支持。当某个线程处于一个对于标记为synchronized的方法的调用中,那么在这个线程从方法返回前,其他所有要调用类中任何标记为synchronized方法的线程都会被阻塞。对刚才的代码稍作修改,如下:
1 | /** |
除了锁定方法,synchronized关键字还能锁定固定代码块:
1 | /** |
在synchronized关键字后的小括号内加入要加锁的对象即可。通过这种方法分离出来的代码段被称为临界区,也叫作同步控制块。
加入了synchronized后,在一个线程访问next方法的时候,另一个线程就无法访问next方法了,使得两个线程的工作互不干扰,循环也变得根本停不下来。
ReentrantLock
除了synchronized关键字外,我们还可以使用Lock对象为我们的代码加锁,Lock对象必须被显示地创建、锁定和释放:
1 | private static Lock lock = new ReentrantLock(); |
一般而言,当我们使用synchronized时,需要写的代码量更少,因此通常只有我们在解决某些特殊问题时,才需要使用到Lock对象,比如尝试去获得锁:
1 | /** |
除了ReentrantLock外,Lock类还有众多子类锁,在此不做深入讨论。值得注意的是,很明显,使用Lock通常会比使用synchronized高效许多,但我们并发编程时都应该从synchronized关键字入手,只有在性能调优时才替换为Lock对象这种做法。
锁的等级:对象锁、类锁
这是关于synchronized关键字的概念,synchronized关键字可以用来锁定对象的非静态方法或其中的代码块,此时关键字是为对象的实例加锁了,所以称为对象锁:
1 | public synchronized void f() {}; |
另外,synchronized也可以用来锁定类的静态方法和其中的代码块,此时关键字就是为类(类的Class对象)加锁了,因此被称为类锁:
1 | public class Test { |
写出生产者消费者模式
生产者消费者模式一般而言有四种实现方法:
- wait和notify方法
- await和signal方法
- BlockingQueue阻塞队列方法
- PipedInputStream和PipedOutputStream管道流方法
第一种方法(wait和notify)的实现:
1 | import java.util.LinkedList; |
第二种方法(await和signal)实现:
1 | import java.util.LinkedList; |
第三种方法(BlockingQueue阻塞队列)实现:
1 | import java.util.concurrent.BlockingQueue; |
第四种方法(PipedInputStream和PipedOutputStream):
1 | import java.io.PipedInputStream; |
输出结果:
ThreadLocal的设计理念与作用
ThreadLocal即线程本地存储。防止线程在共享资源上产生冲突的一种方式是根除对变量的共享。ThreadLocal是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储,ThreadLocal对象通常当做静态域存储,通过get和set方法来访问对象的内容:
1 | import java.util.Random; |
运行部分结果如下:
在上面的例子中虽然多个线程都去调用了ThreadLocalVariableHolder的increment和get方法,但这两个方法都没有进行同步处理,这是因为ThreadLocal保证我们使用的时候不会出现竞争条件。从结果来看,每个线程都在单独操作自己的变量,每个单独的线程都被分配了自己的存储(即便只有一个ThreadLocalVariableHolder对象),线程之间并没有互相造成影响。对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。