1) 概念
计算机中的一个程序运行时至少要有一个进程,一个进程中至少包含一个线程,线程是程序中的一个独立流程
示例:
假设一家餐厅,一个厨师C,一个服务员F:
顾客A和顾客B去餐厅吃饭:
A先进:F-A点菜 3个菜 F-C 炒菜
B进:F-B点菜 3个菜 F-C 炒菜
如果是两个服务员F1 F2,此时A和B就餐时F1-A,F2-B,F1和F2就餐整个流程相互不影响,只是公用一个厨师炒菜
假设AB同时就餐,此时C就同时得到了两份菜单:
处理一:先把A的3个菜全部抄完,然后再炒B的3个菜,出现等待状态严重
处理二:A-B菜交替进行 效率相对较高,AB两位认为是同时就餐
类比:
餐厅:一个应用程序
厨师:CPU(中央处理器) 按照时间片运行 抢占式调度执行(不是按顺序执行,谁抢到谁执行)
线程:F1 F2(两个流程独立运行,相互不影响)
线程并发:线程并发是一种表象,只是因为cpu的运算时间非常快,所以我们认为线程是并发执行的
线程开发:在一个程序中产生多个线程,让这些线程彼此独立完成特定的功能
问题:如何在java中去产生多个线程
2) 线程开发 -------(重写run()方法)
① 继承Thread类
② 实现Runnable接口(没有start()方法),所以要借助它的子类thread实现
先创建一个Runnable线程对象扔给thread构造方法构建一个t2线程出来,得到thread中的start方法
注意:
创建一个线程后需要调用start()方法启动线程,线程启动后由JVM获取cpu时间片调用run()方法执行。
线程中的run()是不能手动调用,如果手动调用即无线程概念,只是普通方法的调用
线程的结果是无规则的,只能从线程运行的结果反推线程执行过程
主线程:main
获取当前类的对象方法:
Thread.currentThread().getName()
3) 线程的状态
① 基本状态
① 阻塞状态 sleep()
当运行中的线程需要等待外部资源或调用线程sleep()时,当前线程进入阻塞状态,当阻塞结束后当前线程进入可运行状态
阻塞状态的线程不会释放掉当前对象的对象锁
业务需求:父母给你1000块生活费,每次给100块,给10次
对于父母:每次给你100块,给10次
对于你:每次接收100块,接10次
父母开始给钱
给第1个100块
给第2个100块
…
给第10个100块
父母给钱结束
问题:在一个线程中需要等到另一个线程全部执行完后才继续往后执行
③ join()方法 ----一定发生
调用当前线程的join()方法,则会一直等待当前线程执行完
join(int time):只等待指定时间,时间一到,放弃等待继续执行
④ yield()方法 -让行 ----可能会发生
yield()方法不会进入到阻塞状态,而是处于可运行状态,当线程调用此方法后,当前线程释放掉系统资源,由系统再次调用执行,此时,当前线程继续抢占资源,有可能继续抢占到资源继、续执行。
线程一旦终止会继续往后执行,不会重头执行
sleep和yield方法的区别:
a、sleep会产生异常,yield不会产生异常;
b、sleep会进入阻塞状态,yield不会
c、当一个线程调用sleep方法,线程会等sleep时间结束之后会进入可运行或就绪状态,而一个线程调用yield方法后会立即重新抢占cpu时间片继续执行
⑤ 线程优先级
线程的优先级表示线程的运行顺序,优先级高就代表先运行
线程的优先级从1-10,级别越高,优先级越高,默认优先级为5
线程的优先级越高并不代表优先执行,线程的执行时通过cpu的调度执行(做开发的时 候通过代码来控制线程执行顺序)
4) 线程安全问题
线程不安全的原因:多个线程同时访问同一个临界资源,如果破坏了操作的原子性,则可能会造成数据不一致,这种情况我们叫做线程不安全。举例,stack中两个线程同时对stack进行操作
临界资源:多个线程共同访问的同一个对象
原子操作:线程中若干行代码不能分开执行,若不同步执行则会发生线程不安全现象
银行转账:
这一系列的操作叫做原子操作,临界资源为银行账户
存钱:插卡---密码---放钱---输入转入账户---修改账户余额---确认成功---取卡---成功
取钱:插卡---密码---取钱---确认成功---取卡---成功
① 解决方法:线程同步--前后有关系
同步:Synchronized 位置: package lgs.hm.practice.test.thread.stack;
A、同步代码块 某一时刻只允许一个线程进入加了锁的同步代码块
加this的原因:当一个线程进入到同步快执行的时候有当前对象,那么这个线程就获得了当前对象的对象锁 ,synchronize同步块必须要拿到对象锁,对象锁只有一把,所以有多个同步块的时候必须要拿到对象锁才能执行,没有对象锁的进入到锁池状态(当前对象的锁池),就要等其它同步块运行完释放掉对象锁才能调用。
B、同步方法
② 锁池状态
当一个线程遇到synchronized同步关键字时,则当前线程立即获取到当前对象的对象锁,开始同步执行,此时,若其他线程同样遇到当前对象的synchronized关键字时,因对象锁只有一把,而进入同步块一定需要拿到对象锁,否则不能进入同步块,所以其他线程得不到对象锁时会进入到锁池状态。当对象锁被释放后,锁池状态中会随机选取一个线程获取到对象锁,从而进入到可运行状态。 如果当前线程执行完释放掉对象锁后,让他在锁池状态中休息不参与线程之间对对象锁的的获取,就在synchronized之外睡眠一段时间
口述线程的状态及各种状态的特点
5) 线程的死锁
① 概念
synchronized(a){ int I = 10; synchronized(b){ } } |
synchronized(b){ int I = 10; synchronized(a){ } } |
实例:
② 解决死锁方法:Object中提供的方法:wait()、notify()、notifyAll()
wait():当一个对象调用其wait()方法后,当前线程会释放掉当前对象的对象锁,当前线程进入到当前对象的等待池中。wait()方法一定是在synchronized同步块中调用。
notifyAll():当一个对象调用其notifyAll()方法后,会在等待池中唤醒需要当前对象锁的所有线程,此时被唤醒的线程会进入到锁池状态等待抢锁,当线程抢到了当前对象锁后进入可运行状态。
notify():和notifyAll一样,只是唤醒的是等待池中随机的一个线程。
6) 线程状态图
7) 生产者和消费者问题
产品类 public class Product { //定义产品的唯一ID int id; //定义构造方法初始化产品id public Product(int id) { this.id=id; // TODO Auto-generated constructor stub } } 仓库 public class Repertory { //定义一个集合类用于存放产品.规定仓库的最大容量为10. public LinkedList<Product> store=new LinkedList<Product>(); public LinkedList<Product> getStore() { return store; } public void setStore(LinkedList<Product> store) { this.store = store; } /* 生产者方法 * push()方法用于存放产品. * 参数含义:第一个是产品对象 * 第二个是线程名称,用来显示是谁生产的产品. * 使用synchronized关键字修饰方法的目的: * 最多只能有一个线程同时访问该方法. * 主要是为了防止多个线程访问该方法的时候,将参数数据进行的覆盖,从而发生出错. */ public synchronized void push(Product p,String threadName) { /* 仓库容量最大值为10,当容量等于10的时候进入等待状态.等待其他线程唤醒 * 唤醒后继续循环,等到仓库的存量小于10时,跳出循环继续向下执行准备生产产品. */ while(store.size()==10){ try { //打印日志 System.out.println(threadName+"报告:仓库已满--->进入等待状态--->呼叫老大过来消费"); //因为仓库容量已满,无法继续生产,进入等待状态,等待其他线程唤醒. this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //唤醒所有等待线程 this.notifyAll(); //将产品添加到仓库中. store.addLast(p); //打印生产日志 System.out.println(threadName+"生产了:"+p.id+"号产品"+" "+"当前库存来:"+store.size()); try { //为了方便观察结果,每次生产完后等待0.1秒. Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } /* 消费者方法 * pop()方法用于存放产品. * 参数含义:线程名称,用来显示是谁生产的产品. * 使用synchronized关键字修饰方法的目的: * 最多只能有一个线程同时访问该方法. * 主要是为了防止多个线程访问该方法的时候,将参数数据进行的覆盖,从而发生出错. */ public synchronized void pop(String threadName){ /* 当仓库没有存货时,消费者需要进行等待.等待其他线程来唤醒 * 唤醒后继续循环,等到仓库的存量大于0时,跳出循环继续向下执行准备消费产品. */ while(store.size()==0) { try { //打印日志 System.out.println(threadName+"下命令:仓库已空--->进入等待状态--->命令小弟赶快生产"); //因为仓库容量已空,无法继续消费,进入等待状态,等待其他线程唤醒. this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //唤醒所有等待线程 this.notifyAll(); //store.removeFirst()方法将产品从仓库中移出. //打印日志 System.out.println(threadName+"消费了:"+store.removeFirst().id+"号产品"+" "+"当前库存来:"+store.size()); try { //为了方便观察结果,每次生产完后等待1秒. Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } 生产者 public class Producer implements Runnable { //定义一个静态变量来记录产品号数.确保每一个产品的唯一性. public static Integer count=0; //定义仓库 Repertory repertory=null; //构造方法初始化repertory(仓库) public Producer(Repertory repertory) { this.repertory=repertory; } /* run()方法因为该方法中存在非原子性操作count++; * 当多个线程同时访问时会发生count++的多次操作,导致出错 * 为该方法添加同步错做,确保每一次只能有一个生产者进入该模块。 * 这样就能保证count++这个操作的安全性. */ @Override public void run() { while (true) { synchronized(Producer.class){ count++; Product product=new Product(count); repertory.push(product,Thread.currentThread().getName()); } } } } 消费者 public class Consumer implements Runnable { //定义仓库 Repertory repertory=null; //构造方法初始化repertory(仓库) public Consumer(Repertory repertory) { this.repertory=repertory; } //实现run()方法,并将当前的线程名称传入. @Override public void run() { while(true){ repertory.pop(Thread.currentThread().getName()); } } } .测试类 public class TestDemo { public static void main(String[] args) { //定义一个仓库,消费者和生产者都使用这一个仓库 Repertory repertory=new Repertory(); //定义三个生产者(p1,p2,p3) Producer p1=new Producer(repertory); Producer p2=new Producer(repertory); Producer p3=new Producer(repertory); //定义两个消费者(c1,c2) Consumer c1=new Consumer(repertory); Consumer c2=new Consumer(repertory); //定义5个线程(t1,t2,t3,t4,t5) Thread t1=new Thread(p1,"张飞"); Thread t2=new Thread(p2,"赵云"); Thread t3=new Thread(p3,"关羽"); Thread t4=new Thread(c1,"刘备"); Thread t5=new Thread(c2,"曹操"); //因为关羽跟赵云的生产积极性高,所以把他们的线程优先级调高一点 t2.setPriority(10); t3.setPriority(10); //启动线程 t1.start(); t2.start(); t3.start(); t4.start(); t5.start(); } } //转自https://www.cnblogs.com/hckblogs/p/7858545.html#blogTitle