目录
什么是多线程?
进程:进程是程序的基本执行实体
线程:线程是操作系统中能够进行运算调度的最小单位,它被包含在进程
中,是进程的实际运作单位。
例如:360软件在运行后,里面有许多功能(木马查杀,电脑清理,系统修复等)
那么360这个软件本身就是一个进程,而木马查杀,电脑清理,系统修复等一系列功能就是线程
线程的简单理解:应用软件中互相独立,又可以同时运行的的功能
这种互相独立的功能比较多,就形成了多线程
以前的代码也叫做单线程程序,因为是从头往下,依次进行的
多线程程序的特点:能同时做多件事情
- 多线程的应用场景
小结:
并发和并行
-
并发:
在同一时刻,有多个指令在单个cpu上交替
执行 -
并行:
在同一时刻,有多个指令在多个cpu上同时
执行
cpu是由多线程的,例如:i5 12400f 是6核12线程,这里的线程数就表示可以同时运行多少条线程 -
小结:
多线程的实现方式
在Java中,多线程有三种实现方式
1.继承Thread类的方式进行实现
2.实现Runnable接口的方式进行实现
3.利用Callable接口和Future接口的方式实现
可以在Api文档中找到Thread类
Thread在api文档中:
线程 是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程。
即:Thread就表示Java中的一个线程,如果想使用线程的话,就创建Thread的对象
创建新执行线程有两种方法。一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。接下来可以分配并启动该子类的实例。
创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后可以分配该类的实例,在创建 Thread 时作为一个参数来传递并启动。
多线程的第一种实现方式
一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。接下来可以分配并启动该子类的实例。
* 多线程的第一种启动方式:
* 1.自己定义一个类继承Thread类
* 2.重写Thread的run方法
* 3.创建子类的对象,并启动线程
//MyThread类
public class MyThread extends Thread{
@Override
public void run() {
//在run方法中,书写线程要执行的代码
//即,run方法里的代码就是新创建的线程要执行的代码
//例如,我想让这个线程输出10次yangyuying
for (int i = 0; i < 10; i++) {
System.out.println(getName()+"yangyuying");
}
}
}
//测试类
public static void main(String[] args) {
/*
* 多线程的第一种启动方式:
* 1.自己定义一个类继承Thread类
* 2.重写Thread的run方法
* 3.创建子类的对象,并启动线程
* */
MyThread s1=new MyThread();
MyThread s2=new MyThread();
//启动线程:用Thread的子类对象调用start方法
s1.setName("线程1");
s2.setName("线程2");
//start方法就表示开启线程
s1.start();
s2.start();
//线程1yangyuying
//线程1yangyuying
//线程1yangyuying
//线程1yangyuying
//线程1yangyuying
//线程1yangyuying
//线程2yangyuying
//线程2yangyuying
//线程1yangyuying
//线程1yangyuying
//线程2yangyuying
//线程1yangyuying
//线程2yangyuying
//线程2yangyuying
//线程2yangyuying
//线程1yangyuying
//线程2yangyuying
//线程2yangyuying
//线程2yangyuying
//线程2yangyuying
}
多线程的第二种实现方式
创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后可以分配该类的实例,在创建 Thread 时作为一个参数来传递并启动。
* 多线程的第二种启动方式:
* 1.自己定义一个类实现Runnable接口
* 2.在自定义类中重写Runnable的run方法
* 3.创建自己的类的对象
* 4.创建一个Thread类的对象,并开启线程
//MyRun,自定义类,继承Runnable接口
package duoXianCheng;
public class MyRun implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
//Thread类的currentThread方法可以获取到当前执行run代码的线程的对象
//即获取到线程对象
Thread t = Thread.currentThread();
//这里输出线程名字就可以使用刚才获取的线程对象来输出
System.out.println(t.getName()+"switch i like");
}
}
}
//测试类
package duoXianCheng;
public class testTwo {
public static void main(String[] args) {
/*
* 多线程的第二种启动方式:
* 1.自己定义一个类实现Runnable接口
* 2.在自定义类中重写Runnable的run方法
* 3.创建自己的类的对象
* 4.创建一个Thread类的对象,并开启线程
* */
//创建MyRun(自定义对象)的对象
//这个对象表示多线程要执行的任务,也可以把这个对象叫做任务对象
MyRun mr=new MyRun();
//创建线程(Thread)对象
Thread t1=new Thread(mr);
Thread t2=new Thread(mr);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
//线程1switch i like
//线程1switch i like
//线程1switch i like
//线程2switch i like
//线程2switch i like
//线程1switch i like
//线程1switch i like
//线程1switch i like
//线程1switch i like
//线程2switch i like
//线程1switch i like
//线程2switch i like
//线程1switch i like
//线程1switch i like
//线程2switch i like
//线程2switch i like
//线程2switch i like
//线程2switch i like
//线程2switch i like
//线程2switch i like
}
}
多线程的第三种实现方式
利用Callable接口和Future接口的方式实现
这种方式可以获取到多线程运行的结果
* 多线程的第三种实现方式
* 特点:可以获取到多线程运行的结果
*
* 1.创建一个自定义类(MyCallable)实现Callable接口
* 2.重写call方法,(call方法有返回值,返回值就是多线程运行的结果)
*
* 3.创建MyCallable的对象,(表示多线程要执行的任务)
* 4.创建FutureTask的对象(管理多线程运行的结果)
* (Future是一个接口,不能直接创建对象)
* 5.创建Thread类的对象,开启线程
//MyCallable类(自定义类)
package duoXianCheng;
public class MyRun implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
//Thread类的currentThread方法可以获取到当前执行run代码的线程的对象
//即获取到线程对象
Thread t = Thread.currentThread();
//这里输出线程名字就可以使用刚才获取的线程对象来输出
System.out.println(t.getName()+"switch i like");
}
}
}
//测试类
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*
* 多线程的第三种实现方式
* 特点:可以获取到多线程运行的结果
*
* 1.创建一个自定义类(MyCallable)实现Callable接口
* 2.重写call方法,(call方法有返回值,返回值就是多线程运行的结果)
*
* 3.创建MyCallable的对象,(表示多线程要执行的任务)
* 4.创建FutureTask的对象(管理多线程运行的结果)(Future是一个接口,不能直接创建对象)
* 5.创建Thread类的对象,开启线程
* */
//创建MyCallable的对象(表示多线程要执行的任务)
MyCallable mc=new MyCallable();
//创建FutureTask的对象(管理多线程运行的结果)
FutureTask<Integer>ft=new FutureTask<>(mc);
//创建线程的对象
Thread t1=new Thread(ft);
t1.start();
// 线程运行后,可以用FutureTask的对象管理运行的结果
//get方法就是获取多线程的运行结果
Integer re = ft.get();
System.out.println(re);
//5050
}
小结
多线程中常见的成员方法
上面的 currentThread()
和sleep
方法都是静态方法,可以直接使用类名调用
上述表格中第5和第6个方法中,可以改变线程的优先级,在java中,线程的优先级最小是1
,最大是10
,默认是5
常用简单方法
//MyThread类
package duoXianCheng;
public class MyThread extends Thread{
public MyThread() {
}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
//在run方法中,书写线程要执行的代码
//即,run方法里的代码就是新创建的线程要执行的代码
//例如,我想让这个线程输出10次yangyuying
for (int i = 0; i < 10; i++) {
System.out.println(getName()+"yangyuying");
}
}
}
//测试类
package duoXianCheng;
public class testOne {
public static void main(String[] args) throws InterruptedException {
//String getName,void setName(String name)方法
//设置和获取线程名字
//注意:
// 1.如果没有给线程设置名字直接获取,线程也是有默认的名字的
// 默认的名字为:Thread-X(这里的X是一个序号,从0开始,表示线程)
//
// 2.此外,Thread的构造方法也可以给线程设置名字,具体可以查看api帮助文档
// 在子类用super去继承父类的构造方法
MyThread s1=new MyThread("ouxiao");
MyThread s2=new MyThread("liangyan");
s1.start();
s2.start();
//currentThread方法,获取当前线程的对象
//注意:
//当java虚拟机启动后,会自动的开启多条线程
//其中有一条就叫做main线程
//main线程的作用就是调用main方法,执行里面的代码
//所以,当不设置线程时调用currentThread方法就会获得main对象
// Thread thread = Thread.currentThread();
// System.out.println(thread.getName());//main
//static void sleep(long time) 让线程休眠指定的时间,单位为毫秒
//注意:
// 1.哪条线程执行到这个方法,那么哪条线程就会在这里停留对应的时间
// 2.方法的参数就表示睡眠的时间,单位毫秒
// 3.当时间到了后,线程就会醒来,继续执行代码
System.out.println("1111111");
Thread.sleep(1000);
//这里上面的代码执行完后,main线程停止了1秒才执行的下面的代码
System.out.println("2222222");
}
}
线程的优先级
线程的调度:
在计算机中有两种调度方式:
1.抢占式调度,很随机
,每条线程调度的时间都是随机的
2.非抢占式调度,不随机
,每条线程轮流调度,
在Java中,对线程的调度就采用抢占式调度,
- 改变线程优先级的方法
这里的优先级就相当于一种权重,优先级越高,那么这条线程抢到cpu的概率就是最大的,在java中,线程的优先级最小是1
,最大是10
,默认是5
。
public static void main(String[] args) {
//创建MyRun(自定义对象)的对象
//这个对象表示多线程要执行的任务,也可以把这个对象叫做任务对象
MyRun mr=new MyRun();
//创建线程(Thread)对象
Thread t1=new Thread(mr);
Thread t2=new Thread(mr);
t1.setName("线程1");
t2.setName("线程2");
//getPriority,获取线程的优先级
int o1 = t1.getPriority();
int o2 = t2.getPriority();
System.out.println(o1);//5
System.out.println(o2);//5
//main的线程优先级也是5
System.out.println(Thread.currentThread().getPriority());//5
//setPriority,设置优先级
t1.setPriority(1);
t2.setPriority(10);//这里的优先级就是一种权重,权重越高,线程被选中的概率越高,但不是绝对的
// t1.start();
// t2.start();
}
守护线程(备胎线程)
//MyThread1
package JUCTest;
public class MyThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+"---"+i);
}
}
}
//MyThread2
package JUCTest;
public class MyThread2 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+"---"+i);
}
}
}
//测试类
public static void main(String[] args) {
/*
* final void setDaemon(boolean on)设置守护线程
* 注意:
* 当非守护线程执行完成后,守护线程就会陆续结束
* 即:当天选线程执行完毕后,备胎线程也就没有存在的必要了
*
* */
MyThread1 m1=new MyThread1();
MyThread2 m2=new MyThread2();
m1.setName("天选");
m2.setName("备胎");
//把第二个线程设置为守护线程(备胎线程)
m2.setDaemon(true);
m1.start();
m2.start();
}
礼让线程和插入线程(了解)
- yield,礼让线程
//MyThread
package duoXianCheng;
public class MyThread extends Thread{
@Override
public void run() {
//在run方法中,书写线程要执行的代码
//即,run方法里的代码就是新创建的线程要执行的代码
//例如,我想让这个线程输出10次yangyuying
for (int i = 0; i < 10; i++) {
System.out.println(getName()+"--"+i);
//yield,出让当前cpu的执行权
Thread.yield();
}
}
}
//测试类
public static void main(String[] args) {
//yield方法会尽可能的让结果输出的均匀一点
Thread t1=new MyThread();
Thread t2=new MyThread();
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
- join,插入线程
//MyThread
package duoXianCheng;
public class MyThread extends Thread{
@Override
public void run() {
//在run方法中,书写线程要执行的代码
//即,run方法里的代码就是新创建的线程要执行的代码
//例如,我想让这个线程输出10次yangyuying
for (int i = 0; i < 10; i++) {
System.out.println(getName()+"--"+i);
//yield,出让当前cpu的执行权
Thread.yield();
}
}
}
//测试类
public static void main(String[] args) throws InterruptedException {
//yield方法会尽可能的让结果输出的均匀一点
Thread t1=new MyThread();
t1.setName("线程1");
t1.start();
//使用join线程可以把t1这个线程插入到当前线程之前执行,
// 等线程1的所有代码都执行完了,main的代码才执行
//t1:线程1
//当前线程:main
t1.join();
for (int i = 0; i < 10; i++) {
System.out.println("main"+i);
}
}
线程的生命周期
下面一张图就可以解释线程的生命周期,蓝色的词就是生命周期的各个状态,红色的部分就是对状态的解释
在启动线程后,并不会直接执行代码,而是抢夺cpu的执行权,抢到后才会执行代码,如果在执行的过程中遇到一些阻塞方法,就会停止一段时间,之后再重新回到抢夺cpu使用权的状态,线程执行完毕后,就是线程死亡
线程安全的问题
//MyThread3 类
package JUCTest;
public class MyThread3 extends Thread{
static int ticket=0;
@Override
public void run() {
while (true){
if(ticket<100){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(getName()+"再卖第"+ticket+"张票");
}else {
break;
}
}
}
}
//test类
package JUCTest;
public class testTwo {
public static void main(String[] args) {
MyThread3 m1=new MyThread3();
MyThread3 m2=new MyThread3();
MyThread3 m3=new MyThread3();
m1.setName("窗口1");
m2.setName("窗口2");
m3.setName("窗口3");
m1.start();
m2.start();
m3.start();
}
}
上述代码是有问题的。
线程在执行代码的时候,cpu的执行权随时有可能被其他的线程给抢走
上述问题出现的原因就是:线程执行的时候,是有随机性的
解决上述问题的方法:同步代码块
同步代码块
可以把操作共享数据的代码给锁起来,
当一个线程在执行操作共享数据的代码的时候,其他线程不能执行这段代码
- 同步代码块的格式:
synchronized(锁对象){
操作共享数据的代码;
}
- 同步代码块的特点
1.锁默认是打开的,有一个线程进去了,锁自动关闭
2.当锁里面的代码执行完毕后,线程出来,锁自动打开
锁对象可以是任意对象,但是,锁对象必须是唯一的
,可以在锁对象前加上static关键字.一般就是用锁所在的类的字节码文件
例如,student
的字节码文件就是student.class
下面代码的锁对象:MyThread3.class
- 上述问题的解决(线程安全问题的解决)
//MyThread3
package JUCTest;
public class MyThread3 extends Thread{
static int ticket=0;//这里的static很重要,他确保使用的是共享数据
static Object o=new Object();
@Override
public void run() {
while (true){
//注意,这里的锁不能写在循环外面,如果写在外面,
//那么就会导致一个线程把循环执行完毕后,其他线程才能进来,
synchronized (MyThread3.class){
if(ticket<100){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(getName()+"再卖第"+ticket+"张票");
}else {
break;
}
}
}
}
}
//test类
package JUCTest;
public class testTwo {
public static void main(String[] args) {
MyThread3 m1=new MyThread3();
MyThread3 m2=new MyThread3();
MyThread3 m3=new MyThread3();
m1.setName("窗口1");
m2.setName("窗口2");
m3.setName("窗口3");
m1.start();
m2.start();
m3.start();
}
}
同步方法
就是把synchronized关键字加到方法上
-
同步方法格式:
修饰符 synchronized 返回值类型 方法名(方法参数){...}
-
特点:
1.同步方法是锁住方法里面的所有代码
2.锁对象不能自己指定,是java已经规定好的
如果方法是非静态方法,那么锁对象就是:this, 如果方法是静态方法,那么锁对象就是:当前类的字节码文件
//还是上面的题目,用同步方法完成
//使用同步方法的技巧
//先写同步代码块,再把同步代码块的代码抽取成方法
//这个题的思路就是:
//1.循环
//2.同步代码块(同步方法)
//3.判断共享数据是否到了末尾,到了末尾代码写法
//4.判断共享数据是否还没到末尾,没到末尾代码写法
//ctrl+alt+m,把选中的代码抽取成方法
//同步方法
package JUCTest;
public class MyRunnable implements Runnable{
int ticket=0;
@Override
public void run() {
while (true){
if (method()) break;
}
}
private synchronized boolean method() {
if(ticket==100){
return true;
}else {
ticket++;
Thread t = Thread.currentThread();
System.out.println(t.getName()+"在卖第"+ticket+"张票");
}
return false;
}
}
//test类
package JUCTest;
public class testThree {
public static void main(String[] args) {
MyRunnable mr=new MyRunnable();
Thread t1=new Thread(mr);
Thread t2=new Thread(mr);
Thread t3=new Thread(mr);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
StringBuilder和StringBuffer的区别,StringBuilder线程不安全,StringBuffer线程安全
如果代码单线程,就用StringBuilder,如果代码多线程,就用StringBuffer
Lock锁
void lock()方法:获得锁
void unlock()方法:释放锁
package JUCTest;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyThread3 extends Thread{
static int ticket=0;
static Object o=new Object();
//这里用静态的原因是:可能会创建多个MyThread3对象,为了保证对象使用同一把锁而使用static
static Lock lock=new ReentrantLock();
//Lock是一个接口,没有对象,所以只能创建ReentrantLock的对象
@Override
public void run() {
while (true){
// synchronized (MyThread3.class){//这里同步代码块的锁会重复,所以注释掉
lock.lock();//上锁
try {
if(ticket<100){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(getName()+"再卖第"+ticket+"张票");
}else {
break;
}
} catch (RuntimeException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();//关锁,这里最后一次循环的时候,由于代码直接跳到循环的外面,导致锁没关闭,从而代码一直执行
//所以这里的代码放到了try/catch/finally中,确保代码一定能执行
}
}
// }
}
}
//test类
public static void main(String[] args) {
MyThread3 m1=new MyThread3();
MyThread3 m2=new MyThread3();
MyThread3 m3=new MyThread3();
m1.setName("窗口1");
m2.setName("窗口2");
m3.setName("窗口3");
m1.start();
m2.start();
m3.start();
}
死锁
在代码中形成了锁的嵌套就是死锁,死锁不是一个知识点,是一个代码的错误
//test类
MyThread t1=new MyThread();
MyThread t2=new MyThread();
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
//MyThread类
public class MyThread extends Thread{
static Object oA=new Object();
static Object oB=new Object();
@Override
public void run(){
while(ture){
if("线程1".equals(getName())){
synchronized(oA){
sout("线程1拿到了A锁,准备拿B锁");
synchronized(oB){
sout("线程1拿到B锁,执行一轮")
}
}
}else if("线程2".equals(getName())){
synchronized(oB){
sout("线程2拿到了A锁,准备拿B锁");
synchronized(oA){
sout("线程2拿到B锁,执行一轮")
}
}
}
}
}
}
上面就是一个死锁,代码执行后很有可能会卡死
关于死锁,就一句话,以后在写锁的时候,千万不要让锁嵌套使用
多线程的等待唤醒机制(生产者和消费者)
生产者和消费者模式是一个很经典的多线程协作的模式
由之前的知识可以知道,多线程在java中的运行是随机的,多条线程会随机的运行,而使用等待唤醒机制后,多线程就会有顺序的执行,你一次,我一次的执行
如果有A,B两条线程正在执行,默认情况下是随机执行的
使用等待唤醒机制后就是
在等待唤醒机制中,其中的一条线程我们叫做:生产者,用来生产数据,另一条线程叫做消费者,用来消费数据
-
生产者和消费者的理想情况
生产者先生产数据,把数据放到一个中间平台,然后消费者从中间平台获取消费数据消费,然后生产者再产生数据,往复循环
-
生产者和消费者的其他情况
1.消费者等待:如果一开始,消费者抢到cpu的执行权,那么消费者就会在中间平台(控制生产者和消费者的第三者)进行判断,判断中间平台是否有数据,如果此时没有数据让消费者消费,此时就会出现消费者等待的情况,代码中就是
wait
,一旦消费者等待,生产者就会抢到cpu的执行权,此时就会产生数据放到中间平台,把数据放到中间平台后,生产者还要唤醒消费者,在代码中就是notify
2.生产者等待:
就是开始生产者抢到cpu的执行权,然后下一次还是生产者抢到,但是此时中间平台已经有数据,此时就要让生产者等待并唤醒消费者
即:(这里的桌子就是中间平台,食物就是数据)
-
生产者与消费者的常用方法
wait,notify,notifyAll
消费者代码实现
书写多线程时,可以大致按照这个套路
//1.循环
//2.同步代码块(同步方法)
//3.判断共享数据是否到了末尾,到了末尾代码的写法
//4.判断共享数据是否还没到末尾,没到末尾代码的写法(执行核心逻辑)
//Desk类
package PACTest;
public class Desk {
/*
* 控制生产者和消费者的执行(中间平台)
*
* */
//定义变量,用来判断中间平台是否有数据
//0表示没有数据,1表示有数据
//这里不用Boolean类型的原因是:
// 为了之后代码的扩展,如果有4条线程,
//就可以用4个数代表不同的状态,Boolean就不能做到这一点
public static int FoodFlag=0;
//消费者总共可以消费的个数
public static int count=10;
//锁对象,线程中要用到锁
public static Object lock=new Object();
}
//test类
package PACTest;
public class Cook extends Thread{
@Override
public void run() {
while (true){
synchronized (Desk.lock){
if(Desk.count==0){
break;
}else {
//判断中间平台是否有数据,没有数据就让消费者等待
if(Desk.FoodFlag==0){
//具体的方法就是,给当前需要执行代码的锁给锁上,保证还是这个消费者线程,
try {
Desk.lock.wait();//这一步就是锁上
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else {
//能吃的总数-1
Desk.count--;
//输出消费语句
System.out.println("还能再吃"+Desk.count+"碗面条");
//解锁,消费后,继续让生产者生产
Desk.lock.notifyAll();
//中间平台数据清空
Desk.FoodFlag=0;
}
}
}
}
}
}
生产者代码实现
package PACTest;
public class Foodie extends Thread{
@Override
public void run() {
while (true){
synchronized (Desk.lock) {
if(Desk.count==0){
break;
}else {
if(Desk.FoodFlag==1){
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else {
System.out.println("生产者生产了一碗面");
Desk.lock.notifyAll();
Desk.FoodFlag=1;
}
}
}
}
}
}
总体代码实现
//生产者
package PACTest;
public class Foodie extends Thread{
@Override
public void run() {
while (true){
synchronized (Desk.lock) {
if(Desk.count==0){
break;
}else {
if(Desk.FoodFlag==1){
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else {
System.out.println("生产者生产了一碗面");
Desk.lock.notifyAll();
Desk.FoodFlag=1;
}
}
}
}
}
}
//消费者
package PACTest;
public class Cook extends Thread{
@Override
public void run() {
while (true){
synchronized (Desk.lock){
if(Desk.count==0){
break;
}else {
//判断中间平台是否有数据,没有数据就让消费者等待
if(Desk.FoodFlag==0){
//具体的方法就是,给当前需要执行代码的锁给锁上,保证还是这个消费者线程,
try {
Desk.lock.wait();//这一步就是锁上
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else {
//能吃的总数-1
Desk.count--;
//输出消费语句
System.out.println("还能再吃"+Desk.count+"碗面条");
//解锁,消费后,继续让生产者生产
Desk.lock.notifyAll();
//中间平台数据清空
Desk.FoodFlag=0;
}
}
}
}
}
}
//中间平台
package PACTest;
public class Desk {
/*
* 控制生产者和消费者的执行(中间平台)
*
* */
//定义变量,用来判断中间平台是否有数据
//0表示没有数据,1表示有数据
//这里不用Boolean类型的原因是:
// 为了之后代码的扩展,如果有4条线程,
//就可以用4个数代表不同的状态,Boolean就不能做到这一点
public static int FoodFlag=0;
//消费者总共可以消费的个数
public static int count=10;
//锁对象,线程中要用到锁
public static Object lock=new Object();
}
//test类
package PACTest;
public class test {
public static void main(String[] args) {
Cook c=new Cook();
Foodie f=new Foodie();
c.setName("吃");
f.setName("厨");
c.start();
f.start();
//生产者生产了一碗面
//还能再吃9碗面条
//生产者生产了一碗面
//还能再吃8碗面条
//生产者生产了一碗面
//还能再吃7碗面条
//生产者生产了一碗面
//还能再吃6碗面条
//生产者生产了一碗面
//还能再吃5碗面条
//生产者生产了一碗面
//还能再吃4碗面条
//生产者生产了一碗面
//还能再吃3碗面条
//生产者生产了一碗面
//还能再吃2碗面条
//生产者生产了一碗面
//还能再吃1碗面条
//生产者生产了一碗面
//还能再吃0碗面条
}
}
阻塞队列实现等待唤醒机制
-
阻塞队列的基本概念
阻塞队列就像一条管道,链接生产者和消费者就是把上面的中间平台换成阻塞队列
阻塞队列可以分为两个词理解,阻塞和队列
队列就是之前学的数据结构的一种,最先进去的数据最后出来
队列的特点:先进先出,后进后出阻塞就是,当生产者向阻塞队列的管道中存放数据的时候,管道的数据满了,数据放不进去在管道外等着,就叫做阻塞
同理,在消费者取出数据的时候,取不到数据也是阻塞 -
阻塞队列的继承结构
阻塞队列一共实现了四个接口,最顶层的接口是Iterable
这里的Queue就是队列接口,BlockingQueue是阻塞队列接口
从上面四个接口可以看出,阻塞队列可以用迭代器或增强for遍历,此外,阻塞队列本质上还是一个单列集合 -
阻塞队列的实现类
ArrayBlockingQueue,底层数组,有界
LinkedBlockingQueue,底层链表,无界,但不是真无界,最大为int的最大值
使用阻塞队列是不用自己创建锁
//生产者类
package PACTest;
import java.util.concurrent.ArrayBlockingQueue;
public class Cook extends Thread{
ArrayBlockingQueue<String>abq;
public Cook(ArrayBlockingQueue<String> abq) {
this.abq = abq;
}
@Override
public void run() {
while (true){
try {
abq.put("数据1");//生产者把数据放到阻塞队列中
//这里没有锁的原因是put源码中已经搞完锁的操作了
System.out.println("存放数据");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消费者类
package PACTest;
import java.util.concurrent.ArrayBlockingQueue;
public class Foodie extends Thread{
ArrayBlockingQueue<String> abq;
public Foodie(ArrayBlockingQueue<String> abq) {
this.abq = abq;
}
@Override
public void run() {
while (true){
try {
String shuJu = abq.take();//消费者不断从阻塞队列中获取数据
System.out.println("得到数据"+shuJu);
//这里没有锁的原因是take源码中已经搞完锁的操作了
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//test类
package PACTest;
import java.util.concurrent.ArrayBlockingQueue;
public class zuseQue {
public static void main(String[] args) {
/*
* 使用阻塞队列实现生产者和消费者(等待唤醒机制的代码)
* 注意:
* 生产者和消费者必须使用同一个阻塞序列
*
* */
//阻塞队列的对象可以在测试类中创建
//然后在生产者和消费者类中分别创建一个阻塞队列的成员变量,
// 只创建,不给值,具体的值在test类中用构造方法给
ArrayBlockingQueue<String>abq=new ArrayBlockingQueue<>(1);
//因为ArrayBlockingQueue有界,所以创建对象时必须给指定范围
Cook c=new Cook(abq);
Foodie f=new Foodie(abq);
c.start();
f.start();
}
}
线程的状态
由上面的生命周期,已经可以了解到一些状态
Java虚拟机中没有定义运行
状态
- api帮助文档里定义的java状态
线程状态。线程可以处于下列状态之一:
NEW
至今尚未启动的线程处于这种状态。
RUNNABLE
正在 Java 虚拟机中执行的线程处于这种状态。
BLOCKED
受阻塞并等待某个监视器锁的线程处于这种状态。
WAITING
无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。
TIMED_WAITING
等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。
TERMINATED
已退出的线程处于这种状态。
在给定时间点上,一个线程只能处于一种状态。这些状态是虚拟机状态,它们并没有反映所有操作系统线程状态。
- 没有定义运行状态的原因:
当线程抢到cpu的执行权后,java就会把线程交给操作系统管理 - 六种状态:
综合练习一
/test类
package JUCie;
public class testOne {
public static void main(String[] args) {
MyThread m1=new MyThread();
MyThread m2=new MyThread();
m1.setName("o1");
m2.setName("o2");
m1.start();
m2.start();
}
}
//MyThread
package JUCie;
public class MyThread extends Thread{
static int ticket=0;
@Override
public void run() {
while (true){
synchronized (MyThread.class){
if(ticket<1000){
ticket++;
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+"在卖第"+ticket+"张票,还剩"+(1000-ticket)+"张票");
}else {
break;
}
}
}
}
}
//test类
package JUCie;
public class testTwo {
public static void main(String[] args) {
MyThreadTwo m1=new MyThreadTwo();
MyThreadTwo m2=new MyThreadTwo();
m1.setName("ren1");
m2.setName("ren1");
m1.start();
m2.start();
}
}
//MyThreadTwo
package JUCie;
public class MyThreadTwo extends Thread{
static int gift=0;
@Override
public void run() {
while (true){
synchronized (MyThreadTwo.class){
if(gift<90){
gift++;
System.out.println(getName()+"发放礼物,还剩"+(100-gift)+"件");
}else {
break;
}
}
}
}
}
package JUCie;
public class testThree {
public static void main(String[] args) {
MyThreadFour m3=new MyThreadFour();
MyThreadFour m4=new MyThreadFour();
m3.start();
m4.start();
}
}
package JUCie;
public class MyThreadFour extends Thread{
static int c=0;
@Override
public void run() {
while (true){
synchronized (MyThreadFour.class){
if(c<100){
c++;
if(c%2!=0){
System.out.println(c);
}
}else {
break;
}
}
}
}
}
package JUCie;
import java.util.Random;
public class MyThreadFive extends Thread{
static double money=100;
static int i=3;
static final double min=0.01;
@Override
public void run() {
while (true){
synchronized (MyThreadFive.class){
if(i==0){
System.out.println(getName()+"没有抢到红包");
}else {
double prize=0;
if(i==1){
prize=money;
}
double Max=money-(i-1)*min;
Random r=new Random();
prize = r.nextDouble(Max);
if(prize<min){
prize=min;
}
money=money-prize;
i--;
System.out.println(getName()+"抢到了"+prize+"元");
}
}
}
}
}
package JUCie;
public class testFour {
public static void main(String[] args) {
MyThreadFive m1=new MyThreadFive();
MyThreadFive m2=new MyThreadFive();
MyThreadFive m3=new MyThreadFive();
MyThreadFive m4=new MyThreadFive();
MyThreadFive m5=new MyThreadFive();
m1.setName("ren1");
m2.setName("ren2");
m3.setName("ren3");
m4.setName("ren4");
m5.setName("ren5");
m1.start();
m2.start();
m3.start();
m4.start();
m5.start();
}
}
我写的
//test类
package JUCOne;
public class test1 {
public static void main(String[] args) {
MyThread m1=new MyThread();
MyThread m2=new MyThread();
m1.setName("抽奖箱1");
m2.setName("抽奖箱2");
m1.start();
m2.start();
}
}
//MyThread类
package JUCOne;
import java.util.Random;
public class MyThread extends Thread{
static int []arr={10,5,20,50,100,200,500,800,2,80,300,700};
static int i=arr.length-1;
@Override
public void run() {
while(true){
synchronized (MyThread.class){
if(i<0){
break;
}else {
if(i==0){
System.out.println(getName()+"又产生了一个"+arr[i]+"大奖");
break;
}
Random r=new Random();
int o = r.nextInt(i);
System.out.println(getName()+"又产生了一个"+arr[o]+"大奖");
i--;
}
}
}
}
}
视频写的
//test类
package JUCOne;
import java.util.ArrayList;
import java.util.Collections;
public class test1 {
public static void main(String[] args) {
ArrayList<Integer>list=new ArrayList<>();
Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700);
MyThread m1=new MyThread(list);
MyThread m2=new MyThread(list);
m1.setName("抽奖箱1");
m2.setName("抽奖箱2");
m1.start();
m2.start();
}
}
//MyThread 类
package JUCOne;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Random;
public class MyThread extends Thread{
ArrayList<Integer>list;
public MyThread(ArrayList<Integer> list) {
this.list = list;
}
@Override
public void run() {
while (true){
synchronized (MyThread.class){
if(list.size()==0){
break;
}else {
Collections.shuffle(list);
Integer remove = list.remove(0);
System.out.println(getName()+"又产生了一个"+remove+"大奖");
}
}
}
}
}
package JUCOne;
import java.util.ArrayList;
import java.util.Collections;
import java.util.StringJoiner;
public class MyThread extends Thread{
ArrayList<Integer>list;
//抽奖箱1的存放位置
static ArrayList<Integer>list1=new ArrayList<>();
//抽奖箱2的存放位置
static ArrayList<Integer>list2=new ArrayList<>();
public MyThread(ArrayList<Integer> list) {
this.list = list;
}
@Override
public void run() {
while (true){
synchronized (MyThread.class){
if(list.size()==0){
if("抽奖箱1".equals(getName())){
String s = list1.toString();
String s2 = s.substring(1, s.length() - 2);
String[] split = s2.split(",");
int Max=0;
int sum=0;
for (int i = 0; i < split.length; i++) {
System.out.println(split[i]);
int c=Integer.parseInt(split[i]);
if(c>Max){
Max=c;
}
sum=sum+c;
}
System.out.println("在此次抽奖中,抽奖箱1共产生了"+list1.size()+"个奖项");
System.out.println("分别为:"+s2+"最高奖项为"+Max+"元,总共"+sum+"元");
}else {
System.out.println(list2);
}
break;
}else {
Collections.shuffle(list);
Integer remove = list.remove(0);
if("抽奖箱1".equals(getName())){
list1.add(remove);
}else {
list2.add(remove);
}
// System.out.println(getName()+"又产生了一个"+remove+"大奖");
}
}
}
}
}
package JUCOne;
import java.util.ArrayList;
import java.util.Collections;
public class test1 {
public static void main(String[] args) {
ArrayList<Integer>list=new ArrayList<>();
Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700);
MyThread m1=new MyThread(list);
MyThread m2=new MyThread(list);
m1.setName("抽奖箱1");
m2.setName("抽奖箱2");
m1.start();
m2.start();
//上面的代码创建了两条线程,在虚拟机中有三条线程在运行
//分别是main主线程,抽奖箱1和抽奖箱2线程
}
}
//写法二
//线程栈
package JUCOne;
import java.util.ArrayList;
import java.util.Collections;
import java.util.StringJoiner;
public class MyThread extends Thread{
ArrayList<Integer>list;
// //抽奖箱1的存放位置
// static ArrayList<Integer>list1=new ArrayList<>();
// //抽奖箱2的存放位置
// static ArrayList<Integer>list2=new ArrayList<>();
public MyThread(ArrayList<Integer> list) {
this.list = list;
}
@Override
public void run() {
//把集合创建在run方法里面,每个线程在运行run方法时都会创建自己的集合
ArrayList<Integer>boxList=new ArrayList<>();
while (true){
synchronized (MyThread.class){
if(list.size()==0){
System.out.println(getName()+boxList);
break;
}else {
Collections.shuffle(list);
Integer remove = list.remove(0);
boxList.add(remove);
// System.out.println(getName()+"又产生了一个"+remove+"大奖");
}
}
}
}
}
核心逻辑:获取线程的最大值(可以看作线程运行的结果)
//test类
package JUCOne;
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class test1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ArrayList<Integer>list=new ArrayList<>();
Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700);
//创建多线程要运行的参数对象
MyThread m=new MyThread(list);
//创建多线程运行结果的管理者对象
//ft1是线程1
FutureTask<Integer>ft1=new FutureTask<>(m);
//ft2是线程2
FutureTask<Integer>ft2=new FutureTask<>(m);
Thread t1=new Thread(ft1);
Thread t2=new Thread(ft2);
t1.setName("抽奖箱1");
t2.setName("抽奖箱2");
t1.start();
t2.start();
Integer max1 = ft1.get();
Integer max2 = ft2.get();
System.out.println(max1);
System.out.println(max2);
}
}
//MyThread类
package JUCOne;
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.Callable;
public class MyThread implements Callable<Integer> {
ArrayList<Integer>list;
public MyThread(ArrayList<Integer> list) {
this.list = list;
}
@Override
public Integer call() throws Exception {
ArrayList<Integer>boxList=new ArrayList<>();
while (true){
synchronized (MyThread.class){
if(list.size()==0){
System.out.println(Thread.currentThread().getName()+boxList);
break;
}else {
Collections.shuffle(list);
Integer remove = list.remove(0);
boxList.add(remove);
// System.out.println(getName()+"又产生了一个"+remove+"大奖");
}
}
}
if(boxList.size()==0){
return null;
}
return Collections.max(boxList);
}
}
线程栈
在java中堆内存是唯一的,栈内存不是,每条线程都有自己的栈内存,我们一般叫做线程栈
线程池
以前写多线程的弊端:
1.用到线程才创建
2.用完线程就消失
-
所以,为了更好的利用线程资源,有了线程池的概念:
线程池就是一个存放线程的容器,刚开始,线程池是空的,当给线程池一个任务时,线程池就会自动的创建一个线程,拿着这个创建的线程去执行任务,任务执行完了线程会返回线程池,而不会消失,第二次再提交一个任务时,此时就不会创建新的线程了,而是会拿着刚才线程池的线程去完成任务
如果任务一还没完成又给线程池添加任务的话,此时线程池就会创建新的线程,拿着这个新线程去完成第二个任务
线程池创建线程有最大线程上限,但是可以自己设置这个上限
-
线程池的核心逻辑
1.创建一个池,池子是空的
2.提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子,下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可
3.但是,如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待 -
线程池的代码实现
1.创建一个线程池
2.提交任务
3.所有的任务执行完毕后,关闭线程池(实际开发中,线程池不会关闭,因为服务器是24小时都在运行) -
创建线程池的方法
Executors
:线程池的工具类,可以通过调用方法返回不同类型的线程池对象
上面的newCachedThreadPool方法并不是没有上限,上限就是int类型的最大值
- 提交任务的方法
用线程池对象调用submit方法
有三个参数,Callable的实现类,Runnable的实现类
package JUCOne;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class test2 {
public static void main(String[] args) {
//创建线程池
//newCachedThreadPool方法,可以获取一个没有上限的线程池对象
//pool1就表示第一个线程池
// ExecutorService pool1 = Executors.newCachedThreadPool(3);
//newCachedThreadPool方法,可以获取一个有上限的线程池对象,上限由自己决定
//线程池的上限为3
ExecutorService pool1 = Executors.newFixedThreadPool(3);
//提交任务
//可以使用线程池对象的submit方法
//在这里提交多个任务时线程池底层就会创建多个线程
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
// pool1.submit(new MyRunnable());
//所有任务执行完毕后,可以用线程池对象的shutdown方法来销毁线程池
//但是一般都是不会销毁线程池的
// pool1.shutdown();
}
}
//MyRunnable
package JUCOne;
public class MyRunnable implements Runnable{
@Override
public void run() {
//线程所要执行的代码
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}
自定义线程池详细解析
newFixedThreadPool方法的底层是创建一个ThreadPoolExecutor类的对象
ThreadPoolExecutor类的构造方法最多可以有7个参数
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
用给定的初始参数创建新的 ThreadPoolExecutor。
这7个参数的大致意思
- 临时线程的创建时间:
临时线程会在核心线程都在工作,且阻塞队列排满的情况下才会创建,用来服务在阻塞队列外的任务
由上面的图可知,任务在执行的时候并不是一定按照提交的顺序执行的,先提交也有可能后执行
如果任务数量超出了线程池的最大容载量(核心线程数+阻塞队列容载量+临时线程数),就会触发任务舍弃策略,默认策略就是舍弃多余任务并抛出异常
主要就是默认策略,其他了解
这里的方法三就会抛弃掉阻塞队列中的第一个等待任务,也就是任务4会被抛弃,然后任务10会加入到阻塞队列中。
package JUCOne;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class MyThreadPoolTest {
public static void main(String[] args) {
ThreadPoolExecutor pool=new ThreadPoolExecutor(
3,//核心线程数量,不能小于0
6,//最大线程数量,不能小于核心线程数量
60,//空闲的临时线程的最大存活时间
TimeUnit.SECONDS,//时间单位
new ArrayBlockingQueue<>(3),//阻塞队列,任务队列
Executors.defaultThreadFactory(),//创建线程工厂
new ThreadPoolExecutor.AbortPolicy()//任务的拒绝策略
);
//提交任务
pool.submit(new MyRunnable());
}
}
最大并行数
cpu的四核八线程:四核就是cpu好比有四个大脑,然后英特尔研发出一个超线程技术,可以让一个大脑有两个线程,也就是八线程
即对八线程的cpu而言,最大并行数就是8
package JUCOne;
public class test3 {
public static void main(String[] args) {
//这里的i就是java虚拟机可用的处理器数目
int i = Runtime.getRuntime().availableProcessors();
System.out.println(i);
}
}//16
线程池到底多大合适?
线程池的大小有公式,根据项目的不同可以分为两个公式计算线程池的大小
项目可以分为两种类型:cpu密集型(读取本地数据库较少),io密集型(读取本地数据或数据库较多)
-
cpu密集型中线程池大小的公式:
最大并行数+1,可以有最大效率
例如,我的电脑是16线程,那么这里的线程池线程大小就应该是16+1=17 -
io密集型中线程池大小的公式:
最大并行数*期望cpu运行率 *(总时间(cpu计算时间+等待时间)/cpu计算时间)
这里读取数据没有用到cpu,只有相加用到cpu
例如,我的电脑是16线程,那么这里的线程池线程大小就应该是16100%(100%/50%)=32
这里的时间可以用工具来测试,thread dump,可以测试时间,然后套用公式来计算线程池大小