一、多线程
1 、并发与并行
- 并行:指两个或多个事件在同一时刻发生(同时执行)。
- 并发:指两个或多个事件在同一个时间段内发生(交替执行)。
注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,cpu会以某种顺序执行多个线程,我们把这种情况称之为线程调度。
2、 线程与进程
- 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
线程:是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
进程与线程的区别
进程:有独立的内存空间,进程是程序的一次执行过程。
线程:是进程中的一个执行单元,一个进程中至少有一个线程,一 进程中也可以有多个线程。
线程调度
分时调度 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
- 抢占式调度 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
3、Thread类(java.lang.Thread类)
构造方法
| 方法 | 说明 | | —- | —- | | public Thread() | 创建线程对象 | | public Thread(String name) | 创建线程对象并指定线程名字 | | public Thread(Runnable target) | 使用Runnable创建线程 | | public Thread(Runnable target,String name) | 使用Runable创建线程并指定线程名字 |
常用方法
方法 | 说明 |
---|---|
String getName() | 获取线程的名字 |
void start() | 开启线程,每个对象只调用一次start |
void run() | run方法写线程执行的代码,此线程要执行的任务在此处定义代码 |
static void sleep(long millis) | 让当前线程睡指定的时间 |
static Thread currentThread() | 获取当前线程对象 |
4、实现多线程方式
继承Thread
将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法
步骤: A:自定义一个类,继承Thread,这个类称为线程类;
B:重写Thread类中的run方法,run方法中就是线程要执行的任务代码;
C:创建线程类的对象;
D:启动线程,执行任务;
实现Runnable接口
创建线程的另一种方法是声明一个实现Runnable接口的类。 那个类然后实现了run方法。 然后可以分配类的实例,在创建Thread时作为参数传递,并启动。
构造方法:
- public Thread(Runnable target):分配一个带有指定目标新的线程对象。
- public Thread(Runnable target,String name):分配一个带有指定目标新的线程对象并指定名字。
关于调用run方法的疑问
1、自定义类继承Thread类,重写run方法,调用start启动线程,会自动调用我们线程类中的run方法。
2、自定义类,实现Runnable接口,把任务对象传给Thread对象。调用Thread对象的start方法,执行Thread的run。那么为什么最后执行的是任务类中的run呢?
实现Runnable接口的好处
1、避免了Java单继承的局限性;
2、把线程代码和任务的代码分离,解耦合(解除线程代码和任务的代码模块之间的依赖关系)。代码的扩展性非常好匿名内部类方式实现线程的创建
使用匿名内部类的方式实现Runnable接口,重新复写Runnable接口中的run方法。public static void main(String[] args) {
//使用匿名内部类实现多线程 r表示任务类的对象
Runnable r=new Runnable(){
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
};
//创建线程类对象
Thread t1=new Thread(r,"t1");
Thread t2=new Thread(r,"t2");
//启动线程
t1.start();
t2.start();
}
五、线程控制
线程休眠
使用Thread类中的sleep()函数可以让线程休眠, static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
二、线程安全
1、多线程安全的问题
多个线程在对共享数据进行读改写的时候,可能导致的数据错乱就是线程的安全问题了
2、多线程安全问题分析
从上图可以看出多线程存在的问题:四个窗口同时出售同一张票、重复票、跳票等问题。
原因分析:
A:多线程程序,如果是单线程就不会出现上述卖票的错误信息;B:多个线程操作共享资源,如果多线程情况下,每个线程操作自己的也不会出现上述问题;
C:操作资源的代码有多行,如果代码是一行或者很少的情况下,那么一行代码很快执行完毕,也不会出现上述情况;
D:CPU的随机切换。本质原因是CPU在处理多个线程的时候,在操作共享数据的多条代码之间进行切换导致的;
3、多线程安全问题解决
可以人为的控制CPU在执行某个线程操作共享数据的时候,不让其他线程进入到操作共享数据的代码中去,这样就可以保证安全。 上述的这个解决方案:称为线程的同步。使用 synchronized关键字。
synchronized关键字概述:
- synchronized关键字:表示“同步”的。它可以对“多行代码”进行“同步”——将多行代码当成是一个完整的整体,一个线程如果进入到这个代码块中,会全部执行完毕,执行结束后,其它线程才会执行。这样可以保证这多行的代码作为完整的整体,被一个线程完整的执行完毕。
- synchronized被称为“重量级的锁”方式,也是“悲观锁”——效率比较低。
- synchronized有几种使用方式:a).同步代码块b).同步方法【常用】
4、线程同步机制
同步代码块:
- synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
synchronized(同步锁){
需要同步操作的代码
}
同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
- 锁对象 可以是任意类型。
- 多个线程对象要使用同一把锁才能起到同步作用。
- 操作共享数据的代码需要加同步
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED锁阻塞)。
同步方法:
上图方法中加了个synchronized关键字代表同步方法.
注意:
1.非静态同步方法的锁是this;
2.如果一个方法内部,所有代码都需要被同步,那么就用同步方法;
静态同步方法
既然有非静态同步方法,那么肯定也会有静态同步方法。 将上述非静态同步方法改为静态同步方法,代码如下所示:
问题:非静态同步方法有隐式变量this作为锁,那么静态方法中没有this,那么静态同步方法中的锁又是什么呢?
静态同步方法的锁是:当前类的字节码文件对象(Class对象)。
总结: 同步代码块:锁是任意对象,但是必须唯一;
非静态同步方法:锁是this;
静态同步方法:锁是当前类的字节码文件对象;类名.class
三、Lock锁
Lock锁也称同步锁
- public void lock():加同步锁。
- public void unlock():释放同步锁。
由于Lock属于接口,不能创建对象,所以我们可以使用它的子类ReentrantLock来创建对象并使用Lock接口中的函数
案例:
/*
* 需求:使用Lock实现线程安全的卖票。
* Lock是接口,只能通过他的子类ReentrantLock创建对象
* 构造函数 ReentrantLock() 创建一个 ReentrantLock 的实例。
* void lock() 获取锁。
* void unlock() 试图释放此锁。
*/
//定义一个任务类用来卖票
class SellTicketTask implements Runnable
{
//定义100张票
private static int tickets=100;
//创建对象作为任意一把锁
// private Object obj=new Object();
//定义一把锁
Lock l=new ReentrantLock();
//模拟卖票
public void run() {
/*while(true)
{
synchronized (obj) {
if(tickets>0)
{
System.out.println(Thread.currentThread().getName()+"出票:"+tickets--);
}
}
}*/
//使用Lock锁替换synchronized
while(true)
{
//获取锁
l.lock();
if(tickets>0)
{
try {Thread.sleep(1);} catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName()+"出票:"+tickets--);
}
//释放锁
l.unlock();
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
// 创建任务类对象
SellTicketTask stt = new SellTicketTask();
//创建线程对象
Thread t1 = new Thread(stt,"窗口1");
Thread t2 = new Thread(stt,"窗口2");
Thread t3 = new Thread(stt,"窗口3");
Thread t4 = new Thread(stt,"窗口4");
//启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
四、死锁
1、什么是死锁?
:::danger 是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待的现象。 :::
2、产生死锁的条件
:::danger
- 有多把锁
- 有多个线程
- 有同步代码块嵌套
:::
1)创建一个任务类DeadLockTask 实现Runnable接口,复写run函数;2)创建两个Object类的对象lock_a,lock_b作为锁对象;
3)定义一个变量flag,让不同的线程切换到不同的地方去执行,按照不同的方式来获取锁;
4)在run函数中使用if-else结构来控制两个线程去执行不同的内容,并使用while循环一直让其执行;
5)在if中嵌套书写两个同步代码块lock_a和lock_b分别作为两个代码块的锁,将if中相同的内容复制一份写到else中;
6)创建测试类DeadThreadLockDemo,在这个类的主函数中创建任务类的对象;
7)创建两个线程对象t1和t2;
8)让主线程休息1毫秒;
9)使用t1对象调用start函数开启线程,让下一个线程进入到else中;
10)开启t2线程;
/*
* 演示线程死锁的问题
*/
//定义一个线程任务类
class DeadLockTask implements Runnable
{
//定义两个锁对象
private Object lock_a=new Object();
private Object lock_b=new Object();
//定义一个变量作为标记,控制取锁的方式
boolean flag=true;
public void run() {
//当线程进来之后,一个线程进入到if中,另一个进入到else中
if(flag)
{
while(true)
{
synchronized(lock_a)
{
System.out.println(Thread.currentThread().getName()+"if.....lock_a");
synchronized(lock_b)
{
System.out.println(Thread.currentThread().getName()+"if.....lock_b");
}
}
}
}else
{
while(true)
{
synchronized(lock_b)
{
System.out.println(Thread.currentThread().getName()+"else.....lock_b");
synchronized(lock_a)
{
System.out.println(Thread.currentThread().getName()+"else.....lock_a");
}
}
}
}
}
}
public class DeadThreadLockDemo {
public static void main(String[] args) {
// 创建任务类对象
DeadLockTask dlt = new DeadLockTask();
//创建线程对象
Thread t1 = new Thread(dlt);
Thread t2 = new Thread(dlt);
//开启第一个线程
t1.start();
//修改标记让下一个线程进入到else中
dlt.flag=false;
t2.start();
}
}
注意:在开发中一旦发生了死锁现象,不能通过程序自身解决。必须修改程序的源代码。
五、线程状态
1、概述
:::danger
线程由生到死的完整过程。
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State这个枚举中给出了六种线程状态
:::
| 线程状态 | 导致状态发生条件 |
| —- | —- |
| NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程对象,没有线程特征。 |
| Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用了t.start()方法 :就绪(经典教法) |
| Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
| Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
| Timed Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
| Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
六、等待唤醒机制(包子铺卖包子)
1、Object类的方法
:::danger
wait() :让当前线程进入等待状态,并且释放锁对象
notify(): 唤醒一个正在等待的线程,唤醒是随机的
void notifyAll() 唤醒在此对象监视器上等待的所有线程。
注意事项: 必须要使用锁对象来调用的。
:::
两个方法为什么要定义在Object类中?
因为需要用锁对象调用这两个方法,任意对象都可以作为锁对象。 也就是说任意类型的对象都可以调用的两个方法,就需要定义在Object类中
两个方法必须写在同步里面吗?
两个方法必须要在同步里面调用,因为在同步里面才有锁对象。
1.定义一个包子类,类中成员变量:
:::danger
pi //皮儿
xian //馅儿
flag://用来表示有没有包子,用true来代表有 用false来代表没有
:::
2.定义一个生产包子的任务类即生产者线程类:
:::danger
生产者线程思想:如果有包子就不需要制作,让生产者线程进入等待状态;如果没有包子,开始制作包子,并且唤醒消费者线程来吃包子
:::
3.定义一个消费包子的任务类即消费者线程类:
:::danger
消费者线程思想:如果没有包子就不消费,让消费者线程进入等待状态;如果有包子,开始吃包子,并且唤醒生产者线程来生产包子
:::
/*
包子类需要定义3个成员变量:
pi
xian
flag:表示是否有包子
*/
//包子类
public class BaoZi {
//皮儿
String pi;
//馅儿
String xian;
//布尔值
boolean flag=false; //用来表示有没有包子,用true来代表有 用false来代表没有
}
//生产包子:生产者线程执行的任务
/*
生产者线程思想:如果有包子就不需要制作,让生产者线程进入等待状态;如果没有包子,开始制作包子,并且唤醒消费者线程来吃包子
*/
public class ZhiZuo implements Runnable {
//成员变量
BaoZi baoZi;
//构造方法
public ZhiZuo(BaoZi baoZi) {
this.baoZi = baoZi;
}
@Override
public void run() {
//制作包子
while (true){
synchronized ("锁"){//t1
if(baoZi.flag == true){
//如果有包子就不需要制作
//就让制作的线程进入等待状态
try {
"锁".wait();
} catch (InterruptedException e) {
}
}else{
//else表示没有包子
//制作包子
baoZi.pi = "白面";
baoZi.xian = "韭菜大葱";
//修改包子状态
baoZi.flag = true;
System.out.println("生产出了一个包子!");
//生产好了包子叫醒吃货(消费者)来吃
"锁".notify();
}
}
}
}
}
//吃包子:消费者线程执行的任务
/*
消费者线程思想:如果没有包子就不消费,让消费者线程进入等待状态;如果有包子,开始吃包子,并且唤醒生产者线程来生产包子
*/
public class ChiHuo implements Runnable {
//成员变量
BaoZi baoZi;
//构造方法
public ChiHuo(BaoZi baoZi) {
this.baoZi = baoZi;
}
@Override
public void run() {
//吃包子
while(true){
synchronized ("锁"){
if(baoZi.flag == false){
//没包子
//让吃包子的线程进入等待
try {
"锁".wait();
} catch (InterruptedException e) {
}
}else{
//else表示有包子
//开吃
System.out.println("吃货吃了一个" + baoZi.pi+"皮儿," + baoZi.xian + "馅儿的大包子");
baoZi.pi = null;
baoZi.xian = null;
//修改包子状态
baoZi.flag = false;
//吃完包子叫醒对方(生产者)来做
"锁".notify();
}
}
}
}
}
//测试类
public class Test01 {
public static void main(String[] args) {
//创建包子
BaoZi baoZi = new BaoZi();
//创建对象
ZhiZuo zz = new ZhiZuo(baoZi);
Thread t1 = new Thread(zz);//生产者线程
t1.start();
//创建对象:消费者线程
ChiHuo ch = new ChiHuo(baoZi);
Thread t2 = new Thread(ch);
t2.start();
}
}
七、wait和sleep区别
:::danger
1.sleep(time) 属于Thread类中的,静态方法直接使用类名调用,让当前某个线程休眠,休眠的线程cpu不会执行,该方法可以使用在同步中也可以不使用在同步中,和锁对象无关,如果使用在同步中,不会释放锁对象,直到线程休眠时间到自然醒,然后cpu继续执行
2.等待方法:
1)wait(time) :可以让某个线程计时等待,时间到自然醒,或者时间未到,中途被其他线程唤醒
2)wait()无参数的方法,让某个线程无限等待,只能被其他线程唤醒
等待方法位于Object类中,必须在synchronized中使用,必须使用锁对象调用, 和锁对象有关。当某个线程遇到等待方法那么会立刻释放锁对象,cpu不会执行等待的线程,如果某个线程被唤醒,那么必须具有锁对象才可以执行,没有锁对象进入到锁阻塞状态。只有获取到锁对象才进入运行状态
:::
八、jdk5后的Lock实现线程之间的通讯
:::danger
1.创建Lock锁对象:Lock l = new ReentrantLock();
2.
上锁 l.lock()
释放锁:l.unlock()
3.使用锁对象调用Lock中的方法获取Condition对象:Condition newCondition() 返回绑定到此 Lock 实例的新 Condition 实例。
Condition c = l.newCondition() ;
说明:
Condition是一个接口,Condition 替代了 Object 监视器方法的使用。 和Lock一起使用完成线程的通讯
4.
线程等待:c.await() ;
唤醒线程:c.signal() 或者c.signalAll()
:::
九、线程池方式
1、概念:
:::danger 程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。因为启动线程的时候会在内存中开辟一块空间,消耗系统资源,同时销毁线程的时候首先要把和线程相关东西进行销毁,还要把系统的资源还给系统。这些操作都会降低操作性能。尤其针对一个线程用完就销毁的更加降低效率。 :::
2、使用线程池的好处
:::danger
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机或者宕机)。
:::
3、线程池的使用
:::danger Java里面线程池的顶级接口是java.util.concurrent.Executor,以及他的子接口java.util.concurrent.ExecutorService ::: Executors类中有个创建线程池的方法如下:
- public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)
获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:
- public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行
4、Callable开启多线程
Future submit(Callable task) : 获取线程池中的某一个线程对象 1、Callable是什么?
Callable是一个接口类似于Runnable,实现Callable可以调用带有返回值类型的任务方法。
2、Future
:::danger
方法:V get() : 获取计算完成的结果。
A:我们自定义类,实现Callable接口
B:实现Call方法,Call方法有返回值
C:然后把任务类对象交给线程池执行
D:执行完成的结果保存Future中
E:最后我们调用Future的get方法拿到真正的结果。
:::
/*
* 演示:带返回值的线程任务
* 需求:通过Callable计算从1到任意数字的和
*/
class SumTask implements Callable<Integer>{
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = 1; i <= 5; i++){
sum += i;
}
return sum ;
}
}
public class CallableDemo02 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 创建任务对象
SumTask st = new SumTask();
// 获取线程池
ExecutorService es = Executors.newFixedThreadPool(1);
// 执行任务
Future<Integer> future = es.submit(st);
// 等待运算结束,获取结果
Integer i = future.get();
System.out.println(i);
}
}
@Configuration
@EnableConfigurationProperties(TaskThreadPoolInfo.class)
@Slf4j
public class TaskExecutePool {
private TaskThreadPoolInfo info;
public TaskExecutePool(TaskThreadPoolInfo info) {
this.info = info;
}
/**
* 定义任务执行器
* @return
*/
@Bean(name = "threadPoolTaskExecutor",destroyMethod = "shutdown")
public ThreadPoolTaskExecutor threadPoolTaskExecutor(){
//构建线程池对象
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
//核心线程数:核心线程数(获取硬件):线程池创建时候初始化的线程数
taskExecutor.setCorePoolSize(info.getCorePoolSize());
//最大线程数:只有在缓冲队列满了之后才会申请超过核心线程数的线程
taskExecutor.setMaxPoolSize(info.getMaxPoolSize());
//缓冲队列:用来缓冲执行任务的队列
taskExecutor.setQueueCapacity(info.getQueueCapacity());
//允许线程的空闲时间:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
taskExecutor.setKeepAliveSeconds(info.getKeepAliveSeconds());
//线程名称前缀
taskExecutor.setThreadNamePrefix("StockThread-");
//设置拒绝策略
// taskExecutor.setRejectedExecutionHandler(rejectedExecutionHandler());
//参数初始化
taskExecutor.initialize();
return taskExecutor;
}
/**
* 自定义线程拒绝策略
* @return
*/
/**
@Bean
public RejectedExecutionHandler rejectedExecutionHandler(){
RejectedExecutionHandler errorHandler = new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable runnable, ThreadPoolExecutor executor) {
//TODO 可自定义Runable实现类,传入参数,做到不同任务,不同处理
log.info("股票任务出现异常:发送邮件");
}
};
return errorHandler;
} */
}