线程生命周期
JDK中用Thread.State类定义了线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类 及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建 状态
- 就绪: 处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已 具备了运行的条件,只是没分配到CPU资源
- 运行: 当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线 程的操作和功能
- 阻塞: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中 止自己的执行,进入阻塞状态
- 死亡: 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
线程状态转换图
线程安全
问题:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有 执行完,另一个线程参与进来执行。导致共享数据的错误。
解决办法: 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以 参与执行。
Java对于多线程的安全问题提供了专业的解决方式:同步机制
同步机制中的锁
同步锁机制:
在《Thinking in Java》中,是这么说的:对于并发工作,你需要某种方式来防 止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法 就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须 锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁 之时,另一个任务就可以锁定并使用它了。
synchronized的锁是什么?
- 任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
- 同步方法的锁:静态方法(类名.class)、非静态方法(this)
- 同步代码块:自己指定,很多时候也是指定为this或类名.class
注意:
- 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
- 一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)
实现
1、同步代码块
1 synchronized (对象){ 2 // 需要被同步的代码; 3 4 }
具体代码如下:
1 /** 2 * 例子:创建5个窗口卖票,总票数为100张,使用继承Thread类的方式 3 * 4 * 1、问题:卖票过程中,出现了重票、错票 --> 出现了线程的安全问题 5 * 6 * 2、问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。 7 * 8 * 3、如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来,直到线程a操作完ticket时, 9 * 其他线程才可以开始操作ticket。这种情况即是线程a出现了阻塞,也不能被改变。 10 * 11 * 4、在Java中,我们同步同步机制,来解决线程的安全问题。 12 * 13 * 方式一:同步代码块 14 * 15 * synchronized(同步监视器){ 16 * // 需要被同步的代码 17 * } 18 * 19 * 说明:1、操作共享数据的代码,即为需要被同步的代码。 20 * 2、共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。 21 * 3、同步监视器:俗称:锁。任何一个类的对象,都可以充当锁 22 * 要求:多个线程必须要共用同一把锁。 23 * 24 * 补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。 25 * 26 * 方式二:同步方法 27 * 如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的。 28 * 29 * 5、同步的方式,解决了线程的安全问题。---好处 30 * 操作同步代码时,只能有一个线程参与,其他线程等待,相当于一个单线程的过程,效率低。---缺点 31 */ 32 33 class Window1 extends Thread { 34 35 // 总票数 36 private static int tickets = 100; 37 38 @Override 39 public void run() { 40 while (true) { 41 synchronized (Window1.class) { 42 if(tickets > 0) { 43 try { 44 Thread.sleep(10); 45 } catch (InterruptedException e) { 46 e.printStackTrace(); 47 } 48 System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + tickets); 49 tickets--; 50 } else { 51 break; 52 } 53 } 54 55 } 56 } 57 } 58 59 public class TicketDemo1 { 60 61 public static void main(String[] args) { 62 Window1 w1 = new Window1(); 63 Window1 w2 = new Window1(); 64 Window1 w3 = new Window1(); 65 Window1 w4 = new Window1(); 66 Window1 w5 = new Window1(); 67 68 w1.setName("窗口1"); 69 w2.setName("窗口2"); 70 w3.setName("窗口3"); 71 w4.setName("窗口4"); 72 w5.setName("窗口5"); 73 74 w1.start(); 75 w2.start(); 76 w3.start(); 77 w4.start(); 78 w5.start(); 79 } 80 }
或者如下代码:
1 class Window2 implements Runnable { 2 3 private int tickets = 100; 4 5 Object obj = new Object(); 6 7 @Override 8 public void run() { 9 while (true) { 10 // 同步代码块 11 // 方式一、自己定一个对象 12 synchronized (obj) { 13 // 方式二、使用唯一的窗口对象this 14 // synchronized (this) { 15 if(tickets > 0) { 16 try { 17 Thread.sleep(10); 18 } catch (InterruptedException e) { 19 e.printStackTrace(); 20 } 21 System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + tickets); 22 tickets--; 23 } else { 24 break; 25 } 26 } 27 28 } 29 } 30 } 31 32 public class TicketDemo2 { 33 public static void main(String[] args) { 34 Window2 window = new Window2(); 35 Thread w1 = new Thread(window); 36 Thread w2 = new Thread(window); 37 Thread w3 = new Thread(window); 38 Thread w4 = new Thread(window); 39 Thread w5 = new Thread(window); 40 41 w1.setName("窗口1"); 42 w2.setName("窗口2"); 43 w3.setName("窗口3"); 44 w4.setName("窗口4"); 45 w5.setName("窗口5"); 46 47 w1.start(); 48 w2.start(); 49 w3.start(); 50 w4.start(); 51 w5.start(); 52 53 } 54 }
2、同步方法
synchronized还可以放在方法声明中,表示整个方法为同步方法。 例如:
1 public synchronized void show (String name){ 2 .... 3 4 }
具体代码如下:
1 /** 2 * 例子:创建5个窗口卖票,总票数为100张,使用继承Thread类的方式 3 * 4 * 关于同步方法的总结: 5 * 1、同步方法仍然涉及到同步监视器,只是不需要我们明显的声明 6 * 2、非静态的同步方法,同步监视器是:this 7 * 静态的同步方法,监视器是:当前类本身 Object.class 8 */ 9 10 class Window3 extends Thread { 11 12 // 总票数 13 private static int tickets = 100; 14 15 @Override 16 public void run() { 17 while (true) { 18 if (show()) { 19 20 } else { 21 break; 22 } 23 } 24 } 25 26 // 同步监视器是当前类对象 Window3.class 27 private static synchronized boolean show(){ 28 // 同步监视器是:t1、t2、t3..此种解决方式是错误的 29 // private synchronized boolean show(){ 30 if (tickets > 0) { 31 try { 32 Thread.sleep(10); 33 } catch (InterruptedException e) { 34 e.printStackTrace(); 35 } 36 System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + tickets); 37 tickets--; 38 return true; 39 } 40 return false; 41 } 42 } 43 44 public class TicketDemo3 { 45 46 public static void main(String[] args) { 47 Window3 w1 = new Window3(); 48 Window3 w2 = new Window3(); 49 Window3 w3 = new Window3(); 50 Window3 w4 = new Window3(); 51 Window3 w5 = new Window3(); 52 53 w1.setName("窗口1"); 54 w2.setName("窗口2"); 55 w3.setName("窗口3"); 56 w4.setName("窗口4"); 57 w5.setName("窗口5"); 58 59 w1.start(); 60 w2.start(); 61 w3.start(); 62 w4.start(); 63 w5.start(); 64 } 65 }
或者:
1 class Window4 implements Runnable { 2 3 private int tickets = 100; 4 5 @Override 6 public void run() { 7 while (true) { 8 if (show()) { 9 } else { 10 break; 11 } 12 } 13 } 14 15 // 同步监视器是:this 16 private synchronized boolean show() { 17 if (tickets > 0) { 18 try { 19 Thread.sleep(10); 20 } catch (InterruptedException e) { 21 e.printStackTrace(); 22 } 23 System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + tickets); 24 tickets--; 25 return true; 26 } 27 return false; 28 } 29 } 30 31 public class TicketDemo4 { 32 public static void main(String[] args) { 33 Window4 window = new Window4(); 34 Thread w1 = new Thread(window); 35 Thread w2 = new Thread(window); 36 Thread w3 = new Thread(window); 37 Thread w4 = new Thread(window); 38 Thread w5 = new Thread(window); 39 40 w1.setName("窗口1"); 41 w2.setName("窗口2"); 42 w3.setName("窗口3"); 43 w4.setName("窗口4"); 44 w5.setName("窗口5"); 45 46 w1.start(); 47 w2.start(); 48 w3.start(); 49 w4.start(); 50 w5.start(); 51 52 } 53 }
释放锁的操作
当前线程的同步方法、同步代码块执行结束。
当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、 该方法的继续执行。
当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导 致异常结束。
当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线 程暂停,并释放锁。
不会释放锁的操作
线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程 挂起,该线程不会释放锁(同步监视器)。应尽量避免使用suspend()和resume()来控制线程
3、使用Lock锁
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同 步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的 工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象 加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和 内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以 显式加锁、释放锁。
1 class A{ 2 private final ReentrantLock lock = new ReenTrantLock(); 3 4 public void m(){ 5 lock.lock(); 6 try{ 7 //保证线程安全的代码; 8 9 } finally{ 10 lock.unlock(); 11 } 12 } 13 }
示例代码:
1 /** 2 * 解决线程安全问题的方式:Lock锁 --- JDK5.0新增 3 * 4 * 1、面试题:synchronized 与 Lock 的异同? 5 * 相同:二者都可以解决线程安全问题 6 * 不同:synchronized机制在执行完相应的同步代码之后,自动的释放同步监视器 7 * Lock需要手动的启动同步(lock()),同时结束同步需要手动的实现(unlock()) 8 * 9 * 2、优先使用顺序: 10 * Lock -> 同步代码块(已经进入了方法体,分配了相应资源) -> 同步方法 (在方法体之外) 11 * 12 * 3、面试题:如何解决线程安全问题?有几种方式 13 * 同步代码块 同步方法 Lock锁 14 */ 15 class Window implements Runnable{ 16 17 private int ticket = 100; 18 19 // 1、实例化ReentrantLock锁 20 private Lock lock = new ReentrantLock(); 21 22 @Override 23 public void run() { 24 while (true) { 25 try { 26 // 2、调用锁定方法lock 27 lock.lock();; 28 29 if(ticket > 0) { 30 31 try { 32 Thread.sleep(100); 33 } catch (InterruptedException e) { 34 e.printStackTrace(); 35 } 36 System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket); 37 ticket --; 38 }else { 39 break; 40 } 41 } finally { 42 // 3、调用解锁方法unlock() 43 lock.unlock(); 44 } 45 46 } 47 } 48 } 49 public class LockTest { 50 public static void main(String[] args) { 51 Window w = new Window(); 52 Thread t1 = new Thread(w); 53 Thread t2 = new Thread(w); 54 Thread t3 = new Thread(w); 55 Thread t4 = new Thread(w); 56 Thread t5 = new Thread(w); 57 58 t1.setName("窗口1"); 59 t2.setName("窗口2"); 60 t3.setName("窗口3"); 61 t4.setName("窗口4"); 62 t5.setName("窗口5"); 63 64 t1.start(); 65 t2.start(); 66 t3.start(); 67 t4.start(); 68 t5.start(); 69 } 70 }
synchronized 与 Lock 的对比
1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是 隐式锁,出了作用域自动释放
2. Lock只有代码块锁,synchronized有代码块锁和方法锁
3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序:
Lock ->同步代码块(已经进入了方法体,分配了相应资源) -> 同步方法 (在方法体之外)
线程的死锁问题
死锁
1、不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃 自己需要的同步资源,就形成了线程的死锁
2、出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于 阻塞状态,无法继续
解决方法
1、专门的算法、原则
2、尽量减少同步资源的定义
3、尽量避免嵌套同步
示例
1 package com.hd.thread; 2 3 /** 4 * 死锁演示 5 */ 6 7 class A { 8 public synchronized void foo(B b) { 9 System.out.println("当前线程名:" + Thread.currentThread().getName() 10 + " 进入了A实例的foo方法"); 11 try { 12 Thread.sleep(1000); 13 } catch (InterruptedException e) { 14 e.printStackTrace(); 15 } 16 System.out.println("当前线程名:" + Thread.currentThread().getName() 17 + " 企图调用B实例的last方法"); 18 b.last(); 19 } 20 21 public synchronized void last(){ 22 System.out.println("进入了A类的last方法内部"); 23 } 24 } 25 26 class B { 27 public synchronized void bar(A a) { 28 System.out.println("当前线程名:" + Thread.currentThread().getName() 29 + " 进入了B实例的bar方法"); 30 try { 31 Thread.sleep(1000); 32 } catch (InterruptedException e) { 33 e.printStackTrace(); 34 } 35 System.out.println("当前线程名:" + Thread.currentThread().getName() 36 + " 企图调用A实例的last方法"); 37 a.last(); 38 } 39 public synchronized void last() { 40 System.out.println("进入了B类的last方法内部"); 41 } 42 } 43 public class DeadLock implements Runnable{ 44 A a = new A(); 45 B b = new B(); 46 47 public void init(){ 48 Thread.currentThread().setName("主线程"); 49 // 调用a对象的foo方法 50 a.foo(b); 51 System.out.println("进入了主线程之后"); 52 } 53 54 @Override 55 public void run() { 56 Thread.currentThread().setName("副线程"); 57 // 调用b对象的bar方法 58 b.bar(a); 59 System.out.println("进入了副线程之后"); 60 } 61 62 public static void main(String[] args) { 63 DeadLock lock1 = new DeadLock(); 64 new Thread(lock1).start(); 65 66 lock1.init(); 67 } 68 }