一、线程创建
Java中常见创建线程有三种,常见的如下两类
1.1 继承Threa类
1public class MyThread extends Thread{ 2 @Override 3 public void run(){ 4 System.out.println("MyThread is running"); 5 } 6} 7 8public static void main(String[] args) { 9 MyThread myThread = new MyThread(); 10 myThread.start(); 11} 12
1.2 实现Runnable接口
由于继承Thread类后无法继承其它类,因此实现Runnable接口是常见的线程创建方式。Runnable接口只有1个抽象方法run,因此可以直接使用lambda表达式。
1public static void main(String[] args) { 2 Thread t1 = new Thread(()->{ 3 System.out.println("Thread 1 is running"); 4 }); 5 t1.start(); 6} 7
二、常用方法
2.1 sleep
sleep是让当前执行的线程让出cpu占用权n毫秒
1public static void main(String[] args) throws InterruptedException { 2 Thread t1 = new Thread(()->{ 3 System.out.println("Thread 1 is running"); 4 try { 5 Thread.sleep(1000); 6 } catch (InterruptedException e) { 7 e.printStackTrace(); 8 } 9 System.out.println("Thread 1 is ending"); 10 }); 11 12 t1.start(); 13 try { 14 Thread.sleep(3000); 15 } catch (InterruptedException e) { 16 e.printStackTrace(); 17 } 18 System.out.println("main thread is running"); 19} 20
- main线程先执行,走到了t1线程,t1和main同时抢占cpu,然后main被休眠了3s。
- t1线程执行thread1 running,此时t1自己线程也被休眠1s,此时main线程和t1都在休眠。
- t1后,抢占cpu,输出Thread 1 is ending
- main最后才醒来,抢到cpu,输出main thread is running
2.2 join
join让当前线程暂停直到目标线程结束后。如下代码,如果没有t1.join方法,最后的输出肯定是Thread 1 is ending。但t1.join让main线程暂停,必须等t1线程执行结束才能继续,所以输出是
- Thread 1 is running
- Thread 1 is ending
- main thread is running
1public static void main(String[] args) throws InterruptedException { 2 Thread t1 = new Thread(()->{ 3 System.out.println("Thread 1 is running"); 4 try { 5 Thread.sleep(1000); 6 } catch (InterruptedException e) { 7 e.printStackTrace(); 8 } 9 System.out.println("Thread 1 is ending"); 10 }); 11 t1.start(); 12 t1.join(); 13 System.out.println("main thread is running"); 14} 15 16
2.3 interrupt
interrupt给目标线程贴请停标签;如果对方阻塞会立刻醒,其它情况需要isInterrupted方法判断是否有停止标签。
休眠被唤醒
1public static void main(String[] args) throws InterruptedException { 2 Thread t1 = new Thread(()->{ 3 System.out.println("Thread 1 is running"); 4 try { 5 Thread.sleep(5000); 6 } catch (InterruptedException e) { 7 System.out.println("Thread 1 is interrupted"); 8 } 9 System.out.println("Thread 1 is ending"); 10 }); 11 12 t1.start(); 13 t1.interrupt(); 14 System.out.println("main thread is running"); 15} 16
- t1线程在执行时,interrupt告诉t1你需要暂停下,此时t1如果刚好在sleep休眠时,立马被唤醒走到catch,输出Thread 1 is interrupted。
- t1与main线程继续互相抢占cpu。也就是Thread 1 is ending与main thread is running输出顺序随机。
其它情况
线程被interrupt告知暂停时,如果线程刚好休眠会被唤醒进入catch。但是如果线程是其它状态,必须使用isInterrupted获取状态来处理
1public static void main(String[] args) throws InterruptedException { 2 Thread t1 = new Thread(()->{ 3 System.out.println("Thread 1 is running"); 4 for(int i=0;i<10000;i++){ 5 if(Thread.currentThread().isInterrupted()){ 6 break; 7 } 8 } 9 System.out.println("Thread 1 is ending"); 10 }); 11 t1.start(); 12 t1.interrupt(); 13 Thread.sleep(2000); 14 System.out.println("main thread is running"); 15} 16 17
- t1和main线程在互相抢占cpu执行,t1被标记暂停了,main线程睡眠了。
- 此时大概率t1线程在跑任务,输出Thread 1 is running。
- t1继续在执行for循环时,判断状态发现自己被标记暂停了,立马退出,执行Thread 1 is ending
- 然后main线程醒来后,执行main thread is running。
2.4 volatile
volatile保证可见性:我写完后,别人能读取到新值。但是它还是无法解决并发写的问题。
1public class VolatileStop { 2 static volatile boolean running = true; 3 public static void main(String[] args) throws InterruptedException { 4 Thread worker = new Thread(() -> { 5 long n = 0; 6 while (running) { 7 n++; 8 } 9 System.out.println("worker 看到 false,退出 n=" + n); 10 }); 11 worker.start(); 12 Thread.sleep(100); 13 running = false; 14 worker.join(); 15 System.out.println("main 结束"); 16 } 17} 18
三、同步与锁
同步与锁是并发编程非常重要环节,首先需要掌握3个核心api。
3.1 synchronized
3.1 概念
锁可以理解成钥匙,synchronized (x)就是多个线程去抢占这个x锁,每次只能有1个线程能抢到,方法执行结束后后释放锁,没有抢到的线程无法进入方法内。
1synchronized(x){ 2 //do 3} 4 5
3.2 条件
成功的锁必须满足两个条件
- 多线程抢的必须是同一个对象(同一把钥匙)
- 保护的是同一份共享数据
锁代码块
线程t1和t2共享变量count,而且锁的作用对象都是lock。
1private static int count = 0; 2private static Object lock = new Object(); 3public static void increment(){ 4 synchronized(lock){ 5 for(int i=0;i<10000;i++){ 6 count++; 7 } 8 } 9} 10public static void main(String[] args) throws InterruptedException { 11 Thread t1 = new Thread(()->{ 12 increment(); 13 }); 14 Thread t2 = new Thread(()->{ 15 increment(); 16 }); 17 t1.start(); 18 t2.start(); 19 t1.join(); 20 t2.join(); 21 System.out.println(count); 22} 23 24
2. 锁函数
这里t1和t2线程的锁的对象是static的Class类上,二者还是同一个对象,所以依旧正常。
1private static int count = 0; 2private static Object lock = new Object(); 3public static synchronized void increment(){ 4 for(int i=0;i<10000;i++){ 5 count++; 6 } 7} 8public static void main(String[] args) throws InterruptedException { 9 Thread t1 = new Thread(()->{ 10 increment(); 11 }); 12 Thread t2 = new Thread(()->{ 13 increment(); 14 }); 15 t1.start(); 16 t2.start(); 17 t1.join(); 18 t2.join(); 19 System.out.println(count); 20} 21 22
3. 错误锁
多线程时,锁的对象明显地址是不同的,无效锁。
1synchronized (new Object()) { 2 count++; 3} 4
3.2 wait
wait和sleep有本质差异,sleep是释放cpu抢占资源,而wait是释放锁的意思。线程wait释放锁后,就一直被阻塞,苏醒后继续抢到锁,然后执行未完成代码。
wait苏醒后从被wait地方继续执行代码,而不是从函数或者synchronized开始执行。
如下代码模拟生产者与消费者,由于生产者的方法入口添加sleep,所以消费者一定先抢到了锁,输出 begin decrement,来到锁里面输出begin decrement1,但是由于队列空,只能wait,把锁给释放了。此时生产者拿到锁,输出了begin increment和begin increment1,给队列生产1个数据,通知消费者消费。注意消费者苏醒后直接从lock.wait()这行代码后继续执行,继续while循环判断。
1begin decrement 2begin decrement1 3begin increment 4begin increment1 5
1public class Main { 2 private static int max = 1; 3 private static Object lock = new Object(); 4 private static List<Integer> list = new ArrayList<>(); 5 public static void increment() { 6 try{ 7 Thread.sleep(1000); 8 } catch (InterruptedException e) { 9 e.printStackTrace(); 10 } 11 System.out.println("begin increment"); 12 synchronized (lock) { 13 System.out.println("begin increment1"); 14 while (list.size() >= max) { 15 try { 16 lock.wait(); 17 } catch (InterruptedException e) { 18 Thread.currentThread().interrupt(); 19 return; 20 } 21 } 22 list.add(1); 23 lock.notifyAll(); 24 } 25 } 26 27 public static void decrement() { 28 System.out.println("begin decrement"); 29 synchronized (lock) { 30 System.out.println("begin decrement1"); 31 while (list.size() == 0) { 32 try { 33 lock.wait(); 34 } catch (InterruptedException e) { 35 Thread.currentThread().interrupt(); 36 return; 37 } 38 } 39 list.remove(list.size() - 1); 40 lock.notifyAll(); 41 } 42 } 43 public static void main(String[] args) throws InterruptedException { 44 Thread t1 = new Thread(()->{ 45 increment(); 46 }); 47 Thread t2 = new Thread(()->{ 48 decrement(); 49 }); 50 t1.start(); 51 t2.start(); 52 } 53} 54
为什么边界条件都是while,而不是if?
if会造成条件失效,模拟下如果3个生产者,1个消费者,队列最大是1。
11. 生产者t1,生产1个,通知其它线程抢锁 22. 生产者t2,发现队列满了,把自己wait, 33. 生产者t3,发现队列还是满了,把自己wait, 44. 消费者进来,消费1个,通知其它线程抢锁 55. t2苏醒,直接走if后面的代码,队列生产1个 66. t3苏醒,直接走if后面的代码,队列生产2个,报错。 7 8
3.3 notify
notify是wait对立面,它手上有锁,告诉被wait的线程,你可以去外面排队了,我马上就释放锁了,注意此时还没有释放锁,只有synchronized代码块执行结束才释放锁。而被wait的线程收到notify通知后,不睡觉了,在门口等待抢锁动作。
| notify() | notifyAll() | |
|---|---|---|
| 叫醒几个 | 一个(哪个不确定) | 这把锁上 所有 wait 的 |
| 风险 | 可能叫错人:叫醒了另一个生产者,消费者还在睡 | 大家醒了再用 while 判断,不会干错 |
| 入门 | 少用 | 优先用这个 |
四、死锁
死锁必须满足
- 互斥: 锁同时只能一人拿
- 持有并等待: t1拿着 lock1 不放,再等 lock2
- 不可剥夺: 不能把别人的锁抢走,只能等他自己放
- 循环等待: t1 等 t2,t2 等 t1,围成环
1public class Main { 2 private static Object lock1 = new Object(); 3 private static Object lock2 = new Object(); 4 public static void main(String[] args) throws InterruptedException { 5 Thread t1 = new Thread(() -> { 6 synchronized(lock1){ 7 try { 8 Thread.sleep(1000); 9 } catch (InterruptedException e) { 10 e.printStackTrace(); 11 } 12 synchronized(lock2){ 13 System.out.println("Thread 1 locked lock2"); 14 } 15 } 16 }); 17 18 Thread t2 = new Thread(() -> { 19 synchronized(lock2){ 20 try { 21 Thread.sleep(1000); 22 } catch (InterruptedException e) { 23 e.printStackTrace(); 24 } 25 synchronized(lock1){ 26 System.out.println("Thread 2 locked lock1"); 27 } 28 } 29 }); 30 31 t1.start(); 32 t2.start(); 33 } 34} 35