文章目录

  • ==多线程基础==
  • 进程
  • 线程
  • 浏览器的进程和线程(案例)
  • 线程的异步和同步
  • 多线程的优势
  • ==多线程的实现方式==
  • 第一种:继承Thread类
  • 第二种:实现Runnable接口
  • 第三种:通过Callable 和 Future 创建线程
  • 第四种:通过线程池创建线程 Executers 和 ThreadPoolExecutor 两种方案
  • 创建线程的方案一:通过Executors创建
  • 方法一:newCachedThreadPool创建线程
  • 方法二:newFixedThreadPool创建线程(创建固定个数的线程池)
  • 方法三:newScheduledThread创建线程
  • 方法四:newSingleThreadExecutor创建线程
  • 案例:Executor类创建线程池
  • 创建线程的方案二:ThreadPoolExecutor
  • 案例
  • ThreadFactory介绍
  • Future类
  • 线程停止执行可调用的方法
  • ==线程池==
  • 概述
  • Thread类的相关方法
  • 线程控制sleep、join、setDaemon
  • Object类的等待和唤醒方法
  • ==线程调度==
  • 线程有两种调度模型
  • java使用抢占式调度模型
  • ==线程生命周期==
  • 概述
  • 案例:卖票
  • 案例:卖票改进
  • 多线程数据安全问题,买票问题解决
  • ==线程安全(同步)==
  • 同步代码块
  • 同步方法
  • 案例:同步代码块 和 同步方法
  • ==lock锁==
  • ==线程安全的类==
  • ==生产者消费者案例==
  • 概述
  • 案例代码
  • ==JMMjava内存模型==
  • 概述
  • 重排序
  • as-if-serial语义
  • happens-bofore原则
  • 概述
  • 1 程序顺序规则
  • 2 监视器锁规则
  • 3 Volatile变量规则
  • 4 传递性
  • 5 start()规则
  • 6 join()规则
  • 7 程序中断规则
  • 8 对象终结规则
  • 构造方法 和 析构方法 作用正好相反
  • finalize()方法
  • as-if-serial规则 和 happens-before规则区别


多线程基础

进程

  • 概念:正在运行的程序
  • 是系统进行资源分配和调用的独立单位
  • 每一个进程都有它自己的内存空间和系统资源

线程

  • 是进程中的单个顺序控制流,是一条执行路径
  • 描述了执行一段指令所需的时间
  • 单线程:一个进程如果只有一条执行路径,则称为单线程程序
    单线程举例:打开记事本,给记事本写内容,当又进行设置纸张大小时,此时再给记事本写内容就写不进去了,这就证明这是一个单线程。
  • 多线程:一个进程如果有多条执行路径,则称为多线程程序
    多线程举例:扫雷程序,鼠标控制的游戏点击、页面右上角的时间控制。
    多线程的运行安全体现在:原子性、可见性、有序性。

浏览器的进程和线程(案例)

浏览器中有渲染进程、插件进程、网络进程、浏览器主进程、GPU进程。

备注2022/8/11

  • 调用start()方法可以使一个线程成为可运行的,但是它不一定立即开始运行。√
    使用start方法只能将进程放到就绪队列中,并不能立即执行。
  • 当一个线程因为抢占机制而停止运行,它被放在可运行队列的后面。
  • 一个线程可能因为不同的原因停止(cease)并进入就绪状态。√
    一个线程可能由于之前资源不可用导致进入阻塞状态,然后当资源可用的时候,就会进入到就绪态。
    线程的时间片用完会从运行态转为就绪态。
    优先级更高的线程会抢占当前线程,使当前线程进入就绪态。

线程的异步和同步

区别

同步

异步

概念

同步过程调用发出后,在没有得到结果之前,该调用不返回或不继续执行后续操作

多个线程同时访问同一个资源,一个线程访问结束之后,别的线程才能访问

异步过程调用后,调用者没有得到结果之前,就可以继续执行后续操作

调用结果,通过状态、通知和回调来通知调用者

操作

必须执行到底才能执行其他操作

操作可以同时执行

使用锁

多个线程使用同一把锁

多个线程在执行过程中使用不同的锁

多线程的优势

  • 为了充分利用计算机资源,让多个程序在同时运行的过程中公平竞争资源,充分利用计算机的处理能力;
  • 线程为多处理器系统中并行的使用硬件提供了一个自然而然的分解,同一个程序的多个线程可以在多个CPU上的情况下同时调度;

举例
CPU、内存、IO之间的差异:CPU运行的单位是cycle,时钟周期,一次简单的运行大约需要1-2个时钟周期,也就是1纳秒;内存一次寻址时间是1-10微秒,则CPU就需要等待1000-10000个时钟周期;硬盘寻址是1-10毫秒,寻址一次就需要等待上百万个时钟周期;一个线程等待IO的时候,可以让CPU去调度别的线程;

多线程的实现方式

  • Thread类
  • 在java.lang包下,使用不需要导包
  • 线程是程序中执行的线程,java虚拟机允许应用程序同时执行多个执行线程。

第一种:继承Thread类

1 将一个类声明为一个Thread的子类
	public class 子类 extends Thread{}
	
2 该子类应该重写Thread类中的run方法,
	@Override
	public void run(){
		线程具体执行操作;
	}
	
3 创建该子类的对象,
	子类 对象名 = new 子类();
	
4 启动线程;
	对象名.start();
使用start()方法启动。
void start()导致此线程开始执行,java虚拟机调用此线程的run方法

案例:

// 310-test1
// 测试类
public class Demo {
    public static void main(String[] args){
        MyThread m1 = new MyThread();
        MyThread m2 = new MyThread();

        // 直接调用MyThread类中的run方法并没有启动线程;
//        m1.run();
//        m2.run();
        // 这种方式 m1执行完0-99的输出之后,m2才开始执行0-99的输出。

        // void start()导致此线程开始执行,java虚拟机调用此线程的run方法
        m1.start();
        m2.start();

        // 这种方式,输出结果不再是0-99 0-99 而变成: 0 0-17 1-7 18-99 8-99 
    }
}

// 继承Thread的子类
public class MyThread extends Thread {
    @Override
    public void run() {
        for(int i = 0 ;i < 100;i++){
            System.out.println(i);
        }
    }
}

第二种:实现Runnable接口

1 声明一个实现Runnable接口的类
	public class 类xxx implements Runnable{}
	
2 类xxx中重写run方法
	@Override
	public void run(){
		这里要获取线程的名字,不能直接使用getName(),因为本类没有继承Thread类,getName()是Thread类方法。
		获取线程名字:Thread.currentThread().getName()
	}
	
3 创建类xxx的对象
	类xxx 对象名x = new 类xxx();
	
