多线程知识点复习巩固
文章目录
一.相关概念
1.并发与并行
并发:多个事件在同一个时间段进行
并行:多个事件在同一个时刻进行(同时发生)
2.线程与进程
进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多
个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创
建、运行到消亡的过程。
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程
中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程
例子:比如我们电脑里面使用到的各大软件,像IDEA,Google,360管家,这样的应用程序为进程,再看360管家,他有扫描病毒,清理垃圾,加速的功能,这样的子功能为线程,这些进程,线程同时进行就是多进程与多线程
3.线程调度
I.分时调度
即所有线程占用CPU的使用权,平均分配CPU的使用时间,比如上面的电脑管家的那几个功能,可能是他们分别使用3秒(只是举个例子,不一定是3秒)CPU,如此循环切换
II.抢占式调度
优先让优先级高的线程使用CPU,优先级高者使用CPU时间越长,如果优先级相同那么随机选择一个(线程随机性),Java使用的便是抢占式调度。
WIndows10电脑的设置优先级的方式:
打开任务管理器,选择详细信息,选择窈调度的线程右键看到设置优先级,选择等级即可
4.主线程
即执行Main方法的线程称之为主线程,JVM在执行Main方法的过程中,首先Main方法会进入到栈内存,同时JVM会从操作系统找一条通往CPU的执行路径,该执行路径就是主线程
二.多线程的实现
多线程程序的创建一般有两种方法,如下:
1.创建Thread的子类
I.步骤及举例
步骤:
1.创建Thread的子类
2.重写子类的run方法
3.创建子类对象
4.调用子类的start方法(接下来会自动调用run方法,不用显式的调用)
start方法的调用使得该线程开始执行,JVM自动调用该线程的run方法,那么就会有主线程与该线程并发的进行。
注意:多次启动一个线程是非法的,尤其在一个线程结束后不可以再次启动
正面举例:下面程序执行结果每次执行结果都会不一样,这与线程调度有关,此时MyThread与Main两个线程并发执行抢占CPU的使用权
public class Test1 {
public static void main(String[] args) {
new MyThread().start();
for(int i=0;i<=10;i++)
{
System.out.println("Main"+i);
}
}
}
class MyThread extends Thread{
@Override
public void run() {
for(int i=0;i<=10;i++)
{
System.out.println("Run"+i);
}
}
}
反面举例:t1线程执行了两次,这是非法的
public class Test1 {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
t1.start();
for(int i=0;i<=10;i++)
{
System.out.println("Main"+i);
}
}
}
class MyThread extends Thread{
@Override
public void run() {
for(int i=0;i<=10;i++)
{
System.out.println("Run"+i);
}
}
}
II.多线程随机性打印结果的实现原理
现在我们来解释一下上面的多线程随机性打印结果的实现原理
看上面的正面例子代码:
首先JVM会找操作系统开辟一条CPU通往Main方法的(在栈内存中)路径,也即主线程,当执行到strart方法时会去调用run方法,这时JVM会再次执行之前的动作开辟一个新线程,那么此时相当于CPU有了通往栈内存的两条路径,有了两条他就会随机选择执行哪一条(当然这个过程会根据线程的优先级来择别),这就出现了两个线程争夺CPU的使用权的问题。
那么下面根据这一原理我们把代码改为下面这样
public class Test1 {
public static void main(String[] args) {
for(int i=0;i<=10;i++)
{
System.out.println("Main"+i);
}
new MyThread().start();
}
}
class MyThread extends Thread{
@Override
public void run() {
for(int i=0;i<=10;i++)
{
System.out.println("Run"+i);
}
}
}
你会发现无论执行多少次结果都是一样的,因为在到达start()方法前只有主线程这一个线程不会出现抢夺现象
III.多线程执行的内存原理
下面介绍一下多线程执行的内存原理
首先了解一下Java堆内存与栈内存的区别。
接下来我们以下面代码为例讲解
public class Test1 {
static void print()
{
System.out.println("abc");
}
public static void main(String[] args) {//第一步开辟了一个栈空间(栈1),将Main方法压栈,开始执行主线程
new MyThread().start();//又开辟一个栈空间(栈2),并将其run方法压栈,此时cpu有2条路径可以选择
print();//将print方法压入栈1
new MyThread().start();//又开辟一个栈空间(栈3),并将其run方法压栈,此时cpu有3条路径可以选择
}
}
class MyThread extends Thread{
@Override
public void run() {
for(int i=0;i<=10;i++)
{
System.out.println("Run"+i);
}
}
}
图解:
IV.Thread类的常用方法
获取线程的名称
1.使用Thread类中的方法getName()
String getName() 返回该线程的名称。
2.可以先获取到当前正在执行的线程,使用线程中的方法getName()获取线程的名称
static Thread currentThread() 返回对当前正在执行的线程对象的引用。
关于线程名称的命名方式:
主线程: main
新线程: Thread-0,Thread-1,Thread-2
如果前面的名字被换了不影响后面的,比如把Thread-0换为1,但是Thread-1还是叫Thread-1,
不会改为Thread-0,他是和创建的顺序相关联的
举例:
public class Test1 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
new MyThread().start();
new MyThread().start();
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println(getName());
}
}
结果为
main
Thread-0
Thread-1
设置线程的名称
设置线程的名称:(了解)
1.使用Thread类中的方法setName(名字)
void setName(String name) 改变线程名称,使之与参数 name 相同。
2.创建一个带参数的构造方法,参数传递线程的名称;调用父类的带参构造方法,把线程名称传递给父类,
让父类(Thread)给子线程起一个名字
Thread(String name) 分配新的 Thread 对象。
第一种方法比较简单这里不做示例了,第二种我们一般是在子类中定义一个带参的方法将参数传递给父类Thread即可
public class Test1 {
public static void main(String[] args) {
new MyThread("111").start();
}
}
class MyThread extends Thread{
public MyThread(String name)
{
super(name);
}
@Override
public void run() {
System.out.println(getName());
}
}
输出111
程序暂停
public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
毫秒数结束之后,线程继续执行
2.实现Runnable接口
创建多线程程序的第二种方式:实现Runnable接口
java.lang.Runnable
Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为 run 的无参数方法。
java.lang.Thread类的构造方法
Thread(Runnable target) 分配新的 Thread 对象。
Thread(Runnable target, String name) 分配新的 Thread 对象
实现步骤:
1.创建Runnable接口的实现类
2.在实现类中重写Runnable接口的run方法
3.创建实现类对象
4.利用Thread的构造方法创建Thread对象,并将实现类对象传入
5.使用Thread对象调用start方法
这里给出一个简单的例子即可
public class Test1 {
public static void main(String[] args) {
Thread myThread = new Thread(new MyThread(), "MyThread");
myThread.start();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
最后补充:还可以使用匿名Runnable接口实现类
3.Runnable与Thread的比较
实现Runnable接口创建多线程程序的好处:
1.避免了单继承的局限性
一个类只能继承一个类(一个人只能有一个亲爹),类继承了Thread类就不能继承其他的类
实现了Runnable接口,还可以继承其他的类,实现其他的接口
2.增强了程序的扩展性,降低了程序的耦合性(解耦)(比如在传递参数时传递不同的Runnable实现类结果不一样)
实现Runnable接口的方式,把设置线程任务和开启新线程进行了分离(解耦)
实现类中,重写了run方法:用来设置线程任务
创建Thread类对象,调用start方法:用来开启新线程
三.线程同步
1.线程安全
线程安全问题是在多个线程访问共享数据时产生的
举例:
比如电影院买票,卖一百100张票
一个窗口时,相当于单线程,没有线程安全问题
多个窗口时,比如两个窗口,第一个窗口卖1-50,第二个卖51-100,这时相当于多线程,仍然没有线程安全问题产生
多个窗口时,比如两个窗口,第一个窗口卖1-100,第二个仍然卖1-100,这时相当于多线程,有线程安全问题产生,比如会出现票1卖了两次这种问题产生,产生的原因正是由于出现了多线程数据共享问题
例子:我把票数改为10了
public class Test1 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
//new Thread时都传入myThread保证用到的ticket是同一个,即数据共享
Thread t1 = new Thread(myThread);
Thread t2 = new Thread(myThread);
Thread t3 = new Thread(myThread);
t1.start();
t2.start();
t3.start();
}
}
class MyThread implements Runnable{
private int ticket=10;
@Override
public void run() {
while(ticket>0){
System.out.println("第"+ticket+"张票售出");
ticket--;
}
}
}
打印结果:
第10张票售出
第9张票售出
第8张票售出
第7张票售出
第6张票售出
第5张票售出
第10张票售出
第3张票售出
第2张票售出
第1张票售出
第10张票售出
第4张票售出
下面我们来解释一些为什么会出现这样的结果
首先看这一部分的代码
Thread t1 = new Thread(myThread);//开启第一个线程
Thread t2 = new Thread(myThread);//开启第二个线程
Thread t3 = new Thread(myThread);//开启第三个线程
那么的开始执行后,可能此时的t1,t2,t3都读取到了ticket为10的这一数据,因为此时CPU调度三个线程(其实还有一个Main线程我们这里不看),比如使用权给了t1,开始执行run方法但是执行到ticket–前使用权让给了另外的比如t2,他又读取到10,当他执行到ticket–前使用权又被t3抢占,ticket=10又被t3读取到了,最终便形成了上面10出现3次的情况
这里看一个思考问题,可能出现4次或更多次的10吗?
如果你看懂了上面的过程,那么自然你会明白这是不可能的,最多只有3次,具体原因不再阐述。
2.解决线程安全问题
解决线程安全问题一共有三种方式
1.同步代码块
2.同步方法
3.锁机制
I.同步代码块
synchronized(锁对象){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
1.通过代码块中的锁对象,可以使用任意的对象(注意是任意,你写个什么Object对象都行,但是必须满足2)
2.必须保证多个线程使用的锁对象是同一个
3.锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行
public class Test1 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread);
Thread t2 = new Thread(myThread);
Thread t3 = new Thread(myThread);
t1.start();
t2.start();
t3.start();
}
}
class MyThread implements Runnable{
private int ticket=10;
Object o = new Object();//写在外面而不再run方法里面是为了保证锁对象是同一个
@Override
public void run() {
synchronized (o){
while(ticket>0){
System.out.print("第"+ticket+"张票售出"+" ");
System.out.println(Thread.currentThread().getName());
ticket--;
}
}
}
}
打印结果:
第10张票售出 Thread-0
第9张票售出 Thread-0
第8张票售出 Thread-0
第7张票售出 Thread-0
第6张票售出 Thread-0
第5张票售出 Thread-0
第4张票售出 Thread-0
第3张票售出 Thread-0
第2张票售出 Thread-0
第1张票售出 Thread-0
我们发现了同步代码块保证了执行同步代码的线程始终是Thread-0
那么这个同步的实现原理又是如何呢?
同步技术的实现原理:
使用了一个锁对象,又称同步锁或对象监视器,t1,t2,t3三个线程同时抢夺CPU的执行权,抢到者执行run方法,那么这个过程如下,比如最开始t1抢到了占用权,并且一直执行到run方法中的 synchronized处,接着他会看有没有锁对象,结果发现有,于是他获取到了锁对象,于是他开始执行锁对象里的代码块,而这时可能CPU被t2夺走,于是t1暂停执行,但是当t2执行run方法时,执行到 synchronized时,看有没有锁对象,然而实际上锁对象被t1夺走了,因此他不会执行所对象里的代码(接下来他会进入到阻塞态一直等待直到所对象被归还),当t1再次抢到CPU使用权时,继续执行锁对象代码块,执行完并归还锁对象,当t2再次抢到使用权时便可以继续执行锁对象的代码块
了解了上面的原理我们看一个实例
public class Test1 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread);
Thread t2 = new Thread(myThread);
Thread t3 = new Thread(myThread);
t1.start();
t2.start();
t3.start();
}
}
class MyThread implements Runnable{
private int ticket=10;
Object o = new Object();
@Override
public void run() {
System.out.println("outer"+Thread.currentThread().getName());
synchronized (o){
while(ticket>0){
System.out.print("第"+ticket+"张票售出"+" ");
System.out.println(Thread.currentThread().getName());
ticket--;
}
System.out.println("ticket:"+ticket);
}
}
}
输出:
outerThread-2
outerThread-1
outerThread-0
第10张票售出 Thread-2
第9张票售出 Thread-2
第8张票售出 Thread-2
第7张票售出 Thread-2
第6张票售出 Thread-2
第5张票售出 Thread-2
第4张票售出 Thread-2
第3张票售出 Thread-2
第2张票售出 Thread-2
第1张票售出 Thread-2
ticket:0
ticket:0
ticket:0
上述过程:Thread-2首先抢到了使用权,执行run方法并获取了锁对象,接着使用权被Thread-1抢走,Thread-1执行到synchronized时却发现锁对象被抢走,他便进入阻塞,接着使用权又被Thread-0抢走,同样执行到synchronized时却发现锁对象被抢走,他也进入阻塞,直到使用权又被Thread-2夺回开始执行锁代码块,直到执行完run方法,接着使用又被Thread-1,Thread-0使用并且都完成run方法于是最后所以线程结束
II.同步方法
步骤:
1.把访问了共享数据的代码抽取出来,放到一个方法中
2.在方法上添加synchronized修饰符
格式:
修饰符 synchronized 返回值类型 方法名(参数列表){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
简单举个例子:
public class Test1 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread);
Thread t2 = new Thread(myThread);
Thread t3 = new Thread(myThread);
t1.start();
t2.start();
t3.start();
}
}
class MyThread implements Runnable{
private int ticket=10;
Object o = new Object();
@Override
public void run() {
System.out.println("outer"+Thread.currentThread().getName());
print();
System.out.println("ticket:"+ticket);
}
public synchronized void print()
{
while (ticket > 0) {
System.out.print("第" + ticket + "张票售出" + " ");
System.out.println(Thread.currentThread().getName());
ticket--;
}
}
}
打印结果:
outerThread-1
outerThread-2
outerThread-0
第10张票售出 Thread-1
第9张票售出 Thread-1
第8张票售出 Thread-1
第7张票售出 Thread-1
第6张票售出 Thread-1
第5张票售出 Thread-1
第4张票售出 Thread-1
第3张票售出 Thread-1
第2张票售出 Thread-1
第1张票售出 Thread-1
ticket:0
ticket:0
ticket:0
**具体过程与同步代码块一样分析即可,另外要补充的三点是:
1.这种方法的锁对象为Runnable的实现类,在上面这个例子中就是myThread这个对象
2.锁方法(拿上面的print()为例)还可以下面这样写 **
public void print(){
synchronized(this){
while (ticket > 0) {
System.out.print("第" + ticket + "张票售出" + " ");
System.out.println(Thread.currentThread().getName());
ticket--;
}
}
}
3.如果print为静态方法,那么他的锁对象就不是this了,而是实现类的class文件对象(涉及到反射机制)
可以参看这篇文章反射入门实战
import org.junit.Test;
public class Test1 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread);
Thread t2 = new Thread(myThread);
Thread t3 = new Thread(myThread);
t1.start();
t2.start();
t3.start();
}
}
class MyThread implements Runnable{
private static int ticket=10;
Object o = new Object();
@Override
public void run() {
System.out.println("outer"+Thread.currentThread().getName());
print();
System.out.println("ticket:"+ticket);
}
public static void print()
{
synchronized(MyThread.class){
while (ticket > 0) {
System.out.print("第" + ticket + "张票售出" + " ");
System.out.println(Thread.currentThread().getName());
ticket--;
}
}
}
}
输出:
outerThread-0
outerThread-2
outerThread-1
第10张票售出 Thread-0
第9张票售出 Thread-0
第8张票售出 Thread-0
第7张票售出 Thread-0
第6张票售出 Thread-0
第5张票售出 Thread-0
第4张票售出 Thread-0
第3张票售出 Thread-0
第2张票售出 Thread-0
第1张票售出 Thread-0
ticket:0
ticket:0
ticket:0
III.Lock锁机制
java.util.concurrent.locks.Lock接口
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。
Lock接口中的方法:
void lock()获取锁。
void unlock()释放锁。
java.util.concurrent.locks.ReentrantLock implements Lock接口
使用步骤:
1.在成员位置创建一个ReentrantLock对象(注意不要写到方法里面去了)
2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
例子:
import org.junit.Test;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test1 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread);
Thread t2 = new Thread(myThread);
Thread t3 = new Thread(myThread);
t1.start();
t2.start();
t3.start();
}
}
class MyThread implements Runnable{
static ReentrantLock el = new ReentrantLock();
private static int ticket=10;
Object o = new Object();
@Override
public void run() {
System.out.println("outer"+Thread.currentThread().getName());
print();
System.out.println("ticket:"+ticket);
}
public static void print()
{
el.lock();
while (ticket > 0) {
System.out.print("第" + ticket + "张票售出" + " ");
System.out.println(Thread.currentThread().getName());
ticket--;
}
el.unlock();
}
}
输出:
outerThread-0
第10张票售出 Thread-0
第9张票售出 Thread-0
第8张票售出 Thread-0
第7张票售出 Thread-0
第6张票售出 Thread-0
第5张票售出 Thread-0
第4张票售出 Thread-0
第3张票售出 Thread-0
第2张票售出 Thread-0
第1张票售出 Thread-0
ticket:0
outerThread-2
outerThread-1
ticket:0
ticket:0
这里的锁对象指的是调用lock与unlock方法的对象
四.线程状态
线程状态,看图理解:
这张图还是比较复杂的,核心状态为Runnable,以此为中心来画图理解更好。
下面我们来解释一下各个状态的含义:
这里是简化的含义,但是更通俗易懂且容易理解
1.NEW态:即新建态,在线程对象被创建时但未调用start方法时
2.RUNNABLE态:正在占用CPU使用权同时在执行程序
3.TERMINATED态:死亡态,已经退出的线程出于该状态,再run方法结束后或者调用stop方法后处于该状态
4.TIMED_WAITING态:休眠态,计时等待(不参与CPU占用权抢夺),也是冻结态
5.WAITINT态:无限期等待态(不参与CPU占用权抢夺),也是冻结态
6.BLOCKED态:阻塞态,当多个线程抢夺CPU占用权时未抢到的处于阻塞态或者遇到锁但是锁对象被夺走不在也处于阻塞态
依照上面的含义及图解更好记忆
进入到TimeWaiting(计时等待)有两种方式
1.使用sleep(long m)方法,在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态
2.使用wait(long m)方法,wait方法如果在毫秒值结束之后,还没有被notify唤醒,就会自动醒来,
线程睡醒进入到Runnable/Blocked状态
进入到Waiting永久等待的方式
1.wait()方法:永久睡眠,等待被唤醒
唤醒的方法:
void notify() 唤醒在此对象监视器上等待的单个线程。
void notifyAll() 唤醒在此对象监视器上等待的所有线程。
下面看一个等待唤醒的案例:
客服案例,用一个线程充当客服,多个线程充当客户,客服一次只能接待一位客户来电
public class Test1 {
public static void main(String[] args) {
Object o = new Object();
//构建客户1线程
new Thread(){
@Override
public void run(){
//通过锁来保证客服与客户线程只可以有一个被执行
synchronized (o){
System.out.println("客户1来电");
try {
o.wait();//wait方法结束后,线程进入等待同时释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("客服接电话");
System.out.println("客户1结束询问");
}
}.start();
//构建客户2线程
new Thread(){
@Override
public void run(){
//通过锁来保证客服与客户线程只可以有一个被执行
synchronized (o){
System.out.println("客户2来电");
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("客服接电话");
System.out.println("客户2结束询问");
}
}
}.start();
//构建客服线程
new Thread(){
@Override
public void run() {
{
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o)
{
o.notify();
}
}
}
}
}.start();
}
}
1.notify()保证把锁对象的睡眠线程唤醒,也可以用notifyAll()一次全部唤醒,当然使用notify如果有多个线程需唤醒那就唤醒等待时间最长的那个。
2.wait()后线程进入永久睡眠并释放锁,等待被唤醒。
输出:
客户1来电
客户2来电
客服接电话
客户1结束询问
客服接电话
客户2结束询问
五.等待与唤醒机制
I.线程间的通信
线程间通信概念:
多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同,比如上面的例子有的线程是打电话,有的是接电话。
为什么要处理线程通信:
保证线程通过协调通信而有规律的执行,从而达到多个线程操作一个数据的目的
如何保证线程间通信有效利用资源:
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。
II.等待与唤醒机制
等待唤醒机制是一种线程间的合作机制,前面我们一直在讲线程间抢夺CPU使用权,这是他们的竞争关系,除了竞争,还有合作关系,等待唤醒机制就是他们合作的体现。
等待唤醒机制的内容:一般是一个线程在完成了指定的代码后,就进入睡眠态(使用wait方法);之后等待其他的进程完成指定的代码后唤醒该机制(调用notify方法);如果有多个睡眠线程,可以使用notifyAll方法一次唤醒所有。
wait/notify就是等待唤醒机制的实现方式
等待唤醒中的方法介绍:
等待唤醒机制解决线程通信问题使用到wait,notify,notifyAll三个方法,
- wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的线程从等待池(wait set) 中释放出来,重新进入到调度队列(ready queue,叫队列但不一定就满足先进先出的特点)中
- notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
- notifyAll:则释放所通知对象的 wait set 上的全部线程。
注意事项:
处于睡眠态的线程再被通知唤醒后不会立即开始执行上一次wait()后的代码,因为此时的所对象可能被占用,因此分下面情况
1.锁对象可以获取,并且可以获取cpu使用权,那么就开始继续执行
2…锁对象被占用,那么该线程就从等待池进入锁池(entry set,锁池就是放那些想要获取锁对象但是暂时获取不到的对象),状态变为BLOCKED态
补充三个问题:
- wait方法与notify方法必须要由同一个锁对象调用。
因为:对应的锁对象可以通过notify唤醒使用同一个锁对
象调用的wait方法后的线程。 - wait方法与notify方法是属于Object类的方法的。
因为:锁对象可以是任意对象,而任意对象的所属类都是继
承了Object类的。 - wait方法与notify方法必须要在同步代码块或者是同步函数中使用
因为:必须要通过锁对象调用这2个方
法。
III.等待唤醒机制的案例
经典的案例就是生产者与消费者案例,下面我们写一个吃货与包子的例子
public class Test1 {
public static void main(String[] args) {
Food food = new Food();
Object o = new Object();
new Eatter(food,o).start();
new Thread(new Producer(food,o)).start();
}
}
class Food{
boolean status;//表示包子是不是被做出来了
}
//消费者类
class Eatter extends Thread{
Food food;
Object o;
public Eatter(Food food,Object o){
this.food=food;
this.o = o;
}
@Override
public void run() {
while(true)
synchronized (o){
System.out.println("我要等包子吃");
if(food.status){
System.out.println("包子好了,我吃包子了");
food.status=false;
}
else {
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("包子好了,我吃包子了");
food.status=false;
}
}
}
}
//生产者类
class Producer implements Runnable{
Food food;
Object o;
public Producer(Food food,Object o){
this.food=food;
this.o = o;
}
@Override
public void run() {
while(true){
synchronized (o){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("包子好了快来吃");
food.status=true;
o.notify();
}
}
}
}
该代码会不停输出,下面是一部分:
我要等包子吃
包子好了快来吃
包子好了快来吃
包子好了,我吃包子了
我要等包子吃
包子好了快来吃
包子好了,我吃包子了
我要等包子吃
包子好了快来吃
包子好了快来吃
包子好了,我吃包子了
六.线程池
线程池概念:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
合理利用线程池能够带来三个好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内
存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
说明:自定义实现线程池可以用泛型为Thread的LinkedList对象
线程池的使用:
线程池:JDK1.5之后提供的
java.util.concurrent.Executors:线程池的工厂类,用来生成线程池
Executors类中的静态方法:
static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用固定线程数的线程池
参数:
int nThreads:创建线程池中包含的线程数量
返回值:
ExecutorService接口,返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接收(面向接口编程)
java.util.concurrent.ExecutorService:线程池接口
用来从线程池中获取线程,调用start方法,执行线程任务
submit(Runnable task) 提交一个 Runnable 任务用于执行
关闭/销毁线程池的方法
void shutdown()
线程池的使用步骤:
1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
2.创建一个类,实现Runnable接口,重写run方法,设置线程任务
3.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
4.调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
简单例子:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test1 {
public static void main(String[] args) {
//创建一个可以容纳2个线程的线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
//获取线程,传入任务,开始执行
executorService.submit(new RunnableImpl());
executorService.submit(new RunnableImpl());
executorService.submit(new RunnableImpl());
//关闭线程池,如果不关闭就会一直开着,那么main方法一直不结束
executorService.shutdown();
}
}
class RunnableImpl implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行了任务");
}
}
输出:
pool-1-thread-2执行了任务
pool-1-thread-2执行了任务
pool-1-thread-1执行了任务
最后附上一篇认为比较好的相关文章sleep()和wait()方法与对象锁、锁池、等待池
上面其实那个工厂类那里用到了工厂模式,为23大设计模式之一。
下一篇:Lambda表达式
下一篇:工厂模式