Java线程的生命周期、线程的同步
1 - 线程的5种状态
JDK中用Thread.State类定义了线程的几种状态
/*
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类 及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的5种状态:
1 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建 状态
2 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已 具备了运行的条件,只是没分配到CPU资源
3 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线 程的操作和功能
4 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中 止自己的执行,进入阻塞状态
5 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
*/
2 - 线程状态转换图(生命周期)
3 - 线程的同步
/*
例子:创建三个窗口买票,总票数为100张,使用Runnable接口的方式
1. 问题:买票过程中,出现了重票和错票 --> 线程安全
2. 问题出现的原因:当某个线程操作车票的过程中。尚未操作完成时,其他线程参与进来,也操作车票
3. 如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket,这种情况即使线程a出现了阻塞,也不能被改变。
4. 在Java中,通过同步机制,来解决线程的安全问题
方式1:同步代码块
synchronized(同步监视器){// 需要被同步的代码}
说明1:需要被同步的代码 --> 操作 共享数据(多个线程共同操作的变量,比如ticket) 的代码
说明2:同步监视器 --> 俗称:锁。任何一个类的对象,都可以充当锁 注意:要求多个线程必须共用同一把锁
说明3:synchronized(this){// 需要被同步的代码} 我们可以考虑使用this充当同步监听器
同步代码块的优点:解决了线程的安全问题
同步代码块的缺点:操作同步代码时,只能有一个线程参与,其他线程等待,相当于是一个单线程的过程,效率低
方式2:同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们就将此方法声明同步的。
关于同步方法的总结:
1-同步方法仍然涉及到同步监视器,只是不需要我们显示的声明
2-非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视器是:当前类本身(MyThread.class)
*/
package com.lzh.java; /* 例子:创建三个窗口买票,总票数为100张,使用Runnable接口的方式 1. 问题:买票过程中,出现了重票和错票 --> 线程安全 2. 问题出现的原因:当某个线程操作车票的过程中。尚未操作完成时,其他线程参与进来,也操作车票 3. 如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket,这种情况即使线程a出现了阻塞,也不能被改变。 4. 在Java中,通过同步机制,来解决线程的安全问题 方式1:同步代码块 synchronized(同步监视器){// 需要被同步的代码} 说明1:需要被同步的代码 --> 操作 共享数据(多个线程共同操作的变量,比如ticket) 的代码 说明2:同步监视器 --> 俗称:锁。任何一个类的对象,都可以充当锁 注意:要求多个线程必须共用同一把锁 说明3:synchronized(this){// 需要被同步的代码} 我们可以考虑使用this充当同步监听器 同步代码块的优点:解决了线程的安全问题 同步代码块的缺点:操作同步代码时,只能有一个线程参与,其他线程等待,相当于是一个单线程的过程,效率低 方式2:同步方法 如果操作共享数据的代码完整的声明在一个方法中,我们就将此方法声明同步的。 关于同步方法的总结: 1-同步方法仍然涉及到同步监视器,只是不需要我们显示的声明 2-非静态的同步方法,同步监视器是:this 静态的同步方法,同步监视器是:当前类本身 */ public class WindowTest { public static void main(String[] args) { MyThread myThread = new MyThread(); Thread window1 = new Thread(myThread); window1.setName("窗口1"); Thread window2 = new Thread(myThread); window2.setName("窗口2"); Thread window3 = new Thread(myThread); window3.setName("窗口3"); window1.start(); window2.start(); window3.start(); } } class MyThread implements Runnable{ private int ticket = 100; Object obj = new Object(); // 锁,要求多个线程必须共用同一把锁 唯一性 @Override public void run() { while(true){ // 给操作共享数据的线程设置同步机制 --> 解决线程问题 synchronized(obj){ // synchronized(this) 也是可以的,此时this:唯一的MyThread的对象 if(ticket > 0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"卖出了id为"+ticket+"的票"); ticket--; }else{ break; } } } } }
package com.lzh.java; /* 例子:创建三个窗口买票,总票数为100张,使用Runnable接口的方式 1. 问题:买票过程中,出现了重票和错票 --> 线程安全 2. 问题出现的原因:当某个线程操作车票的过程中。尚未操作完成时,其他线程参与进来,也操作车票 3. 使用同步方法解决Runnable接口的线程安全 */ public class WindowTest2 { public static void main(String[] args) { Window window = new Window(); Thread t1 = new Thread(window); t1.setName("窗口1"); Thread t2 = new Thread(window); t2.setName("窗口2"); Thread t3 = new Thread(window); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } } class Window implements Runnable{ private int ticket = 100; // 设置同步方法 private synchronized void show(){ // 同步监视器:this try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } if(ticket > 0){ System.out.println(Thread.currentThread().getName()+"卖出了id为"+ticket+"的票"); ticket--; } } @Override public void run() { while(true){ show(); if(ticket == 0){ break; } } } }
package com.lzh.java.exer; /* 银行有一个账户。 有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打 印账户余额。 分析: 1.是否是多线程问题? 是,两个存储线程 2.是否有共享数据? 有,账户(或账户余额) 3.是否有线程安全问题? 有 4.需要考虑如何解决线程安全问题? 同步机制:有三种方式。 */ class Account{ private double balance; public Account(double balance){ this.balance = balance; } public synchronized void deposit(double money){ balance += money; System.out.println(Thread.currentThread().getName()+": 存了"+balance); } } class Customer extends Thread{ private Account account; public Customer(Account account){ this.account = account; } @Override public void run() { for(int i = 0;i < 3;i++){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } account.deposit(1000); } } } public class AccountTest { public static void main(String[] args) { Account account = new Account(0); Customer c1 = new Customer(account); Customer c2 = new Customer(account); c1.setName("李白"); c2.setName("韩信"); c1.start(); c2.start(); // System.out.println("最后账户余额:"+account.getBalance()); } }
线程安全问题图解
同步原理
4 - 单例设计模式之懒汉式(线程安全)
package com.lzh.java; /* 使用同步机制将单例模式中的懒汉式改写为线程安全的 */ public class BankTest { } // 单例模式之懒汉式 class Bank{ private Bank(){} private static Bank instance = null; // 同步方法解决线程安全 public static synchronized Bank getInstance1(){ // 此时同步监视器:Bank.class if(instance == null){ instance = new Bank(); } return instance; } // 同步代码块 public static Bank getInstance2(){ // 效率稍差 synchronized(Bank.class){ if(instance == null){ instance = new Bank(); } return instance; } } // 改进版,效率提升 public static Bank getInstance3(){ if(instance == null){ synchronized (Bank.class){ if(instance == null){ instance = new Bank(); } } } return instance; } }
5 - 线程的死锁问题
package com.lzh.java; /* 演示线程的死锁问题 1. 死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁 2. 说明: 1-出现死锁时,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续 2-我们使用同步时,要避免出现死锁 */ public class ThreadTest { public static void main(String[] args) { final StringBuffer s1 = new StringBuffer(); final StringBuffer s2 = new StringBuffer(); new Thread(){ @Override public void run() { synchronized (s1){ s1.append("a"); s2.append("1"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s2){ s1.append("b2"); s2.append("2"); System.out.println(s1); } } } }.start(); new Thread(new Runnable() { @Override public void run() { synchronized (s2) { s1.append("c"); s2.append("3"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s1) { s1.append("b2"); s2.append("2"); System.out.println(s2); } } } }).start(); } }
6 - 更强大的线程同步机制-Lock(锁)
Lock(锁)
Lock(锁)的使用
package com.lzh.java; import java.util.concurrent.locks.ReentrantLock; /* 例子:创建三个窗口买票,总票数为100张,使用Runnable接口的方式 Lock锁的使用方式 1. 实例化ReentrantLock private ReentrantLock lock = new ReentrantLock(); 2. 调用锁定方法:lock() 3. 调用解锁方法:unlock() 解决线程安全问题的方式3:Lock锁 ---JDK5.0新增 4. 面试题:synchronized 与 Lock的异同 同:二者都可以解决线程安全问题 异:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器。Lock需要手动的启动同步(Lock()),同时结束同步也需要手动的实现(unLock()) */ class TicketWindow implements Runnable{ private int ticket = 100; // 1. 实例化ReentrantLock private ReentrantLock lock = new ReentrantLock(); @Override public void run() { while(true){ try{ // 2. 调用锁定方法:lock() lock.lock(); if(ticket > 0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"卖出了ID为"+ticket+"的票"); ticket--; } }finally{ // 3. 调用解锁方法:unlock() lock.unlock(); } if(ticket == 0){ break; } } } } public class LockTest { public static void main(String[] args) { TicketWindow window = new TicketWindow(); Thread t1 = new Thread(window); Thread t2 = new Thread(window); Thread t3 = new Thread(window); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } }
7 - synchronized 与 Lock 的对比
8 - 线程的通信
package com.lzh.java2; /* 线程通信的例子:使用两个线程打印 1-100。1,线程2 交替打印 涉及到的3个方法: wait() 一旦执行此方法,当前线程就会进入阻塞状态,并释放同步监视器 notify() 一旦执行此方法,就会唤醒被wait()的一个线程,若果有多个线程被wait(),就会唤醒优先级高的线程 notifyAll() 一旦执行此方法,就会唤醒所有被wait的线程 说明: 1. wait() notify() notify() 三个方法必须使用在同步代码块或同步方法中 2. wait() notify() notify() 三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否侧或出现异常 3. wait() notify() notify() 三个方法是定义在java.lang.Object类中 */ class Number implements Runnable{ private int number = 1; @Override public void run() { while(true){ // 设置同步代码块解决线程安全问题 synchronized(this){ notify(); // 唤醒阻塞的线程 // this.notify(); if(number <= 100){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+": 打印了 "+number); number++; try { // 使得调用如下wait()方法的线程进入阻塞状态,且释 放同步锁 而sleep()不会 wait(); // this.notify(); } catch (InterruptedException e) { e.printStackTrace(); } }else{ break; } } } } } public class CommunicationTest { public static void main(String[] args) { Number number = new Number(); Thread t1 = new Thread(number); Thread t2 = new Thread(number); t1.setName("线程1"); t2.setName("线程2"); t1.start(); t2.start(); } }
面试题:sleep() 和wait() 的异同?
/*
1-相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态
2-不同点:
①两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()
②调用的要求不同:sleep()可以在任何需要的场景下调用。wait()必须使用在同步代码块或同步方法中
③如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放同步监视器,wait()会释放同步监视器
*/
9 - 经典例题之生产者/消费者问题
/*
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处 取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图 生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通 知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如 果店中有产品了再通知消费者来取走产品。
这里可能出现两个问题:
1. 生产者比消费者快时,消费者会漏掉一些数据没有取到。
2. 消费者比生产者快时,消费者会取相同的数据
*/
package com.lzh.java2; /* 线程通信的应用:生产者/消费者模型 分析: 1.是否是多线程问题? 是,生产者线程,消费者线程 2.是否有共享数据? 是,店员(或产品) 3.如何解决线程的安全问题? 同步机制(3中方法) 4.是否涉及线程的通信? 是 */ // 共用的数据(店员) class Clerk{ private int number = 0; // 生产数据 public synchronized void productData() { if(number < 20){ number++; System.out.println(Thread.currentThread().getName()+"生产了数据:"+number); notify(); }else{ // 等待 try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } // 消费数据 public synchronized void consumerData() { if(number > 0){ System.out.println(Thread.currentThread().getName()+"消费了数据:"+number); number--; notify(); }else{ // 等待 try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } // 生产者 class Producer extends Thread{ private Clerk clerk; public Producer(Clerk clerk){ this.clerk = clerk; } @Override public void run(){ System.out.println(getName()+":开始生产数据..."); while(true){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } clerk.productData(); } } } // 消费者 class Customer extends Thread{ private Clerk clerk; public Customer(Clerk clerk){ this.clerk = clerk; } @Override public void run() { System.out.println(getName()+":开始消费数据..."); while(true){ try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } clerk.consumerData(); } } } public class ProductTest { public static void main(String[] args) { Clerk clerk = new Clerk(); Producer p = new Producer(clerk); p.setName("生产者"); Customer c = new Customer(clerk); c.setName("消费者"); p.start(); c.start(); } }
10 - JDK5.0新增线程创建方式1 实现Callable接口
新增方式1:实现Callable接口(不常用了解)
package com.lzh.java2; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /* 创建线程的方式3:实现Callable接口 JDK5.0新增 1-创建过程: 1. 创建一个实现Callable的实现类 2. 重写call方法,将此线程需要执行的操作声明在call()中 3. 创建Callable接口实现类的对象 4. 将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象 5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start() 6. 获取返回值 2-如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大? 1. call() 方法可以有返回值 2. call() 可以抛出异常,被外面的操作捕获,获取异常信息 3. Callable是支持泛型的 */ class NumThread implements Callable { @Override public Object call() throws Exception { int sum = 0; for(int i = 0;i < 100;i++){ if(i % 2 == 0){ sum += i; System.out.println(i); } } return sum; } } public class ThreadNew { public static void main(String[] args) { NumThread numThread = new NumThread(); FutureTask futureTask = new FutureTask(numThread); new Thread(futureTask).start(); try { // get() 返回值即为FutureTask构造器参数Callable实现重写的call()的返回值 Object sum = futureTask.get(); System.out.println(sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
11 - JDK5.0新增线程创建方式2 线程池
使用线程池
/*
创建线程的方式4:使用线程池
创建过程:
1. 提供指定线程数量的线程池
2. 执行指定的线程操作,需要提供实现Runnable接口或Callable接口实现类的对象
3. 好处:
1 提高响应速度(减少了创建新线程的时间)
2 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3 便于线程管
*/
package com.lzh.java2; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; /* 创建线程的方式4:使用线程池 创建方式: 1. 提供指定线程数量的线程池 2. 执行指定的线程操作,需要提供实现Runnable接口或Callable接口实现类的对象 3. 好处: 1 提高响应速度(减少了创建新线程的时间) 2 降低资源消耗(重复利用线程池中线程,不需要每次都创建) 3 便于线程管 */ class myThread implements Runnable{ @Override public void run() { for(int i = 0;i <= 100;i++){ if(i % 2 == 0){ System.out.println(Thread.currentThread().getName()+":"+i); } } } } public class ThreadPool { public static void main(String[] args) { ExecutorService service = Executors.newFixedThreadPool(10); service.execute(new myThread()); // 适合适用于Runnable // service.submit(); 适合适用于Callable // 设置线程属性 ThreadPoolExecutor service1 = (ThreadPoolExecutor) service; service1.setCorePoolSize(15); // service1.setKeepAliveTime(); // ... System.out.println(service.getClass()); // class java.util.concurrent.ThreadPoolExecutor service.shutdown(); } }