阅读skywang12345的Java多线程系列的笔记与总结。
记录一下,防止读过就忘了。下面大部分都是摘自原博。
1.线程的五种状态
新建状态(New) : 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
就绪状态(Runnable) : 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
运行状态(Running) : 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
阻塞状态(Blocked) : 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(01) 等待阻塞 – 通过调用线程的wait()方法,让线程等待某工作的完成。
(02) 同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
(03) 其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
死亡状态(Dead) : 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
2.Thread与Runnable
- Runnable 是一个接口,该接口中只包含了一个run()方法。它的定义如下:
1 2 3
| public interface Runnable { public abstract void run(); }
|
- Thread 是一个类。Thread本身就实现了Runnable接口。它的声明如下:
1
| public class Thread implements Runnable {}
|
此处参见Thread类的源代码,很清楚。
1 2 3 4 5 6 7 8 9 10 11 12
| private Runnable target; public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); } @Override public void run() { if (target != null) { target.run(); } }
|
3.start() 和 run()的区别
原理还是看上面的java源代码。
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class Demo { public static void main(String[] args) { Thread mythread = new MyThread2("mythread"); System.out.println(Thread.currentThread().getName() + " call mythread.run()"); mythread.run(); System.out.println(Thread.currentThread().getName() + " call mythread.start()"); mythread.start(); } } class MyThread2 extends Thread { public MyThread2(String name) { super(name); } public void run() { System.out.println(Thread.currentThread().getName() + " is running"); } };
|
运行结果:
1 2 3 4
| main call mythread.run() main is running main call mythread.start() mythread is running
|
4.synchronized关键字
4.1.同步锁是依赖于对象而存在
在java中,每一个对象有且仅有一个同步锁。当我们调用某对象的synchronized方法时,就获取了该对象的同步锁。别的线程此时若企图获取该同步锁,就会失败并且需要等待。
我们将synchronized的基本规则总结为下面3条,并通过实例对它们进行说明。
当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的该“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程仍然可以访问“该对象”的非同步代码块。
当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的其他的“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
示例代码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
| class MyRunable implements Runnable { @Override public void run() { synchronized(this) { try { for (int i = 0; i < 5; i++) { Thread.sleep(100); System.out.println(Thread.currentThread().getName() + " loop " + i); } } catch (InterruptedException ie) { } } } } public class Demo1_1 { public static void main(String[] args) { Runnable demo = new MyRunable(); Thread t1 = new Thread(demo, "t1"); Thread t2 = new Thread(demo, "t2"); t1.start(); t2.start(); } }
|
代码1运行结果:
1 2 3 4 5 6 7 8 9 10
| t1 loop 0 t1 loop 1 t1 loop 2 t1 loop 3 t1 loop 4 t2 loop 0 t2 loop 1 t2 loop 2 t2 loop 3 t2 loop 4
|
示例代码2:
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
| class MyThread extends Thread { public MyThread(String name) { super(name); } @Override public void run() { synchronized(this) { try { for (int i = 0; i < 5; i++) { Thread.sleep(100); System.out.println(Thread.currentThread().getName() + " loop " + i); } } catch (InterruptedException ie) { } } } } public class Demo1_2 { public static void main(String[] args) { Thread t1 = new MyThread("t1"); Thread t2 = new MyThread("t2"); t1.start(); t2.start(); } }
|
代码2运行结果:
1 2 3 4 5 6 7 8 9 10
| t1 loop 0 t2 loop 0 t1 loop 1 t2 loop 1 t1 loop 2 t2 loop 2 t1 loop 3 t2 loop 3 t1 loop 4 t2 loop 4
|
区别在于,示例1的两个线程都访问的是demo这个对象的锁,示例2的两个线程访问的是t1,t2各自的锁。
一定要搞清锁是谁的锁,这一点明白之后上面总结的所谓123条完全可以不看。
4.2.synchronized方法 和 synchronized代码块
“synchronized方法”是用synchronized修饰方法,而 “synchronized代码块”则是用synchronized修饰代码块。
synchronized方法示例:
1 2 3
| public synchronized void foo1() { System.out.println("synchronized methoed"); }
|
synchronized代码块:
1 2 3 4 5
| public void foo2() { synchronized (this) { System.out.println("synchronized methoed"); } }
|
synchronized代码块中的this是指当前对象。也可以将this替换成其他对象,例如将this替换成obj,则foo2()在执行synchronized(obj)时就获取的是obj的同步锁。
synchronized代码块可以更精确的控制冲突限制访问区域,效率更高
4.3.实例锁 和 全局锁
实例锁 : 锁在某一个实例对象上。如果该类是单例,那么该锁也具有全局锁的概念。
实例锁对应的就是synchronized关键字。
全局锁 : 该锁针对的是类,无论实例多少个对象,那么线程都共享该锁。
全局锁对应的就是static synchronized(或者是锁在该类的class或者classloader对象上)。
示例代码3:
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| class Something { public static synchronized void cSyncA(){ try { for (int i = 0; i < 5; i++) { Thread.sleep(100); System.out.println(Thread.currentThread().getName()+" : cSyncA"); } }catch (InterruptedException ie) { } } public static synchronized void cSyncB(){ try { for (int i = 0; i < 5; i++) { Thread.sleep(100); System.out.println(Thread.currentThread().getName()+" : cSyncB"); } }catch (InterruptedException ie) { } } } public class LockTest3 { Something x = new Something(); Something y = new Something(); private void test3() { Thread t31 = new Thread( new Runnable() { @Override public void run() { x.cSyncA(); } }, "t31"); Thread t32 = new Thread( new Runnable() { @Override public void run() { y.cSyncB(); } }, "t32"); t31.start(); t32.start(); } public static void main(String[] args) { LockTest3 demo = new LockTest3(); demo.test3(); } }
|
代码3结果:
1 2 3 4 5 6 7 8 9 10
| t31 : cSyncA t31 : cSyncA t31 : cSyncA t31 : cSyncA t31 : cSyncA t32 : cSyncB t32 : cSyncB t32 : cSyncB t32 : cSyncB t32 : cSyncB
|
示例代码4:
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| class Something { public synchronized void isSyncA(){ try { for (int i = 0; i < 5; i++) { Thread.sleep(100); System.out.println(Thread.currentThread().getName()+" : isSyncA"); } }catch (InterruptedException ie) { } } public static synchronized void cSyncA(){ try { for (int i = 0; i < 5; i++) { Thread.sleep(100); System.out.println(Thread.currentThread().getName()+" : cSyncA"); } }catch (InterruptedException ie) { } } } public class LockTest4 { Something x = new Something(); private void test4() { Thread t41 = new Thread( new Runnable() { @Override public void run() { x.isSyncA(); } }, "t41"); Thread t42 = new Thread( new Runnable() { @Override public void run() { x.cSyncA(); } }, "t42"); t41.start(); t42.start(); } public static void main(String[] args) { LockTest4 demo = new LockTest4(); demo.test4(); } }
|
代码4结果:
1 2 3 4 5 6 7 8 9 10
| t41 : isSyncA t42 : cSyncA t41 : isSyncA t42 : cSyncA t41 : isSyncA t42 : cSyncA t41 : isSyncA t42 : cSyncA t41 : isSyncA t42 : cSyncA
|
示例3不能被同时访问。因为cSyncA()和cSyncB()都是static类型,x.cSyncA()相当于Something.isSyncA(),y.cSyncB()相当于Something.isSyncB(),因此它们共用一个同步锁,不能被同时访问。
示例4可以被同时访问。因为isSyncA()是实例方法,x.isSyncA()使用的是对象x的锁;而cSyncA()是静态方法,Something.cSyncA()可以理解对使用的是“类的锁”。因此,它们是可以被同时访问的。
5.线程等待与唤醒
5.1.wait(), notify(), notifyAll()等方法介绍
在Object.java中,定义了wait(), notify()和notifyAll()等接口。wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。而notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。
5.2.wait()和notify()示例
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
| class ThreadA extends Thread{ public ThreadA(String name) { super(name); } public void run() { synchronized (this) { System.out.println(Thread.currentThread().getName()+" call notify()"); notify(); } } } public class WaitTest { public static void main(String[] args) { ThreadA t1 = new ThreadA("t1"); synchronized(t1) { try { System.out.println(Thread.currentThread().getName()+" start t1"); t1.start(); System.out.println(Thread.currentThread().getName()+" wait()"); t1.wait(); System.out.println(Thread.currentThread().getName()+" continue"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
|
运行结果:
1 2 3 4
| main start t1 main wait() t1 call notify() main continue
|
结果说明:
如下图,说明了“主线程”和“线程t1”的流程。
(01) 注意,图中”主线程” 代表“主线程main”。”线程t1” 代表WaitTest中启动的“线程t1”。 而“锁” 代表“t1这个对象的同步锁”。
(02) “主线程”通过 new ThreadA(“t1”) 新建“线程t1”。随后通过synchronized(t1)获取“t1对象的同步锁”。然后调用t1.start()启动“线程t1”。
(03) “主线程”执行t1.wait() 释放“t1对象的锁”并且进入“等待(阻塞)状态”。等待t1对象上的线程通过notify() 或 notifyAll()将其唤醒。
(04) “线程t1”运行之后,通过synchronized(this)获取“当前对象的锁”;接着调用notify()唤醒“当前对象上的等待线程”,也就是唤醒“主线程”。
(05) “线程t1”运行完毕之后,释放“当前对象的锁”。紧接着,“主线程”获取“t1对象的锁”,然后接着运行。
注意:jdk的解释中,说wait()的作用是让“当前线程”等待,而“当前线程”是指正在cpu上运行的线程!
这也意味着,虽然t1.wait()是通过“线程t1”调用的wait()方法,但是调用t1.wait()的地方是在“主线程main”中。而主线程必须是“当前线程”,也就是运行状态,才可以执行t1.wait()。所以,此时的“当前线程”是“主线程main”!因此,t1.wait()是让“主线程”等待,而不是“线程t1”!
4.3.wait() 和 notifyAll()示例
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 42 43 44 45 46 47 48 49 50
| public class NotifyAllTest { private static Object obj = new Object(); public static void main(String[] args) { ThreadA t1 = new ThreadA("t1"); ThreadA t2 = new ThreadA("t2"); ThreadA t3 = new ThreadA("t3"); t1.start(); t2.start(); t3.start(); try { System.out.println(Thread.currentThread().getName()+" sleep(3000)"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized(obj) { System.out.println(Thread.currentThread().getName()+" notifyAll()"); obj.notifyAll(); } } static class ThreadA extends Thread{ public ThreadA(String name){ super(name); } public void run() { synchronized (obj) { try { System.out.println(Thread.currentThread().getName() + " wait"); obj.wait(); System.out.println(Thread.currentThread().getName() + " continue"); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
|
运行结果:
1 2 3 4 5 6 7 8
| t1 wait t3 wait t2 wait main sleep(3000) main notifyAll() t2 continue t3 continue t1 continue
|
流程图如下:
6.线程让步yield()
yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class ThreadA extends Thread{ public ThreadA(String name){ super(name); } public synchronized void run(){ for(int i=0; i <10; i++){ System.out.printf("%s [%d]:%d\n", this.getName(), this.getPriority(), i); if (i%4 == 0) Thread.yield(); } } } public class YieldTest{ public static void main(String[] args){ ThreadA t1 = new ThreadA("t1"); ThreadA t2 = new ThreadA("t2"); t1.start(); t2.start(); } }
|
理论上的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| t1 [5]:0 t2 [5]:0 t1 [5]:1 t1 [5]:2 t1 [5]:3 t1 [5]:4 t2 [5]:1 t2 [5]:2 t2 [5]:3 t2 [5]:4 t1 [5]:5 t1 [5]:6 t1 [5]:7 t1 [5]:8 t2 [5]:5 t2 [5]:6 t2 [5]:7 t2 [5]:8 t1 [5]:9 t2 [5]:9
|
但是,yield()方法不会释放锁,上例中sync…关键字是对各自对象加锁,如果对同一个对象,那么t2会一直阻塞直到t1结束。这一点与wait不同。
7.线程休眠sleep()
sleep() 定义在Thread.java中。
sleep() 的作用是让当前线程休眠,即当前线程会从“运行状态”进入到“休眠(阻塞)状态”。sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由“阻塞状态”变成“就绪状态”,从而等待cpu的调度执行。
使用方法:
同yield()一样,sleep不会释放对象锁。如果sleep期间还一直占用对象锁,其他线程会一直阻塞。
8. join()介绍
join() 定义在Thread.java中。join() 的作用:让“主线程”等待“子线程”结束之后才能继续运行。
使用方法:
则father线程会阻塞到son线程完成。join的原理是用wait做的,因此其行为类似wait。
9.interrupt()和线程终止
首先,Thread中的stop()和suspend()方法,由于固有的不安全性,已经建议不再使用!
interrupt()的作用是中断本线程。具体原理原博客也没说清楚。下面结合实际说说终止线程的方式。
9.1.终止处于“阻塞状态”的线程
当线程由于被调用了sleep(), wait(), join()等方法而进入阻塞状态;若此时调用线程的interrupt()将线程的中断标记设为true。由于处于阻塞状态,中断标记会被清除,同时产生一个InterruptedException异常。将InterruptedException放在适当的位置就能终止线程,形式如下:
1 2 3 4 5 6 7 8 9 10
| @Override public void run() { try { while (true) { } } catch (InterruptedException ie) { } }
|
说明:在while(true)中不断的执行任务,当线程处于阻塞状态时,调用线程的interrupt()产生InterruptedException中断。中断的捕获在while(true)之外,这样就退出了while(true)循环!
9.2.终止处于“运行状态”的线程
9.2.1.通过“中断标记”终止运行中的线程
1 2 3 4 5 6
| @Override public void run() { while (!isInterrupted()) { } }
|
说明:isInterrupted()是判断线程的中断标记是不是为true。当线程处于运行状态,并且我们需要终止它时;可以调用线程的interrupt()方法,使用线程的中断标记为true,即isInterrupted()会返回true。此时,就会退出while循环。
注意:interrupt()并不会终止处于“运行状态”的线程!它会将线程的中断标记设为true
9.2.2.通过“额外添加标记”终止运行中的线程
1 2 3 4 5 6 7 8 9 10 11
| private volatile boolean flag= true; protected void stopTask() { flag = false; } @Override public void run() { while (flag) { } }
|
说明:线程中有一个flag标记,它的默认值是true;并且我们提供stopTask()来设置flag标记。当我们需要终止该线程时,调用该线程的stopTask()方法就可以让线程退出while循环。
注意:将flag定义为volatile类型,是为了保证flag的可见性。即其它线程通过stopTask()修改了flag之后,本线程能看到修改后的flag的值。
9.3.通用的终止线程的形式
线程处于“阻塞状态”和“运行状态”的终止方式:
1 2 3 4 5 6 7 8 9 10 11
| @Override public void run() { try { while (!isInterrupted()) { } } catch (InterruptedException ie) { } }
|
10.线程优先级和守护线程
java 中的线程优先级的范围是1~10,默认的优先级是5。“高优先级线程”会优先于“低优先级线程”执行。
java 中有两种线程:用户线程和守护线程。可以通过isDaemon()方法来区别它们:如果返回false,则说明该线程是“用户线程”;否则就是“守护线程”。
用户线程一般用户执行用户级任务,而守护线程也就是“后台线程”,一般用来执行后台任务。需要注意的是:Java虚拟机在“用户线程”都结束后会后退出。
11.综合应用:生产消费者问题
生产/消费者问题是个非常典型的多线程问题,涉及到的对象包括“生产者”、“消费者”、“仓库”和“产品”。他们之间的关系如下:
(01) 生产者仅仅在仓储未满时候生产,仓满则停止生产。
(02) 消费者仅仅在仓储有产品时候才能消费,仓空则等待。
(03) 当消费者发现仓储没产品可消费时候会通知生产者生产。
(04) 生产者在生产出可消费产品时候,应该通知等待的消费者去消费。
代码来自原博客,有改动,使过程更清晰:
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
| class Depot { private int capacity; private int size; public Depot(int capacity) { this.capacity = capacity; this.size = 0; } public synchronized void produce(int val) { try { int left = val; while (left > 0) { while (size >= capacity) wait(); int inc = (size + left) > capacity ? (capacity - size) : left; size += inc; left -= inc; System.out.printf("%s 生产:(%3d)个的过程 --> 还需生产=%3d, 已生产=%3d, 仓库库存=%3d\n", Thread.currentThread().getName(), val, left, inc, size); notifyAll(); } } catch (InterruptedException e) { } } public synchronized void consume(int val) { try { int left = val; while (left > 0) { while (size <= 0) wait(); int dec = (size < left) ? size : left; size -= dec; left -= dec; System.out.printf("%s 消费:(%3d)个的过程 <-- 还需消费=%3d, 已消费=%3d, 仓库剩余=%3d\n", Thread.currentThread().getName(), val, left, dec, size); notifyAll(); } } catch (InterruptedException e) { } } public String toString() { return "capacity:" + capacity + ", actual size:" + size; } } class Producer { private Depot depot; public Producer(Depot depot) { this.depot = depot; } public void produce(final int val) { new Thread() { public void run() { depot.produce(val); } }.start(); } } class Customer { private Depot depot; public Customer(Depot depot) { this.depot = depot; } public void consume(final int val) { new Thread() { public void run() { depot.consume(val); } }.start(); } } public class Demo1 { public static void main(String[] args) throws InterruptedException { Depot mDepot = new Depot(100); Producer mPro = new Producer(mDepot); Customer mCus = new Customer(mDepot); mPro.produce(60); Thread.sleep(1000); mPro.produce(120); Thread.sleep(1000); mCus.consume(90); Thread.sleep(1000); mCus.consume(150); Thread.sleep(1000); mPro.produce(110); } }
|
说明:
(01) Producer是“生产者”类,它与“仓库(depot)”关联。当调用“生产者”的produce()方法时,它会新建一个线程并向“仓库”中生产产品。
(02) Customer是“消费者”类,它与“仓库(depot)”关联。当调用“消费者”的consume()方法时,它会新建一个线程并消费“仓库”中的产品。
(03) Depot是“仓库”类,仓库中记录“仓库的容量(capacity)”以及“仓库中当前产品数目(size)”。
“仓库”类的生产方法produce()和消费方法consume()方法都是synchronized方法,进入synchronized方法体,意味着这个线程获取到了该“仓库”对象的同步锁。这也就是说,同一时间,生产者和消费者线程只能有一个能运行。通过同步锁,实现了对“残酷”的互斥访问。
对于生产方法produce()而言:当仓库满时,生产者线程等待,需要等待消费者消费产品之后,生产线程才能生产;生产者线程生产完产品之后,会通过notifyAll()唤醒同步锁上的所有线程,包括“消费者线程”,即我们所说的“通知消费者进行消费”。
对于消费方法consume()而言:当仓库为空时,消费者线程等待,需要等待生产者生产产品之后,消费者线程才能消费;消费者线程消费完产品之后,会通过notifyAll()唤醒同步锁上的所有线程,包括“生产者线程”,即我们所说的“通知生产者进行生产”。
运行结果:
1 2 3 4 5 6 7 8
| Thread-0 生产:( 60)个的过程 --> 还需生产= 0, 已生产= 60, 仓库库存= 60 Thread-1 生产:(120)个的过程 --> 还需生产= 80, 已生产= 40, 仓库库存=100 Thread-2 消费:( 90)个的过程 <-- 还需消费= 0, 已消费= 90, 仓库剩余= 10 Thread-1 生产:(120)个的过程 --> 还需生产= 0, 已生产= 80, 仓库库存= 90 Thread-3 消费:(150)个的过程 <-- 还需消费= 60, 已消费= 90, 仓库剩余= 0 Thread-4 生产:(110)个的过程 --> 还需生产= 10, 已生产=100, 仓库库存=100 Thread-3 消费:(150)个的过程 <-- 还需消费= 0, 已消费= 60, 仓库剩余= 40 Thread-4 生产:(110)个的过程 --> 还需生产= 0, 已生产= 10, 仓库库存= 50
|