多线程技术概述
线程与进程
进程
-
是指一个内存中运行的应用程序,每个进程都有一块独立的内存空间
线程
- 是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行。一个进程最少有一个线程
- 线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面若干个执行路径又可以划分成若干个线程
线程调度
分时调度
- 所有线程轮流使用CUP,平均分配每个线程占用CPU的时间
抢占式调度
- 优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性)。Java使用的为抢占式调度。
- CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核心而言,某个时刻,只能执行一个线程,而CPU的在多个线程间切换速度相对于我们的感觉要快,看上去就是在同一时刻运行,其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行的效率,让CPU的使用率更高。
同步与异步
同步:排队执行,效率低但安全
异步:同时执行,效率高但数据不安全
并发与并行
并发:指两个或多个事件在同一时间段内发生
并行:指两个或多个事件在同一时间刻内发生(同时发生)
继承Thread类
一个类想要开辟新线程,那么必须继承Thread类,重写run()方法,然后在main主线程实例化该类对象,调用继承Thread而来的start()方法,即可开启一条新的线程。
例
主线程
public class Demo1 {
public static void main(String[] args) {
/**
* 主线程
*/
MyThread m =new MyThread();
m.start();
for(int i =0;i<10;i++){
System.out.println("汗滴禾下土"+i);
}
}
}
分支线程
public class MyThread extends Thread{
/**
* 分支线程
* run方法就是县城要执行的任务方法
*/
@Override
public void run() {
//这里的代码 就是一条新的执行路径
//这个执行路径的触发方式,不是调用run方法,而是通过thread对象的start()来启动任务
for(int i =0;i<10;i++){
System.out.println("锄禾日当午"+i);
}
}
}
现在执行主线程的main方法,看结果
汗滴禾下土0
汗滴禾下土1
汗滴禾下土2
汗滴禾下土3
汗滴禾下土4
汗滴禾下土5
汗滴禾下土6
锄禾日当午0
汗滴禾下土7
锄禾日当午1
锄禾日当午2
锄禾日当午3
锄禾日当午4
汗滴禾下土8
锄禾日当午5
汗滴禾下土9
锄禾日当午6
锄禾日当午7
锄禾日当午8
锄禾日当午9
两个线程并发执行。执行结果比较混乱的原因是JAVA的抢占式调度机制。
实现Runnable接口
一个类想要在新线程完成任务,除了继承Thread类,还可以实现Runable接口,实现抽象run()方法。然后在main主线程中实例化一个新线程,传入这该类,调用新线程的start方法,即可开启新线程。
例:
主线程
public class Demo1 {
public static void main(String[] args) {
/**
* 主线程
*/
//1.创建了一个任务对象
MyRunnable mr = new MyRunnable();
//2.创建一个新线程,将任务给到这个线程
Thread t = new Thread(mr);
//3.开始执行该线程
t.start();
for(int i =0;i<10;i++){
System.out.println("汗滴禾下土"+i);
}
}
}
分支线程
public class MyRunnable implements Runnable{
@Override
public void run() {
//线程的任务
for(int i = 0;i<10;i++){
System.out.println("一二三四五"+i);
}
}
}
执行结果
一二三四五0
一二三四五1
汗滴禾下土0
一二三四五2
汗滴禾下土1
一二三四五3
汗滴禾下土2
一二三四五4
汗滴禾下土3
一二三四五5
汗滴禾下土4
一二三四五6
汗滴禾下土5
一二三四五7
汗滴禾下土6
一二三四五8
汗滴禾下土7
一二三四五9
汗滴禾下土8
汗滴禾下土9
两个线程并发执行。
关于多线程的实现,实现Runnable接口相比于继承Thread方法的优势:
- 通过创建任务,然后给线程分配任务的方式来实现多线程,更适合多个线程同时执行一个任务时使用,只需创建多个线程,传入该任务即可
- 可以避免单继承的局限性
- 任务与线程本身是分离的,提高了程序的健壮性
- 后续学习的线程池技术,接受Runnable类型的任务,而不接受Thread类型的线程
Thread类
此类定义如下
public class Thread extends Object implements Runnable
常用构造方法
No. | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | Thread() | 构造 | 分配新的线程对象 |
2 | Thread(Runnable target) | 构造 | 分配新的线程对象,传入任务 |
3 | Thread(Runnable traget,String name) | 构造 | 分配新的线程对象,传入任务和线程名称 |
4 | Thread(String name) | 构造 | 分配新的线程对象,传入线程名称 |
常用方法
No. | 方法名称 | 类型 | 描述 |
---|---|---|---|
1 | public String getName() | 普通 | 返回此线程的名称 |
2 | public long getId() | 普通 | 返回此线程的标识符 |
3 | public int getPriority() | 普通 | 返回此线程的优先级 |
4 | public void setPriority() | 普通 | 更改此线程的优先级 |
5 | public void run() | 普通 | 将任务编写在该方法内 |
6 | public void setDaemon(boolean on) | 普通 | 将此线程标记为守护线程或用户线程 |
7 | public static void sleep(long millis) | 普通 | 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数 |
8 | public void start() | 普通 | 导致此线程开始执行Java虚拟机,调用此线程的run方法 |
9 | public void setName(String name) | 普通 | 更改此线程的名称 |
10 | public static Thread currentThread() | 普通 | 返回当前正在执行的线程对象的引用 |
11 | public void inerrupt() | 普通 | 中断此线程(打中断标记) |
线程休眠sleep
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
for(int i =0;i<10;i++){
System.out.println(i);
Thread.sleep(1000); //调用sleep方法,传入1000毫秒,也就是一秒,作用是每一秒循环一次
}
}
}
线程阻塞
所有比较消耗时间的操作,比如常见的文件读取,它会导致线程短时间停止,等待文件读取完毕,才会接着往下走,接收用户输入也属于线程阻塞。
线程中断
一个线程是一个独立的执行路径,他是否应该结束,有其自身决定,在之前的Java版本,是使用stop方法强制关闭线程,这会导致某些情况下系统资源没有来得及释放,现版本已经不建议使用stop,而是给线程做中断标记,当线程发现标记时,执行程序员自己编写的死亡流程,由我们来帮它释放资源,让它安心上路。
例:
主线程
public class Demo2 {
//主线程
public static void main(String[] args) throws InterruptedException {
Thread t1 =new Thread(new MyRunnable()); //实例化一个t1线程,传入任务
t1.start(); //开始执行t1
for(int i =0;i<5;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
Thread.sleep(1000); //每隔一秒输出一次
}
t1.interrupt();//当主线程循环结束时,调用interrupt方法给t1线程打中断标记,告诉它该死了
}
}
分支线程
public class MyRunnable implements Runnable{
@Override
public void run() {
for(int i =0;i<10;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//当线程被打上标记后,会立即产生一个异常,由该try/catch块捕获,进入catch块进行处理
//我们的线程死亡操作,就在这个catch块中进行。
System.out.println("线程死亡");
return; //return即立刻结束该方法
}
}
}
}
运行结果
Thread-0:0
main:0
main:1
Thread-0:1
Thread-0:2
main:2
main:3
Thread-0:3
Thread-0:4
main:4
Thread-0:5
线程死亡
守护线程
线程分为守护线程和用户线程
用户线程:当一个进程不包含任何一个存活的用户线程结束时,进程结束。
守护线程:守护用户线程的,当最后一个用户线程结束时,所有守护线程自动死亡。
当t1线程是用户线程时
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
Thread t1 =new Thread(new MyRunnable()); //实例化一个t1线程,传入任务
t1.start(); //开始执行t1
for(int i =0;i<3;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
Thread.sleep(1000); //每隔一秒输出一次
}
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for(int i =0;i<5;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
输出
Thread-0:0
main:0
main:1
Thread-0:1
main:2
Thread-0:2
Thread-0:3
Thread-0:4
当t1线程是用户线程时,main主线程执行完毕后,t1线程仍在执行
当t1线程守护线程时
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
Thread t1 =new Thread(new MyRunnable()); //实例化一个t1线程,传入任务
t1.setDaemon(true); //设置为守护线程
t1.start(); //开始执行t1
for(int i =0;i<3;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
Thread.sleep(1000); //每隔一秒输出一次
}
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for(int i =0;i<5;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
输出
Thread-0:0
main:0
main:1
Thread-0:1
Thread-0:2
main:2
Thread-0:3
当main主线程执行完毕死亡后,t1线程也相继死亡,不再继续执行循环
线程安全问题
线程不安全
实例:卖票操作
public class Demo3 { public static void main(String[] args) { //创建卖票任务 Runnable r = new Ticket(); //将任务交给两个线程同时去卖 new Thread(r).start(); new Thread(r).start(); } //卖票任务 static class Ticket implements Runnable{ private int count = 5;//总票数五张
@Override public void run() { //买票操作 //票数大于零时,允许卖票 while(count > 0){ System.out.println("开始卖票"); //休眠一秒钟,模拟买票过程 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println("卖票成功,剩余"+count); } } }
}
结果
开始卖票
开始卖票
卖票成功,剩余4
开始卖票
卖票成功,剩余4
开始卖票
卖票成功,剩余3
开始卖票
卖票成功,剩余2
开始卖票
卖票成功,剩余1
开始卖票
卖票成功,剩余0
卖票成功,剩余-1
进程已结束,退出代码0
我们发现出现了及其不合理的情况,是由于两个线程都在操控同一个变量,而导致的数据不安全。
- 两个线程同时卖,两个卖完剩余票数还是4
- 出现了剩余票数-1
线程不安全的解决方案(线程安全)
只需要在一个线程处理任务时,其他线程待命。我们采用加锁的方式处理。总共有三种加锁方式。
方式一:同步代码块(代码块加锁)
/**
* 线程同步(synchronized)
*/
public class Demo3 {
public static void main(String[] args) {
//方案一:同步代码快
//格式:synchronized (锁对象) {上锁的代码块}
//创建卖票任务
Runnable r = new Ticket();
//将任务交给两个线程同时去卖
new Thread(r).start();
new Thread(r).start();
}
static class Ticket implements Runnable{
private int count = 5;//总票数五张
private Object o = new Object(); //定义一个锁对象o
@Override
public void run() { //买票操作
//票数大于零时,允许卖票
while (true) {
synchronized (o) { //用锁对象将该代码块包裹,即上锁。
if(count>0){
System.out.println("开始卖票");
//休眠一秒钟,模拟买票过程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName() + "卖票成功,剩余" + count);
} else{
System.out.println(Thread.currentThread().getName() + "票卖完了!");
break;
}
}
}
}
}
}
运行结果
开始卖票
Thread-0卖票成功,剩余4
开始卖票
Thread-0卖票成功,剩余3
开始卖票
Thread-0卖票成功,剩余2
开始卖票
Thread-1卖票成功,剩余1
开始卖票
Thread-1卖票成功,剩余0
Thread-1票卖完了!
Thread-0票卖完了!
程序在if语句外上了一把锁,每个线程运行到while(true)里后,开始等待。依次排队,这样可以保证数据安全,每次只有一个线程在操控数据。但是也导致了运行效率低的问题
注:每个线程应该都看同一把锁。而不是各看各的锁。
方式二:同步方法(方法加锁)
public class Demo4 {
public static void main(String[] args) {
//创建卖票任务
Runnable r = new Ticket();
//将任务交给两个线程同时去卖
new Thread(r).start();
new Thread(r).start();
}
static class Ticket implements Runnable{
private int count = 5;//总票数五张
@Override
public void run() { //买票操作
//票数大于零时,允许卖票
while(true){
if(!sale()) break;
}
}
//将卖票操封装成一个方法 卖票成功返回true,失败返回false,在权限修饰符后加上synchronized关键字,即将该方法上锁,每个线程调用该方法前都必须排队。
public synchronized boolean sale(){
if(count > 0){
System.out.println("开始卖票");
//休眠一秒钟,模拟买票过程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票成功,剩余"+count);
return true;
}
return false;
}
}
}
运行结果
开始卖票
Thread-1卖票成功,剩余4
开始卖票
Thread-1卖票成功,剩余3
开始卖票
Thread-1卖票成功,剩余2
开始卖票
Thread-0卖票成功,剩余1
开始卖票
Thread-0卖票成功,剩余0
之前同步代码块,我们看的是锁对象,那么同步方法,当上锁的方法是非静态时,锁对象就是调用该方法的对象,当上锁的方法是静态时,锁对象是(对象名.class)
当有两个上锁方法在run方法中被调用时,来看看线程是怎么样运行的
public class Demo { public static void main(String[] args) { Runnable r = new Preson(); new Thread(r).start(); new Thread(r).start(); } static class Preson implements Runnable{ public synchronized void eat(){ System.out.println(Thread.currentThread().getName()+"在吃饭"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } public synchronized void sleep(){ System.out.println(Thread.currentThread().getName()+"在睡觉"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }
@Override public void run() { eat(); sleep(); } }
}
运行结果
Thread-0在吃饭
Thread-1在吃饭
Thread-1在睡觉
Thread-0在睡觉
我们可以发现,在一个线程执行上锁方法并将它上锁后,其他的线程也无法执行任何上锁方法。因为都是同一个任务对象里的锁方法,它的锁对象,就是这个任务方法,仅有一个。看同一把锁
方法三:显式锁Lock
知识点:Lock类及其子类ReentrantLock
之前的两种方式属于隐式锁,所谓的显示和隐式就是在使用的时候,使用者要不要手动写代码去获取锁和释放锁。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo5 {
public static void main(String[] args) {
//创建卖票任务
Runnable r = new Ticket();
//将任务交给两个线程同时去卖
new Thread®.start();
new Thread®.start();
}
static class Ticket implements Runnable{
private int count = 5;//总票数五张
private Lock l = new ReentrantLock();//创建锁对象
@Override
public void run() { //买票操作
//票数大于零时,允许卖票
while(true) {
l.lock(); //上锁
if (count > 0) {
System.out.println("开始卖票");
//休眠一秒钟,模拟买票过程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("卖票成功,剩余" + count);
l.unlock();//解锁
}else{
l.unlock();//解锁
break;
}
}
}
}
}
显式锁与隐式锁的区别(转自博主:凯哥Java)
解决的方法 | 格式 | 描述 |
---|---|---|
同步代码块(关键字) | synchronized(锁对象){} | 隐式锁,多个线程的锁对象必须唯一 |
同步方法(修饰符) | synchronized 返回类型 方法名(){} | 隐式锁,谁调用该方法谁就是锁对象 |
显示锁 | ReentrantLock类的lock()/unlock()方法 | 显式锁,有程序员决定在那开启/关闭锁 |
一、构成不同
Sync 和 Lock 的出身(原始的构成)不同:
-
Sync:Java中的关键字,是由JVM来维护的。是JVM层面的锁。
-
Lock:是JDK5以后才出现的具体的类。使用 Lock 是调用对应的API。是API层面的锁。
-
Sync 底层是通过 monitorenter 进行加锁(底层是通过 monitor 对象来完成的,其中的wait/notify等方法也是依赖于 monitor 对象的。只有在同步代码块或者同步方法中才可以调用wait/notify等方法。因为只有在同步代码块或者是同步方法中,JVM才会调用 monitory 对象);通过 monitorexit 来退出锁。
-
而 Lock 是通过调用对应的API方法来获取锁和释放锁。
二、使用方法不同
Sync是隐式锁;Lock是显示锁。
所谓的显示和隐式就是在使用的时候,使用者要不要手动写代码去获取锁和释放锁。
- 在使用sync关键字的时候,程序能够自动获取锁和释放锁。那是因为当sync代码块执行完成之后,系统会自动的让程序释放占用的锁。Sync是由系统维护的,如果非逻辑问题的话,是不会出现死锁的。
- 在使用lock的时候,我们使用者需要手动的获取和释放锁。如果没有释放锁,就有可能导致出现死锁的现象。手动获取锁方法:
lock()
;释放锁:unlock()
。
三、等待是否可中断
Sync是不可中断的。除非抛出异常或者正常运行完成。
Lock是可以中断的。中断方式:
- 调用设置超时方法
tryLock(long timeout ,timeUnit unit)
- 调用
lockInterruptibly()
放到代码块中,然后调用interrupt()方法可以中断
四、加锁的时候是否公平
Sync:非公平锁。
Lock:两者都可以。默认是非公平锁,在其构造方法的时候可以传入Boolean值(true:公平锁;false:非公平锁)
五、锁绑定多个条件来condition
Sync:没有。要么随机唤醒一个线程;要么是唤醒所有等待的线程。
Lock:用来实现分组唤醒需要唤醒的线程,可以精确的唤醒,而不是像sync那样,不能精确唤醒线程。
公平锁与非公平锁
公平锁:(俗话)先到先得,且所有线程规整的排队,出来的线程自动排到末尾。整齐且规律。
非公平锁:(大俗话)一堆线程在锁前蜂拥,锁开后插队争抢。可能出现一个线程一直在做事其他线程抢不过它的情况。
之前学习的三种方式:同步代码块,同步方法,Lock都是非公平锁,但是Lock的构造方法可以传入boolean值将其修改为公平锁。
Lock公平锁模式:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo5 {
public static void main(String[] args) {
//创建卖票任务
Runnable r = new Ticket();
//将任务交给两个线程同时去卖
new Thread®.start();
new Thread®.start();
}
static class Ticket implements Runnable{
private int count = 5;//总票数五张
private Lock l = new ReentrantLock(true);//创建锁对象,传入true将其变为公平锁
@Override
public void run() { //买票操作
//票数大于零时,允许卖票
while(true) {
l.lock(); //上锁
if (count > 0) {
System.out.println("开始卖票");
//休眠一秒钟,模拟买票过程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票成功,剩余" + count);
l.unlock();//解锁
}else{
l.unlock();//解锁
break;
}
}
}
}
}
运行结果
开始卖票
Thread-0卖票成功,剩余4
开始卖票
Thread-1卖票成功,剩余3
开始卖票
Thread-0卖票成功,剩余2
开始卖票
Thread-1卖票成功,剩余1
开始卖票
Thread-0卖票成功,剩余0
线程死锁
public class Demo6 { /** * 线程死锁实例 * 绑架案 * @param args */ public static void main(String[] args) { Culprit c = new Culprit(); //创建罪犯对象 Police p = new Police(); //创建警察对象 Thread t = new MyThread(c,p); // 创建一个线程t ,传入p,c两个对象 t.start(); //线程开启 c.say(p);
} //罪犯 static class Culprit{ public synchronized void say(Police p){ System.out.println("罪犯:你放了我,我放人质"); p.fun(); } public synchronized void fun(){ System.out.println("罪犯被放了,最终罪犯也放了人质"); } } //警察 static class Police{ public synchronized void say(Culprit c){ System.out.println("警察:你放了人质,我放了你"); c.fun(); } public synchronized void fun(){ System.out.println("警察救了人质,但是罪犯跑了"); } } //线程类 static class MyThread extends Thread{ private Culprit c; private Police p; public MyThread(Culprit c, Police p) { this.c = c; this.p = p; } @Override public void run() { p.say(c); } }
}
运行结果:死锁发生
双方都卡在了各自的say方法中,等待对象解锁。未解锁前无法调用对方的fun方法,此时的情况如图所示
注:为了避免死锁的发生,我们在编程时要尽量避免在同步方法中调用另一个同步方法让另一个锁产生。
多线程通信问题(生产者与消费者)
多线程通信最典型的就是生产者与消费者问题:生产者产生数据,然后生产者休眠,消费者获取数据,使用数据,唤醒生产者,然后消费者休眠,如此循环,可以保证数据的安全性。
我们来看一下生产者与消费者不采用休眠和唤醒机制的时候会产生的问题:
例:生产者消费者数据错乱问题
//生产者消费者 public class Demo7 { public static void main(String[] args) { Food f = new Food(); //创建一个食物对象,也可以理解为装食物的盘子 Cook c = new Cook(f); //创建厨师对象,传入f Waiter w = new Waiter(f); //创建服务员对象,传入f c.start(); w.start(); } //厨师 static class Cook extends Thread{ private Food f;
public Cook(Food f){ this.f = f; } @Override public void run() { //做菜 for(int i = 0;i < 100;i++){ if(i%2 == 0){ f.setNameAndTaste("宫保鸡丁","香辣味"); }else{ f.setNameAndTaste("水果沙拉","酸甜味"); } } } } //服务生 static class Waiter extends Thread{ private Food f; public Waiter(Food f){ this.f = f; } @Override //端菜 public void run() { for(int i = 0;i < 100;i++){ //配合做菜过程需要耗时 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } f.getFood(); } } } //食物 static class Food{ private String name; private String taste; //为菜品赋予名称和味道的方法,厨师做饭需要调用该方法 public void setNameAndTaste(String name,String taste){ this.name = name; //模拟做菜过程需要耗时 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } this.taste = taste; } //服务员取菜需要调用该方法 public void getFood(){ System.out.println("服务员端来的菜是:"+name+"味道是:"+taste); } }
}
运行结果
结果是部分菜名和味道不符 ,甚至初选了连续两次端走同一盘菜的情况,这是极其不符合实际的,也就是数据错乱的问题,原因是厨师刚赋值菜名,还没有赋值味道时,服务员就将菜端走了。
那么我们现在来加上休眠与唤醒机制,再来看看效果
public class Demo7 { public static void main(String[] args) { Food f = new Food(); Cook c = new Cook(f); Waiter w = new Waiter(f); c.start(); w.start();
} //厨师 static class Cook extends Thread{ private Food f; public Cook(Food f){ this.f = f; } @Override public void run() { //做菜 for (int i = 0; i < 100; i++) { if (i % 2 == 0) { f.setNameAndTaste("宫保鸡丁", "香辣味"); } else { f.setNameAndTaste("水果沙拉", "酸甜味"); } } } } //服务生 static class Waiter extends Thread{ private Food f; public Waiter(Food f){ this.f = f; } @Override //端菜 public void run() { for (int i = 0; i < 100; i++) { //配合做菜过程需要耗时 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } f.getFood(); } } } //食物 static class Food{ private String name; private String taste; private boolean flag = true; //给一个指示器,true时允许做菜,false时允许端菜 //为菜品赋予名称和味道的方法,厨师做饭需要调用该方法 public synchronized void setNameAndTaste(String name,String taste) { if (flag) { this.name = name; //模拟做菜过程需要耗时 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } this.taste = taste; flag = false; this.notifyAll(); //唤醒所有休眠的线程 try { this.wait(); //自身休眠 } catch (InterruptedException e) { e.printStackTrace(); } } } //服务员取菜需要调用该方法 public synchronized void getFood() { if (!flag) { System.out.println("服务员端来的菜是:" + name + "味道是:" + taste); flag = true; this.notifyAll(); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } }
}
运行结果
生产者消费者交替进行,没有发生数据错乱的问题。
在添加休眠唤醒机制时,不要忘了给线程上锁,如果没有上锁,那么会发生线程还未休眠就被要求唤醒的异常从而导致程序崩溃。
线程的六种状态
带返回值的线程Callable
是Java中创建线程的第三种方式,但并不常用,作为了解即可
此接口定义如下
public interface Callable<V> {
V call() throws Exception;
}
Callable使用步骤
1. 编写类实现Callable接口 , 实现call方法
class XXX implements Callable<T> {
@Override
public <T> call() throws Exception {
return T;
}
}
2. 创建FutureTask对象 , 并传入第一步编写的Callable类对象
FutureTask<Integer> future = new FutureTask<>(callable);
3. 通过Thread,启动线程
new Thread(future).start();
Runnable 与 Callable的相同点
-
都是接口
-
都可以编写多线程程序
-
都采用Thread.start()启动线程
Runnable 与 Callable的不同点
- Runnable没有返回值;Callable可以返回执行结果
- Callable接口的call()允许抛出异常;Runnable的run()不能抛出
Callable获取返回值
Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
线程池
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程 就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容 器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。Java有四种线程池。
线程池的好处
- 降低资源消耗。
- 提高响应速度。
- 提高线程的可管理性。
缓存线程池
import java.util.concurrent.*;
public class Demo8 {
/**
* 缓存线程池
* 长度无限制
* 人物加入后的执行流程:
* 1.判断线程池是否存在空闲线程
* 2.存在则使用
* 3.不存在,则创建线程,并放入线程池,然后使用
*/
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool(); //获取缓存线程池
//向池中加入新任务并执行
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
主线程休眠一秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//加入并执行第四个任务
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
}
}
结果
前三个线程是缓存时创建,第四个任务执行时,池中三个线程全是空闲,选取一个执行该任务
定长线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
- 定长线程池
- 长度是指定的数量
- 任务加入后执行流程
- 1.判断线程池是否存在空闲线程
- 2.存在则使用
- 3.不存在空闲线程,且线程池未满的情况下,则创建线程加入线程池,然后使用
- 4.不存在空闲线程,且线程池已满情况下,则任务排队等待空闲线程。
*/
public class Demo9 {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(2);//创建长度为2的定长线程池
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
}
}
结果
单线程线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo10 {
/**
* 单线程线程池
* 池中只有一个线程
* 执行流程
* 1.判断线程池的那个线程是否空闲
* 2.空闲则使用
* 3.不空闲则等待
*/
public static void main(String[] args) {
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
}
}
结果
无论几个任务,都是一个线程在完成
周期定长线程池
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Demo11 {
/**
* 周期定长线程池
* 执行流程
* 1.判断线程池是否存在空闲线程
* 2.存在则使用
* 3.不存在空闲线程,且线程池未满的情况下,则创建线程加入线程池,然后使用
* 4.不存在空闲线程,且线程池已满情况下,则任务排队等待空闲线程。
*
* 周期性任务执行时:
* 定时执行,当某个时机触发时,自动执行某任务。
* @param args
/
public static void main(String[] args) {
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
/*
* 1.定时执行一次
* 参数1.任务
* 参数2.时间数字
* 参数3.时间单位,由TimeUnit的常量指定
/
service.schedule(new Runnable() {
@Override
public void run() {
System.out.println("锄禾日当午");
}
},3, TimeUnit.SECONDS);
/*
* 2.周期执行
* 参数1.任务
* 参数2.延迟时长数字(第一次执行在什么时间之后)
* 参数3.周期时长数字(间隔多久)
* 参数4.单位
*/
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("汗滴禾下土");
}
},3,1,TimeUnit.SECONDS);
}
}
Lambda表达式
Lambda表达式可以简化接口的实现,举个极端的栗子:按照以往的语法,可能我们需要定义接口,实现接口,只为利用它的抽象方法完成一个打印。这代码就比较冗余,那么我们就可以使用Lambda表达式对接口的定义与实现做一个简化,最终只保留传递的参数和抽象方法那一部分。注:只有在接口只存在一个抽象方法时,才能使用Lambda表达式进行简化。
例:传统Java:利用匿名内部类实例化接口,重写方法,调用方法,获得输出
public class Demo12 { public static void main(String[] args) { print(new MyMath() { @Override public int sum(int x, int y) { return x+y; } },100,200);
} public static void print(MyMath m,int x,int y){ int num = m.sum(x,y); System.out.println(num); } static interface MyMath{ int sum(int x,int y); }
}
输出
300
Lambda表达式
public class Demo12 {
public static void main(String[] args) {
print((int x, int y) ->{ return x+y; },100,200) ; //Lambda表达式
}
public static void print(MyMath m,int x,int y){
int num = m.sum(x,y);
System.out.println(num);
}
static interface MyMath{
int sum(int x,int y);
}
}
输出
300
总的来说,Lambda表达式本身就是一个接口的实现。
转载自https://blog.csdn.net/LJW123487/article/details/114272716