多线程摘要

1.并发与并行

	 - 并发:指的是在一段时间内多个事件的发生。 
	 - 并行:指的是在某一个时刻多个事件的发生。

2.进程与线程

  • 进程:
    • 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,进程之间的地址空间和资源是分开的。
  • 线程:
    • 是进程中的一个执行路径,多个线程共享一个内存空间(也就是进程的空间),线程之间可以自由切换,并发执行. 一个进程最少 有一个线程。
    • 线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分 成若干个线程。由于线程具有很多传统进程的特点,因此可以称为轻量进程。
  • 区别:进程是操作系统分配资源的基本单位,多个进程之间的资源是独立的;线程是任务调度和执行的基本单位,同一个进程中的多个线程共享一份资源。一个进程必须包含一个或多个线程。

例如:进程可以看作一个App的运行,而多个工作支持完成这个App的运行,那么这“多个工作”分别有一个线程。

3.线程的分类

**1.用户线程:**当一个进程不包含任何的存活的用户线程时,进程结束
**2.守护线程:**用于守护用户线程,当最后一个用户线程结束时,所有守护线程自动死亡

4.线程的调度方式

  1. 抢占式调度:每条线程执行的时间、线程之间的切换都由系统控制。它的缺点是:有的线程能够很容易的抢到时间片得以执行,而有的线程却一直抢不到时间片不能执行。java使用的就是抢占式调度。
  2. **协同式(非抢占式)调度:**某一线程执行完毕后通知系统切换到另一线程上执行。线程执行事件由本身控制,线程切换可以预知,不存在多线程同步问题。他的致命缺陷是:如果一个线程有问题,执行到一半一直被阻塞,那么整个系统就可能崩溃。

5.同步与异步

  • 同步:多个线程排队执行,效率低但是安全。
  • 异步:多个线程同时执行,效率高但是不安全。

6.实现多线程的三种方式:

1、继承Thread类实现多线程。(常用)

Thread的本质上也是实现了Runnable接口的一个实例,它代表一个线程的实例。通过自己的类直接extend Thread,并重写run()方法,就可以启动新线程并执行自己定义的run()方法,run()方法实际上就是我们要它执行的任务方法。

public class MyThread extends Thread{

    @Override
    public void run() {
        //这里的代码就是一条新的执行路径
        //这个执行路径的调用方式,不是调用run方法,而是通过thread对象的start()来启动任务
        for (int i = 0; i < 10; i++) {
            System.out.println("床前明月光"+i);
        }
    }
}

2、实现Runnable接口方式实现多线程。(常用)

当一个类已经继承其他类时就无法在继承Thread,此时可以实现Runnable接口并重写run()方法即可。

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        //线程的任务
        for (int i = 0; i < 10; i++) {
            System.out.println("疑是地上霜"+i);
        }
    }
}

3、实现Callable接口,该接口必须返回一个值。(不常用)

public class MyCallable implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        //Thread.sleep(1000);
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+i);
        }
        return 100;
    }
}

7.线程的状态:

在这里插入图片描述
在这里插入图片描述

8.线程池:

1.引入

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程非常耗费系统资源,会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?在Java中可以通过线程池来达到这样的效果。

2.概念

线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

3.合理利用线程池能够带来的好处

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

4.线程池的使用

Java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。

5.线程池的分类

Java通过Executors提供四种线程池,分别为:

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

/**
 * 缓冲线程池(长度无限制)
 * 任务加入后执行流程
 *      1.判断线程池是否存在空闲线程
 *      2.存在则使用
 *      3.不存在则创建线程,并放入线程池,然后使用
 */
static class Test01 {
    public static void main(String[] args) {

        //创建缓存线程池
        ExecutorService service = Executors.newCachedThreadPool();
        //指挥线程池执行新的任务
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "春眠不觉晓");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "春眠不觉晓");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "春眠不觉晓");
            }
        });

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "春眠不觉晓");
            }
        });
    }
}

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

/**
 * 定长线程池(线程数量固定不可改变)
 * 任务加入后执行流程
 *      1.判断线程池是否存在空闲线程
 *      2.存在则使用
 *      3.不存在空闲线程,且线程池未满的情况下,则创建线程并放入线程池,然后使用
 *      4.不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
 */
public static class Test02 {

    public static void main(String[] args) {
        //创建定长线程池
        ExecutorService service = Executors.newFixedThreadPool(2);//线程个数为2
        //指挥线程池执行新的任务
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "床前明月光");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "床前明月光");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "床前明月光");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

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

/**
 * 单线程线程池
 * 执行流程:
 *      1.判断线程池的那一个线程是否空闲
 *      2.空闲则使用
 *      3.不空闲则等待池中的线程空闲后再使用
 */
