Keywords: Thread
类、synchronized
关键字、同步代码块、同步函数、死锁、多线程间通信、等待/唤醒机制、停止线程、守护线程、线程的内部类体现
多线程
基本概念
进程: 是一个正在进行中的程序(直译),每一个进程执行都有一个执行的顺序,该顺序就是一个执行路径,或者叫一个控制单元
线程: 就是进程中一个负责程序执行的控制单元(执行路径)
一个进程中至少要有一个线程,开启多个线程是为了同时运行多部分代码。每一个线程都有自己运行的内容,这个内容可以称为线程要执行的任务。
多线程好处:解决了多部分同时运行的问题。
多线程的弊端:线程太多会导致效率的降低。
一个栗子:
JVM启动时就启动了多个线程,至少有两个线程可以分析的出来。
1. 执行main函数的线程,该线程的任务代码都定义在main函数中
2. 负责垃圾回收的线程
线程的创建方式
通过对api的查找,java已经提供了对线程这类事物的描述——Thread类。
创建线程方式一:继承Thread类
步骤:
- 定义一个类继承
Thread
类。 - 覆盖
Thread
类中的run
方法。目的:将自定义的代码存储在run
方法中,让线程运行。 - 直接创建
Thread
的子类对象创建线程。 - 调用
start
方法开启线程并调用线程的任务run
方法执行。
一个栗子:(day13\ThreadDemo1.java)
class Demo extends Thread
{
public void run()
{
for(int x=0; x<10; x++)
{
//for(int y=-9999999; y<999999999; y++){}
System.out.println("Demo run");
}
}
}
class ThreadDemo1
{
public static void main(String[] args)
{
Demo d = new Demo();
d.start();//开启线程并执行该线程的run方法
//d.run();//仅仅是对象调用方法,而线程创建了,并没有执行
for(int x=0; x<10; x++)
{
System.out.println("main run");
}
}
}
多线程的随机性: 发现每次运行结果都不同,因为多个线程多获取cpu的执行权,cpu执行到谁,谁就运行。但在某一时刻,只能由一个程序在运行(多核除外)。cpu在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象的把多线程的运行行为看作在抢夺cpu的执行权。
为什么要覆盖run
方法?
Thread
类用于描述线程,该类就定义了一个功能,用于存储线程要运行的代码,该存储功能就是run
方法。简言之:Thread
类中的run
方法用于存储线程要运行的代码。
线程的状态图:
没有执行资格:冻结状态
由执行资格没有执行权:临时堵塞状态
既有执行资格又有执行权:运行状态
[图片上传失败...(image-8e484d-1589686906648)]
注:线程都有自己的默认名称Thread-编号
,该编号从0开始,用getName()
方法获取
static Thread currentThread()
:获取当前线程对象
getName()
:获取线程名称
设置线程名称:setName()
或者构造函数
创建线程方式二:实现Runnable接口
简单的买票程序:多个窗口同时卖票。(day13\TicketDemo.java)
步骤:
- 定义类实现
Runnable
接口 - 覆盖
Runnable
接口中的run
方法,将线程要运行的代码存放在该run
方法中 - 通过
Thread
类建立线程对象 - 将
Runnable
接口的子类对象作为实际参数传递给Thread
类的构造函数。为什么:自定义的run
方法所属的对象是Runnable
接口的子类对象,所以要让线程去执行指定对象的run
方法,就必须明确该方法所属的对象。 - 调用
Thread
类的start
方法开启线程并调用Runnable
接口子类的run
方法
两种方式(实现方式和继承方式)的比较:
实现方式好处:
- 避免了单继承的局限性,在定义线程时建议使用实现方式
- 将线程的任务从线程的子类中分离出来,进行了单独的封装。按照面向对象的思想将任务的封装成对象。
两种方式的区别:
继承方式:线程代码存放在Thread
类的子类run
方法中
实现方式:线程代码存放在Runnable
接口的子类run
方法中
同步
多线程中的安全问题
导致线程出现的原因: 1.多个线程访问出现延迟;2.线程随机性
线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来,导致共享数据的错误
注:线程安全问题在理想状态下,不容易出现,但一旦出现对软件的影响是非常大的
解决方法: 对多条操作共享数据的语句,只能让一个线程先执行完,在执行过程中,其他线程不可以参与执行。同步。
同步代码块
格式:
synchronized(对象)
{
需要被运行的代码
}
一个栗子:
Object obj = new Object();
synchronized(obj)
{
需要被运行的代码
}
对象如同锁,持有锁的线程可以在同步中执行,没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。eg.火车上的卫生间= =。必须保证同步中只有一个线程在运行。
同步的前提:
- 必须要有两个或者两个以上的线程
- 必须是多个线程使用同一个锁
好处: 解决多线程的安全问题
弊端: 多个线程都需要判断锁,较为消耗资源
应用: day13\BankDemo.java
需求:储户,两个,每个都到银行存钱每次存100,各存三次
目的:该程序是否有安全问题,如果有,怎么解决?
如何找问题:
1. 明确哪些代码是多线程运行代码
2. 明确共享数据
3. 明确多线程运行代码中哪些语句是操作共享数据的
同步函数
两种表现形式:同步代码块(eg.上面的栗子);同步函数(eg.public synchronized void 函数名(参数)
)
同步函数的锁: 函数需要被对象调用,那么函数都有一个所属对象引用,就是this
,所以同步函数使用的锁是this
。如果同步函数被static
修饰后,使用的锁不是this
,静态函数进内存后,内存中没有本类对象,但一定有该类对应的字节码文件对象类名.class
,该对象的类型是class
,故静态同步方法使用的锁是该方法所在类的字节码文件对象类名.class
。(day14\StaticLockDemo.java)
单例设计模式
饿汉式:
class Single
{
private Single(){}
private static Single s = new Single();
public static Single getInstance()
{
return s;
}
}
懒汉式:
class Single
{
private static Single s = null;
private Single(){}
public static /*synchronized*/ Single getInstance()
{
if(s==null)
{
synchronized(Single.class)
{
if(S == null)
s = new Single();
return s;
}
}
}
}
懒汉式和饿汉式的区别: 懒汉式的特点是实例的延迟加载,多线程访问时会出现安全问题,用同步函数和同步代码块都可以解决安全问题,但效率相对低下,使用双重判断可提高效率。加同步时使用的锁是该类所属的字节码对象。
死锁
(day14\DeadLockTest.java)
多线程间通信
多个线程在操作同一个资源,但是操作的动作不同。(day14\ResourceDemo.java)
等待/唤醒机制
涉及的方法:(day14\ResourceDemo2.java,简化后day14\ResourceDemo3.java)
-
wait()
: 让线程处于冻结状态,被wait的线程会被存储到线程池中 -
notify()
:唤醒线程池中一个线程(任意) -
notifyAll()
:唤醒线程池中的所有线程
都使用在同步中,因为要对持有监视器(锁)的线程操作。所以要使用在同步中,因为只有同步才具有锁的概念。
为什么这些操作线程的方法要定义在Object
类中?
因为这些方法在操作同步中的线程时,都必须要标识他们所操作线程持有的锁,只有同一个锁上的被等待线程可以被同一个锁上的notify
唤醒,不可以对不同锁中的线程进行唤醒。也就是硕,等待和唤醒必须是同一个锁,而锁可以是任意对象,所以可以被任意对象调用的方法定义在Object
类中。
一个栗子:
生产者和消费者
day14\ProducerConsumerDemo(0-3).java
JDK5.0升级版
jdk1.5以后将同步和锁封装成了对象,并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显示动作。
Lock
接口:替代了同步代码块或者同步函数。将同步的隐式锁操作变成现实锁操作,同时更为灵活。可以一个锁上加上多组监视器。
lock()
:获取锁。
unlock()
:释放锁,通常需要定义finally
代码块中。
Condition
接口:替代了Object
中的wait notify notifyAll
方法,将这些监视器方法单独进行了封装,变成Condition监视器对象,可以任意锁进行组合。
await()
:相当于wait()
signal()
:相当于notify()
signalAll()
:相当于notifyAll()
停止线程
stop
方法(已经过时)run
方法结束
如何控制线程的任务结束?
任务中都会有循环结构,只要控制住循环就可以结束任务,控制循环通常就用定义标记来完成。
但是如果线程处于冻结状态,无法读取标记。如何结束?
当没有指定的方式让冻结的线程恢复到运行状态时,需要对冻结进行清除,可以使用interrupt()
方法将线程从冻结状态强制恢复到运行状态中来,让线程具备cpu
的执行资格,当时强制动作发生时会产生异常InterruptedException
,记得要处理。
线程类的其他方法
守护线程: 在程序运行的时候在后台提供一种通用服务的线程,守护线程随主线程一起销毁。将线程设置为守护线程:setDaemon(boolean b)
(day14\StopThreadDemo.java)
非守护线程: 也叫用户线程,由用户创建,非守护线程和主线程互不影响
join()
:当A线程执行到了B线程的.join()
方法时,A就会等待,等B线程都执行完,A才会执行。join()
可以用来临时加入线程执行。
形象比喻:排队打饭,主线程在队列中遇到了A线程的
join
,就把自己的位置让给A,自己站在对外等A线程打完饭,主线程再回到队列中和其他线程抢打饭的机会。
setPriority(int num)
:设置优先级
toString()
:返回此线程的字符串表示,包括线程的名称,优先级和线程组
开发中线程的内部类体现:
三个线程同时执行:(day14\ThreadTest.java)
class ThreadTest
{
new Thread()
{
public void run()
{
for(int x=0; x<50; x++)
{
System.out.println(Thread.currentThread().getName()+"....x="+x);
}
}
}.start();
for(int x=0; x<50; x++)
{
System.out.println(Thread.currentThread().getName()+"....y="+x);
}
Runnable r = new Runnable()
{
public void run()
{
for(int x=0; x<50; x++)
{
System.out.println(Thread.currentThread().getName()+"....z="+x);
}
}
};
new Thread(r).start();
}
}
一个面试题:
class ThreadTest
{
//面试题:
public static void main(String[] args)
{
new Thread(new Runnable()
{
public void run()
{
System.out.println("runnable run");
}
})
{
public void run()
{
System.out.println("subThread run");
}
}.start();
}
}
>>> subThread run