java多线程与JUC
进程线程
进程:进程是操作系统分配资源的基本单位。在电脑中,一个软件就是一个进程
线程:线程是CPU调度的基本单位,是进程内的执行单元。相当于一个软件中的不同功能
多线程程序的特点:程序可以同时去做多件事,CPU可以在多件事之间切换,把等待的空余时间充分利用起来,提高程序的运行效率
多线程的应用场景?
只要你想让多个事情同时运行就需要用到多线程。
比如:软件中的耗时、等待操作、所有的聊天软件、所有的服务器。
并发并行
并发:在一个CPU交替执行
并行:在多个CPU同时执行
电脑CPU线程数就表示同一时间可以同时运行的线程数量
4线程,表示可以同时并行4个线程,当时要运行的线程数量过多时,就会并发和并行同时发生
多线程实现方式
1,继承Thread类
- 先自己定义一个类进程Thread类
- 重写run方法,编写线程要执行的代码
- 创建子类对象,调用start方法,启动线程
2,实现Runnable接口
- 创建一个自己类实现Runnable接口
- 重写run方法,编写线程要执行的代码
- 创建自己类的对象
- 创建一个Thread类的对象并传递自己类的对象,并开启线程
3,实现Callable接口或者Future接口(有返回值)
对前面两种的优化,可以获取到多线程运行的结果。
- 创建一个类MyCallable(自己随便创建的类)实现callable接口
- 重写call方法(是有返回值的,表示多线程运行的结果)
- 以下在测试类中写
- 创建MyCallable的对象(表示多线程要执行的任务)
- 创建FutureTask的对象(将MyCallable对象传入。作用管理多线程运行的结果,Future是一个接口)
- 创建Thread类的对象,并启动(将FutureTask对象传入。表示线程)
三种实现方式对比
Thread中常见的成员方法
细节:
- 如果我们没有给线程设置名字,线程也是有默认的名字(格式:Thread-X,X是序号,从0开始)。创建Thread对象时,也可以利用构造方法设置名字
- 当JVM虚拟机启动之后,会自动的启动多条线程。其中有一条线程就叫做main线程,他的作用就是去调用main方法,并执行里面的代码。
- 哪条线程执行到sleep方法,那么哪条线程就会在这里停留对应的时间。方法的参数:就表示睡眠的时间,单位是毫秒。1 秒=1000毫秒。当时间到了之后,线程会自动的醒来,继续执行下面的其他代码
线程优先级相关方法
线程调度:抢占式调度(随机性,相互抢),非抢占式调度(所有线程轮流执行)
java中是采用了抢占式调度,所以有设置线程优先级的方法。
设置线程的优先级越大,抢到CPU的概率越大,执行的机会就越大。一共分10档(1-10,没有设置默认为5)
守护线程方法
将某个线程设置为守护线程(备胎线程),其他非守护线程(女神线程)执行结束,守护线程陆续结束,而不是立刻结束,守护线程会执行一小会。
出让线程/插入线程(了解即可)
前面线程执行都是随机的,为了让线程执行尽可能雨露均沾就提出了出让/礼让线程
哪个线程执行了yield方法,就表示出让了当前CPU执行权,让另一个线程执行
哪个线程执行了join方法,就表示把该线程插入到当前线程之前,执行完后再执行别的
线程生命周期
注意:一个线程sleep完后,不会立马执行,需要去抢CPU执行权,抢到了才能执行
线程安全问题
在卖票时,在还没打印出来时,就被别的线程抢走,票的数量加1,再打印出现超卖和没有第1张票
利用同步代码块
将操作共享数据的代码锁起来。
特点1:锁默认打开,有一个线程进去了,锁自动关闭
特点2:里面代码全部执行完,线程出来,锁自动打开
锁:锁对象,只要保持唯一就行,加static关键字。
这种方式叫同步代码块
同步代码块的细节:
- synchronized不能写在循坏外面
- synchronized(锁对象),其中锁对象一定要是唯一的,不唯一,那就说明不同线程看的锁是不一样的,那锁相当于没有
public class MyThread extends Thread {//表示这个类所有的对象,都共享ticket数据static int ticket = 0;//0 ~ 99@Overridepublic void run() {while (true) {synchronized (MyThread.class) {//同步代码块if (ticket < 100) {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}ticket++;System.out.println(getName() + "正在卖第" + ticket + "张票!!!");} else {break;}}}}}
public class ThreadDemo {public static void main(String[] args) {/*需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票*///创建线程对象MyThread t1 = new MyThread();MyThread t2 = new MyThread();MyThread t3 = new MyThread();//起名字t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");//开启线程t1.start();t2.start();t3.start();}
}
利用同步方法
直接把synchronized关键字加到方法上
不知道将哪些操作写到同步方法里。那就先写同步代码块,再将同步代码块抽取成一个方法即可。
写多线程的套路:
- 先写一个循环
- 写synchronized的同步代码块/同步方法/lock锁
- 判断共享数据是否到了末尾,先写到了末尾(不可用)的情况,到了末尾写起来更简单
- 判断共享数据是否到了末尾,没末尾的情况,写核心逻辑
public class MyRunnable implements Runnable {//不用写static,因为MyRunnable对象是作为一个参数,让线程去执行的,所以MyRunnable只会创建一个MyRunnable对象int ticket = 0;@Overridepublic void run() {//1.循环while (true) {//2.同步代码块(同步方法)if (method()) break;}}//thisprivate synchronized boolean method() {//3.判断共享数据是否到了末尾,如果到了末尾if (ticket == 100) {return true;} else {//4.判断共享数据是否到了末尾,如果没有到末尾try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}ticket++;System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票!!!");}return false;}
}
public class ThreadDemo {public static void main(String[] args) {/*需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票利用同步方法完成技巧:同步代码块*/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();}
}
StringBuffer:多线程下是安全的,里面每一个方法都加了synchronized锁
StringBuilder:多线程下是不安全的
synchronized是自动加锁解锁的,有没有办法手动控制加锁解锁呢?
Lock锁
一定要保证多个线程共享同一把锁
保证每个线程执行完后都要释放锁
死锁
死锁是一个错误,以后不要放错。死锁就是锁出现了嵌套。A锁着B要的资源,B锁着A要的资源
生产者和消费者(等待唤醒机制)
线程执行是随机顺序,因此提出了生产者和消费者(等待唤醒机制)。可以让生产者生产数据,消费者消费数据,还需要有一个东西来控制线程的执行(核心思想)。
理想情况
生产者抢到CPU,生产数据,将数据放到桌子上,吃货来吃。相当于厨师做一个,吃货吃一个
消费者等待情况
假设消费者先抢到CPU执行权,桌子上没有东西,只有等待wait,等待时,CPU执行权被厨师抢到,厨师看一看桌子上有没有面条,没有就开始做面条,做完放到桌子上,此时吃货还在wait,厨师就要唤醒notify吃货来吃。
生产者等待情况
生产者先抢到CPU执行权,看桌子上有没有面条,没有就做面条,放到桌子上,喊人来吃,结果还是生产者抢到CPU执行权,此时生产者做不了面条,所以生产者只能等待wait,CPU执行权就会被消费者抢走
涉及到的方法
wait:一直等待,等到有人唤醒
notify方法:如果等待的线程较多,只能随机唤醒其中一个
等待唤醒机制(阻塞队列方式实现)
好比是连接生产者和消费之间是一个管道,厨师做完面放到管道里,消费者直接从管道里取面条吃,我们可以规定管道里最多能放的面条数量,这个管道就是阻塞队列。
阻塞:put数据时:放不进去,会等着,也叫做阻塞。take数据时:取出第一个数据,取不到会等着,也叫做阻塞。
阻塞队列的继承结构:
创建阻塞队列的对象:ArrayBlockingQueue/LinkedBlockingQueue
put/take方法底层自动加了锁,不用再加锁了
线程的状态
JVM中实际上是没有运行状态的,为了方便理解加上的。
API帮助文档查询结果:
JVM为什么没有运行状态?
当线程抢到CPU执行权,JVM会把当前线程交给操作系统去管理了,所以JVM没有定义