哈哈哈哈哈哈哈!
这一篇文章参考《Java 并发编程之美》的第一章,实现一下里面的一些示例。
线程的创建方式
让一个类可以作为线程运行有三种方式——继承Thread
类并重写run
方法,实现Runnable
接口并实现run
方法,使用FutureTask
方式。其中第二个方式需要通过Thread
类的接受Runnable
对象的构造器进行,它也可以使用 lambda 表达式或匿名实现类来进行,因为Runnable
是一个函数式接口。
继承方法代码如下——
| class ThreadFromExtends extends Thread { @Override public void run() { System.out.println("Hello, Happy World!"); } public static void main(String[] args) { Thread instance = new ThreadFromExtends(); instance.start(); } }
|
通过实现Runnable
接口的方法如下——
| class ThreadFromImplements implements Runnable { @Override public void run() { System.out.println("I was happy"); } public static void main(String[] args) { Thread instance = new Thread(new ThreadFromImplements()); instance.start(); } }
|
还可以使用匿名实现类和 lambda 表达式——
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class Main { public static void main(String[] args) throws Exception { Thread threadA = new Thread(new Runnable() { @Override public void run() { System.out.println("通过 Runnable 接口的匿名实现类来实现创建 Thread"); } }); Thread threadB = new Thread(()->{ System.out.println("有 lambda 表达式,方便很多!"); }); Thread threadC = new Thread(Main::method); } public static void method() { System.out.println("也可以用方法作为参数,这和 lambda 是一致的"); } }
|
FutureTask 方式等将来遇到再说。
synchronized,wait,notify
Java 中任何对象都有监视器锁(这是Object
类所持有的,而任何对象都是Object
对象)。其提供了wait
,notify
,notifyAll
等方法用来进行某种线程间的通知和等待。这些方法都应当在同步代码块中进行,否则会抛出IllegalMonitorStateException
异常。
当线程进入某个对象的同步代码块(使用synchronized
关键字定义的代码块或方法),它就持有该对象的监视器锁。此时,任何其它线程都将被阻塞到这个对象的同步代码块之外,除非它们获得锁。
如果线程调用wait
方法,它将被阻塞挂起,并释放锁。该线程遇到两个情况会恢复到 runnable 状态——其它线程调用notify
或notifyAll
方法将其唤醒,或该线程被 interrupted(也就是该线程的interrupt
方法被调用)。
线程从挂起到可以运行可能在没有被 notify,没有被 interrupt,没有超时的时候也能发生,这称作虚假唤醒,为防虚假唤醒,应当用 while 包裹 wait 方法(当然,虚假唤醒仅仅是其中一个因素,如果该线程被唤醒后发现其执行的条件不满足,其应当必须再次放弃锁并挂起阻塞)。
| synchronized (obj) { while (条件不满足) obj.wait(); ... }
|
下面给出一个简单的生产者消费者的例子,使用监视器锁来协调消费者线程和生产者线程的同步。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| public class Main { static Queue<Object> queue = new LinkedList<>(); static final int MAX_SIZE = 10; static final Object ele = new Object();
static void produce() { synchronized (queue) { while (queue.size() == MAX_SIZE) try { queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } queue.add(ele); System.out.println("生产 1"); queue.notifyAll(); } }
static void consume() { synchronized (queue) { while (queue.size() == 0) try { queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } queue.remove(); System.out.println("消费 1"); queue.notifyAll(); } } public static void main(String[] args) throws Exception { Thread producer = new Thread(()->{while (true) produce();}); Thread consumer = new Thread(()->{while (true) consume();}); producer.start(); consumer.start(); Thread.sleep(5); System.exit(0); } }
|
调用 wait 时,线程会释放锁并阻塞,因为 wait 方法而阻塞的线程可以是多个的。如果有线程调用 notify,则其中一个线程会被唤醒,如果调用 notifyAll,则所有阻塞在 wait 方法的线程都会被唤醒。
调用 wait 方法时,只有该锁会被释放,该线程持有的其它锁是不会被释放的,下面用代码展示,线程 A 持有资源 1 后持有资源 2 并阻塞,线程 B 延迟执行并试图持有资源 2 然后持有资源 1——
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| public static void main(String[] args) { final Object resource1 = new Object(); final Object resource2 = new Object(); Thread threadA = new Thread(() -> { synchronized (resource1) { System.out.println("线程 A 持有资源 1"); System.out.println("线程 A 试图持有资源 2"); synchronized (resource2) { System.out.println("线程 A 持有资源 2"); System.out.println("线程 A 释放资源 2 并阻塞"); try { resource2.wait(); } catch (InterruptedException e) { } } } System.out.println("线程 A 释放资源 1"); }); Thread threadB = new Thread(() -> { System.out.println("线程 B 试图获取资源 2"); synchronized (resource2) { System.out.println("线程 B 获取资源 2\n 线程 B 试图获取资源 1"); synchronized(resource1) {} } }); threadA.start(); threadB.start(); }
|
可以看到,线程 A 被阻塞后只释放了资源 2 的锁,仍持有资源 1 的锁。
yield
yield 是一个静态方法,它表示当前线程让出剩余的时间片,立即进行线程调度。yield 并不常用。线程调度器可以无条件忽略该请求。(如果被忽略,这剩余的时间里它都要阻塞吗?)
给两个例子——
| public class Main { public static void yieldTest() { final Thread ME = Thread.currentThread(); for (int i = 0; i < 5; i++) { if (i % 5 == 0) { System.out.println(ME+" yield"); Thread.yield(); } } System.out.println(ME+" Over"); } public static void main(String[] args) throws Exception { new Thread(Main::yieldTest).start(); } }
|
结果似乎同书中所讲的不一样……搁置。
中断
当线程阻塞在wait
,sleep
,join
的时候如果被其它线程中断,将会抛出InterruptedException
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class Main { public static synchronized void interruptTest() { try { System.out.println("a 启动并阻塞"); Main.class.wait(); } catch (InterruptedException e) { e.printStackTrace(); System.out.println("被中断啦"); return; } System.out.println("这一段不可能被执行,除非虚假唤醒了"); } public static void main(String[] args) throws Exception { Thread a = new Thread(Main::interruptTest); a.start(); Thread.sleep(10); a.interrupt(); a.join(); } }
|
与查看和设置对象的中断标志位相关的方法有三个——interrupt
, isInterrupted
, interrupted
。其中 interrupted 是 static 方法,它处理调用该方法的线程(这就是为什么它采取过去式的形式)。
interrupt
方法中断线程,它实质是设置线程的中断标志位为 true,并非是直接“终结”线程。如果该线程被中断时阻塞在 wait,sleep 或 join 方法中,它会在这些地方抛出 InterruptedException。
isInterrupted
方法检查线程是否被中断。
interrupted
检查当前线程是否中断。如果中断,则返回 true,并清除中断标志位。如果未被中断,则返回 false。总之无论如何,调用该方法后中断标志位会被重置为 false。调用它应该使用 Thread.interrupted() 的形式。
一个不断检查中断并在被中断时优雅退出的例子是——
| public void run() { try { while (!Thread.currentThread().isInterrupted() && workNotOver()) { } } catch (InterruptedException e) { } finally { } }
|
死锁
当所有线程都因为无法获得所需资源被阻塞,这种情况就称为死锁。一个典型的例子是,线程 A 独占资源 1, 线程 B 独占资源 2, 此时线程 A 请求资源 2 才能继续进行,线程 B 请求资源 1 才能继续进行,A 和 B 因此就都无法进行了。
HR:只要你能给我解释死锁,我就给你 Offer。
我:只要你给我 Offer,我就给你解释死锁。
死锁有四个条件,任意条件不满足,死锁就不会发生——
- 互斥条件:线程对资源的访问是排他性的,即该资源只容许被一个线程占用,如果有其它线程尝试获取,则只能等待该线程释放资源。
- 请求并持有(不如说持有并请求):一个线程已经持有至少一个资源,但又要请求其他资源,而该其它资源被其它线程占用。
- 不可剥夺:线程占有的资源只能由自己释放,其它线程无法干涉。
- 环路等待:发生死锁时必然存在线程——资源的环形链。定义这样的线程集合{T0,T1,T2,..,Tn},其中 T0 请求 T1 占有的资源,T1 请求 T2 占有的资源……Tn 请求 T0 占有的资源。
考虑之前提的死锁的例子——
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| public class Main { static final Object resource1 = new Object(); static final Object resource2 = new Object();
public static void main(String[] args) throws Exception { new Thread(() -> { synchronized (resource1) { System.out.println("A 获取资源 1");
try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("A 尝试获取资源 2"); synchronized (resource2) { System.out.println("A 获取资源 2(不可能发生!)"); } } }).start(); new Thread(()->{ synchronized (resource2) { System.out.println("B 获取资源 2");
try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("B 尝试获取资源 1"); synchronized (resource1) { System.out.println("B 获取资源 1(不可能发生!)"); } } }).start(); } }
|
synchronized 满足互斥条件和不可剥夺条件——任意资源只能同时被一个线程占有,锁只能被持有线程释放。
请求并持有条件也是满足的,A 在持有 1 的情况下请求 2,B 在持有 2 的情况下请求 1。这里也是满足环路等待的。
避免死锁通过让死锁的必要条件不满足来实现,但是我们只能破坏环路等待和请求并持有条件。在这里,只需要让 A 和 B 申请资源的顺序一致即可,也就是确定资源申请的有序性。
守护线程和用户线程
Java 中线程分为两类,守护线程(daemon 线程,这个词在 linux 里也有用到,所谓的 systemd,这里的 d 就是 daemon)和是用户线程(user 线程)。JVM 中很多线程,如垃圾回收线程,是 daemon 进程。一个典型的用户线程是 main 方法的线程。
JVM 只有在所有用户线程都结束的时候才会退出。见该实例——
| public static void main(String[] args) throws Exception { new Thread(()->{ while(true); }).start(); System.out.println("主线程完了"); }
|
可以通过setDaemon
方法设置该线程为 daemon 线程。该方法不能在线程执行的时候调用,否则会抛IllegalThreadStateException
。
ThreadLocal
ThreadLocal 类能够让每个线程对变量进行访问的时候,访问的是自己线程的变量(也就是说相当于把状态变量变成局部变量,给了它封闭性了)。
ThreadLocal 变量的行为是,让访问这个变量的每个线程都有该变量的一个本地副本,当线程对该变量进行操作的时候,实际上是操作自己本地内存(栈)里面的变量。(这样是不是就能避免本地缓存和主内存不一致的问题,从而保证可见性?它可以替代 volatile 吗?)
剩下的,之后再说。
并发真是个麻烦的东西……再次认识到,现在的编程语言对并发的抽象是差劲的,完全不能从编程语言本身来判断结果……必须要付诸到机器底层……好家伙,这更暴露出来了我的基本功不足。需要加强了!
思来想去,决定现在先搁置并发,对我来说,这样一个全新(而且庞大,晦涩)的领域如果一头扎进去,我基本上在毕业之前就找不到工作了。
当前需要专注的东西实在是非常明显的——Java EE,框架,设计模式,计网以及数据结构和算法!别再雨露均沾了!