同步方法
- 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法的外面等待着,排队
- 格式:
public synchronized void method() { // 可能会产生线程安全问题的代码 }
备注:同步锁是谁>
对于非static方法,同步锁就是this
对于static方法,我们使用当前方法所在类的字节码对象(类名.class文件)
同步方法代码示例:
@Override public void run() { System.out.println(RunnableImpl.class); System.out.println("this ---->" + this); // 先判断票是否存在 System.out.println(); while(true){ saleTicket(); } } /* * 静态的同步方法 * 锁对象 * 不能是this * this是创建对象之后产生的,静态方法优先于对象的创建 * 静态同步方法中的锁对象是本类的class属性--->class文件对象(反射) */ public static synchronized void saleTicket() { if (ticket > 0) { // 提高卖票的体验感 ,让程序睡眠下 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } // 票存在,卖出第ticket张票 System.out.println(Thread.currentThread().getName() + "---->正在售卖第" + ticket + "张票"); ticket--; } }
Lock锁
java.util.concurrent.lock.Lock机制提供了比synchronize代码块和synchronize同步方法更加广泛的锁操作,同步代码快/同步方法具有的功能,Lock都有,除此之外更强大,更能体现出面向对象的特征.
Lock锁也称为同步锁,定义了加锁与解锁的动作,方法如下:
- public void lock(): 加同步锁
- public void unlock(): 释放同步锁
备注: 锁是控制多个线程对共享资源进行访问的工具.通常,锁提供了对共享资源的独占访问.一次只能有一个线程获得锁,对共享资源的所有访问都需要先获得锁.
示例代码:
public class RunnableImpl implements Runnable{ //定义一个多线程共享的资源 电影票 private int ticket = 100; //1.在成员位置创建一个ReentrankLock对象 Lock Lock = new ReentrantLock(); //设置线程的任务:卖票 此时窗口--->线程 @Override public void run(){ //先判断票是否存在 while(true){ //2.在可能引发线程安全的代码前调用Lock接口中的lock方法获取锁 Lock.lock(); if(ticket > 0){ System.out.println(Thread.currentThread().getName()+"-->正在售卖第"+ticket+"张票"); ticket--; } finally{ //finally中的语句块一般都会执行到,用来释放资源 //释放锁 Lock.unlock(); } } } }
线程状态概述
当线程被创建并启动之后,他既不是一启动就进入到了执行状态,也不是一直处于执行状态,在县城的生命周期中有6中状态.
线程状态 |
导致状态发生条件 |
NEW(新建) | 线程刚被创建,但是还没有被启动,还没有调用start方法 |
RUNNABLE(可运行) | 线程可以在java虚拟机中运行的状态,可以是正在运行自己的代码,也可能没有,这取决与操作系统处理器 |
BLOCKED(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他线程所持有,则该线程进入到BLOCKED状态;当该线程持有锁时,该线程就进入到Runnable状态 |
WAITING(无限等待) | 一个线程在等待另一个线程执行一个动作(新建)时,该线程就进入到Wating状态,进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能都唤醒 |
TIME_WAITING(计时等待) | 同Wating状态,有几个方法有超时参数,调用他们将进入TimeWaiting状态,这一状态建议值保持到超时期满或者是收到了唤醒通知.带有超时参数的常用方法有Thread.sleep(),Object.wait |
TERMINATED(被终止) | 因为run方法是正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡 |
状态描述:
TimeWaiting(计时等待)
TineWiting在JavaAPI中描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态
其实,当我们调用了sleep这一方法之后,当前正在执行的线程就进入到了计时等待状态.
备注:
- 进入到TimeWaiting状态的一种常见的操作是调用sleep方法,单独的线程也可以调用,不一定非要有协作关系
- 为了让其他线程有机会执行到,一般建议将Thread.sleep()调用放到线程run方法内,这样才能保证该线程执行过程中会睡眠
- sleep与锁无关,线程睡眠到期会自动苏醒,并返回到Runnable状态.sleep()里面的参数指定的时间是线程不会运行的最短时间,因此,sleep()方法不能保证该线程睡眠到期后就会立刻开始执行.
Blocked锁阻塞状态
Blocked状态在JavaAPI中的描述为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态.
比如:线程A与线程B代码中使用通一把锁,如果线程A获取到锁对象,线程A就进入Runnable状态,反之线程B就进入到Blocked锁阻塞状态.
Waiting无线等待状态
Waiting状态在JavaAPI中的描述为:一个正在无限等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态.
一个调用了某个对象的Object.wait()方法的线程,会等待另一个线程调用此对象的Object.notify()或者Object.notifyAll()方法
其实waiting状态他并不是一个线程的操作,它体现的十多个线程之间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系.
等待唤醒机制
线程间通信
概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却又不相同.
比如说,线程A用来生产一个饮料,线程B用来消费,饮料可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,name线程A与线程B之间就存在线程通信问题.
为什么要处理线程之间的通信:
多个线程并发在执行时,在默认情况下CPU是随机切换线程的,当我们需要多个线程共同来完成一件任务时,并且帮助我们达到多线程共同操作一份数据.
如何保证线程间通信有效利用资源:
多个线程在处理同一个资源的时候,并且任务还不相同,需要线程通信来帮助我们解决线程之间对同一个变量的使用或者操作.就是多个线程在操作同一份数据时,避免对同一共享变量的争夺,也就是我们需要通过一定的手段使各个线程有效的利用资源.
而这种手段就是----->等待唤醒机制.
等待唤醒机制
什么是等待唤醒机制呢?
这是多个线程间的一种协作机制.
就是一个线程进行了规定操作之后,就进入到了等待状态(wait()),等待其他线程执行完他们指定代码后,再将其唤醒(notify());
在有多个线程进行等待时,如果需要,可以使用notifyAll()来唤醒所有的等待线程.
wait/notify就是线程间的一种协作机制.
等待唤醒中的方法:
等待唤醒机制就是用来解决线程间通信问题的.可以使用到的方法有三个,如下:
- wait(): 线程不再活动,不再参与调度,进入到wait set中,因此不会浪费CPU资源,也不再去竞争锁,这时的线程就是WAITYING.他还要等着别的线程执行一个特别的动作,就是唤醒通知(notify)在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列(ready queue)中.
- notify(): 选取说通知对象的wait set中的一个线程释放.例如:餐厅有空位置后,等候最久的顾客最先入座
- notifyAll(): 释放所通知对象的wait set中的全部线程.
备注:
哪怕只通知了一个等待线程,被通知的线程也不能立即回复执行,因为他当初中断的地方是在同步代码快中,而此刻他已经不持有锁了,所以他需要再次尝试着去获取锁(很可能面临着其他线程的竞争),成功后才能在当初调用wait方法之后的地方恢复执行.
总结如下:
如果能获取到锁,线程就从WAITING状态变成RUNNABLE状态
否则,从wait set中出来,又进入set中,线程就从WAITING状态转变成BLOCKED状态.
调用wait和notify方法的注意细节:
- wait方法与notify方法必须由同一个锁对象调用.因为,对用的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程
- wait方法与notify方法是属于Object类的方法的.因为,锁对象可以是任意对象,而任意对象的所属类都是集成了Object类的.
- wait方法与notify方法必须要在听不代码块或者同步方法中使用.因为,必须通过锁对象调用这两个方法来实现等待与唤醒.
线程池
线程池的概念
线程池:其实就是一个可以容纳多个线程的容器,其中的线程可以反复的使用,省去了频繁的创建线程对象的操作,无需反复创建线程而消耗过多的系统资源.
由于线程池中有很多操作都是与优化系统资源有关的,我们今天先来介绍下线程池的工作原理
合理利用线程池能够带来什么样的好处:
- 降低资源消耗,减少了线程的创建与销毁的次数,每个工作线程都可以被重复利用,可执行多个任务
- 提高了响应速度.当任务到达时,任务可以不需要等待线程的创建就能立即执行.
- 提高了线程的可管理性.可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而导致服务器的宕机(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,死机的风险也就更高).
线程池的使用
java里面线程池的顶级接口是java.util.concurrent.Executor , 但严格意义讲,Executor它并不是一个线程池,它只是执行线程的一个工具,真正的线程池接口是java.util.concurrent.ExecutorSrevice.
因此java.utilconcurrent.Executors线程工程类童工了一些静态工厂,生成一些常用的线程池.官方建议使用Executors来创建线程池对象.
Executors创建线程池的方法如下:
- public staticExecutorSrevice newFixedThreadPool(int nThreads): 返回的就是线程池对象.(创建的是有界的线程池,也就是线程池中的线程个数可以指定最大数量).
获取到了一个线程池ExecutorSrevice对象,在该类中定义了一个使用线程池对象的方法如下:
public Future<?> submit(Runnable task) : 获取线程池中的某一个线程对象,并执行.
- Future接口 : 用来记录线程任务执行完毕后产生的结果.线程的创建与使用.
使用线程池中线程对象的步骤:
- 创建线程池对象
- 创建Runnable接口子类对象.(task)
- 提交Runnable接口子类对象.(take task)
- 关闭线程池(一般不做).