4 创建Thread类对象,把类xxx对象作为构造方法的参数
	Thread 对象名th = new Thread(对象名x);
	
5 启动线程
	对象th.start();

实现Runnable接口的方式创建线程的好处:
①没有继承Thread类,好处是可以有它自己的父类 ——避免了java单继承的局限性;

②创建实现Runnable接口的类,所创建的对象,作为参数传递到Thread构造方法,这样看成一个资源有多个线程。
适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码、数据有效分离,较好的体现了面向对象的设计思想。

缺点
Runnable接口里面的run方法不能返回操作结果。

代码:

//310-test5
实现Runnable接口的类
public class MyRunnable  implements Runnable{
    @Override
    public void run() {
        for(int i = 0 ; i < 100 ; i ++){
            System.out.println(Thread.currentThread().getName() + ": " + i);
            // 不能直接使用getName方法
        }
    }
}

测试:
public class Demo {
    public static void main(String[] args) {
        // 创建类xxx的对象
        MyRunnable mr = new MyRunnable();

        // 创建Thread类的对象
        // 构造方法:Thread (Runnable target)
/*        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);*/
        // 构造方法 Thread (Runnable target,String name)
        Thread t1 = new Thread(mr,"pretty");
        Thread t2 = new Thread(mr,"sunshine");
        // 启动线程
        t1.start();
        t2.start();
    }
}

第三种:通过Callable 和 Future 创建线程

为了解决Runnable接口中run方法不能返回操作结果的问题。java提供Callable接口。

涉及内容

  • Callable接口
    Callable接口实现线程创建,需要重写call方法;
  • FutureTask类
    public class FutureTask< V > implements RunnableFuture< V >
    该接口主要负责Callable接口对象操作,且RunnableFuture接口继承自Runnable、Future接口。

创建过程

1 创建实现Callable接口的类
	public class 子类 implements Callable{}
	
2 重写Callable接口的call方法
	@Override
	public object call() throws Exception{
		所要执行的操作
		return 返回值;
		}
		
3 创建子类对象
	子类 对象名son = new 子类();
	
4 创建FutureTask对象 包装Callable对象
	FutureTask ft = new FutureTask(对象名son);
	
5 启动线程 通过Thread(Runnable target) 创建线程 start()方法启动线程
	new Thread(ft).start();
	
6 获取线程的返回值,通过FutureTask对象的get方法
	ft.get();

第四种:通过线程池创建线程 Executers 和 ThreadPoolExecutor 两种方案

创建线程的方案一:通过Executors创建

类名

说明

newCachedThreadPool

一个可缓冲线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程

newFixedThreadPool

一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待

newScheduledThread

一个定长线程池,支持定时及周期性任务执行

newSingleThreadExecutor

一个单线程化的线程池,只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO、LIFO、优先级)执行

Executor方法的弊端

  • FixedThreadPool 和 SingleThreadExecutor:允许请求的队列长度是Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM(Out of Memory)。
  • CachedThreadPool 和 ScheduledThreadPool:允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量线程,从而导致OOM(Out of Memory)。

四种方法总结

  • 四种方式都是Executors类中的方法。
  • 线程池具体的操作,newCachedThreadPool、newFixedThreadPool、newSingleThreadExecutor都是execute方法,而newScheduledThread是schedule方法
  • 四种方法创建的对象都是ExecutorService对象。
方法一:newCachedThreadPool创建线程
1 通过newCachedThreadPool创建ExecutorService对象,Executors.newCachedThreadPool()方法 
	 ExecutorService es = Executors.newCachedThreadPool();
	 
2 通过ExecutorService对象的execute(Runnable target)方法创建对象
	es.execute(new Runnable(){//采用匿名内部类的形式
		@Override
		public void run(){
			要执行的操作;
			}
		});
方法二:newFixedThreadPool创建线程(创建固定个数的线程池)
1 通过newFixedThreadPool创建ExecutorService对象,Executors.newFixedThreadPool()方法 
	 ExecutorService es = Executors.newFixedThreadPool();
	 
2 通过ExecutorService对象的execute(Runnable target)方法创建对象
	es.execute(new Runnable(){//采用匿名内部类的形式
		@Override
		public void run(){
			要执行的操作;
			}
		});
方法三:newScheduledThread创建线程
1 通过newScheduledThread创建ScheduledExecutorService对象,Executors.newScheduledThread()方法 
	 ScheduledExecutorService ses = Executors.newFixedThreadPool();
	 
2 通过ScheduledExecutorService对象的schedule(Runnable target)方法创建对象
	ses.schedule(new Runnable(){//采用匿名内部类的形式
		@Override
		public void run(){
			要执行的操作;
			}
		},3,TimeUnit.SECONDS)};

介绍

ScheduledFuture<?> ScheduledExecutorService对象.schedule(Runnable command,long delay,TimeUnit unit) 创建并执行在给定延迟后启动的单次操作。

Runnable command:要执行的任务;long delay:从现在开始延迟执行的时间;TimeUnit unit:延迟参数的时间单位。
方法四:newSingleThreadExecutor创建线程
1 通过newSingleThreadExecutor 创建ExecutorService 对象,Executors.newSingleThreadExecutor()方法 
	ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
	
2 通过ExecutorService对象的execute(Runnable target)方法创建对象
	es.execute(new Runnable(){//采用匿名内部类的形式
		@Override
		public void run(){
			要执行的操作;
			}
		});
