Java多线程
本节目标:
- 1.线程的相关概念
- 2.线程的创建和启动 ★
- 3.线程的停止
- 4.线程的常见方法(Thread)
- 5.线程的生命周期 ★
- 6.线程的同步
- 7.线程的通信
- 8.线程的创建和启动:方式三
- 9.线程的创建和启动:方式四(线程池)
一.线程的相关概念
1.程序、进程、线程
1)程序:为了让计算机完成某个特定的功能,编写的一系列有序指令的集合(静止的代码集合)
- 静态代码集合
- 指令集合
2)进程:正在运行的程序,具有自己的生命周期(有开始,有死亡)。是程序的一次执行过程,或是正在运行的一个程序。
- 如:运行中的QQ,运行中的MP3播放器
- 程序是静态的,进程是动态的
- 进程作为资源分配的单位, 系统在运行时会为每个进程分配不同的内存区域
3)线程:进程可进一步细化为线程,是一个程序内部的一条执行路径;一个进程可以拆分为若干个小的执行单位,每个执行单位可以成为线程
- 一个进程可以包含1个或多个线程
- 线程:CPU执行和调度的最小单位
2.单核CPU和多核CPU的理解
1)单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程
的任务;
2)如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
3)一个Java应用程序java.exe,其实至少有三个线程:
- main()主线程,
- gc()垃圾回收线程,
- 异常处理线程。当然如果发生异常,会影响主线程
3.并行与并发
并行: 多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发: 一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事
4.单线程 & 多线程
单进程:同一个时间,只能执行一个进程(任务),比如单任务式操作系统,dos
多进程:同一个时间,可以同时执行多个进程(任务),比如多任务式操作系统,windows
单线程:在同一个时刻,一个进程只能执行一个线程
多线程:在同一个时刻,一个进程可以“同时”执行多个线程
- 同时:貌似同时,抢占式的策略,轮流交替 进行cpu的资源占用,给我们造成同时执行的假象
每个Java程序都有一个隐含的主线程: main 方法
5.多线程的好处
-
①有效的占用了CPU的空闲时间,提高了效率
-
②提高用户的体验性:可以一边加载,一边浏览
-
③将一个复杂的进程拆分成多个小的简单的线程,提高代码的维护性和分离性!
6.多线程的应用场景
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等——耗时操作:放到一个子线程执行
- 需要一些后台运行的程序时。
二.线程的创建和启动 ★
Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来实现。
1.java.lang.Thread类
1)Thread类的特性:
- 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体
- 通过该Thread对象的start()方法来调用这个线程
2)Thread类构造方法:
- Thread():创建新的Thread对象
- Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run方法
- Thread(Runnable target, String name):创建新的Thread对象
- target :其 run 方法被调用的对象。
- name :新线程的名称。
2.创建线程的两种方式
1)继承Thread类
- 1)定义子类继承Thread类。
- 2)子类中重写Thread类中的run方法。
- 3)创建Thread子类对象,即创建了线程对象。
- 4)调用线程对象start方法:即启动线程,调用run方法。
a.定义线程类:
class MyThread extends Thread{
@Override
public void run(){
//编写新线程要执行的任务体
}
}
b.创建新线程,并启动
new MyThread().start();
2)实现Runnable接口
- 1)定义子类,实现Runnable接口。
- 2)子类中重写Runnable接口中的run方法。
- 3)通过Thread类含参构造器创建线程对象。
- 4)将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
- 5)调用Thread类的start方法:开启线程, 调用Runnable子类接口的run方法
a.定义任务类:
class MyRunnable implements Runnable{
@Override
public void run(){
//编写新线程的任务体
}
}
b.创建新线程,并启动
new Thread(new MyRunnable()).start();
案例:
public class TestThread {
public static void main(String[] args) {
//打印-当前线程-名称
System.out.println(Thread.currentThread().getName());
//启动子线程1:extends Thread
MyThread1 m = new MyThread1();
m.start();//启动线程,默认调用run方法
//启动子线程2:implements Runnable
MyThread2 m = new MyThread2();
Thread t = new Thread(m);
t.start();
//new Thread(new MyThread2()).start();
for(int i=1;i<=1000;i++){
System.out.println("小绿吃饭~~"+i);
}
}
}
//创建线程的方式一:
class MyThread1 extends Thread{
@Override
public void run() {
for(int i=1;i<=1000;i++){
System.out.println("小红看电影~~"+i);
}
}
}
//创建线程的方式二
class MyThread2 implements Runnable{
@Override
public void run() {
for(int i=1;i<=1000;i++){
System.out.println("^^^小黄KTV唱歌"+i);
}
}
}
3.start与run的区别
-
1)start:启动新线程,系统为新线程分配栈空间,新线程具备CPU的占用资格
- 默认执行run方法
-
2) run:普通的任务方法,里面放线程的任务体
区别 | 调用者 | 功能 |
---|---|---|
start方法 | 线程对象 | 让线程启动,让该线程进入CPU执行和调用的队列;默认调用run方法 |
run方法 | 系统 | 是一个普通方法,执行线程的任务体,包含线程被调度时执行的操作 |
4.两种创建方式的区别:窗口买票案例
1)区别:
- 继承Thread:线程代码存放Thread子类的run方法中。
- 实现Runnable:线程代码存在接口的实现类的run方法。
2)实现Runnable接口方式创建的优点==
- 1)继承Thread的方式,存在单继承的局限性;而实现Runnable接口的方式,避免了单继承的局限性!
- 2)实现Runnable接口的方式,更适合处理有共享资源的情况;
- Runnable接口的方式,可以创建多个线程对象,启动多个线程;
new Thread(new MyRunnable()).start();
- 而继承Thread类,共享资源需要使用静态资源(每个线程对象只能调用一次start()方法)
new MyThread().start();
- 静态资源的生命周期太长,需要慎用(如果是个对象,资源占用会很大)
- Runnable接口的方式,可以创建多个线程对象,启动多个线程;
3)存在问题:存在线程安全(不同步)问题——举例
- 当执行到最后 1 张票时,某个线程可能在操作
--tickets
步骤之前阻塞; - 此时其他线程,依旧可以继续执行;
- 若其他线程执行完毕,该线程继续执行之前
--tickets
操作,tickets将变为-1;
4)案例:三个售票窗口卖票的例子
public class TestSellTicket {
public static void main(String[] args) {
//方式1:SellTicket extends Thread
SellTicket st1 = new SellTicket();
st1.start();
SellTicket st2 = new SellTicket();
st2.start();
SellTicket st3 = new SellTicket();
st3.start();
//-----------------------------
//方式2:SellTicket2 implements Runnable
SellTicket2 st = new SellTicket2();
new Thread(st).start();
new Thread(st).start();
new Thread(st).start();
}
}
//方式1
class SellTicket extends Thread{
static int tickets = 100;//票数
@Override
public void run() {
while(true){
if(tickets<=0){
System.out.println("票卖完了!");
break;
}
//sleep():令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖了一张票,剩余票数:"+(--tickets));
}
}
}
//方式2
class SellTicket2 implements Runnable{
int tickets = 100;//票数
@Override
public void run() {
while(true){
if(tickets<=0){
System.out.println("票卖完了!");
break;
}
System.out.println(Thread.currentThread().getName()+"卖了一张票,剩余票数:"+(--tickets));
}
}
}
三.线程的停止方式
1.线程停止的时机:
- 线程的任务体正常执行结束(老死)
- 线程的任务体中遇到了break(自杀)
- 线程的任务体中遇到了error或Exception(病死)
- 通知停止(被杀)
2.通知停止方式:
- 步骤1:待停止的线程的循环变量设置成boolean变量,默认值为true
- 步骤2:待停止的线程添加一个更新循环变量的方法
public void setLoop(boolean flag){
this.flag = flag;
}
- 步骤3:在需要停止线程的地方,调用setLoop方法,更新flag的值为false
public static void main(String[] args) {
StopThread st = new StopThread();
st.start();
for (int i = 0; i < 1000; i++) {
System.out.println("小绿正在逛街"+i);
if (i==100)
st.setFlag(false);
}
}
class StopThread extends Thread{
boolean flag = true;
@Override
public void run() {
int i=1;
while(flag){
System.out.println("小红在看电影~~~"+(i++));
}
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
四.线程的其他知识
1.线程的常见方法总结
1)void start()
- 启动新线程:系统为新线程分配栈空间,新线程具备CPU的占用资格
- 默认执行对象的run()方法
2)void run()
- 线程在被调度时执行的操作
3)String getName()
- 返回线程的名称
4)void setName(String name)
- 设置该线程名称
5)static currentThread()
- 返回当前线程
6)static void sleep(long millis)
- 让当前线程休眠指定的毫秒数
- 一般用于实现多线程的交错效果,也就是让当前线程休眠,cpu相对空闲,其他线程容易抢占到cpu时间片
7)void setDaemon(boolean on)
- 将该线程标记为守护线程或用户线程。
8)void join()
- API:等待该线程终止
- 插队:当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止
- 插队的线程一旦插队成功,则肯定全部执行完该线程所有任务体,其他线程才会执行
- 但插队的线程不一定插队成功!
- 低优先级的线程也可以获得执行
9)static void yield()
- API:暂停当前正在执行的线程对象,并执行其他线程。
- 礼让线程放弃cpu的占用权,礼让给优先级更高或相同线程,[但是礼让时间不确定,有可能刚放弃,又重新回到抢占的队伍]
- 1>把执行机会让给优先级相同或更高的线程
- 2>若队列中没有同优先级的线程,忽略此方法
10)void interrupt()
- 中断线程
- 并没有真正的中断,仅仅只是做了一个中断的标记
- 如果中断的线程正在休眠sleep,则会生成一个InterruptedException
11)static boolean interrupted()
- 测试当前线程是否已经中断。
12)boolean isAlive()
- 测试线程是否处于活动状态。
2.线程的优先级
1.线程的优先级控制:
- MAX_PRIORITY(10);
- MIN _PRIORITY (1);
- NORM_PRIORITY (5);
//The minimum priority that a thread can have.
public final static int MIN_PRIORITY = 1;
//The default priority that is assigned to a thread.
public final static int NORM_PRIORITY = 5;
//The maximum priority that a thread can have.
public final static int MAX_PRIORITY = 10;
2.涉及的方法:
- getPriority() :返回线程优先值
- setPriority(int newPriority) :改变线程的优先级
3.优先级的特性:
- 1)线程创建时继承父线程的优先级
- 每个线程都有优先级,初始优先级为创建该线程的线程的优先。
- 2)main线程的优先级为5
- 3)高优先级的线程具有抢占cpu的优先权
- 4)优先级的值:1——10
3.线程的分类
Java中的线程分为两类:
- 1)守护线程
- 2)用户线程
守护线程:
-
是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程
-
Java垃圾回收就是一个典型的守护线程
-
若JVM中都是守护线程,当前JVM将退出
区别:判断Jvm何时离开,两者正常停止的时机
- 用户线程:线程本身任务体执行结束,
- 守护线程:[所有的用户线程都执行结束],守护线程才伴随着结束
4.线程的调度
调度策略:
- 时间片
- 抢占式:高优先级的线程抢占CPU
Java的调度方法:
- 同优先级线程组成先进先出队列(先到先服务),仍然使用抢占式策略
- 对高优先级,使用优先调度的抢占式策略
案例:龟兔赛跑
跑30米,看谁先抛到终点
兔子每跑1米休息1000ms
乌龟每跑10米休息1000ms
五.线程的生命周期*
JDK中用Thread.State枚举表示了线程的几种状态
- NEW:至今尚未启动的线程处于这种状态。
- RUNNABLE:正在 Java 虚拟机中执行的线程处于这种状态。
- BLOCKED:受阻塞并等待某个监视器锁的线程处于这种状态。
- WAITING:无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。
- TIMED_WAITING:等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。
- TERMINATED:已退出的线程处于这种状态。
在给定时间点上,一个线程只能处于一种状态。
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件
- 运行:当就绪的线程被调度并获得处理器资源时,便进入运行状态, run()方法定义了线程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止
六.线程的同步*
1)理解:使用线程同步的机制解决线程不安全的问题!
2)线程不安全的问题发生的原因:
- 多个线程共同操作共享资源时
- 其中一个线程在操作过程中(没有执行完),其他线程参与进来,导致线程不安全。
3)解决的方式:上锁 — 使用线程同步机制
- 让其中一个线程在操作共享资源的过程中,其他线程不能参与进来
4)如何上锁?synchronized+锁对象
- 表明该对象在任一时刻只能由一个线程访问
注意:
- 必须确保使用同一个资源的多个线程共用一把锁, 这个非常重要, 否则就无法保证共享资源的安全
- 一个线程类中的所有静态方法共用同一把锁(类名.class) , 所有非静态方法共用同一把锁(this) , 同步代码块(指定需谨慎)
方式一:同步代码块
synchronized(锁对象){
需要上锁的代码
}
锁对象特点:
- 锁对象可以自己指定,但一般选择this
- 也可以选用类名.class
方式二:同步方法
修饰符 synchronized 返回类型 方法名(参数列表){
需要上锁的代码
}
注意:
- 不能自己定义锁对象
- 普通方法:默认的锁对象是this
- 静态方法:默认的锁对象是类名.class
方式三:Lock锁—jdk5.0出现的新特性
ReentrantLock lock = new ReenTrantLock();
class A{
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
lock.lock();
try{
//保证线程安全的代码;
}
finally{
lock.unlock();
}
}
}
特点:使用显式锁对象 lock
小贴士:
- 多个线程使用的是同一把锁!!!
- 必须执行锁的关闭
Lock和synchronize的区别
-
1.Lock是显式锁,语义性更强;synchronized是隐式锁 --> 出了作用域自动释放
-
2.Lock只有代码块锁,synchronized有代码块锁和方法锁
-
3.Lock需手动开启和关闭锁,要求必须显式关闭锁
-
4.使用Lock锁,JVM将花费较少的时间来调度线程,性能更好;并且具有更好的扩展性(提供更多的子类)
七.单例设计模式的线程安全性
1.饿汉式
- 类加载时就会创建对象 --> 缺点:???
- 不会出现线程安全问题;
class Hungry{
private static Hungry hungry = new Hungry();
private Hungry(){}
public static Hungry getInstance(){
return hungry;
}
}
2.懒汉式:
- 需要时才会创建对象;
- 会出现线程安全问题:因为有是否需要创建对象的判断,所以多线程执行创建对象的方法时,可能会出现创建出多个对象的情况
class Singleton {
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){ //多线程时,是否为null可能出现不同步的问题
instance=new Singleton();
}
return instance;
}
}
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
while(true){
Lazy z = Lazy.getLazy();
try { //增加多线程安全性出现的概率
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印当前线程名称与实例化对象的地址,用于判断单例模式是否失败
System.out.println(Thread.currentThread().getName() + ":" +z);
}
}
};
//创建三个线程,分别创建单例模式的对象
new Thread(r).start();
new Thread(r).start();
new Thread(r).start();
}
改进:
- 1)为了实现线程安全:该类的私有类引用成员,进行是否为null判断时 进行加锁
- 2)为了提高效率:只需要在第一次时进行判断;而不是每次都进入执行锁的代码块
- 双重保险
class Lazy {
private static Lazy lazy = null;
private Lazy() {}
public static Lazy getLazy() {
/* 该语句中的同步代码块,并不是每一次都需要执行,只需在没有创建该对象时才需要执行;
* 为了提高执行效率,可以增加一个外层的if判断
* 虽用到lazy共享资源,但并不会出现不同步的问题,因为里层锁中,还有一次是否为null的判断
*/
if(lazy==null) {
synchronized(Lazy.class) { //同步代码块
if(lazy == null)
lazy = new Lazy();
}
}
return lazy;
}
}
八.线程的死锁问题
死锁:
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
解决方法:
- 专门的算法、原则
- 尽量减少同步资源的定义
九.线程的通信
线程通信的理解:
- 通过一些方法,让线程之间可以“交流”,最终达到控制线程执行的目的。
通信的三类方法:wait() 与 notify() 和 notifyAll()
- 1)wait():令当前线程挂起同步资源、并放弃CPU,使别的线程可访问并修改共享资源,而当前线程排队等候再次对资源的访问
- 注意:wait()之后重新被加载后,此时wait()之后的代码依旧会执行,该线程这次执行要完全结束
- 2)notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待,唤醒单个线程
- 3)notifyAll ():唤醒正在排队等待资源的所有线程结束等待.
注意:Java.lang.Object提供的这三个方法
- 只有在synchronized方法或synchronized代码块中才能使用;
- 否则会报java.lang.IllegalMonitorStateException异常
1.wait() **
1)用法:在当前线程中调用方法,调用者是锁对象
- 锁对象.wait()
2)调用方法的必要条件:
- 当前线程必须具有对该对象的监控权(加锁)——在同一把锁监视下
3)功能:使当前线程进入等待状态 ,直到另一线程对该对象发出 notify (或notifyAll) 为止。
4)调用此方法后,当前线程将释放对象监控权(释放锁),然后进入等待;其之后代码,在其重新获得监控权才会执行
5)wait方法在当前线程被notify唤醒后,要重新获得监控权,然后从断点处继续代码的执行。
可能抛出的异常:
- IllegalMonitorStateException -如果当前线程不是此对象监视器的所有者。
- InterruptedException - 如果在当前线程等待通知之前或者正在等待通知时,任何线程中断了当前线程。在抛出此异常时,当前线程的中断状态 被清除。
2.notify()
1)用法:在当前线程中调用方法,调用者是锁对象
- 锁对象.notify()
2)功能:
- 唤醒当前锁对象下,正在等待的单个线程;
- 如果没有正在等待的线程,不会报异常;
- 如果等待的线程有很多,优先唤醒优先级高的
3)wait方法的调用者和notify方法的调用者以及当前线程的锁对象,必须是一把锁
3.notifyAll()
1)调用者是锁对象
2)功能:唤醒当前锁对象下,正在等待的所有线程,如果没有正在等待的线程,不会报异常。
3)wait方法的调用者和notify方法的调用者以及当前线程的锁对象,必须是一把锁
4.notify & notifyAll 两者区别
- notify():唤醒在同一对象上调用wait()处于等待的线程。应该指出的是,调用notify()实际上并没有放弃对资源上的锁。它告诉等待线程可以唤醒。然而,如果对某一资源调用notify(),但同步块内的资源的执行还需要进行10秒,那么线程需要等待额外的10秒,对象上的锁被释放,被唤醒的线程才能执行。
- notifyAll():它唤醒所有在同一对象上调用wait()的线程。在大多数情况下,优先级最高的线程将首先运行,但没有保证。其他的都和notify()方法一样。
调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
5.sleep()方法和wait()方法之间差异
sleep():
- 1)作用:都会导致当前线程进入阻塞状态,被挂起
- 2)调用方式:static方法–>Thread.sleep()
- 3)是否释放锁:不会释放锁操作
- 4)结束方式:睡眠时间到自动醒来,回到就绪状态
wait():
- 1)作用:
- 2)调用方式:非static方法–>锁对象.wait()
- 3)是否释放锁:会释放锁操作,
- 4)结束方式:通过notify()或notifyAll()唤醒,回到就绪状态
6.线程通信步骤:两个线程交替打印 1-100
1)添加同步锁机制
2)根据需要添加wait或notiy方法的调用(难点)
3)检测 wait、notify、notifyAll和当前线程的锁对象是同一个!!!
案例:
使用两个线程打印 1-100. 线程1, 线程2 交替打印
public class TestCommunication {
public static void main(String[] args) {
PrintNumber printNumber = new PrintNumber();
new Thread(printNumber).start();
new Thread(printNumber).start();
}
}
class PrintNumber implements Runnable{
int number = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
this.notify(); //唤醒当前锁对象下的其他正在等待的单个线程
if (number > 100) {
break;
}
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + (number++));
try {
this.wait(); //让当前线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
经典例题:生产者/消费者问题
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
这里可能出现两个问题:
- 生产者比消费者快时,消费者会漏掉一些数据没有取到。
- 消费者比生产者快时,消费者会取相同的数据。
1)一个生产者和一个消费者
class Clerk{
int count = 0;//产品数量
//存储产品,当生产者生产产品后,调用save方法
public synchronized void save(){//默认锁对象:this——>clerk
if (count>0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生产者生成了"+(++count)+"个产品!!!");
notify();//唤醒消费者
}
//消费产品,当消费者消费产品后,调用get方法
public synchronized void get(){//默认锁对象:this——>clerk
if (count<=0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费者消费了"+(--count)+"个产品!!!");
notify();//唤醒生产者
}
}
public class TestProductorAndConsumer {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Productor productor = new Productor(clerk);
Consumer consumer = new Consumer(clerk);
new Thread(productor).start();
new Thread(consumer).start();
}
}
//消费者线程
class Consumer implements Runnable{
Clerk clerk ;
public Consumer(Clerk clerk) {
super();
this.clerk = clerk;
}
@Override
public void run() {
while(true){
clerk.get();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//生产者线程
class Productor implements Runnable{
Clerk clerk ;
public Productor(Clerk clerk) {
super();
this.clerk = clerk;
}
@Override
public void run() {
while(true){
clerk.save();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2)多个生产者和多个消费者**
class Clerk{
int count = 0;//产品数量
//存储产品,当生产者生产产品后,调用save方法
public synchronized void save(){//默认锁对象:this——>clerk
while (count>20) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"生成了"+(++count)+"个产品!!!");
notifyAll();//唤醒当前锁对象下的所有正在等待的其他线程(包含生产者和消费者)
}
//消费产品,当消费者消费产品后,调用get方法
public synchronized void get(){//默认锁对象:this——>clerk
while (count<=0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"消费了"+(--count)+"个产品!!!");
notifyAll();//唤醒生产者
}
}
public class TestProductorAndConsumer {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Productor productor = new Productor(clerk);
Consumer consumer = new Consumer(clerk);
new Thread(productor,"马云").start();
new Thread(productor,"李彦宏").start();
new Thread(consumer,"消费者:小花").start();
new Thread(consumer,"消费者:小红").start();
new Thread(consumer,"消费者:小白").start();
}
}
//消费者线程
class Consumer implements Runnable{
Clerk clerk ;
public Consumer(Clerk clerk) {
super();
this.clerk = clerk;
}
@Override
public void run() {
while(true){
clerk.get();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//生产者线程
class Productor implements Runnable{
Clerk clerk ;
public Productor(Clerk clerk) {
super();
this.clerk = clerk;
}
@Override
public void run() {
while(true){
clerk.save();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
十.释放锁的操作
1.释放锁的操作
1)当前线程的同步方法、同步代码块执行结束
2)当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
3)当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
4)当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
2.不会释放锁的操作
1)线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
2)线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。
应尽量避免使用suspend()和resume()来控制线程
十一.线程的另两种创建方式
1.创建线程方式三:实现Callable接口
实现Callable接口,通过FutureTask包装器来创建Thread线程
实现Runnable接口和实现Callable接口的区别:
- 1、实现Callable接口,支持泛型
- 2、实现Callable接口,要求实现的是call方法,实现Runnable接口的方式,要求实现的是run方法
- 3、实现Callable接口,任务结束后有返回值,实现Runnable接口的方式,任务结束后没有返回值
- 4、实现Callable接口,call方法可以抛异常,实现Runnable接口的方式,run方法只能try catch
public class TestThread3 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
FutureTask<String> fTask=new FutureTask<>(new A());
new Thread(fTask).start();
System.out.println(fTask.get());
// for (int i = 1; i <= 100; i++) {
// System.out.println("================张飞和小绿在看电影"+i);
// }
}
}
//创建
class A implements Callable<String>{
@Override
public String call() throws Exception {
for (int i = 1; i <= 100; i++) {
System.out.println("小红和张飞在吃饭~~"+i);
}
return "约会成功";
}
}
2.线程创建方式三:线程池
使用ExecutorService、Callable、Future实现有返回结果的多线程。
public static void main(String[] args) {
//1.创建线程池对象
ExecutorService es = Executors.newFixedThreadPool(10);
//2.提交任务,启动线程
es.submit(new Runnable() {
@Override
public void run() {
System.out.println("任务1,哈哈哈");
}
});
es.submit(new Runnable() {
@Override
public void run() {
System.out.println("任务2,呵呵呵");
}
});
es.submit(new Runnable() {
@Override
public void run() {
System.out.println("任务3,嘿嘿嘿");
}
});
//3.关闭线程池,不再接受新任务
es.shutdown();
}