多线程
了解多线程
并发和并行
进程和线程
- 总结
多线程的实现方式--继承Thread
- 实现步骤
package com.thread;
public class MyThread extends Thread{
@Override
public void run(){
//run()里面的代码就是线程开启之后执行的代码
for (int i = 0; i < 100; i++) {
System.out.println("线程开始了"+i);
}
}
}
package com.thread;
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();//线程1
MyThread myThread1 = new MyThread();//线程2
myThread.start();//开启线程1
myThread1.start();//开启线程2
}
}
我们从一个线程的执行可能开不出来什么,什么我们同时开启了2个线程。可以看到这2个线程在并发交替执行
2个小问题
多线程的实现方式--实现Runnable接口
Thread构造方法里面传递的参数,表示线程执行对应myrnnnable的run 方法
- 实现步骤
package com.runnable;
public class MyRunnableTest {
public static void main(String[] args) {
//创建了一个参数的对象
MyRunnable myRunnable = new MyRunnable();
//创建了一个线程的的对象并把参数传递给它
Thread thread = new Thread(myRunnable);
//开启线程
//创建并执行线程2
thread.start();
MyRunnable myRunnable1 = new MyRunnable();
Thread thread1 = new Thread(myRunnable1);
thread1.start();
}
}
package com.runnable;
public class MyRunnable implements Runnable{
@Override
public void run() {
//表示线程启动后执行的代码
for (int i = 0; i < 100; i++) {
System.out.println("线程开始了"+i);
}
}
}
多线程的实现方式--实现callable接口
package com.callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//线程开启后需要执行里面的cll方法
MyCallable myCallable = new MyCallable();
//Thread thread = new Thread(myCallable);不能直接将myCallable传递给Thread
//1.FutureTask的泛型和MyCallable的泛型相同
//2.将MyCallable传递给FutureTask
//可以获取线程执行结束之后的结果
FutureTask<String > future = new FutureTask<>(myCallable);
//将FutureTask传递给Thread
Thread thread = new Thread(future);
thread.start();
//获取线程执行结束的结果
final String reason = future.get();
System.out.println(reason);
}
}
package com.callable;
import java.util.Objects;
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {//泛型类型表示返回值的数据类型
//返回值表示线程运行结束之后的结果
@Override
public String call() throws Exception {
return "hell world";
}
}
- ouput:hello world
注意:当我们的get()方法在我们t1.start()线程开启之前执行,此时我们将不可能获取到线程执行的结果。并且由于get()方法
当线程还没有执行结束将会一直处于等待状态。当我们将get方法放在get之前,此时我们的程序将一直停留在get()处,不会继续运行
三种实现方式的对比
Thread方式--设置获取名字
- 通过构造方法设置线程名
我们Mythread的父类是有类似Thread(String name)的构造方法专门用来设置线程名的,但是由于构造方法不能继承,所有我们的子类要想使用设置线程名的构造方法来创建子类的话,就需要在MyThread中创创建单参构造并调用父类单参构造
Thread方法--获取线程对象
获取当先线程对象的一般使用场景
Thread----sheep方法
- 异常小计
如果一个类或者接口里面的方法没有抛出异常,那么这个类或者接口的实现类所重写的方法也不能抛出异常
线程的优先级
- 优先级的获取和设置
1.优先级1-10,默认为5
- 2.线程优先级越高,只能说抢到cup的概率越高,不是只可能是该线程执行**
package com.callable;
import java.util.concurrent.Callable;
public class MyCallable1 implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"线程开始了"+i);
}
return null;
}
}
package com.callable;
import java.util.concurrent.FutureTask;
public class Test1 {
public static void main(String[] args) {
//线程1
MyCallable1 myCallable = new MyCallable1();
FutureTask<String> futureTask = new FutureTask<>(myCallable);
Thread thread = new Thread(futureTask);
System.out.println(thread.getPriority());//默认5
//线程2
MyCallable1 myCallable2 = new MyCallable1();
FutureTask<String> futureTask2 = new FutureTask<>(myCallable2);
Thread thread2 = new Thread(futureTask2);
System.out.println(thread2.getPriority());//默认5
//设置线程名
thread.setName("飞机");
thread2.setName("坦克");
//启动线程
// thread2.start();
//thread.start();
}
}
Thread方法--守护线程
- 解释
我们将QQ里面的聊天和传递文件看成是2个线程,如果我们将QQ关闭,聊天和传递文件也会随之关闭,没有存在的必要了。聊天和传递文件就是2个守护线程
package com.thread;
public class MyThread1 extends Thread{
@Override
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println(currentThread().getName()+"线程开始了"+i);
}
}
}
package com.thread;
public class Test1 {
public static void main(String[] args) {
//守护线程:当普通线程执行完了,守护线程也没有继续运行下去的必要了
MyThread1 myThread1 = new MyThread1();
MyThread1 myThread2 = new MyThread1();
myThread1.setName("女神");
myThread2.setName("备胎");
myThread2.setDaemon(true);//将第二个线程设置成守护线程
myThread1.start();
myThread2.start();
}
}
02线程安全问题
线程安全问题--买票案例的实现
package com.itheima.threadsecture;
public class Ticket implements Runnable
{
private int ticketCount = 100;//剩下的票数
@Override
public void run() {
while (true){
if(ticketCount>0){
ticketCount--;
System.out.println(Thread.currentThread().getName()+"正在出售票"+"还剩下"+ticketCount);
}else {
//当票数为0时,线程结束
break;
}
}
}
}
package com.itheima.threadsecture;
public class TicketDemo {
public static void main(String[] args) {
//Ticket作为参数相当于是要执行的内容
//各个线程的参数必须一致,要不然将会出现3份票数
Ticket ticket = new Ticket();
Thread thread1 = new Thread(ticket);
Thread thread2 = new Thread(ticket);
Thread thread3 = new Thread(ticket);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
线程安全--原因分析
- 在Ticket类中增加延迟
package com.itheima.threadsecture;
public class Ticket implements Runnable
{
private int ticketCount = 100;//剩下的票数
@Override
public void run() {
while (true){
if(ticketCount>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticketCount--;
System.out.println(Thread.currentThread().getName()+"正在出售票"+"还剩下"+ticketCount);
}else {
//当票数为0时,线程结束
break;
}
}
}
}
出现线程安全的原因
- 本质上是多个线程操作共享数据
同步代码块解决线程安全
用同步代码块实现购票代码
- 我们将操作共享资源的代码放在同步代码块中
package com.itheima.threadsecture;
public class Ticket implements Runnable
{
private int ticketCount = 100;//剩下的票数
private Object obj = new Object();
@Override
public void run() {
while (true){
synchronized (obj) {//锁对象是任意的,但是必须保证各个线程面对的是同一把锁
if(ticketCount>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticketCount--;
System.out.println(Thread.currentThread().getName()+"正在出售票"+"还剩下"+ticketCount);
}else {
//当票数为0时,线程结束
break;
}
}
}
}
}
package com.itheima.threadsecture;
public class TicketDemo {
public static void main(String[] args) {
//Ticket作为参数相当于是要执行的内容
//各个线程的参数必须一致,要不然将会出现3份票数
Ticket ticket = new Ticket();
Thread thread1 = new Thread(ticket);
Thread thread2 = new Thread(ticket);
Thread thread3 = new Thread(ticket);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
线程安全问题--锁对象唯一
同步方法
- 证明同步方法的锁对象是this
package com.runnable;
public class MyRunnable1 implements Runnable{
private int ticketCount = 100;//剩下票数
@Override
public void run() {
while (true)
{
if("窗口1".equals(Thread.currentThread().getName())){
try {
synchronizedMethod();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if("窗口2".equals(Thread.currentThread().getName())){
synchronized (this){//锁对象为当前调用的对象
if(ticketCount<=0){
break;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticketCount--;
System.out.println(Thread.currentThread().getName()+"正在卖票 还剩下"+ticketCount+"张票");
}
}
}
}
}
}
private void synchronizedMethod() throws InterruptedException {//锁对象默认为this
if(ticketCount<=0){
return;
}else {
Thread.sleep(100);
ticketCount--;
System.out.println(Thread.currentThread().getName()+"正在卖票 还剩下"+ticketCount+"张票");
}
}
}
package com.runnable;
import com.thread.MyThread1;
public class MyRunnableTest1 {
public static void main(String[] args) {
MyRunnable1 runnable1 = new MyRunnable1();//参数对象相同
Thread thread1 = new Thread(runnable1);
Thread thread2 = new Thread(runnable1);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread1.start();
thread2.start();
}
}
我们使用Runnable接口实现同步。我们故意使用同步方法和同步代码块实现了相同的内容,但是我们的Run方法的内容是由MyRunnable负责的,即如果我们使用的是同一个MyRunnable对象,我们面对的就是同一把锁。我们的Thread对象传递的都是相同的参数对象。最后发现2种情况下是同步执行的。可以得出结论,同步方法的锁对象是this
Lock
主要是因为我们的之前的锁不够形象,使用这个Lock对象,操作起来比较形象
- 使用Lock代替同步代码块
package com.itheima.threadsecture;
import java.util.concurrent.locks.ReentrantLock;
public class Ticket1 implements Runnable
{
private int ticketCount = 100;//剩下的票数
private Object obj = new Object();
ReentrantLock lock = new ReentrantLock();//创建锁对象
@Override
public void run() {
while (true){
// synchronized (obj) {//锁对象是任意的,但是必须保证各个线程面对的是同一把锁
lock.lock();//上锁
if(ticketCount>0){
try {
Thread.sleep(100);
ticketCount--;
System.out.println(Thread.currentThread().getName()+"正在出售票"+"还剩下"+ticketCount);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
lock.unlock();//关锁
}
}else {
//当票数为0时,线程结束
break;
}
// }
}
}
}
package com.itheima.threadsecture;
public class TicketDemo {
public static void main(String[] args) {
//Ticket作为参数相当于是要执行的内容
//各个线程的参数必须一致,要不然将会出现3份票数
Ticket1 ticket = new Ticket1();
Thread thread1 = new Thread(ticket);
Thread thread2 = new Thread(ticket);
Thread thread3 = new Thread(ticket);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
由于锁是一种资源,所有为了确保其被关闭,一般放在finaly语句中
死锁
- 如果锁进行了嵌套,可能会出现死锁问题
03生产者和消费者(等待唤醒机制)
生产者和消费者思路分析
-
理想情况(生产者抢到执行权,然后消费者再抢到执行权,并且以此反复)
描述:生产者和消费者2个线程轮流执行。生产者生产一个食物,消费者然后吃这个食物 -
消费者等待
-
生产者等待的情况
-
等待唤醒机制总结(结合前面的所有情况)
生产者消费者代码实现
- 需要使用的方法
关于此时的锁对象
我们有生产者和消费者2个线程,我们需要保证2个线程的锁是一样的,就必须保证有相同的锁对象。如果我们将锁对象定义再其中一个
线程中,这样在另外一个线程中用该锁对象就需要创建对象,这样不是很方便,故将锁对象定义在Desk中
不能随意调用等待和唤醒方法
-
必须要使用什么对象当作锁,必须使用该对象去调用等待和唤醒方法
-
关于notify和notifyAll
多个线程对应同一把锁,notify()方法只能唤醒多个等待线程中的任意一个线程,而notifyAll方法则是可以唤醒所有的正在等待的线程 -
书写的套路
-
Cook
package com.itheima.foodieandcook;
public class Cook extends Thread{
@Override
public void run() {
while (true){
synchronized (Desk.lock){
if(Desk.count == 0){//没有食物可以卖了(在执行步骤之前先判断是否有食物)
break;
}else {
//1.判断桌子上是否有食物
if(Desk.flag){
//2.1如果有则进行等待
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else {
//2.2如果没有则进行生产
System.out.println("开始做食物了");
//2.3把做好的食物放在桌子上
Desk.flag = true;
//2.4叫醒消费者开吃
Desk.lock.notifyAll();
}
}
}
}
}
}
- Foodie
package com.itheima.foodieandcook;
public class Foodie extends Thread{
@Override
public void run() {
while (true){
synchronized (Desk.lock){
if(Desk.count == 0){//没有食物可以卖了(在执行步骤之前先判断是否有食物)
break;
}else {
//1.判断桌子上是否有食物
if(Desk.flag){
//3.如果有就开吃
System.out.println("开始吃食物");
//4.1吃完后桌子上就没有食物了
//4.2 通知生产者进行生产
//4.3食物的总数-1
Desk.flag = false;
Desk.lock.notifyAll();
Desk.count--;
}else {
//2.如果没有就进行等待
//必须要使用锁的对象去调用等待和唤醒方法
try {
Desk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
}
}
- Desk
package com.itheima.foodieandcook;
//Desk类用于标记桌子上是否有食物
public class Desk {
//定义一个标记
//true:表示桌子上有食物,此时运行吃货执行
//false:表示桌子上没有食物,此时运行厨师执行
public static boolean flag = false;
//定义可以卖的食物的总数量
public static int count = 10;
//锁对象
public static final Object lock = new Object();
}
- 测试类
package com.itheima.foodieandcook;
public class Demo {
public static void main(String[] args) {
Foodie foodie = new Foodie();
Cook cook = new Cook();
foodie.start();
cook.start();
}
}
- 线程有序执行
生产者消费者---代码改写
- 改进目标
为了让我们的代码更面向对象,努力将Desk类修改成javaBean类,并随即改进Foodie和Cook类
注意:
我们不能在Foodie和Cook类中创建Desk对象,因为这样将会导致,FOODie和Cook各自创建了一个Desk对象,这样将会导致创建了2个不同的Lock对象。将会导致2个线程所面对的锁不同 - 解决方法
我们在Fooie和Cook中仅仅定义Desk的引用,然后在此时类中创建Desk对象,并通过构造方法进行传递。这样就会保证2个线程所面对的是同一个锁 - Cook
package com.itheima.foodieandcook;
public class Cook extends Thread{
private Desk desk;
public Cook(){
}
public Cook(Desk desk){
this.desk = desk;
}
@Override
public void run() {
while (true){
synchronized (desk.getLock()){
if(desk.getCount() == 0){//没有食物可以卖了(在执行步骤之前先判断是否有食物)
break;
}else {
//1.判断桌子上是否有食物
if(desk.isFlag()){
//2.1如果有则进行等待
try {
desk.getLock().wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else {
//2.2如果没有则进行生产
System.out.println("开始做食物了");
//2.3把做好的食物放在桌子上
desk.setFlag(true);
//2.4叫醒消费者开吃
desk.getLock().notifyAll();
}
}
}
}
}
}
- Foodie
package com.itheima.foodieandcook;
public class Foodie extends Thread{
private Desk desk;
//定义构造方法
public Foodie(){
}
public Foodie(Desk desk){
this.desk = desk;
}
@Override
public void run() {
while (true){
synchronized (desk.getLock()){
if(desk.getCount() == 0){//没有食物可以卖了(在执行步骤之前先判断是否有食物)
break;
}else {
//1.判断桌子上是否有食物
if(desk.isFlag()){
//3.如果有就开吃
System.out.println("开始吃食物");
//4.1吃完后桌子上就没有食物了
//4.2 通知生产者进行生产
//4.3食物的总数-1
desk.setFlag(false);
desk.getLock().notifyAll();
desk.setCount(desk.getCount()-1);
}else {
//2.如果没有就进行等待
//必须要使用锁的对象去调用等待和唤醒方法
try {
desk.getLock().wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
}
}
package com.itheima.foodieandcook;
//Desk类用于标记桌子上是否有食物
public class Desk {
//定义一个标记
//true:表示桌子上有食物,此时运行吃货执行
//false:表示桌子上没有食物,此时运行厨师执行
private boolean flag;
//定义可以卖的食物的总数量
private int count = 10;
//锁对象
private final Object lock = new Object();
public Desk() {
}
public Desk(boolean flag, int count) {
this.flag = flag;
this.count = count;
}
/**
* 获取
* @return flag
*/
public boolean isFlag() {//注意:方法名不同
return flag;
}
/**
* 设置
* @param flag
*/
public void setFlag(boolean flag) {
this.flag = flag;
}
/**
* 获取
* @return count
*/
public int getCount() {
return count;
}
/**
* 设置
* @param count
*/
public void setCount(int count) {
this.count = count;
}
public Object getLock(){//注意lock只有getLock方法
return lock;
}
public String toString() {
return "Desk{flag = " + flag + ", count = " + count + ", lock = " + lock + "}";
}
}
package com.itheima.foodieandcook;
public class Demo {
public static void main(String[] args) {
Desk desk = new Desk();//创建Desk的对象
Foodie foodie = new Foodie(desk);
Cook cook = new Cook(desk);
foodie.start();
cook.start();
}
}
阻塞队列的基本使用
使用等待队列会让我们的代码更加间接
- 2种阻塞队列
- 正常的存储一个取出一个
package com.itheima.foodieandcook;
import java.util.concurrent.ArrayBlockingQueue;
//阻塞队列的使用
public class BlockQueueTest0 {
public static void main(String[] args) throws InterruptedException {
//创建了阻塞队列的对象,其容量为10
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
//存储元素
queue.put("汉堡包");
//取元素
System.out.println(queue.take());
System.out.println("程序结束了");
}
}
- 当我们存储一个,然后连续取出2个,当取出第二个时,按照前面说的当娶不到时将会进行等待
package com.itheima.foodieandcook;
import java.util.concurrent.ArrayBlockingQueue;
//阻塞队列的使用
public class BlockQueueTest0 {
public static void main(String[] args) throws InterruptedException {
//创建了阻塞队列的对象,其容量为10
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
//存储元素
queue.put("汉堡包");
//取元素
System.out.println(queue.take());//取一个
System.out.println(queue.take());//取第二个
System.out.println("程序结束了");
}
}
- 此时程序陷入了等待
阻塞对象实现等待唤醒机制
- Cooker
package com.itheima.queuefoodieandcook;
import java.util.concurrent.ArrayBlockingQueue;
public class Cooker extends Thread{
private ArrayBlockingQueue<String> list;//阻塞队列成员
public Cooker(ArrayBlockingQueue<String> list) {
this.list = list;
}
@Override
public void run() {
while (true){
try {
list.put("汉堡包");
System.out.println("厨师放了一个汉堡包");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- Foodie
package com.itheima.queuefoodieandcook;
import java.util.concurrent.ArrayBlockingQueue;
public class Foodie extends Thread{
private ArrayBlockingQueue<String> list;//阻塞队列成员
public Foodie(ArrayBlockingQueue<String> list) {
this.list = list;
}
@Override
public void run() {
while (true) {
try {
final String take = list.take();
System.out.println("吃货从队列中获取了"+take);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 测试类
package com.itheima.queuefoodieandcook;
import java.util.concurrent.ArrayBlockingQueue;
public class Demo {
public static void main(String[] args) {
//创建阻塞队列
ArrayBlockingQueue<String> list = new ArrayBlockingQueue<>(1);
//创建消费者和生产者线程
Cooker cook = new Cooker(list);
Foodie foodie = new Foodie(list);
cook.start();
foodie.start();
}
}
从输出结果看好像有连续打印,不符合,做一个,吃一个。其实这时因为我们的输出语句不在锁中,因为锁是由put和take方法底层加的。这种情况并不影响,等待唤醒机制
线程池和volatile(多线程高级)
线程状态
线程处于运行状态时是和cup产生的关于,所以在我们的虚拟机中没有没有定义运行态
- 在Thread类中的内部类State定义了线程的状态
线程池---基本原理
- 情景引入
小明同学要吃饭,但是没有碗,所有要到小佳同学开的超市去买完,买了碗后吃完饭,(因为没有地方放碗)即把碗摔了,以此反复.......
- 解决方案
- 以前写法的弊端
线程池--Executors默认线程池(线程池不指定线程上限)
我们在提交线程任务后,如果没有关闭线程池则我们的程序不会截至,因为我们的线程还存在,我们需要关闭线程池我们的程序才结束
- 注意:如果有空闲的线程将不会去重新创建新的线程
Executors创建制定线程上限的线程池对象
ThreadPoolExecutor(自定义线程池)
在前面的创建线程池的对象中,我们都是调用Executors的静态方法(newCaceThreadPool或newFixedThreadPool)来创建线程池的对象,在实际上是调用其他类的代码来间接创建的而不是直接创建的
- newCachedThreadPool的原码
- newFixedThreadPool原码
- 代码简单截图(和之前的一样)
线程池-- 参数详解
- 任务队列 让任务在队列里面等待,等有线程空闲了再从队列中获取任务并执行
- 创建线程工厂:通过查看原码,本质上就是按照默认的方式创建线程(我们规定的数量的线程)
任务拒绝策略
-1.什么时候会拒绝
- 不会拒绝
- 不会拒绝
- 刚好装满,不会拒绝
当线程池里面的线程全部再执行任务,而且任务队列全部都装满了,如果还有多余的任务将会被拒绝
非默认任务拒绝策略
从上面可以看到它仅仅是打印出来了1,2,3,所有可以知道它拒绝了任务4和5,并且没有抛出异常
将任务10前面的的任务都决绝了(等待时间长)而执行了任务10
**此时当我们的线程忙不过来了,就会交给别的线程执行
volatile问题
但是此时A线程还再循环中无法获取到moeny的最新数据,还是使用10w
volatile解决
- 内存图理解volatile问题
我们的线程有自己的内存,对于共享数据,他们会复制一份作为变量副本存于字节的内存中,当他们要改变共享数据时,会先改变自己
的变量副本,然后将变量副本复制到共享数据中,从而改变共享数据。而其他的线程要知道共享数据是否发生改变是需要自己到访问
共享数据进行查看的,至于什么时候进行查看,这个我们无法控制
- 解决方案
- 此时我们发现我们的程序打印出来了结果
synchronized同步代码块解决
-
线程1中加锁
-
线程2中加锁
-
运行代码:此时已经有了运行结果
-
为什么同步代码块也可以解决上述的问题呢