案例:Executor类创建线程池
// 310-test5
public class ExecutorsDemo {
    public static void main(String[] args) {
        test4();
    }
    public static void test1(){
        // 创建可缓冲的线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        for(int i = 0 ; i <10 ; i++){
            int temp = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("ThreadName" + Thread.currentThread().getName() + ",i" + temp);
                }
            });
        }
        //输出结果 :创建线程的顺序和执行println语句的顺序不一样,按理说第一个创建的线程是thread-1,i0、第二个是thread-2,i1,但是第二个创建的线程是第五个执行println的。
        // ThreadNamepool-1-thread-1,i0
        //ThreadNamepool-1-thread-5,i4
        //ThreadNamepool-1-thread-4,i3
        //ThreadNamepool-1-thread-3,i2
        //ThreadNamepool-1-thread-2,i1
        //ThreadNamepool-1-thread-6,i5
        //ThreadNamepool-1-thread-7,i6
        //ThreadNamepool-1-thread-8,i7
        //ThreadNamepool-1-thread-9,i8
        //ThreadNamepool-1-thread-10,i9
    }
    public static void test2(){
        // 创建定长线程池,超出的线程会在队列等待;
        ExecutorService es = Executors.newFixedThreadPool(3);
        for(int i = 0 ; i <10 ; i++){
            int temp = i;
            es.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("ThreadName" + Thread.currentThread().getName() + ",i" + temp);
                }
            });
        }
    }
    //输出结果 创建一个定长线程,最大并发数是3,其余线程会在队列中等待。
    // 并发最大数是3, Thread.currentThread().getName()获得内容是1-3 说明并发线程数是3 
    // ThreadNamepool-1-thread-1,i0
    //ThreadNamepool-1-thread-2,i1
    //ThreadNamepool-1-thread-3,i2
    //ThreadNamepool-1-thread-2,i3
    //ThreadNamepool-1-thread-3,i4
    //ThreadNamepool-1-thread-1,i6
    //ThreadNamepool-1-thread-2,i5
    //ThreadNamepool-1-thread-1,i8
    //ThreadNamepool-1-thread-3,i7
    //ThreadNamepool-1-thread-2,i9

    public static void test3(){
        // 创建定长线程池,且定时及周期性执行任务
        ScheduledExecutorService ses = Executors.newScheduledThreadPool(5);
        for(int i = 0 ; i <10 ; i++){
            final int temp = i;
            ses.schedule(new Runnable() {
                public void run() {
                    System.out.println("i:" + temp);
                }
            },3, TimeUnit.SECONDS);
        }
    }
    // 输出结果 :表示延迟三秒
    //ThreadNamepool-1-thread-5,i4
    //ThreadNamepool-1-thread-1,i0
    //ThreadNamepool-1-thread-2,i2
    //ThreadNamepool-1-thread-4,i3
    //ThreadNamepool-1-thread-3,i1
    //ThreadNamepool-1-thread-4,i8
    //ThreadNamepool-1-thread-2,i7
    //ThreadNamepool-1-thread-1,i6
    //ThreadNamepool-1-thread-5,i5
    //ThreadNamepool-1-thread-3,i9

    public static void test4(){
        //1.创建单线程
        ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            newSingleThreadExecutor.execute(new Runnable() {

                @Override
                public void run() {
                    System.out.println("index:" + index);
                    try {
                        Thread.sleep(200);
                    } catch (Exception e) {
                        // TODO: handle exception
                    }
                }
            });
        }
        newSingleThreadExecutor.shutdown();
    }
    //输出结果:结果依次输出,相当于顺序执行各个任务,执行shutdown 停止线程。
    //ThreadNamepool-1-thread-1,i0
    //ThreadNamepool-1-thread-1,i1
    //ThreadNamepool-1-thread-1,i2
    //ThreadNamepool-1-thread-1,i3
    //ThreadNamepool-1-thread-1,i4
    //ThreadNamepool-1-thread-1,i5
    //ThreadNamepool-1-thread-1,i6
    //ThreadNamepool-1-thread-1,i7
    //ThreadNamepool-1-thread-1,i8
    //ThreadNamepool-1-thread-1,i9
    
}

介绍
ExecutorService类的shutdown()方法,返回值是void,描述:启动有序关闭,其中先提交的任务将被执行,但不会接收任何新任务。

创建线程的方案二:ThreadPoolExecutor

ThreadPoolExecutor构造方法有7个参数:

参数名

介绍

int corePoolSize

核心线程数量

要保留在池中的线程数,即使它们处于空闲状态,除非设置了allowCoreThreadTimeOut

int maximuPoolSize

最大线程数量

long keepAliveTime

存活时间

当线程数大于核心线程数时,多于的空闲线程在终止之前等待新任务的最大时间

TimeUnit unit

时间单位

BlockingQueue< Runnable > workQueue

阻塞队列

执行任务之前用于保存任务的队列,

ThreadFactory threadFactory

拒绝策略

线程工厂:执行程序创建线程时使用的工厂方法。该方法可以设置线程的优先级、线程命名规则以及线程类型(用户线程还是守护线程)等

RejectedExecutionHandler handler

执行被阻止时使用的处理程序,也就是说队列和线程池都满了,线程池处于一种饱和状态,需要采取的一种策略处理提交的新任务。

默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。

ThreadPoolExecutor参数值介绍:

参数名

参数值说明

TimeUnit unit

配合 keepAliveTime使用,参数值:TimeUnit.xxx其中xxx = DAYS HOURS MINUTES SECONDS MILLISECONDS MICROSECONDS MANOSECONDS

BlockingQueue< Runnable > workQueue

ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue、DelayQueue、LinedTransferQueue、LinkedBlockingDeque

BlockingQueue参数具体值介绍:

阻塞队列名字

说明

ArrayBlockingQueue

数组结构组成的有界阻塞队列

LinkedBlockingQueue

链表结构组成的有界阻塞队列

LinkedTransferQueue

链表结构组成的无界阻塞队列,和SynchronousQueue类似,还含有非阻塞方式

LinkedBlockingDeque

链表解耦组成的双向阻塞队列

SynchronousQueue

不存储元素的阻塞队列,即直接提交给线程不保持它们

PriorityBlockingQueue

支持优先级排序的无界阻塞队列

DelayQueue

使用优先级队列实现的无界阻塞队列,只有在延时期满时才能从中提取元素

RejectExecutionHandler参数介绍

策略

说明

AbortPolicy

默认

直接抛出异常

CallerRunsPolicy

只用调用者所在线程来运行任务

线程调用运行该任务的execute本身

DiscardPolicy

不能执行的任务将被删除

DiscardOldestPolicy

丢弃队列里面最近的一个任务,并执行当前任务

如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序

threadPoolExecutor常用方法介绍

方法名

说明

public Future< ? > submit(Runnable task)

提交一个可运行的任务执行,并返回一个表示该任务的Future

该方法继承自AbstractExecutorService类

Future 表示异步计算的结果

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue< Runnable > workQueue, ThreadFactory threadFactory)

使用给定的初始参数和默认拒绝的执行处理程序 创建一个线程工厂

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue< Runnable > workQueue)

使用给定的初始参数 创建一个线程工厂

思路

1 通过ThreadFactory创建线程工厂,重写 Thread newThread(Runnable r) 方法
	ThreadFactory threadfactoryxhj = new ThreadFactory(){
		@Override
		public Thread newThread(Runnable r){ }
	};
	
2 通过ThreadPoolExecutor创建线程池
	ThreadPoolExecutor threadPoolExecutorxhj 
	= new threadPoolExecutor(corePoolsize核心线程数,maximumPoolSize最大线程数,
	keepAliveTime等待时间,unit等待时间的单位,
	workQueue阻塞队列,threadfactoryxhj创建线程的工厂);
			