static class Test03 {
    public static void main(String[] args) {
        //创建单线程线程池
        ExecutorService service = Executors.newSingleThreadExecutor();
        //指挥线程池执行新的任务
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"飞流直下三千尺");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"飞流直下三千尺");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"飞流直下三千尺");
            }
        });

    }
}

4) newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

/**
 * 周期任务 定长线程池
 * 执行流程:
 *      1.判断线程池是否存在空闲线程
 *      2.存在则使用
 *      3.不存在空闲线程,且线程池未满的情况下,创建线程并放入线程池,然后使用
 *      4.不存在空闲线程,且线程池已满的情况下。等待线程池存入空闲线程
 *
 * 周期性任务执行时:
 *      定时执行,当某个时机触发时,自动执行任务
 */
static class Test04{
    public static void main(String[] args) {
        ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
        /**
         * 1.   定时执行一次
         * 参数一:定时执行的任务
         * 参数二:时长数字
         * 参数三:时长数字的时间单位,TimeUnit的常量指定
         */
       /* service.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("桃花潭水深千尺!");
            }
        },5, TimeUnit.SECONDS);*/

        /**
         * 2.   周期性执行任务
         * 参数一:任务
         * 参数二:延迟时长数字(第一次执行在什么时间以后)
         * 参数三:周期时长数字(每隔多久执行一次)
         * 参数四:时常数字的单位
         */
        service.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println("不及汪伦送我情!");
            }
        },5,1,TimeUnit.SECONDS);

    }

}

9.什么是线程安全?

如果有多个线程在同时运行同一段代码(有写操作,读操作不涉及),多个线程每次运行的结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的,否则就是线程不安全。

通过案例演示一个线程安全问题:

模拟卖票
public class Ticket implements Runnable {
	private int ticket = 100;//票的总数

	/*
	 * 执行卖票操作
	 */
	@Override
	public void run() {
		// 每个窗口卖票的操作
		// 窗口 永远开启
		while (true) {
			if (ticket > 0) {// 有票 可以卖
			// 出票操作
			// 使用sleep模拟一下出票时间
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					// TODO Auto‐generated catch block
					e.printStackTrace();
				}
				// 获取当前线程对象的名字
				String name = Thread.currentThread().getName();
				System.out.println(name + "正在卖:" + ticket);
				ticket--;
			}
		}
	}
}

测试类
public class Test {
	public static void main(String[] args) {
		// 创建线程任务对象
		Ticket ticket = new Ticket();
		// 创建三个窗口对象
		Thread t1 = new Thread(ticket, "窗口1");
		Thread t2 = new Thread(ticket, "窗口2");
		Thread t3 = new Thread(ticket, "窗口3");
		// 同时卖票
		t1.start();
		t2.start();
		t3.start();
	}
}
结果

在这里插入图片描述
在这里插入图片描述

***结果:***同一张票被卖了多次,还出现了不存在的0票和-1票。这种问题就是几个窗口(线程)的票数不同步导致的问题,我们称为线程不安全问题。

10.如何做到线程安全?

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。就是要解决上述多线程并发访问一个资源的安全性问题。

按照上面的示例来说就是解决重复票与不存在票的问题:窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码 去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU 资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

有三种方式完成同步操作:

1. 同步代码块。 (隐式锁)

synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

2. 同步方法。 (隐式锁)

使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

3. 锁机制。(显式锁)

Lock锁也称同步锁(一般使用其子类ReentrantLock),加锁与释放锁方法化了,如下:

public void lock() :加同步锁。

public void unlock() :释放同步锁。

锁的分类:

公平锁:按照先后顺序进行排队,永远是队列的第一个获得锁。

非公平锁:大家一起抢,线程直接去获取锁,当获取不到时才会进入队列,如果能获取到就直接获取到。

解决了线程不安全的现象。

有三种方式完成同步操作:

  1. 同步代码块。 (隐式锁)

    synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

  2. 同步方法。 (隐式锁)

    使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

  3. 锁机制。(显式锁)

    Lock锁也称同步锁(一般使用其子类ReentrantLock),加锁与释放锁方法化了,如下:

    public void lock() :加同步锁。

    public void unlock() :释放同步锁。

锁的分类:

**公平锁:**按照先后顺序进行排队,永远是队列的第一个获得锁。

**非公平锁:**大家一起抢,线程直接去获取锁,当获取不到时才会进入队列,如果能获取到就直接获取到。

**注:**我们所使用的三种解决方案均为非公平锁,公平锁的实现方式:当new一个ReentrantLock时传参为true即可,其默认为false。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值