3 使用自定义的线程工厂,使用匿名构造类的形式,submit参数是Runnable接口,run里面内容是对于创建的线程具体执行什么
	threadPoolExecutorxhj.submit( new Runnable(){
		@Override
		public void run(){}
	};
案例
package com.javaface430.test1;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadFactoryTest {
    public static void main(String[] args) {
        // 创建线程工厂
        ThreadFactory threadFactory = new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                // 创建线程池中的线程
                Thread thread = new Thread(r);
                // 设置线程名称 hashcode方法是Object中的,用于返回对象的哈希码值
                thread.setName("Thread-" + r.hashCode());
                // 设置线程优先级(最大值:10)
                thread.setPriority(Thread.MAX_PRIORITY);
                // ......
                return thread;
            }
        };

        // 创建线程池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 0,TimeUnit.SECONDS, new LinkedBlockingQueue<>(),threadFactory);
        // 参数说明:核心线程数 = 10;最大线程数 = 10;线程数多于核心线程数时,多于空闲线程等待新任务的时间 = 0;等待时间的单位 = 秒;阻塞队列 = 链表组成的有界阻塞队列;threadFactory = 创建线程的工厂

        // 使用自定义的线程工厂
        threadPoolExecutor.submit(new Runnable() {
            // submit 提交一个可运行的任务执行;采用的匿名构造类的形式
            @Override
            public void run() {
                // 获取当前正在执行的线程的引用
                Thread thread = Thread.currentThread();
                // 输出线程的名字以及线程的优先级
                System.out.println(String.format("线程:%s,线程优先级:%d",thread.getName(), thread.getPriority()));                                           
            }    
        });
    }
    // 输出结果
    // 线程:Thread-1879492184,线程优先级:10
}
ThreadFactory介绍

在软件包java.util.concurrent下,使用需要导包;
public interface ThreadFactory 根据需要创建新的线程;
具体实现类,需要重写public Thread newThread(Runnable r){} 方法。

最简单的案例:

class SimpleThreadFactory implements ThreadFactory{
	@Override
	public Thread newThread(Runnable r){
		return new Thread(r);
	}
}
Future类

在软件包java.util.concurrent包下,使用的时候需要导包;
public interface Future < V > 表示异步计算的结果。提供方法来检查计算是否完成,等待其完成,并检索计算结果;

常用方法

方法名

说明

boolean cancel(boolean mayInterruptIfRunning)

尝试取消执行此任务

V get()

等待计算完成,然后检索其结果

V get(long timeout,TimeUnit unit)

如果需要等待最多在给定时间计算完成,然后检索其结果

boolean isCancelled()

如果此任务在正常完成之前被取消,则返回true

boolean isDone()

如果此任务完成,则返回true

线程停止执行可调用的方法

①程序中可以直接使用thread.stop()来强行终止线,调用之后创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放所有的子线程所持有的所有锁。
②sleep() Thread类的方法,主要是为了暂停当前线程,让出cpu,使线程进入阻塞状态,直到休眠时间满了,就进入就绪状态;
③yield() Thread类的方法,让线程进入就绪状态,让出CPU的使用权。
④join() 调用该方法的线程强制执行,其他线程处于阻塞状态,该线程执行完毕后,其他线程在执行。

线程池

概述

线程池是运用场景最多的并发框架,几乎所有需要异步或者并发执行任务的程序都可以使用线程池。

线程池好处:

好处

说明

降低资源消耗

通过重复利用已创建的线程,降低线程创建和销毁造成的消耗

提高响应速度

当任务到达时,任务可以不需要等待线程创建就能立即执行

提高线程的可管理性

线程是稀缺资源,无限制地创建,会消耗系统资源并降低系统的稳定性。

两个小问题:

  • 为什么重写run()方法?
    run()是用来封装被线程执行的代码
  • run方法和start方法的区别?
    run():封装线程执行的代码,直接调用,相当于普通方法的调用,无法达到多线程的目的。一个线程可以多次调用run方法。
    start():启动线程,轮到该线程执行由JVM调用此线程的run方法。一个线程只能调用一次start方法。

Thread类的相关方法

方法名

说明

void setName(String name)

将此线程的名称更改等于参数name

String getName()

返回此线程的名称

构造方法

Thread(Runnable target,String name)

Thread(Runnable target)

构造方法含义

通过构造方法设置线程名称

创建线程

static Thread currentThread()

返回当前正在执行的线程对象的引用

Thread.currentThread.getName()

获取当前正在执行的线程对象的名称

public final int getPriority()

返回该线程的优先级

public final void setPriority(int newPriority)

设置该线程的优先级为newPriority

static void sleep(long millis)

使当前正在执行的线程停留(暂停执行)指定的毫秒数

void join()

等待这个线程死亡

也就是当一个线程调用了join方法,则其他线程必须等待这个线程执行完毕之后才能执行

void setDaemon(boolean on)

将此线程标记为守护线程,当运行的线程都是守护线程时,java虚拟机将退出

extends Object

public int hashcode()

返回对象的哈希码值

注意:

  • getName方法是在继承自Thread的子类中使用的
    没有设置线程名称的时候,默认的线程名称是 Thread-x
    线程名称是Thread-x的原因: 因为子类使用无参构造方法创建对象,会自动调用父类的无参构造方法。

父类的无参构造方法:

public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
   }
  • setName方法是在 测试类中书写
    对象.setName(String s)实现线程名的设置
    要想使用Thread类的带参构造方法直接在创建线程的时候设置线程名,需要在其子类中定义带参构造方法,并使用super(参数);
// 创建继承Thread类的子类
	public class 子类 extends Thread{
		// 构造方法
		public 子类(String name){
			super(name);
		}
}
  • 获取main方法的线程的名称
    static Thread currentThread() 返回当前正在执行的线程对象的引用
    Thread.currentThread.getName() 获取当前正在执行的线程对象的名称

案例:

// 310-test2
测试:
public class Demo {
    public static void main(String[] args){
        // 无参构造方法给出默认值
       /* MyThread m1 = new MyThread();
        MyThread m2 = new MyThread();

        m1.setName("pretty");
        m2.setName("sunshine");
        */
        /* setName的底层代码,说明它是一个同步方法。
        public final synchronized void setName(String name) {
            checkAccess();
            if (name == null) {
                throw new NullPointerException("name cannot be null");
            }

            this.name = name; 最关键
            if (threadStatus != 0) {
                setNativeName(name);
            }
        }
    */
    /*
        m1.start();
        m2.start();

        // 没有设置线程名称 有默认的,是Thread-x
        // 为什么线程名称是Thread-x 因为子类使用无参构造方法创建对象,会自动调用父类的无参构造方法。*/


        // 带参构造方法
/*        MyThread m1 = new MyThread("one");
        MyThread m2 = new MyThread("two");

        m1.start();
        m2.start();*/

        System.out.println(Thread.currentThread().getName());
    }
}

Thread的子类:
public class MyThread extends Thread{

    public MyThread(){}
    public MyThread(String name){
        super(name);
    }

    @Override
    public void run() {
        for(int i = 0 ;i <100 ; i++){
            System.out.println(getName() + ": " + i);
        }
    }
    /* 	Thread的构造方法、成员方法getName、
    	private volatile String name;
    	// 该成员变量使用volatile修饰,说明该成员变量可能会被其他线程所修改。
		构造方法
        public Thread() {
            init(null, null, "Thread-" + nextThreadNum(), 0);
        }

        public Thread(String name) {
            init(null, null, name, 0);
        }

       private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);

        private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
                            this.name = name;
                      }

        private static int threadInitNumber; // 0,1,2...
        private static synchronized int nextThreadNum() {
            return threadInitNumber++; // 0,1....
        }
		成员方法
        public final String getName() {
            return name;
        }

    * */
}

线程控制sleep、join、setDaemon

方法名

说明

static void sleep(long millis)

使当前正在执行的线程停留(暂停执行)指定的毫秒数

void join()

等待这个线程死亡

也就是当一个线程调用了join方法,则其他线程必须等待这个线程执行完毕之后才能执行

void setDaemon(boolean on)

将此线程标记为守护线程,当运行的线程都是守护线程时,java虚拟机将退出

注意:

  • 三个方法都是Thread类的方法
  • sleep在继承自Thread类的子类中使用; Thread.sleep()
  • join在测试类中使用;Thread对象.join()
  • setDaemon在测试类中使用;Thread对象.setDaemon()

代码:

// 310-test4
sleep:
public class ThreadSleep extends Thread {
    @Override
    public void run() {
        for(int i = 0 ;i < 100 ; i++){

            System.out.println(getName() + ": " + i);
            // 使用sleep方法 使每次执行之后的线程休眠1s
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

join:
public class ThreadJoinDemo {
    public static void main(String[] args) {
        ThreadJoin tj1 = new ThreadJoin();
        ThreadJoin tj2 = new ThreadJoin();
        ThreadJoin tj3 = new ThreadJoin();

        tj1.setName("康熙");
        tj2.setName("四阿哥");
        tj3.setName("八阿哥");

        tj1.start();
        try {
            tj1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tj2.start();
        tj3.start();
    }
}

setDaemon:
public class ThreadDaemonDemo {
    public static void main(String[] args) {
        ThreadDaemon td1 = new ThreadDaemon();
        ThreadDaemon td2 = new ThreadDaemon();


        td1.setName("关羽");
        td2.setName("张飞");

        // 设置主线程为刘备
        Thread.currentThread().setName("刘备");

        // 设置守护进程
        td1.setDaemon(true);
        td2.setDaemon(true);

        td1.start();
        td2.start();

        for(int i = 0; i < 10 ; i++){
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }

        // 案例背景:由于关羽、张飞、刘备桃园三结义,刘备为主线程,所以当刘备执行结束时,关羽张飞也应该停止。
    }
}

注意
把某些线程设置为守护线程, 当主线程停止时,其余设置为守护进程的不是立即消失,而是等一小段时间再消失。

Object类的等待和唤醒方法

方法名

说明

void wait()

导致当前线程等待,直到另一个线程调用该对象的notify()方法或者notifyAll()方法

void notify()

唤醒正在等待 对象监视器 的单个线程

void notifyAll()

唤醒正在等待 对象监视器 的所有线程

线程调度

线程有两种调度模型

  • 分时调度模型:所有线程轮流获得CPU的使用权,平均分配每个线程占用CPU的时间片;
  • 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,则随机选择一个。优先级高的线程获取的CPU时间片相对多一些。

java使用抢占式调度模型

  • 解释:假设只有一个cpu,它在某一个时刻只能执行一条指令,线程只有得到CPU的时间片(使用权)才可以执行指令;多线程程序的执行具有随机性,谁抢到CPU的使用权是不一定的。
  • Thread类中获得和设置线程优先级的方法:
    public final int getPriority():返回该线程的优先级
    public final void setPriority(int newPriority):更改此线程的优先级

注意:

  • 线程默认的优先级是 NORM_PRIORITY = 5,最大的优先级:MAX_PRIORITY = 10,最小的优先级:MIN_PRIORITY = 1
  • 线程优先级高仅仅表示线程获取的CPU时间片的几率高,但是要在次数比较多,或者多次运行的时候才能看到想要的效果。

代码:

// 310-test3
public class Demo {
    public static void main(String[] args){
    // ThreadPriority该类是继承自Thread类的子类 自己创建的。
        ThreadPriority tp1 = new ThreadPriority();
        ThreadPriority tp2 = new ThreadPriority();
        ThreadPriority tp3 = new ThreadPriority();

        tp1.setName("rose");
        tp2.setName("lily");
        tp3.setName("sunflower");

/*        tp1.start();
        tp2.start();
        tp3.start();*/

        // 获取线程的优先级
        /*System.out.println(tp1.getPriority()); // 5
        System.out.println(tp2.getPriority()); // 5
        System.out.println(tp3.getPriority()); // 5*/

        // 设置参数优先级
//        tp1.setPriority(10000);//IllegalArgumentException
        // 表示优先级参数不在指定范围内。优先级范围是 MIN_PRIORITY = 1 和 MAX_PRIORITY = 10
        // NORM_PRIORITY= 5

        tp1.setPriority(1);
        tp2.setPriority(5);
        tp3.setPriority(10);

        tp1.start();
        tp2.start();
        tp3.start();

    }
}

线程生命周期

概述

包括五个部分:

一:新建——创建线程对象

二:就绪——有执行资格,没有执行权

三:运行——有执行资格,有执行权

四:阻塞——没有执行资格,没有执行权

五:死亡——线程死亡,变成垃圾

Java 使用的池化_ide

案例:卖票

需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口买票,请设计一个程序模拟该电影院买票。

// 310-test6
类:
public class SellTicket implements Runnable {

    private int tickets = 100;
    @Override
    public void run() {
        while(true){
            if(tickets > 0){
                System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                tickets--;
            }
        }
    }
}

测试代码:
public class Demo {
    public static void main(String[] args) {

        SellTicket st = new SellTicket();

        Thread th1 = new Thread(st,"窗口1");
        Thread th2 = new Thread(st,"窗口2");
        Thread th3 = new Thread(st,"窗口3");

        th1.start();
        th2.start();
        th3.start();
    }
}

案例:卖票改进

需求:每次出票时间100毫秒,用sleep方法实现

代码出现问题:

  • 相同的票出售了多次
  • 出现售卖负数票的情况

问题的原因:

  • 线程执行的随机性导致的。
// 310-test7
public class SellTicket implements Runnable {

    private int tickets = 100;
//    @Override
//    public void run() {
//        while(true){
//            if(tickets > 0){
//                // 通过sleep方法来模拟出票时间
//                try {
//                    Thread.sleep(100);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
//
//                System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
//                tickets--;
//            }
//        }
//    }

    // 问题分析:
    // 采用sleep模拟出票时间的时候,出现问题
    ** 问题1 : 相同的票出现多次
    // 视频中相同票出现很多次的情况,在自己电脑上运行 出现的是 窗口1售卖所有的票。
    // 证实是由于jdk版本问题导致的问题。
//    @Override
//    public void run() {
//        while(true){
//            // tickets = 100
//            // t1 t2 t3
//            // 假设t1线程抢到CPU的执行权
//            if(tickets > 0){
//                // 通过sleep方法来模拟出票时间
//                try {
//                    Thread.sleep(100);
//                    // t1 需要等待100毫秒
//                    // t2 抢到了CPU的执行权,t2开始执行,执行到这里,t2也会等待100毫秒
//                    // t3 抢到了CPU的执行权,t3开始执行,执行到这里,t3也会等待100毫秒
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
//                // 假设 按照抢到CPU的执行权的顺序 解释等待的100毫秒
//                // t1 抢到CPU的执行权,在控制台输出,窗口1正在出售第100张票
//                // t2 抢到CPU的执行权,在控制台输出,窗口1正在出售第100张票
//                // t3 抢到CPU的执行权,在控制台输出,窗口1正在出售第100张票
//                System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
//                // 按理说这里应该是 和 上一代码一起执行的,但是由于三个线程同时运行,这里也会被依次执行三次,最终票变成了97
//                tickets--;
//            }
//        }
//    }

    ** 问题2 : 输出结果售卖的票 变成负数
    @Override
    public void run() {
        while(true){
            // tickets = 1
            // t1 t2 t3
            // 假设t1线程抢到CPU的执行权
            if(tickets > 0){
                // 通过sleep方法来模拟出票时间
                try {
                    Thread.sleep(100);
                    // t1 需要等待100毫秒
                    // t2 抢到了CPU的执行权,t2开始执行,执行到这里,t2也会等待100毫秒
                    // t3 抢到了CPU的执行权,t3开始执行,执行到这里,t3也会等待100毫秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 假设 按照抢到CPU的执行权的顺序 解释等待的100毫秒
                // t1 抢到CPU的执行权,在控制台输出,窗口1正在出售第1张票
                // 继续执行tickets--,这时tickets = 0;
                // t2 抢到CPU的执行权,在控制台输出,窗口1正在出售第0张票
                // 继续执行tickets--,这时tickets = -1;
                // t3 抢到CPU的执行权,在控制台输出,窗口1正在出售第-1张票
                // 继续执行tickets--,这时tickets = -2;

                System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                tickets--;
            }
        }
    }
}

多线程数据安全问题,买票问题解决

判断多线程程序是否会有数据安全问题的标准

  • 是否 是多线程环境
  • 是否 有共享数据
  • 是否 有多条语句操作共享数据

解决多线程安全问题:

  • 基本思想:让程序没有安全问题的环境。

实现:

  • 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行。
  • Java提供了同步代码块的方式来解决。

代码:

// 310-test8
售票类:
public class SellTicket implements Runnable {

    private int tickets = 100;
    // 要想实现锁住程序,必须使用同一把锁,也就是同一个Object对象
    private Object obj = new Object();
    @Override
    public void run() {
        while (true) {
            // tickets = 100
            // 线程t1 t2 t3
            // 假设t1抢到了cpu执行权
            // 假设t2抢到了cpu执行权 继续向下执行 发现被锁了 只能等待

# 使用new Object()这样当每一个线程进来的时候,都会创建一个新的锁,这样是锁不住的。
   也就是当synchronized的参数是new Object()的时候,是不能实现锁住程序的,因为每次进来都是新的obj。
            synchronized (obj) {
                // t1 进来,把这段代码锁起来了
                if (tickets > 0) {
                    // 通过sleep方法来模拟出票时间

                    try {
                        Thread.sleep(100);
                        // t1休息100毫秒 此时假设t2抢到了cpu执行权
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    // 窗口1正在出售第100张票
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                    tickets--; // tickets = 99
                }else{
                    break;
                }
                // t1出来,锁被释放。
            }
        }
    }
}

测试类:
public class Demo {
    public static void main(String[] args) {

        SellTicket st = new SellTicket();

        Thread th1 = new Thread(st,"窗口1");
        Thread th2 = new Thread(st,"窗口2");
        Thread th3 = new Thread(st,"窗口3");


        th1.start();
        th2.start();
        th3.start();
    }
}

线程安全(同步)

同步的好处和弊端:
好处:解决了多线程的数据安全问题。
弊端:当线程很多时,每个线程都会去判断同步上的锁,会造成资源的浪费,会降低程序的运行效率。

死锁
概念:
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情况。

某一个同步代码块同时拥有“两个以上对象锁”时,就可能会发生死锁问题。

产生死锁的四个必要条件:

条件

解释

互斥条件

一个资源每次只能被一个进程使用

请求与保持条件

一个进程因请求资源而阻塞时,对已获得的资源保持不放

不可剥夺条件

进程已经获得的资源,在未使用完毕之前,不能强行剥夺

循环等待条件

若干进程之间形成一种头尾相接的循环等待资源关系

同步代码块

锁多条语句操作共享数据,可以使用同步代码块实现。

格式:

synchronized(obj){
	多条语句操作共享数据的代码。
}
synchronized(obj):将相当于给代码加锁了,任意对象obj就可以看成是一把锁

内置锁

普通方法中的同步代码块的锁是:this
静态方法中的同步代码块的锁是:类名.class

解释
obj称之为同步监视器,可以是任意对象,推荐使用共同资源作为同步监视器;
同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是对象本身、或者是class(反射中讲解)
同步监视器的执行过程:
第一个线程访问,锁定同步监视器,执行其中代码;
第二个线程访问,发现同步监视器被锁定,无法访问;
第一个线程执行完毕,解锁同步监视器;
第二个线程访问,发现同步监视器没有被锁定,然后锁定并访问。

同步方法

同步方法:就是把synchronized关键字加到方法上

格式:

修饰符 synchronized 返回值类型 方法名(方法参数){}

同步静态方法:就是把synchronized关键字加到static方法上

格式:

public static synchronized 修饰符 方法名(方法参数){}

案例:同步代码块 和 同步方法

// 310-test9
public class SellTicket implements Runnable {

    //    private int tickets = 100;
    // 将sellTicket方法设置为static,那么其中的变量tickets也得是static修饰
    private static int tickets = 100;
    // 要想实现锁住程序,必须使用同一把锁,也就是同一个Object对象
    private Object obj = new Object();
    private int x = 0;

    @Override
    public void run() {
        while (true) {
            if (x % 2 == 0) {
                // 原始的是: synchronized (obj) {

                // 使用方法sellTicket出现错误,这里改为this就可以了
                // 视频中是这样的,但是自己电脑没有出现问题。
                //  synchronized (this) {

                // 使用静态方法 视频出错,
                // 这里改为  synchronized (方法名.class) {
                // 这种形式 在自己电脑上报错
                synchronized (this) {
                    if (tickets > 0) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                        tickets--;
                    } else {
                        break;
                    }
                }
            } else {
                sellTicket();
                if (tickets == 0) {
                    break;
                }
            }
            x++;
        }
    }

    // 视频中,一个采用方法,一个直接书写,会出现两个窗口同时售卖一张票的情况,原因是 if 和 else 的锁不一样,方法的锁是this,但是现在却没有问题,可能的原因是:jdk视频是1.9 jdk自己是1.8
    /*private synchronized void sellTicket() {
            if (tickets > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
                tickets--;
            }
    }*/


// 同步静态方法
    // 视频中使用静态方法 没有加锁 会报错,但是实际操作中却没有报错
    // 视频中给静态方法加synchronized 但是还是报错
    // 需要将if中的锁改为sellTicket.class
    private static synchronized void sellTicket() {
        if (tickets > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
            tickets--;
        }
    }
}

lock锁

  • interface Lock 说明它是接口。
  • 它实现提供比 synchronized方法和语句 更广泛的锁定操作。
  • 它提供了获得锁和释放锁的方法:void lock()获得锁;void unlock() 释放锁
  • 它是接口不能直接创建对象,所以需要它的实现类,使用ReentrantLock(可重入锁)
  • ReentrantLock():创建一个ReentrantLock的实例

理解:

加锁和释放锁的工作:在Runnable的实现类中加的

一般采用如下形式加锁以及释放锁:
private Lock lock = new ReentrantLock();
try{
	lock.lock();
}finally{
	lock.unlock();
}

代码:

//311-test1
public class SellTicket implements Runnable {
    private int ticket = 100;
    private Lock lock = new ReentrantLock();
    @Override
    public void run() {
        while(true) {
            try {
                lock.lock();
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // Thread.currentThread().getName() 获取当前正在运行的线程的名字
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "张票");
                    ticket--;
                }
            }finally {
                lock.unlock();
            }
        }
    }
}

线程安全的类

StringBuffer

  • 线程安全,可变的字符序列。
  • jdk5之后被StringBuilder替代,它被设计为使用一个线程。
  • 一般情况下使用StringBuilder类,因为它支持与StringBuffer相同的所有操作,但它更快,因为它不执行同步。

Vector

  • implements List,说明它是List体系的集合,使其成为java Collections Framework的成员;
  • Vector类实现了可扩展的对象数组,说明和ArrayList用法相同。
  • 两者的区别:Vector被同步,如果不需要线程安全,建议使用ArrayList替代Vector;

HashTable

  • implements Map< K, V >说明它和HashMap差不多,使其成为java collections Framework的成员;
  • 该类实现了一个哈希表,它将键映射到值。任何非null对象都可以用作键值或值;
  • Hashtable被同步,如果不需要线程安全,建议使用HashMap替代Hashtable;

总结:

  • 1 上述三个类 其中的方法都加了synchronized关键字,说明它是同步的,也就是线程安全的;
  • 2 StingBuffer在多线程环境中被使用,剩余两个Vector和Hashtable被xx替代。
1 Vector可以被这个方法所替代。
Collections.synchronizedList(new ArrayList<>())
2 Hashtable可以被这个方法所替代
Collections.synchronizedMap(new HashMap<K,V>())

生产者消费者案例

概述

  • 生产者消费者模式是一个十分经典的多线程写作模式
  • 生产者消费者问题,包含两个线程:①生产者线程用于生产数据 ②消费者线程用于消费数据
  • 生产者消费者关系解耦 :生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;消费者只需要从共享数据区中获取数据,并不需要关心生产者的行为。

案例代码

//311-test2
// 奶箱类 表示共享数据区
public class Box {
    private int milk;
    // 定义成员变量,表示奶箱的状态
    private boolean state = false;

    // 报异常IllegalMonitorStateException 因为 wait方法没有使用在同步中。

    // 存储牛奶 同步方法
    public synchronized void put(int milk){
        // 如果奶箱有牛奶,等待消费
        if(state){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 如果没有牛奶,就生产牛奶
        this.milk = milk;
        System.out.println("送奶工将第" + this.milk + "瓶奶放入奶箱");
        // 生产完毕之后,修改奶箱状态
        state = true;
        // 唤醒其他等待的线程
        notifyAll();
    }
    
    // 获取牛奶  同步方法
    public synchronized void get(){
        // 如果奶箱没有牛奶,就等待 
        if(!state){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 如果 有就可以取牛奶
        System.out.println("用户拿到第" + this.milk + "瓶奶");
        // 这里不应该milk--吗?
        // 消费完毕之后,修改奶箱状态
        state = false;
        // 唤醒其他线程
        notifyAll();
    }
}

// 生产者类
public class Producer implements Runnable {
    private Box b;
    public Producer(Box b){
        this.b = b;
    }
    @Override
    public void run() {
        for(int i=1;i<=5;i++){
            b.put(i);
        }
    }
}

// 消费者类
public class Customer implements Runnable {
    private Box b;
    public Customer(Box b){
        this.b = b;
    }
    @Override
    public void run() {
        while(true){
            b.get();
        }
    }
}

// 测试类
public class Demo {
    public static void main(String[] args) {
        Box b = new Box();

        Producer p = new Producer(b);

        Customer c = new Customer(b);

        Thread t1 = new Thread(p,"生产者");
        Thread t2 = new Thread(c,"消费者");

        t1.start();
        t2.start();
 		// 输出结果
        // 送奶工将第1瓶奶放入奶箱
        //用户拿到第1瓶奶
        //送奶工将第2瓶奶放入奶箱
        //用户拿到第2瓶奶
        //送奶工将第3瓶奶放入奶箱
        //用户拿到第3瓶奶
        //送奶工将第4瓶奶放入奶箱
        //用户拿到第4瓶奶
        //送奶工将第5瓶奶放入奶箱
        //用户拿到第5瓶奶
    }
}

JMMjava内存模型

概述

  • 概念:
    java内存模型即Java Memory Model,简称JMM,它定义了java虚拟机(JVM)在计算机内存(RAM)中的工作方式。
    用于描述java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量的底层细节。
    JVM是整个计算机虚拟模型,则JMM隶属于JVM。
  • 作用:
    java线程之间的通信采用的是共享内存模型,JMM决定一个线程对共享变量的写入何时对另一个线程可见,以及如何同步访问共享变量。
    JMM定义了线程与主内存之间的抽象关系。
    Java内存模型JMM:为了解决在并发环境下由于CPU缓存、编译器和处理器的指令重排序 导致的可见性、有序性问题。
  • 其他:
    JMM解决指令重排其实是定义了一项happens-before规则。
    不同的平台内存模型是不同的,可以把内存模型理解成在特定的操作下,对特定的内存或高速缓存进行读写访问的过程的抽象。

java内存模型图示

Java 使用的池化_线程池_02


线程执行过程读取主内存与工作内存过程:

线程执行的时候,①从主内存读数据 ②load到工作内存中的副本中 ③ 传给处理器执行 ④给工作内存中的副本赋值 ⑤将工作内存的值赋值给主内存。

线程之间的通信机制是 共享内存和消息传递。
共享内存:线程之间共享内存的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信,典型的就是通过共享对象进行通信;
消息传递:线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信,典型消息传递方式wait和notify。
线程之间的同步机制:

共享内存的并发模型中,同步是显式的,程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行(比如使用synchronized);
消息传递的并发模型中,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

重排序

重排序的设计角度:程序员而言,需要内存模型易于理解、编程,需要一个强内存模型来编码;编译器和处理器而言,希望约束少一点,这样不会限制他们的执行效率。
提出原因:
为了提高并行度、优化程序性能,编译器和处理器会对代码进行指令重排序。

重排序是JMM设计策略:
①对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序;而对于不会改变执行结果的重排序,JMM不做处理;
②JMM为了满足对编译器和处理器的约束尽量少的条件,遵循规则是:只要不改变程序的执行结果,编译器和处理器想怎么优化就怎么优化;

java内存模型底层是通过内存屏障(memory barrier)来禁止重排序的。

as-if-serial语义

含义:
编译器和处理器不管如何重排序,单线程(程序)的执行结果都不能被改变;
编译器、runtime和处理器都必须遵守as-if-serial语义;
编译器和处理器不会对存在数据依赖的的操作进行重排序,这会改变执行结果。
案例:

int a=1;
int b=2;
int c=a+b;

//分析:
a和c、b和c之间都存在数据依赖关系,最终执行的指令序列中,c不能排到a和b的前面。

happens-bofore原则

概述
  • 出现原因:
    为了解决编译器和处理器的指令重排序;
  • happens-before规则:前一个操作的结果对后续操作是可见的。
  • JMM设计分为两部分:程序员 + 编译器处理器
    JMM为程序员提供的视角是:按顺序执行,且满足一个操作happens-before于另一个操作,即第一个操作执行结果对第二个操作可见。(JMM向程序员做出的保证)
    JMM为编译器和处理器提供的视角是:在不改变程序结果的前提下,编译器和处理器怎么优化都可以。
  • 目的:
    都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。
  • happens-before本质:
    一种可见性,A happens-before B 意味着A事件对B事件来说是可见的,无论A事件和B事件是否发生在同一个线程里。
    假设事件A发生在线程1上,事件B发生在线程2上,happens-before规则保证线程2上能看到A事件的发生。
  • 一个happens-before规则对应于一个或多个编译器和处理器重排序规则。
  • happens-before定义了8大原则。
1 程序顺序规则

一个线程中,按照程序顺序,前面的操作happens-before于后续的任意操作。
xhj理解:即前面操作的结果对后续操作是可见的

double pi = 3.14; //A
double r = 1.0; // B
double area = pi * r * r; // C

// 操作A happens-before 于操作B
2 监视器锁规则

一个unlock操作 先行发生于 后面对同一个锁的lock操作;

该锁说的就是synchronized。
xhj:解锁操作 执行可见于 之后(时间顺序)对同一把锁的加锁操作。

synchronized(this){ // 自动加锁
// x共享变量,初始值是10
	if(this.x < 12){
		this.x = 12;
	}
}
// 自动解锁

// 加锁和释放锁操作都是编译器帮我们实现的。
// 也就是说线程A对共享变量的修改,在线程A释放锁之后,线程B进入代码块的时候,能够看到共享变量修改后的值 x = 12
3 Volatile变量规则

对一个volatile域的写,happens-before于任意后续对这个volatile域的读。

xhj:对volatile域的写,对于后续对这个volatile域的读 是 可见的。(感觉像是禁用缓存的意思)

4 传递性

如果A happens-before B,且 B happens-before C,那么A happens-before C。

Java 使用的池化_Java 使用的池化_03

5 start()规则

Thread对象的start操作,先行于 此线程的任何一个动作;

6 join()规则

线程中所有的操作 先行于 对此线程的终止检测(Thread.join结束、Thread.isAlive返回检测结果)

7 程序中断规则

对线程interrupt方法调用 优先发生于 被中断线程的代码检测到该中断时间的发生( Thread.interrupt() 方法检测 )

8 对象终结规则

一个对象的初始化完成(构造函数执行结束) 先行于 发生它的finalize()方法的开始。
构造器的最后一个操作 先行于 析构器的第一个操作。

构造方法 和 析构方法 作用正好相反

构造方法

类构造对象时调用的方式,通常用来实例化对象以及开辟存储空间。

析构方法

destruct

用来做清理垃圾碎片的工作

例如:在建立对象的时候,用new开辟了一块内存空间,应在退出前在析构方法中将它释放。

finalize()方法

在java的Object类中,protected void finalize() 是一个受保护的方法;
由于其在Object类中,且所有类都继承自Object类,那么任何类都能够覆盖这个方法,该方法用于释放对象所占有的相关资源。
特点:

  • 垃圾回收器是否会执行该方法以及何时执行该方法,都是不确定的;
  • finalize()方法有可能使对象复活,使对象恢复到可触及状态;
  • 垃圾回收器在执行finalize()方法的时候,假设出现异常,垃圾回收器不会报告异常,程序继续正常执行。

as-if-serial规则 和 happens-before规则区别

区别

as-if-serial

happens-before

保证xxx的执行结果不被改变

单线程内的程序

正确同步的多线程

执行顺序

单线程程序,幻觉 = 按程序的顺序执行

多线程程序,幻觉 = 按照该规则顺序执行

作用

不改变执行结果的前提下,尽可能提高程序的执行并行度

不改变执行结果的前提下,尽可能提高程序的执行并行度